선형성 확인¶
분산분석에서 선형성이 관련되는 이유¶
선형성이 분산분석의 요구 조건으로 늘 명시되지는 않지만, 분산분석을 일반선형모형의 관점에서 보면 관련성이 드러난다. 일원배치 분산분석의 모형은
이며 \(\mu\)는 전체 평균, \(\alpha_i\)는 집단 \(i\)의 효과, \(\varepsilon_{ij}\)는 무작위 오차이다. 이 모형은 본래 모수에 대해 선형이다. 선형성은 이원배치 분산분석, 공분산분석(ANCOVA), 그리고 분산분석에 연속형 공변량을 넣어 확장할 때 더 분명하게 중요해진다.
선형성 가정은 연속형 독립변수와 종속변수의 관계가 각 집단 안에서 선형이라는 것이다. 비선형 관계는 잔차에 체계적인 패턴을 남기고 모형 오설정으로 이어질 수 있다.
설정¶
보기 1. 진단에 쓸 모형 준비. 세 집단 각 \(n = 20\)에 공변량 \(x \sim U(0, 10)\)을 넣고 기울기 \(0.4\)의 선형 관계를 심어 response ~ C(group) + covariate를 적합한다.
(1) 최소제곱 잔차 \(e = y - X\hat\beta\)가 설계행렬의 모든 열과 직교하므로, 이 모형에서는 집단별 잔차 합이 \(0\)인 것에 더해
까지 성립한다. 여기서 잔차를 공변량에 회귀한 최소제곱 기울기가 정확히 \(0\)이고, 적합값에 회귀한 기울기도 \(0\)임을 보이시오. 그러므로 아래 잔차 그림에서 찾아야 하는 것은 무엇인가.
(2) 모형을 적합해 (1)을 확인하고, 공변량 계수의 추정값과 표준오차를 재어 참값 \(0.4\)와 비교하시오. 아울러 이 쪽의 적합값이 일원배치와 어떻게 다른지 적으시오.
풀이
(1) 해석적으로. 정규방정식에서 \(X^\top e = 0\)이다. 이 모형의 설계행렬은 절편 \(\mathbf 1\), 더미 \(\mathbf 1_B\), \(\mathbf 1_C\), 그리고 공변량 \(x\) 네 열이므로 네 개의 등식을 얻는다.
(집단 A의 합은 앞의 셋에서 뺄셈으로 \(0\)이다.) 마지막 등식이 이 쪽의 주인공이다.
잔차를 공변량에 회귀한 최소제곱 기울기는
인데, 첫째 등식에서 \(\bar e = 0\)이므로 분자가
이다. 따라서 \(\hat b = 0\)이다. 표본에 따라 작아지는 것이 아니라 항등적으로 \(0\)이다.
적합값에 대해서도 같다. \(\hat y = X\hat\beta\)는 \(X\)의 열들의 선형결합이므로
이고, 위와 같은 계산으로 잔차를 적합값에 회귀한 기울기도 \(0\)이다.
그러므로 잔차 그림에서 기울기를 찾는 것은 뜻이 없다. 어떤 자료를 넣어도, 참 관계가 아무리 심하게 휘어 있어도, 최소제곱이 직선 성분을 이미 전부 빨아들였기 때문에 잔차에는 직선 성분이 남지 않는다. 남을 수 있는 것은 직선으로 설명되지 않는 부분, 곧 휘어짐(곡률)이다. 잔차 대 적합값 그림에서 U자나 역U자를 찾으라고 하는 이유가 이것이고, "잔차가 오른쪽으로 올라간다"는 말이 애초에 성립할 수 없는 이유도 이것이다.
(2) 수치적으로.
import numpy as np
import pandas as pd
from scipy import stats
from statsmodels.formula.api import ols
# 이 페이지의 모든 진단은 아래 모형 하나를 놓고 수행한다.
# 집단마다 표준편차를 1.0, 1.3, 1.6으로 다르게 주어 진단이 무엇을 잡아내는지
# (그리고 무엇을 못 잡아내는지) 볼 수 있게 했다.
rng = np.random.default_rng(42)
n = 20
data = pd.DataFrame({
"group": np.repeat(["A", "B", "C"], n),
"response": np.concatenate([
rng.normal(10.0, 1.0, n),
rng.normal(10.8, 1.3, n),
rng.normal(12.0, 1.6, n),
]),
})
# 선형성은 연속형 공변량이 있을 때 비로소 문제가 된다.
# 그래서 이 페이지에서는 공변량을 하나 넣은 공분산분석 모형을 쓴다.
data["covariate"] = rng.uniform(0, 10, 3 * n)
data["response"] = data["response"] + 0.4 * data["covariate"]
model = ols("response ~ C(group) + covariate", data=data).fit()
print(data.groupby("group").response.agg(["count", "mean", "std"]).round(3))
print(f"\nF = {model.fvalue:.4f}, p = {model.f_pvalue:.4f}")
print(f"covariate 계수 = {model.params['covariate']:.4f}")
# (1) 의 네 등식을 설계행렬에 직접 내적해 확인한다.
e = model.resid
x = data["covariate"].values
print(f"\n설계행렬 열이름: {model.model.exog_names}")
print("X^T e =", np.array2string(model.model.exog.T @ e.values, precision=3))
print(f" sum e_i = {e.sum():+.3e}")
print(f" sum_(i in B) e_i = {e[data['group'] == 'B'].sum():+.3e}")
print(f" sum_(i in C) e_i = {e[data['group'] == 'C'].sum():+.3e}")
print(f" sum x_i e_i = {(x * e.values).sum():+.3e}")
# 그래서 잔차 그림의 직선 기울기는 항등적으로 0 이다.
print(f"\n잔차를 공변량에 회귀한 기울기 = {stats.linregress(x, e.values).slope:+.3e}")
print(f"잔차를 적합값에 회귀한 기울기 = "
f"{stats.linregress(model.fittedvalues.values, e.values).slope:+.3e}")
b = model.params["covariate"]
se = model.bse["covariate"]
print(f"\ncovariate 계수 {b:.4f}, 표준오차 {se:.4f}, 참값 0.4")
print(f" z = (추정 - 참)/표준오차 = {(b - 0.4) / se:+.3f}")
print(f" 95% 신뢰구간 = [{model.conf_int().loc['covariate', 0]:.4f}, "
f"{model.conf_int().loc['covariate', 1]:.4f}]")
print(f"\n적합값의 범위 = [{model.fittedvalues.min():.4f}, {model.fittedvalues.max():.4f}]"
f" (서로 다른 값 {model.fittedvalues.nunique()}개)")
출력:
count mean std
group
A 20 11.820 1.205
B 20 12.679 1.243
C 20 14.351 1.513
F = 35.4445, p = 0.0000
covariate 계수 = 0.3275
설계행렬 열이름: ['Intercept', 'C(group)[T.B]', 'C(group)[T.C]', 'covariate']
X^T e = [-2.292e-13 -2.114e-13 4.974e-14 -5.962e-13]
sum e_i = -2.292e-13
sum_(i in B) e_i = -2.114e-13
sum_(i in C) e_i = +4.974e-14
sum x_i e_i = -5.969e-13
잔차를 공변량에 회귀한 기울기 = +1.214e-15
잔차를 적합값에 회귀한 기울기 = +3.007e-15
covariate 계수 0.3275, 표준오차 0.0506, 참값 0.4
z = (추정 - 참)/표준오차 = -1.432
95% 신뢰구간 = [0.2260, 0.4289]
적합값의 범위 = [10.4040, 15.7564] (서로 다른 값 60개)
(1)의 네 등식이 모두 확인된다. \(X^\top e\)의 네 성분이 \(10^{-13}\) 수준이고, 아래 네 줄의 손계산이 그 성분들과 같은 수다. 넷째 성분과 sum x_i e_i가 \(-5.962\)와 \(-5.969\)로 끝자리만 다른 것은 덧셈 순서가 달라 반올림이 다르게 쌓인 결과이며, 둘 다 수학적으로는 \(0\)이다.
기울기는 더 깨끗하다. 잔차를 공변량에 회귀한 기울기가 \(1.2 \times 10^{-15}\), 적합값에 회귀한 기울기가 \(3.0 \times 10^{-15}\)다. 나눗셈에서 \(\sum(x_i-\bar x)^2\)이라는 큰 수로 나누기 때문에 찌꺼기가 더 눌렸다. 항등적으로 \(0\)이라는 유도가 그대로 보인다.
공변량 계수는 참값과 맞는다. 추정값 \(0.3275\)에 표준오차 \(0.0506\)이니 참값 \(0.4\)에서 \(1.43\) 표준오차 떨어져 있고, \(95\%\) 신뢰구간 \([0.2260,\ 0.4289]\)가 \(0.4\)를 담고 있다. 관측값 \(60\)개로는 기울기를 \(\pm 0.1\) 정도까지만 좁힐 수 있다는 뜻이다. "추정값이 \(0.3275\)라 참값 \(0.4\)와 다르다"가 아니라 "\(0.4\)와 어긋나지 않는다"가 옳은 읽기다.
집단평균도 바뀌었다. 공변량을 더한 만큼 \(11.820\), \(12.679\), \(14.351\)로 올라갔고 표본표준편차도 \(1.205\), \(1.243\), \(1.513\)으로 커졌다. 공변량이 집단 안에서도 흔들리므로 \(0.4x\)의 변동이 집단 내 산포에 얹혔기 때문이다. 집단 A의 경우 공변량을 더하기 전의 표준편차가 \(0.870\)이었다(등분산성 쪽 보기 1).
마지막 줄이 이 쪽과 등분산성 쪽의 결정적 차이다. 적합값이 \([10.4040,\ 15.7564]\)에 서로 다른 값 \(60\)개로 퍼져 있다. 일원배치에서는 적합값이 집단평균 세 개뿐이라 잔차 그림이 세로 띠 세 줄이었다(등분산성 쪽 보기 4). 공변량이 들어오면 적합값이 연속적으로 퍼지므로 휘어짐을 볼 가로 해상도가 생긴다. 선형성 진단이 공변량 없이는 뜻이 없다는 말의 실제 내용이 이것이다.
확인 방법¶
산점도¶
(공분산분석처럼) 연속형 공변량이 있으면 종속변수를 각 공변량에 대해 집단별 색으로 그린다.
보기 2. 집단별 산점도. 보기 1의 자료를 집단별 색으로 흩뿌린다.
(1) 그림을 그리고 무엇을 읽을 수 있는지 말하시오. "세 집단 모두 직선이고 기울기가 비슷하다"는 판단을 수치로 뒷받침하시오.
(2) 이 그림이 가리는 것은 무엇인가. "기울기가 비슷해 보인다"가 어느 정도의 증거인지 모의실험으로 재시오.
풀이
이 보기에는 유도할 식이 없다. 그림에서 무엇을 읽어야 하는지가 전부다. 그러므로 눈으로 보는 두 가지 — 직선인가와 기울기가 같은가 — 를 각각 수로 바꾸는 일에 집중한다.
(1) 그림이 말하는 것.
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
from scipy import stats
from statsmodels.formula.api import ols
from statsmodels.stats.anova import anova_lm
# 그림에서 읽으려는 것을 먼저 수로 적어 둔다.
print(f"{'기울기':>8s} {'표준오차':>9s} {'상관 r':>8s} {'x 범위':>16s} 집단")
for name, d in data.groupby("group"):
sl = stats.linregress(d["covariate"], d["response"])
print(f"{sl.slope:8.4f} {sl.stderr:9.4f} {sl.rvalue:8.4f}"
f" [{d['covariate'].min():5.2f},{d['covariate'].max():6.2f}] {name}")
# 평행성(기울기 동질성)은 눈이 아니라 교호작용 항으로 판정한다.
m_int = ols("response ~ C(group) * covariate", data=data).fit()
print("\n교호작용 검정 (기울기가 집단마다 같은가)")
print(anova_lm(m_int, typ=2).round(4).to_string())
# 휘어짐도 항을 넣어 재 본다.
m_quad = ols("response ~ C(group) + covariate + I(covariate ** 2)", data=data).fit()
print(f"\n이차항 계수 {m_quad.params['I(covariate ** 2)']:+.5f}, "
f"p = {m_quad.pvalues['I(covariate ** 2)']:.4f}")
# 집단마다 색을 달리해 흩뿌린다. 각 색의 점들이 직선을 이루는지,
# 그리고 그 직선들의 기울기가 서로 비슷한지를 본다. 기울기가 다르면
# 공변량과 집단 사이에 교호작용이 있는 것이라 공분산분석의 전제가 깨진다.
sns.scatterplot(data=data, x='covariate', y='response', hue='group', alpha=0.6)
plt.title("Response vs. Covariate by Group")
plt.show()
출력:
기울기 표준오차 상관 r x 범위 집단
0.3567 0.0866 0.6966 [ 0.31, 8.53] A
0.2677 0.0760 0.6391 [ 0.23, 9.62] B
0.3816 0.1039 0.6544 [ 0.96, 9.69] C
교호작용 검정 (기울기가 집단마다 같은가)
sum_sq df F PR(>F)
C(group) 51.5285 2.0 24.6561 0.0000
covariate 42.9301 1.0 41.0835 0.0000
C(group):covariate 1.0613 2.0 0.5078 0.6046
Residual 56.4271 54.0 NaN NaN
이차항 계수 -0.00556, p = 0.7870

세 집단 모두 공변량이 커질수록 반응이 커진다. 그 판단의 정량적 내용은 집단별 상관 \(r = 0.6966,\ 0.6391,\ 0.6544\)와 기울기 \(0.3567,\ 0.2677,\ 0.3816\)이다. 자료를 만들 때 기울기 \(0.4\)의 선형 관계를 넣었으므로 기대한 모습이다.
휘어짐이 없다는 것도 수로 확인된다. 이차항을 넣으면 계수가 \(-0.00556\)에 \(p = 0.7870\)이다. 기울기 \(0.4\)에 비해 이차항의 기여는 \(x = 10\)에서도 \(-0.56\)에 불과하다. 그림에서 휘어짐이 안 보인다는 판단이 검정과 일치한다.
기울기가 서로 비슷하다는 것은 교호작용 검정으로 판정한다. C(group):covariate 행의 \(F = 0.5078\), \(p = 0.6046\)으로 평행성을 기각하지 않는다. 공분산분석은 기울기가 집단마다 같다고 가정하므로(평행성 가정) 이 검정이 통과해야 집단 효과를 "하나의 상수 차이"로 말할 수 있다.
살펴볼 것:
- 각 집단 안의 선형 추세는 선형성 가정을 확인해 준다.
- 휘어진 패턴은 다항 항이나 다른 모형이 필요할 수 있는 비선형 관계를 시사한다.
(2) 그림이 가리는 것. 가장 큰 것은 "비슷해 보인다"가 생각만큼 강한 증거가 아니라는 점이다. 위 표를 보면 세 기울기의 표준오차가 각각 \(0.0866\), \(0.0760\), \(0.1039\)다. 집단 A와 B의 기울기 차이 \(0.3567 - 0.2677 = 0.089\)에 대한 표준오차만 해도 \(\sqrt{0.0866^2 + 0.0760^2} = 0.115\)이니, 관측된 차이가 표준오차보다 작다. 눈으로 "평행하다"고 말할 수 있는 정밀도가 애초에 없다.
얼마나 없는지 모의실험으로 재 본다.
import numpy as np
import pandas as pd
from statsmodels.formula.api import ols
from statsmodels.stats.anova import anova_lm
# 평행성 검정이 기울기 차이를 얼마나 잡아내는가. n = 20 에서 재 본다.
rng_sim = np.random.default_rng(2026)
B = 2000
grp = np.repeat(["A", "B", "C"], 20)
print(f"반복 {B}회, 집단당 n = 20, 명목수준 0.05, 몬테카를로 오차 "
f"{np.sqrt(0.05 * 0.95 / B):.4f}")
print(f"\n{'참 기울기':>22s} {'기각률':>8s}")
for slopes in [(0.4, 0.4, 0.4), (0.4, 0.5, 0.6), (0.2, 0.4, 0.6), (0.0, 0.4, 0.8)]:
s = dict(zip("ABC", slopes))
hit = 0
for _ in range(B):
xv = rng_sim.uniform(0, 10, 60)
yv = np.array([10.0 + s[g] * xx for g, xx in zip(grp, xv)]) \
+ rng_sim.normal(0, 1, 60)
d = pd.DataFrame({"group": grp, "covariate": xv, "response": yv})
m = ols("response ~ C(group) * covariate", data=d).fit()
hit += anova_lm(m, typ=2).loc["C(group):covariate", "PR(>F)"] < 0.05
print(f"{str(slopes):>22s} {hit / B:8.4f}")
출력:
반복 2000회, 집단당 n = 20, 명목수준 0.05, 몬테카를로 오차 0.0049
참 기울기 기각률
(0.4, 0.4, 0.4) 0.0550
(0.4, 0.5, 0.6) 0.3220
(0.2, 0.4, 0.6) 0.8650
(0.0, 0.4, 0.8) 1.0000
기울기가 정말 같을 때 기각률이 \(0.0550\)이다. 명목 \(0.05\)에 몬테카를로 오차 \(0.0049\)이니 \(1\) 오차 안쪽이고, 검정이 제대로 작동한다는 확인이다.
그런데 기울기가 달라도 잘 못 잡는다. \((0.4,\ 0.5,\ 0.6)\), 곧 끝과 끝이 \(0.2\) 차이인 경우를 세 번에 한 번(\(0.3220\))만 잡아낸다. 열 번 중 일곱 번은 "평행하다"고 통과시킨다. \((0.2,\ 0.4,\ 0.6)\)까지 벌어져야 \(0.8650\)이 되고, \((0,\ 0.4,\ 0.8)\)에서 비로소 확실해진다.
그러므로 \(p = 0.6046\)은 "기울기가 같다"의 증거가 아니다. 이 설계에서는 \(0.2\)짜리 차이도 대체로 통과하므로, 통과했다는 사실만으로는 \(0.2\) 이하의 차이를 배제할 수 없다. 그리고 \(0.2\)는 참 기울기 \(0.4\)의 절반이다. 작은 차이가 아니다.
가리는 것이 둘 더 있다.
공변량이 관측된 구간 밖은 보이지 않는다. \(x\)의 범위가 집단마다 \([0.31,\ 8.53]\), \([0.23,\ 9.62]\), \([0.96,\ 9.69]\)다. \(x = 12\)에서 관계가 휘어지든 꺾이든 이 그림은 아무 말도 못 한다. 그런데 공분산분석의 조정된 평균은 보통 전체 평균 \(\bar x\)에서 평가하므로 이 구간 안이면 안전하지만, 외삽해 해석하면 근거가 없다.
집단별로 점이 \(20\)개뿐이라 휘어짐도 약하면 안 보인다. 이차항 \(p = 0.7870\)은 "휘어짐이 없다"가 아니라 "\(20\)개 셋으로는 이 정도 휘어짐을 구별할 수 없다"는 뜻이다. 두 판정이 모두 검정력 부족 쪽에 걸려 있고, 그것이 집단당 \(20\)개짜리 설계의 실제 한계다.
잔차 그림¶
잔차를 독립변수(또는 적합값)에 대해 그린 그림에는 체계적인 패턴이 없어야 한다. 0 주위의 무작위한 흩어짐이 선형성을 확인해 준다.
보기 3. 잔차 대 적합값 그림. 보기 1의 모형으로 그린다.
(1) 그림을 그리고 무엇을 읽을 수 있는지 말하시오. 보기 1에서 직선 기울기가 항등적으로 \(0\)임을 보았으니 휘어짐만 남는다. 그 휘어짐을 수치로 재시오.
(2) 이 그림이 가리는 것은 무엇인가. 참 관계가 이차인 자료를 선형으로 적합해, 잔차를 적합값에 대해 보는 것과 공변량에 대해 보는 것이 어떻게 다른지 수치로 보이시오.
풀이
이 보기에는 유도할 식이 없다. 그림에서 무엇을 읽어야 하는지가 전부다. 휘어짐을 재는 방법을 두 가지 쓴다.
- 사분위 구간별 잔차 평균. 적합값을 사분위로 나누어 각 구간의 잔차 평균을 본다. 선형이면 네 수가 모두 \(0\) 근처여야 하고, U자면
+ − − +꼴이 된다. - 이차항 검정. 잔차를 적합값의 이차식에 회귀해 이차항 계수와 \(p\)-값을 본다. 보기 1에서 일차항은 항등적으로 \(0\)이므로 이차항이 정보를 갖는 첫 항이다.
(1) 그림이 말하는 것.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from statsmodels.formula.api import ols
# 그림에서 읽으려는 것을 먼저 수로 적어 둔다.
def read_curve(v, e, label):
"""사분위 구간별 잔차 평균과 이차항 검정으로 휘어짐을 잰다."""
q = np.quantile(v, [0, .25, .5, .75, 1.0])
b = np.clip(np.digitize(v, q[1:-1]), 0, 3)
means = " ".join(f"{e[b == j].mean():+.3f}" for j in range(4))
m = ols("e ~ v + I(v ** 2)", data=pd.DataFrame({"e": e, "v": v})).fit()
print(f"{label} 구간 평균 {means} "
f"이차항 {m.params['I(v ** 2)']:+.5f} (p = {m.pvalues['I(v ** 2)']:.4f})")
read_curve(model.fittedvalues.values, model.resid.values, "적합값에 대해 ")
read_curve(data["covariate"].values, model.resid.values, "공변량에 대해 ")
# 비교: 참 관계가 이차인 자료를 선형으로 적합하면 두 그림이 어떻게 달라지는가
dq = data.copy()
dq["response"] = (10.0 + 0.4 * dq["covariate"]
+ 0.10 * (dq["covariate"] - 5) ** 2
+ np.random.default_rng(2026).normal(0, 1, 60))
mq = ols("response ~ C(group) + covariate", data=dq).fit()
print()
read_curve(mq.fittedvalues.values, mq.resid.values, "[이차] 적합값에 대해")
read_curve(dq["covariate"].values, mq.resid.values, "[이차] 공변량에 대해")
print(f"\n[이차] 적합값과 공변량의 상관 = "
f"{np.corrcoef(mq.fittedvalues.values, dq['covariate'].values)[0, 1]:.4f}")
# 잔차 대 적합값 그림은 진단의 출발점이다. 점들이 0 선 둘레에 폭을 일정하게
# 유지하며 흩어져 있으면 좋다. 깔때기 모양이면 등분산이 깨진 것이고, 굽은
# 모양이면 모형이 놓친 구조가 남아 있다는 뜻이다.
plt.scatter(model.fittedvalues, model.resid, alpha=0.6)
plt.axhline(y=0, color='r', linestyle='--')
plt.xlabel("Fitted Values")
plt.ylabel("Residuals")
plt.title("Residuals vs. Fitted Values")
plt.show()
출력:
적합값에 대해 구간 평균 +0.015 +0.051 -0.090 +0.023 이차항 +0.01221 (p = 0.8597)
공변량에 대해 구간 평균 -0.058 +0.038 +0.247 -0.227 이차항 -0.00532 (p = 0.7877)
[이차] 적합값에 대해 구간 평균 +0.274 -0.224 -0.068 +0.017 이차항 +0.30729 (p = 0.0245)
[이차] 공변량에 대해 구간 평균 +0.365 -0.494 -0.305 +0.434 이차항 +0.06945 (p = 0.0004)
[이차] 적합값과 공변량의 상관 = 0.9257

앞의 등분산성 쪽과 달리 적합값이 연속적으로 퍼져 있다. 보기 1에서 센 대로 서로 다른 적합값이 \(60\)개다. 모형에 연속형 공변량이 들어갔기 때문이고, 그래서 비로소 휘어짐을 볼 가로 해상도가 생겼다.
휘어짐이 없다는 판단의 정량적 내용은 두 줄이다. 적합값 사분위별 잔차 평균이 \(+0.015,\ +0.051,\ -0.090,\ +0.023\)으로 가장 큰 \(-0.090\)도 잔차 표준편차 \(0.9871\)의 \(9\%\)에 지나지 않으며, + − − +나 − + + − 같은 모양이 아니다. 이차항은 \(+0.01221\)에 \(p = 0.8597\)이다. 공변량에 대해 보아도 이차항이 \(-0.00532\)에 \(p = 0.7877\)이다. 어느 쪽으로도 휘어짐의 흔적이 없다.
- 무작위한 흩어짐: 선형성이 만족된다.
- 곡률: 휘어진 패턴은 다항 항이나 비선형 모형이 필요함을 시사한다.
- 뚜렷한 군집: 추가적인 집단 변수가 필요함을 나타낼 수 있다.
(2) 그림이 가리는 것. 뒤쪽 두 줄이 그 답이다. 같은 공변량에 참 관계를 \(10 + 0.4x + 0.10(x-5)^2\)으로 바꾸어 일부러 휘어 놓고 선형 공분산분석을 적합했다. 그런데 두 그림이 그 휘어짐을 다르게 보여 준다.
| 무엇에 대해 보는가 | 사분위 잔차 평균 | 이차항 \(p\) |
|---|---|---|
| 적합값 | \(+0.274,\ -0.224,\ -0.068,\ +0.017\) | \(0.0245\) |
| 공변량 | \(+0.365,\ -0.494,\ -0.305,\ +0.434\) | \(\mathbf{0.0004}\) |
공변량에 대해 보면 + − − +가 교과서처럼 깨끗하게 나온다. 양 끝이 \(+0.365\)와 \(+0.434\)로 올라가고 가운데가 \(-0.494\), \(-0.305\)로 내려가는 U자다. 그런데 적합값에 대해 보면 마지막 구간이 \(+0.017\)로 주저앉아 U자가 무너진다.
까닭은 적합값이 집단 이동과 공변량을 섞은 값이기 때문이다. 둘의 상관이 \(0.9257\)로 \(1\)이 아니므로, 적합값으로 정렬하면 서로 다른 \(x\)를 가진 점들이 같은 구간에 섞여 들어가고 U자가 문질러진다. \(p\)-값이 \(0.0004\)에서 \(0.0245\)로 수십 배 나빠진 것이 그 손실의 크기다.
그러므로 공분산분석에서 선형성을 보려면 잔차를 적합값이 아니라 공변량에 대해 그려야 한다. 잔차 대 적합값 그림은 등분산성 진단의 표준 도구지만, 선형성 진단으로는 공변량이 여럿이거나 집단 효과가 크면 흐려진다. 공변량이 여러 개면 각각에 대해 따로 그려야 한다.
가리는 것이 하나 더 있다. 이 그림에는 집단 정보가 없다. 한 집단만 휘어 있고 나머지는 직선이어도 세 집단을 한 덩어리로 섞어 보므로 희석된다. 집단별로 색을 달리하거나 집단별 잔차 그림을 따로 보아야 잡힌다. 그래서 보기 2의 집단별 산점도와 이 그림이 서로를 대신하지 못한다.
선형성이 어긋날 때¶
- 다항 항: 이차 이상의 항을 모형에 추가한다.
- 비선형 변환: 종속변수나 공변량을 변환한다(예: 로그, 제곱근).
- 일반화가법모형(GAM): 선형 관계를 가정하는 대신 공변량의 매끄러운 함수를 쓴다.
- 비모수 방법: 비선형성이 심하면 함수 형태에 대한 가정을 두지 않는 비모수 대안을 고려한다.
연습문제¶
연습문제 1. 순수하게 범주형인 요인만 있고 연속형 공변량이 없는 일원배치 분산분석에서 선형성 가정이 자동으로 만족되는 이유를 설명하라.
풀이
일원배치 분산분석 모형 \(Y_{ij} = \mu + \alpha_i + \varepsilon_{ij}\)는 집단 소속을 나타내는 지시변수를 쓴다. 이 지시변수는 정의상 모형에 선형으로 들어간다. 연속형 설명변수가 없으므로 비선형일 수 있는 함수 관계 자체가 없다. 선형성은 공분산분석이나 회귀 기반 표현처럼 연속형 공변량이 포함될 때에만 문제가 된다.
연습문제 2. 어떤 공분산분석 모형이 집단 지시변수와 함께 연속형 공변량(사전 점수)을 포함한다. 잔차 대 적합값 그림에 뚜렷한 U자 패턴이 보인다. 무엇을 뜻하는지 설명하고 두 가지 처방을 제시하라.
풀이
잔차 대 적합값 그림의 U자 패턴은 공변량과 반응 사이의 관계가 비선형임을 나타낸다. 선형모형이 양 극단에서는 체계적으로 과소예측하고 가운데에서는 과대예측하고 있다.
두 가지 처방:
-
다항 항 추가. 공변량의 \(X^2\)(필요하면 \(X^3\))을 모형에 넣어 선형회귀 틀 안에서 곡률을 담는다.
-
비선형 변환 적용. 공변량을 변환하여(예: \(\log(X)\)나 \(\sqrt{X}\)) 반응과의 관계가 근사적으로 선형이 되게 한다.
연습문제 3. \(Y\) 대 \(X\)의 산점도로 비선형성을 탐지하는 것과 잔차 그림으로 탐지하는 것의 차이를 설명하라. 한 방법은 성공하고 다른 방법은 실패하는 상황은 어떤 경우인가?
풀이
\(Y\) 대 \(X\)의 산점도는 두 변수의 원래 관계를 보여주는데, 다중회귀나 분산분석 상황에서는 다른 설명변수의 영향 때문에 이 관계가 가려질 수 있다. 잔차 그림은 다른 모든 설명변수의 효과를 제거한 뒤의 관계를 보여주므로 문제되는 변수의 기여만 떼어낸다.
다중회귀나 공분산분석에서는 (다른 설명변수를 조정한 뒤의) 부분 관계가 비선형인데도 하나의 공변량에 대한 \(Y\)의 산점도가 선형으로 보일 수 있다. 잔차 그림은 이를 탐지한다. 반대로 단순회귀에서는 두 방법이 대체로 동등하다.
연습문제 4. 비선형을 무시하면 집단 효과 검정이 얼마나 망가지는지 재라. 공변량 분포가 집단마다 다를 때 특히 위험한 이유를 보여라.
풀이
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
import statsmodels.api as sm
from statsmodels.formula.api import ols
rng = np.random.default_rng(7001)
B = 4_000
print("y = (x-1.5)² + ε, 집단 효과는 실제로 0 (명목 0.05)")
print(f"{'공변량 분포':>26s} {'선형 적합 오류율':>14s} {'이차항 포함':>12s}")
for lab, same in [("모든 집단 x ~ U(0,3) (동일)", True),
("집단마다 x 의 중심이 다름", False)]:
a = b = 0
for _ in range(B):
rows = []
for g in range(3):
x = rng.uniform(0, 3, 30) if same else rng.uniform(g * 0.6,
g * 0.6 + 2, 30)
y = (x - 1.5)**2 + rng.normal(0, 1.0, 30)
rows.append(pd.DataFrame({"y": y, "x": x, "g": f"G{g}"}))
df = pd.concat(rows, ignore_index=True)
t1 = sm.stats.anova_lm(ols("y ~ x + C(g)", data=df).fit(), typ=2)
t2 = sm.stats.anova_lm(ols("y ~ x + I(x**2) + C(g)", data=df).fit(),
typ=2)
a += t1.loc["C(g)", "PR(>F)"] < 0.05
b += t2.loc["C(g)", "PR(>F)"] < 0.05
print(f"{lab:>26s} {a / B:14.4f} {b / B:12.4f}")
y = (x-1.5)² + ε, 집단 효과는 실제로 0 (명목 0.05)
공변량 분포 선형 적합 오류율 이차항 포함
모든 집단 x ~ U(0,3) (동일) 0.0530 0.0508
집단마다 x 의 중심이 다름 0.1650 0.0498
공변량 분포가 같으면 비선형이 해롭지 않다(0.053). 분포가 다르면 0.165로 세 배가 된다.
편향이 어디서 생기는지 보자.
rng = np.random.default_rng(7005)
B = 3_000
print("집단별 추정 효과의 평균 (참값 모두 0), x 중심이 집단마다 다름")
print(f"{'적합':>18s} {'G1-G0':>9s} {'G2-G0':>9s}")
for lab, form in [("선형 (y ~ x + g)", "y ~ x + C(g)"),
("이차항 포함", "y ~ x + I(x**2) + C(g)")]:
a, b = [], []
for _ in range(B):
rows = []
for g in range(3):
x = rng.uniform(g * 0.6, g * 0.6 + 2, 30)
y = (x - 1.5)**2 + rng.normal(0, 1.0, 30)
rows.append(pd.DataFrame({"y": y, "x": x, "g": f"G{g}"}))
df = pd.concat(rows, ignore_index=True)
f = ols(form, data=df).fit()
a.append(f.params["C(g)[T.G1]"])
b.append(f.params["C(g)[T.G2]"])
print(f"{lab:>18s} {np.mean(a):9.4f} {np.mean(b):9.4f}")
집단별 추정 효과의 평균 (참값 모두 0), x 중심이 집단마다 다름
적합 G1-G0 G2-G0
선형 (y ~ x + g) -0.3599 0.0039
이차항 포함 0.0059 0.0068
\(G_1-G_0\)의 추정값이 \(-0.36\)으로 치우쳐 있다. 참값은 0이다. 이차항을 넣으면 \(+0.006\)으로 교정된다.
왜 \(G_1\)만 치우치는가. 세 집단의 \(x\) 범위가 \([0,2]\), \([0.6,2.6]\), \([1.2,3.2]\)이고 참 관계가 \((x-1.5)^2\)이므로, 각 집단의 평균 반응이
로 \(G_1\)이 가장 낮다. 직선은 이 \(\cup\)자 패턴을 따라갈 수 없어, 부족분이 집단 지시변수로 흡수된다.
이것이 교란(confounding)의 한 형태다.
| 조건 | 결과 |
|---|---|
| 공변량 분포가 집단마다 같다 | 비선형이 집단에 고르게 흡수 → 무해 |
| 공변량 분포가 집단마다 다르다 | 비선형이 집단 효과로 오인됨 |
관찰연구에서 특히 위험하다. 무작위 배정이 있으면 공변량 분포가 집단마다 같으므로 비선형이 크게 해롭지 않다. 무작위화가 없으면 공변량 분포가 다르고, 그때 선형성 오설정이 가짜 처치 효과를 만든다.
경험칙. 공변량 분포가 집단마다 겹치는 정도를 먼저 확인한다. 크게 다르면 선형성을 훨씬 신중히 다뤄야 한다.
연습문제 5. 연습문제 3이 말한 부분잔차 그림(성분+잔차 그림)을 구현하고, 곡률을 실제로 복원하는지 확인하라.
풀이
정의. 공변량 \(x\)에 대한 성분+잔차는
이다. 잔차에 \(x\)의 선형 성분을 되돌려 놓은 것이므로, 이를 \(x\)에 대해 그리면 \(x\)와 \(y\)의 참 관계 모양이 드러난다.
일반 잔차 그림과의 차이. 잔차 그림은 \(\hat e\)를 그리므로 선형 성분이 이미 제거되어 있다. 곡률이 작으면 잡음에 묻힌다. 성분+잔차는 신호를 되살려 곡률을 키워 보여 준다.
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
from statsmodels.formula.api import ols
rng = np.random.default_rng(7009)
rows = []
for g in range(3):
x = rng.uniform(0, 3, 40)
y = 2.0 + 1.5 * (x - 1.5)**2 + 0.8 * g + rng.normal(0, 0.8, 40)
rows.append(pd.DataFrame({"y": y, "x": x, "g": f"G{g}"}))
df = pd.concat(rows, ignore_index=True)
fit = ols("y ~ x + C(g)", data=df).fit()
b = fit.params["x"]
df["cr"] = fit.resid + b * df["x"] # 성분 + 잔차
print(f"선형 적합의 x 계수 = {b:.4f}")
print("\nx 를 6구간으로 나눠 성분+잔차의 평균을 본다")
df["bin"] = pd.cut(df.x, 6)
t = df.groupby("bin", observed=True).agg(
x평균=("x", "mean"), 성분잔차평균=("cr", "mean"),
잔차평균=("y", lambda s: fit.resid[s.index].mean()), n=("x", "size"))
print(t.round(4).to_string())
q = np.polyfit(df.x, df.cr, 2)
print(f"\n성분+잔차에 2차 곡선을 맞추면: "
f"{q[0]:.4f}·x² + {q[1]:.4f}·x + {q[2]:.4f}")
print(" (참 곡률 계수는 1.5)")
f2 = ols("y ~ x + I(x**2) + C(g)", data=df).fit()
print(f"\n이차항 모형의 x² 계수 = {f2.params['I(x ** 2)']:.4f} "
f"(p = {f2.pvalues['I(x ** 2)']:.6f})")
print(f"집단 효과 추정: 선형 {fit.params['C(g)[T.G2]']:.4f} / "
f"이차항 포함 {f2.params['C(g)[T.G2]']:.4f} (참값 1.6)")
선형 적합의 x 계수 = 0.2489
x 를 6구간으로 나눠 성분+잔차의 평균을 본다
x평균 성분잔차평균 잔차평균 n
bin
(0.104, 0.588] 0.3747 1.0736 0.9804 21
(0.588, 1.069] 0.8376 -0.0565 -0.2650 16
(1.069, 1.55] 1.3165 -0.2544 -0.5821 22
(1.55, 2.031] 1.7797 -0.4981 -0.9410 22
(2.031, 2.513] 2.2216 0.0673 -0.4857 15
(2.513, 2.994] 2.7283 1.6975 1.0185 24
성분+잔차에 2차 곡선을 맞추면: 1.3570·x² + -3.9902·x + 2.3871
(참 곡률 계수는 1.5)
이차항 모형의 x² 계수 = 1.3602 (p = 0.000000)
집단 효과 추정: 선형 1.6839 / 이차항 포함 1.5862 (참값 1.6)
성분+잔차가 곡률 1.357을 복원한다. 참값 1.5, 이차항 모형의 추정값 1.360과 잘 맞는다.
구간별 평균이 \(\cup\)자를 그린다.
x 평균 0.37 0.84 1.32 1.78 2.22 2.73
성분잔차 1.07 -0.06 -0.25 -0.50 0.07 1.70
↓ ↓ ↑
높음 ────── 낮아짐 ──── 최저 ──── 다시 높아짐
잔차 평균도 같은 \(\cup\)자를 보인다(0.98, \(-0.27\), \(-0.58\), \(-0.94\), \(-0.49\), 1.02). 이 예에서는 곡률이 커서 일반 잔차 그림으로도 보인다.
성분+잔차의 이점은 기울기를 함께 보여 주는 것이다. 이 예에서 선형 계수가 0.249로 작은데, 성분+잔차 그림은 "기울기는 거의 0이고 곡률이 지배한다"를 한눈에 보여 준다.
집단 효과 추정도 개선된다(1.684 → 1.586, 참값 1.6). 이 예는 공변량 분포가 집단마다 같아서 편향이 작지만(연습문제 4), 정밀도는 분명히 좋아진다.
실무 절차 넷.
statsmodels의sm.graphics.plot_ccpr(fit, "x")로 바로 그릴 수 있다.- 평활선(lowess)을 겹쳐 그리면 곡률이 뚜렷해진다.
- 공변량이 여러 개면 각각 그린다.
- 곡률이 보이면 다항항이나 스플라인으로 대응한다(연습문제 7).
연습문제 6. 선형성을 검정하는 두 방법(RESET 검정, 이차항의 \(t\) 검정)을 구현하고 탐지력을 비교하라.
풀이
RESET 검정(Ramsey, 1969)의 발상. 적합값 \(\hat y\)의 거듭제곱을 모형에 추가해 유의하면 함수형이 잘못된 것이다.
어떤 형태의 비선형인지 몰라도 된다는 것이 장점이다.
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
from statsmodels.formula.api import ols
def reset_p(fit, df, power=3):
"""램지 RESET 검정: 적합값의 거듭제곱을 넣어 본다."""
d2 = df.copy()
for p in range(2, power + 1):
d2[f"yh{p}"] = fit.fittedvalues**p
big = ols("y ~ x + C(g) + "
+ " + ".join(f"yh{p}" for p in range(2, power + 1)),
data=d2).fit()
idx = list(big.params.index)
R = np.zeros((power - 1, len(idx)))
for r, p in enumerate(range(2, power + 1)):
R[r, idx.index(f"yh{p}")] = 1
return float(big.f_test(R).pvalue)
rng = np.random.default_rng(7003)
B = 3_000
print("k=3, 집단당 n=30, 오차 SD 1.0, 명목 0.05")
print(f"{'참 관계':>18s} {'RESET 탐지율':>12s} {'이차항 t 검정':>13s}")
for lab, f in [("선형 (H0 참)", lambda x: 2 * x),
("약한 곡률", lambda x: 2 * x + 0.3 * (x - 1.5)**2),
("뚜렷한 곡률", lambda x: 2 * x + 1.0 * (x - 1.5)**2),
("로그형", lambda x: 4 * np.log(x + 0.5))]:
a = b = 0
for _ in range(B):
rows = []
for g in range(3):
x = rng.uniform(0, 3, 30)
y = f(x) + rng.normal(0, 1.0, 30)
rows.append(pd.DataFrame({"y": y, "x": x, "g": f"G{g}"}))
df = pd.concat(rows, ignore_index=True)
fit = ols("y ~ x + C(g)", data=df).fit()
a += reset_p(fit, df) < 0.05
f2 = ols("y ~ x + I(x**2) + C(g)", data=df).fit()
b += f2.pvalues["I(x ** 2)"] < 0.05
print(f"{lab:>18s} {a / B:12.4f} {b / B:13.4f}")
k=3, 집단당 n=30, 오차 SD 1.0, 명목 0.05
참 관계 RESET 탐지율 이차항 t 검정
선형 (H0 참) 0.0410 0.0450
약한 곡률 0.3310 0.4590
뚜렷한 곡률 0.9997 1.0000
로그형 0.9710 0.9833
둘 다 크기를 지킨다(0.041, 0.045).
| 참 관계 | RESET | 이차항 \(t\) |
|---|---|---|
| 선형(\(H_0\)) | 0.041 | 0.045 |
| 약한 곡률 | 0.331 | 0.459 |
| 뚜렷한 곡률 | 1.000 | 1.000 |
| 로그형 | 0.971 | 0.983 |
이차항 \(t\) 검정이 모든 경우에 약간 더 강력하다. 자유도 1로 집중하기 때문이다. RESET은 자유도 2를 쓰므로 손해를 본다.
그런데 이차항 검정은 곡률이 이차 형태일 때만 유리하다. 로그형에서도 잘 작동하는 것은 로그가 구간 \([0,3]\)에서 이차로 잘 근사되기 때문이다. 주기적이거나 계단형인 비선형은 둘 다 놓친다.
약한 곡률에서 0.33~0.46뿐이라는 것도 중요하다. 오차 SD 1.0에 곡률 계수 0.3이면 곡률의 진폭이 0.68로 잡음의 3분의 2 수준인데, 셋 중 둘은 놓친다.
검정과 그림의 역할.
| 검정 | 그림 | |
|---|---|---|
| 있음/없음 판정 | 가능 | 주관적 |
| 모양 파악 | 불가 | 가능 |
| 작은 표본 | 검정력 없음 | 역시 어렵다 |
| 큰 표본 | 사소한 곡률도 유의 | 크기를 볼 수 있다 |
정규성 검정과 같은 딜레마다. \(n\)이 크면 실질적으로 무의미한 곡률도 유의해진다. 곡률의 크기(예: 예측값이 얼마나 달라지는가)를 함께 봐야 한다.
권고. 부분잔차 그림을 먼저 보고, 곡률이 보이면 모형에 넣는다. 검정은 그림의 인상을 확인하는 보조 수단이다.
연습문제 7. 연습문제 2가 제시한 처방들을 실제로 비교하라. 이차항·스플라인·층화 중 무엇이 나은가?
풀이
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
import statsmodels.api as sm
from statsmodels.formula.api import ols
forms = [("선형", "y ~ x + C(g)"),
("이차항", "y ~ x + I(x**2) + C(g)"),
("자연 스플라인(df=4)", "y ~ cr(x, df=4) + C(g)"),
("x 를 4분위로 층화", "y ~ C(xq) + C(g)")]
rng = np.random.default_rng(7011)
B = 2_000
print("참 관계는 로그형 y = 4·log(x), 집단 효과 = (0, 0.4, 0.8), 집단당 n=40")
print(f"{'방법':>22s} {'검정력':>8s} {'잔차 SD':>9s}")
res = {k: [0, []] for k, _ in forms}
for _ in range(B):
rows = []
for g in range(3):
x = rng.uniform(0.2, 5, 40)
y = 4 * np.log(x) + 0.4 * g + rng.normal(0, 1.0, 40)
rows.append(pd.DataFrame({"y": y, "x": x, "g": f"G{g}"}))
df = pd.concat(rows, ignore_index=True)
df["xq"] = pd.qcut(df.x, 4, labels=False).astype(str)
for k, form in forms:
f = ols(form, data=df).fit()
t = sm.stats.anova_lm(f, typ=2)
res[k][0] += t.loc["C(g)", "PR(>F)"] < 0.05
res[k][1].append(np.sqrt(f.mse_resid))
for k, (c, s) in res.items():
print(f"{k:>22s} {c / B:8.4f} {np.mean(s):9.4f}")
참 관계는 로그형 y = 4·log(x), 집단 효과 = (0, 0.4, 0.8), 집단당 n=40
방법 검정력 잔차 SD
선형 0.5915 1.4243
이차항 0.8215 1.0975
자연 스플라인(df=4) 0.8530 1.0451
x 를 4분위로 층화 0.5250 1.5074
스플라인이 가장 낫다(검정력 0.853, 잔차 SD 1.045). 참 오차 SD가 1.0이므로 거의 완벽하게 곡선을 잡아냈다.
| 방법 | 검정력 | 잔차 SD | 참값 대비 |
|---|---|---|---|
| 선형 | 0.592 | 1.424 | \(+42\%\) |
| 이차항 | 0.822 | 1.098 | \(+10\%\) |
| 자연 스플라인 | 0.853 | 1.045 | \(+\mathbf{5\%}\) |
| 4분위 층화 | 0.525 | 1.507 | \(+51\%\) |
층화가 선형보다 못하다. 의외지만 이유가 분명하다.
- 자유도를 3개 쓰면서(4구간 → 3개 모수) 구간 안의 변동을 전혀 설명하지 못한다.
- 로그 곡선은 구간 안에서도 계속 변하는데 계단으로는 따라갈 수 없다.
연속형 공변량을 범주화하는 것은 대체로 나쁜 생각이다. 정보를 버리고 자유도만 쓴다. (구간을 20개로 늘리면 나아지지만 자유도를 19개 쓴다.)
이차항과 스플라인의 차이는 작다(0.822 대 0.853). 구간 \([0.2,5]\)에서 로그가 이차로 꽤 잘 근사되기 때문이다. 더 복잡한 곡선일수록 스플라인의 이점이 커진다.
선택 지침.
| 상황 | 권장 |
|---|---|
| 곡률이 단순(단조·볼록) | 이차항(해석이 쉽다) |
| 모양을 모름 | 자연 스플라인(df 3~5) |
| 이론이 함수형을 지정 | 그 함수형(로그, 지수 등) |
| 반응 자체를 변환하는 것이 자연스러움 | \(\log y\) 등 |
| 연속형을 범주로 | 피한다 |
자연 스플라인의 df는 어떻게 정하나.
| df | 의미 |
|---|---|
| 2 | 거의 직선 |
| 3~4 | 부드러운 곡선(기본 권장) |
| 5 이상 | 복잡한 모양, 과적합 위험 |
df를 자료로 고르면(예: AIC 최소화) 후속 추론이 왜곡되므로, 사전에 정하거나 교차검증을 별도 자료로 하는 것이 원칙이다.
주의 — 여기서는 오류율이 아니라 검정력의 문제다. 공변량 분포가 집단마다 같아 편향이 없었다. 분포가 다르면 오류율까지 무너진다(연습문제 4).
연습문제 8. 공분산분석에는 선형성과 별개의 가정이 하나 더 있다 — 기울기 동질성(평행성). 이것이 깨지면 무슨 일이 생기는지 보여라.
풀이
모형. 공분산분석은
로 \(\beta\)가 집단에 무관하다고 가정한다. 집단마다 기울기가 다르면 \(\beta_i\)가 되고, 그러면 "집단 효과"가 \(x\)에 따라 달라진다.
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
import statsmodels.api as sm
from statsmodels.formula.api import ols
rng = np.random.default_rng(7008)
B = 3_000
print("참 모형: y = (1 + g·Δ)·x + ε, 절편은 세 집단 모두 0 (집단 효과 없음)")
print(f"{'Δ':>5s} {'공통기울기 C(g) 오류율':>20s} {'교호작용 탐지율':>14s} "
f"{'G2-G0 @x=0.5':>13s} {'@x=1.5':>9s} {'@x=2.5':>9s}")
for D in [0.0, 0.5, 1.0]:
a = c = 0
e = [[], [], []]
for _ in range(B):
rows = []
for g in range(3):
x = rng.uniform(0, 3, 30)
y = (1 + g * D) * x + rng.normal(0, 1.5, 30)
rows.append(pd.DataFrame({"y": y, "x": x, "g": f"G{g}"}))
df = pd.concat(rows, ignore_index=True)
t1 = sm.stats.anova_lm(ols("y ~ x + C(g)", data=df).fit(), typ=2)
a += t1.loc["C(g)", "PR(>F)"] < 0.05
f2 = ols("y ~ x * C(g)", data=df).fit()
c += sm.stats.anova_lm(f2, typ=2).loc["x:C(g)", "PR(>F)"] < 0.05
for i, xv in enumerate([0.5, 1.5, 2.5]):
p0 = f2.predict(pd.DataFrame({"x": [xv], "g": ["G0"]}))[0]
p2 = f2.predict(pd.DataFrame({"x": [xv], "g": ["G2"]}))[0]
e[i].append(p2 - p0)
print(f"{D:5.1f} {a / B:20.4f} {c / B:14.4f} "
f"{np.mean(e[0]):13.4f} {np.mean(e[1]):9.4f} {np.mean(e[2]):9.4f}")
참 모형: y = (1 + g·Δ)·x + ε, 절편은 세 집단 모두 0 (집단 효과 없음)
Δ 공통기울기 C(g) 오류율 교호작용 탐지율 G2-G0 @x=0.5 @x=1.5 @x=2.5
0.0 0.0520 0.0490 -0.0069 -0.0063 -0.0058
0.5 0.9173 0.4633 0.4997 1.4986 2.4975
1.0 1.0000 0.9677 1.0047 3.0053 5.0058
\(\Delta=0.5\)에서 공통기울기 모형이 92% 기각한다. 집단 효과가 실제로 0인데도 그렇다.
그런데 교호작용 검정은 46%만 잡는다. 가짜 주효과를 만들어 내는 확률이 그 원인을 탐지하는 확률의 두 배다.
| \(\Delta\) | 가짜 주효과 | 교호작용 탐지 |
|---|---|---|
| 0.0 | 0.052 | 0.049 |
| 0.5 | 0.917 | 0.463 |
| 1.0 | 1.000 | 0.968 |
"집단 효과"가 \(x\)에 따라 달라진다. \(\Delta=0.5\)에서
| \(x\) | \(G_2-G_0\) |
|---|---|
| 0.5 | \(+0.50\) |
| 1.5 | \(+1.50\) |
| 2.5 | \(+2.50\) |
"평균적인 집단 효과"라는 것은 존재하지 않는다. 공분산분석이 보고하는 하나의 숫자는 \(\bar x\)에서의 값일 뿐이다.
이것은 이원배치의 교호작용과 정확히 같은 문제다. 한 요인(집단)의 효과가 다른 변수(\(x\))에 의존한다.
| 이원배치 | 공분산분석 | |
|---|---|---|
| 두 번째 변수 | 범주형 요인 | 연속형 공변량 |
| 이름 | 교호작용 | 기울기 동질성 위반 |
| 결과 | 주효과 해석 불가 | 조정평균 해석 불가 |
선형성과 어떻게 다른가.
| 가정 | 내용 | 위반의 진단 |
|---|---|---|
| 선형성 | \(y\)와 \(x\)의 관계가 직선 | 부분잔차 그림, RESET |
| 기울기 동질성 | 그 직선의 기울기가 집단마다 같다 | \(x\times g\) 교호작용 검정 |
둘 다 깨질 수 있고, 진단도 처방도 다르다.
처방 넷.
- 교호작용을 모형에 포함하고 특정 \(x\)에서의 효과를 보고한다.
- 존슨-네이먼 절차로 "집단 차이가 유의한 \(x\) 구간"을 구한다.
- \(x\)의 여러 값에서 단순효과를 보고한다(위 표처럼).
- 하나의 조정평균 차이를 보고하지 않는다.
실무 절차. 공분산분석을 할 때는 언제나 먼저 y ~ x * C(g)를 적합해 교호작용을 확인한다. 유의하지 않으면 공통기울기 모형으로 간다. 다만 교호작용 검정의 검정력이 낮다(0.46)는 점을 감안해, 기울기 추정값을 눈으로도 비교한다.
연습문제 9. 공변량의 범위가 집단마다 겹치지 않으면 무슨 일이 생기는가? 조정평균 차이의 추정과 그 불확실성을 재라.
풀이
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
from statsmodels.formula.api import ols
rng = np.random.default_rng(7007)
B = 3_000
print("참 관계는 이차 y=(x-1.5)², 집단 효과 0, 선형 공분산분석으로 적합")
print(f"{'겹침':>10s} {'조정평균 차이 추정':>16s} {'표준오차':>10s} {'오류율':>8s}")
for lab, (r0, r1) in [("완전 겹침", ((0, 3), (0, 3))),
("절반 겹침", ((0, 2), (1, 3))),
("접점만", ((0, 1.5), (1.5, 3))),
("완전 분리", ((0, 1.2), (1.8, 3)))]:
d, se, hit = [], [], 0
for _ in range(B):
rows = []
for g, (lo, hi) in enumerate([r0, r1]):
x = rng.uniform(lo, hi, 40)
y = (x - 1.5)**2 + rng.normal(0, 1.0, 40)
rows.append(pd.DataFrame({"y": y, "x": x, "g": f"G{g}"}))
df = pd.concat(rows, ignore_index=True)
f = ols("y ~ x + C(g)", data=df).fit()
d.append(f.params["C(g)[T.G1]"])
se.append(f.bse["C(g)[T.G1]"])
hit += f.pvalues["C(g)[T.G1]"] < 0.05
print(f"{lab:>10s} {np.mean(d):16.4f} {np.mean(se):10.4f} {hit / B:8.4f}")
참 관계는 이차 y=(x-1.5)², 집단 효과 0, 선형 공분산분석으로 적합
겹침 조정평균 차이 추정 표준오차 오류율
완전 겹침 -0.0112 0.2691 0.0527
절반 겹침 -0.0066 0.3551 0.0253
접점만 0.0110 0.5434 0.0347
완전 분리 -0.0265 0.7445 0.0373
표준오차가 0.269에서 0.745로 2.8배 커진다. 표본 크기는 같은데도 그렇다.
| 겹침 | 표준오차 | 오류율 |
|---|---|---|
| 완전 | 0.269 | 0.053 |
| 절반 | 0.355 | 0.025 |
| 접점만 | 0.543 | 0.035 |
| 완전 분리 | 0.745 | 0.037 |
겹침이 없으면 집단 효과와 공변량 효과를 분리할 수 없다.** 극단적으로 \(G_0\)은 \(x<1.2\)에만, \(G_1\)은 \(x>1.8\)에만 있으면
가 되어 완전한 교락이다. 표준오차의 폭증이 이를 반영한다.
오류율은 오히려 보수적이다(0.025~0.037). 표준오차가 커진 만큼 기각을 덜 한다. 모형은 정직하게 "모르겠다"고 말한다.
그러나 이것은 이 모의실험이 선형 관계를 가정했기 때문이다. 실제로는
| 문제 | 내용 |
|---|---|
| 외삽 | 관측되지 않은 \(x\) 영역에서의 관계를 가정으로 메운다 |
| 모형 의존 | 선형이냐 이차냐에 따라 답이 크게 달라진다 |
| 진단 불가 | 겹치지 않는 영역에서 곡률을 확인할 방법이 없다 |
두 번째가 치명적이다. 겹침이 없으면 자료가 함수형을 결정하지 못한다. 연습문제 4에서 본 대로, 잘못된 함수형은 가짜 집단 효과를 만든다.
진단 방법 셋.
- 공변량의 집단별 분포를 그린다(상자그림, 히스토그램).
- 공통 지지(common support) 영역을 계산한다.
- 성향점수의 겹침을 확인한다(관찰연구에서 표준 진단).
처방 넷.
| 처방 | 내용 |
|---|---|
| 공통 지지 영역으로 제한 | 겹치는 \(x\) 범위의 관측만 분석 |
| 매칭 | 비슷한 \(x\)를 가진 짝을 만든다 |
| 범위를 명시하고 보고 | "\(1.2<x<1.8\)에서만 비교 가능" |
| 설계를 고친다 | 애초에 겹치도록 표집 |
가장 정직한 답은 세 번째다. "이 자료로는 \(x\)가 겹치는 영역에서만 집단을 비교할 수 있다"고 밝히는 것이 외삽으로 만든 숫자를 제시하는 것보다 낫다.
연습문제 10. 공분산분석에서 공변량과 관련된 가정과 진단을 정리하라.
풀이
공변량이 들어오면 가정이 셋 늘어난다.
| 가정 | 내용 | 진단 |
|---|---|---|
| 선형성 | \(y\)와 \(x\)의 관계가 직선 | 부분잔차 그림, RESET |
| 기울기 동질성 | 기울기가 집단마다 같다 | \(x\times g\) 교호작용 검정 |
| 공통 지지 | 공변량 범위가 겹친다 | 집단별 \(x\)의 분포 |
여기에 공변량이 처치의 영향을 받지 않아야 한다는 조건이 더해진다. 처치 후에 측정한 변수를 공변량으로 넣으면 처치 효과의 일부를 제거한다.
핵심 수치 넷.
| 사실 | 값 |
|---|---|
| 공변량 분포가 다를 때 선형 오설정의 오류율 | 0.165 |
| 같은 상황의 \(G_1\) 효과 편향 | \(-0.36\) |
| 기울기가 다를 때 가짜 주효과 | 0.917 |
| 같은 상황 교호작용 탐지율 | 0.463 |
마지막 두 줄의 대비가 이 절의 핵심이다. 문제를 만드는 확률이 문제를 발견하는 확률의 두 배다.
진단 순서.
1. 집단별 x 의 분포를 그린다 ──→ 겹치는가?
↓ (겹치지 않으면 여기서 멈추고 범위를 제한)
2. y ~ x * C(g) 를 적합한다 ──→ 기울기가 같은가?
↓ (다르면 교호작용을 유지하고 특정 x 에서 보고)
3. 부분잔차 그림을 본다 ──→ 곡률이 있는가?
↓ (있으면 이차항이나 스플라인)
4. y ~ x + C(g) 로 조정평균을 보고
1번을 먼저 하는 이유. 겹치지 않으면 2·3번의 진단 자체가 불가능하다.
처방 정리.
| 위반 | 처방 |
|---|---|
| 비선형 | 자연 스플라인(df 3~4) 또는 이차항 |
| 기울기 이질 | 교호작용 유지, 존슨-네이먼, 특정 \(x\)에서 보고 |
| 겹침 부족 | 공통 지지로 제한, 매칭, 범위 명시 |
| 공변량이 처치 후 측정 | 공변량에서 제외 |
하지 말아야 할 것 넷.
| 실수 | 결과 |
|---|---|
| 공변량을 범주화 | 검정력 손실(0.53 대 0.85) |
| 기울기 동질성을 확인하지 않음 | 가짜 주효과 0.92 |
| 겹침 없이 조정평균 비교 | 외삽으로 만든 숫자 |
| 곡률을 무시 | 관찰연구에서 가짜 처치 효과 |
범주형 요인만 있으면 이 모든 것이 사라진다(연습문제 1). 공변량을 넣는 순간 회귀의 가정이 따라 들어온다는 것을 잊지 않는 것이 요점이다.
한 문장. 공분산분석은 분산분석과 회귀의 결합이며, 그래서 양쪽의 가정을 모두 져야 한다.
정리하며¶
분산분석을 일반선형모형으로 보면 선형성의 자리가 보인다.
- 모수에 대해 선형이다. 집단 효과 \(\alpha_i\) 가 더해지는 구조이며, 이것이 13장 회귀의 특수한 경우다. 범주형 설명변수를 더미로 바꾼 회귀가 곧 분산분석이다.
- 일원배치에서는 선형성이 거의 자동으로 성립한다. 집단마다 자유로운 평균을 두므로 위반할 여지가 적다.
- 공변량이 들어오면 달라진다. 공분산분석에서는 공변량과 반응의 관계가 실제로 선형인지 확인해야 하며, 그렇지 않으면 항을 추가하거나 변환한다.
- 이원배치에서는 가법성이 관련된다. 교호작용 항을 넣지 않은 모형은 두 요인의 효과가 더해진다고 가정하는 것이며, 그 가정이 곧 검정 대상이다.
- 잔차 그림이 진단 도구다. 적합값 대 잔차에 곡선 패턴이 보이면 모형이 구조를 놓치고 있다는 신호다.
다음 절부터 등분산 검정들을 구체적으로 다룬다.