부록 A: 직교화로 편향 제거하기

앞 장에서는 인과 모델을 평가하는 방법을 살펴봤습니다. 그것만으로도 대단한 일입니다. 인과 모델이 추정하는 탄력성(elasticity) \(\frac{\delta y}{\delta t}\)는 관측할 수 없는 값입니다. 모델이 추정하려는 대상의 참값(ground truth)을 볼 수 없으니, 모델을 평가하려면 꽤 창의적인 방법을 짜내야 했습니다.

앞 장에서 소개한 기법은 처치가 무작위로 배정된 데이터에 크게 기대고 있었습니다. 단일 변수 선형회귀 y ~ t의 계수로 탄력성 \(\frac{\delta y}{\delta t}\)를 추정한다는 아이디어였습니다. 하지만 이 방법은 처치가 무작위로 배정되었을 때만 통합니다. 그렇지 않으면 누락 변수 편향(omitted variable bias) 때문에 곤란해집니다.

이 문제를 피하려면 데이터를 마치 처치가 무작위로 배정된 것처럼 보이게 만들어야 합니다. 이를 위한 주요 기법은 크게 두 가지라고 생각합니다. 하나는 성향점수(propensity score)를 쓰는 방법이고, 다른 하나는 직교화(orthogonalization)를 쓰는 방법입니다. 이번 장에서는 직교화를 다룹니다.

본론에 들어가기 전에 마지막으로 한 가지 당부하겠습니다. 무작위가 아닌 데이터에서 벗어나는 가장 안전한 길은 아마도 직접 실험을 해서 무작위 데이터를 모으는 것입니다. 저는 편향 제거(debiasing) 기법을 그다지 믿지 않는 편입니다. 모든 교란 요인(confounder)을 고려했는지 결코 확신할 수 없기 때문입니다. 그렇더라도 직교화는 충분히 배울 가치가 있습니다. 앞으로 나올 수많은 인과 모델의 토대가 되는, 놀랄 만큼 강력한 기법이기 때문입니다.

선형회귀의 부활

직교화의 아이디어는 1933년 세 명의 계량경제학자 Ragnar Frisch, Frederick V. Waugh, Michael C. Lovell이 고안한 정리에서 출발합니다. 간단히 말하면, 어떤 다중 선형회귀 모델이든 세 단계, 즉 세 개의 모델로 쪼갤 수 있다는 정리입니다. 특성이 행렬 \(X\)에 들어 있다고 해 봅시다. 이 행렬을 나누어 특성 일부를 담은 \(X_1\)과 나머지 특성을 담은 \(X_2\)를 만듭니다.

1단계에서는 첫 번째 특성 집합으로 다음 선형회귀 모델을 추정합니다.

\[ y_i = \theta_0 + \pmb{\theta_1 X}_{1i} + e_i \]

여기서 \(\pmb{\theta_1}\)은 매개변수 벡터입니다. 그다음 이 모델의 잔차(residual)를 구합니다.

\[ y^* = y_i - (\hat{\theta}_0 + \pmb{\hat{\theta}_1 X}_{1i}) \]

2단계에서는 다시 첫 번째 특성 집합을 쓰되, 이번에는 두 번째 특성 집합을 추정하는 모델을 돌립니다.

\[ \pmb{X}_{2i} = \gamma_0 + \pmb{\gamma_1 X}_{1i} + e_i \]

첫 번째 특성 집합으로 두 번째 특성 집합을 예측하는 것입니다. 이 2단계에서도 마찬가지로 잔차를 구합니다.

\[ \pmb{X}^*_{2i} = \pmb{X}_{2i} - (\hat{\gamma}_0 + \pmb{\hat{\gamma}_1 X}_{1i}) \]

마지막으로 1단계와 2단계의 잔차를 가지고 다음 모델을 추정합니다.

\[ y_i^* = \beta_0 + \pmb{\beta_2 X}^*_{2i} + e_i \]

프리슈-워-로벨 정리(Frisch-Waugh-Lovell theorem)에 따르면, 이 모델에서 얻은 매개변수 추정값 \(\pmb{\hat{\beta}_2}\)는 모든 특성을 넣은 전체 회귀에서 얻는 추정값과 같습니다.

\[ y_i = \beta_0 + \pmb{\beta_1 X}_{1i} + \pmb{\beta_2 X}_{2i} + e_i \]

자, 조금 더 풀어 보겠습니다. 회귀분석은 아주 특별한 모델입니다. 각 매개변수는 편미분으로 해석할 수 있습니다. 즉 다른 모든 특성을 고정한 채로 특성 하나를 늘리면 \(Y\)가 얼마나 늘어나는지를 뜻합니다. 인과추론에는 정말 반가운 성질입니다. 데이터를 수집할 때 고정해 두지 않은 변수라도 분석 단계에서 통제할 수 있다는 뜻이기 때문입니다.

회귀에서 변수를 빠뜨리면 편향이 생긴다는 것도 알고 있습니다. 구체적으로는 누락 변수 편향(또는 교란편향)입니다. 그런데 프리슈-워-로벨 정리에 따르면 회귀 모델을 두 부분으로 쪼갤 수 있습니다. 어느 쪽에도 전체 특성 집합이 들어 있지 않은데도, 전체 회귀를 돌렸을 때와 똑같은 추정값이 나옵니다. 그뿐이 아닙니다. 이 정리는 선형회귀가 실제로 무슨 일을 하는지도 보여 줍니다. 회귀는 변수 \(X_k\)의 계수를 구하기 위해 먼저 나머지 모든 변수로 \(X_k\)를 예측하고 잔차를 구합니다. 이렇게 하면 \(X_k\)에서 그 변수들의 영향이 ‘씻겨’ 나갑니다. 그래서 \(X_k\)가 \(Y\)에 미치는 영향을 살펴볼 때 누락 변수 편향이 없습니다. 다음으로 회귀는 나머지 모든 변수로 \(Y\)를 예측하고 잔차를 구합니다. 이렇게 하면 \(Y\)에서 그 변수들의 영향이 ‘씻겨’ 나가고 \(Y\)의 분산이 줄어들어, \(X_k\)가 \(Y\)에 어떤 영향을 주는지 더 쉽게 볼 수 있습니다.

이게 얼마나 멋진 일인지 와닿지 않을 수도 있습니다. 하지만 선형회귀가 무엇을 하는지 떠올려 보세요. \(X_1\)을 고려하면서 \(X_2\)가 \(y\)에 미치는 영향을 추정합니다. 인과추론에서는 엄청나게 강력한 성질입니다. 이 정리대로라면 이렇게 해도 됩니다. 특성 \(X\)로 처치 \(t\)를 예측하는 모델과, 같은 특성으로 결과 \(y\)를 예측하는 모델을 만듭니다. 두 모델의 잔차를 구한 뒤, \(t\)의 잔차가 \(y\)의 잔차에 어떤 영향을 주는지 추정하는 모델을 돌립니다. 이 마지막 모델은 \(X\)를 통제한 상태에서 \(t\)가 \(y\)에 미치는 영향을 알려 줍니다. 앞의 두 모델이 교란 요인을 통제해 주는 셈입니다. 두 모델은 무작위나 다름없는 데이터를 만들어 냅니다. 이것이 바로 데이터의 편향을 제거하는 과정이고, 이렇게 얻은 데이터로 마지막 모델에서 탄력성을 추정합니다.

왜 그런지 보여 주는 수학적 증명도 있지만(그렇게 복잡하지는 않습니다), 이 정리는 직관이 워낙 명쾌하니 바로 직관으로 넘어가겠습니다.

직교화의 직관

코드
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split

import statsmodels.formula.api as smf
import statsmodels.api as sm

from nb21 import cumulative_elast_curve_ci, elast, cumulative_gain_ci

이번에도 가격 데이터를 쓰겠습니다. 다만 이번에는 가격이 무작위로 배정되지 않은 표본만 가져옵니다. 이전과 마찬가지로 데이터를 훈련 셋과 테스트 셋으로 나눕니다. 인과 모델을 평가할 때는 테스트 셋을 쓸 테니, 직교화로 테스트 셋의 편향을 어떻게 제거하는지 알아보겠습니다.

prices = pd.read_csv("./data/ice_cream_sales.csv")

train, test = train_test_split(prices, test_size=0.5)
train.shape, test.shape
((5000, 5), (5000, 5))

테스트 셋에서 상관계수를 확인해 보면 가격과 판매량이 양의 상관관계를 보입니다. 가격을 올리면 판매량이 늘어나야 한다는 뜻입니다. 말이 안 되는 결과입니다. 아이스크림이 비싸다고 더 사 먹는 사람은 없습니다. 여기에는 아마 어떤 편향이 숨어 있을 것입니다.

test.corr()
temp weekday cost price sales
temp 1.000000 0.003630 0.006605 -0.011977 0.379108
weekday 0.003630 1.000000 0.011889 0.002610 0.004589
cost 0.006605 0.011889 1.000000 0.388046 -0.009410
price -0.011977 0.002610 0.388046 1.000000 0.080040
sales 0.379108 0.004589 -0.009410 0.080040 1.000000

데이터를 그려 보면 왜 이런 일이 생기는지 알 수 있습니다. 주말(토요일과 일요일)에는 가격도 높지만 판매량도 높습니다. 주말에 해당하는 점들이 그래프의 오른쪽 위에 몰려 있는 것을 보면 알 수 있습니다.

여기서는 주말이 편향에 큰 역할을 하고 있을 가능성이 큽니다. 주말에는 수요가 많아서 아이스크림이 더 많이 팔립니다. 그리고 그 수요에 맞춰 가격이 오릅니다. 그러니 가격이 올라서 판매량이 늘어나는 것이 아닙니다. 그저 주말에는 판매량과 가격이 둘 다 높을 뿐입니다.

코드
np.random.seed(123)
sns.scatterplot(data=test.sample(1000), x="price", y="sales", hue="weekday");

이 데이터셋의 편향을 제거하려면 모델이 두 개 필요합니다. 첫 번째 모델은 교란 요인으로 처치(여기서는 가격)를 예측하는 모델로, \(M_t(X)\)라고 부르겠습니다. 앞에서 본 프리슈-워-로벨 정리의 단계 가운데 하나입니다.

m_t = smf.ols("price ~ cost + C(weekday) + temp", data=test).fit()
debiased_test = test.assign(**{"price-Mt(X)":test["price"] - m_t.predict(test)})

이 모델을 만들고 나면 다음과 같이 잔차를 구합니다.

\[ \hat{t}_i = t_i - M_t(X_i) \]

이 잔차는 편향이 없는 처치, 더 정확히 말하면 교란 요인 \(X\)로는 예측할 수 없는 처치라고 생각할 수 있습니다. 교란 요인을 이미 \(t\)를 예측하는 데 썼으니, 잔차는 정의상 \(X\)로 예측할 수 없습니다. 달리 말하면 모델 \(M_t(X_i)\)가 편향을 설명해서 걷어 냈고, 그 결과로 무작위 배정이나 다름없는 \(\hat{t}_i\)가 남은 것입니다. 물론 이는 \(T\)와 \(Y\)에 모두 영향을 주는 교란 요인이 \(X\)에 빠짐없이 들어 있을 때만 성립합니다.

이 데이터도 그려서 어떤 모습인지 확인해 보겠습니다.

코드
np.random.seed(123)
sns.scatterplot(data=debiased_test.sample(1000), x="price-Mt(X)", y="sales", hue="weekday")
plt.vlines(0, debiased_test["sales"].min(), debiased_test["sales"].max(), linestyles='--', color="black");

주말이 더 이상 오른쪽 위에 몰려 있지 않습니다. 가운데로 밀려났습니다. 게다가 요일로는 가격 수준(처치)을 더 이상 구분할 수 없습니다. x축에 그린 잔차 \(price-M_t(X)\)는 원래 처치를 ’무작위’로 만든, 즉 편향을 제거한 버전이라고 할 수 있습니다.

데이터셋의 편향을 제거하는 데는 이것만으로 충분합니다. 새로 만든 처치는 무작위 배정이나 다름없습니다. 하지만 편향을 제거한 데이터셋을 더 낫게 만들 방법이 하나 더 있습니다. 바로 결과의 잔차도 구하는 것입니다.

\[ \hat{y}_i = y_i - M_y(X_i) \]

이것이 프리슈-워-로벨 정리의 또 다른 단계입니다. 이 단계가 데이터의 편향을 더 줄이지는 않지만, \(y\)의 분산을 줄여서 탄력성을 추정하기 쉽게 만들어 줍니다. 이번에도 \(\hat{y}_i\)는 \(X\)로 예측할 수 없는 \(y_i\), 또는 \(X\)로 생긴 분산을 모두 설명해서 걷어 낸 버전이라고 생각할 수 있습니다. 잘 생각해 보세요. \(M_y(X_i)\)로 \(X\)를 써서 이미 \(y\)를 예측했습니다. 그리고 \(\hat{y}_i\)는 그 예측의 오차입니다. 그러니 정의상 \(X\)로는 이 오차를 예측할 수 없습니다. \(y\)를 예측하는 데 쓸 수 있는 \(X\)의 정보는 이미 다 썼습니다. 그렇다면 \(\hat{y}_i\)를 설명할 수 있는 것은 이를 만들 때 쓰지 않은 것(즉 \(X\)에 들어 있지 않은 것)뿐이고, 그런 것은 처치밖에 없습니다(다시 말하지만, 측정되지 않은 교란 요인이 없다는 가정 아래에서입니다).

m_y = smf.ols("sales ~ cost + C(weekday) + temp", data=test).fit()

debiased_test = test.assign(**{"price-Mt(X)":test["price"] - m_t.predict(test),
                               "sales-My(X)":test["sales"] - m_y.predict(test)})

두 가지 변환을 모두 하고 나면 요일로는 가격 잔차를 예측할 수 없을 뿐 아니라 판매량 잔차 \(\hat{y}\)도 예측할 수 없습니다. 이 잔차들을 예측할 수 있는 것은 이제 처치뿐입니다. 흥미로운 점도 하나 있습니다. 앞의 그래프에서는 가격 탄력성의 방향을 알기 어려웠습니다. 가격이 오를수록 판매량이 줄어드는 것처럼 보이긴 했지만, 판매량의 분산이 너무 커서 확신하기 어려웠습니다.

이제 두 잔차를 함께 그려 보면, 가격이 오르면 판매량이 실제로 줄어든다는 사실이 훨씬 분명하게 드러납니다.

힌트옮긴이

원문은 “판매량이 가격을 떨어뜨린다(sales causes prices to go down)”라고 썼지만, 그래프와 앞뒤 문맥상 가격이 오르면 판매량이 줄어든다는 뜻이라 그렇게 옮겼습니다.

코드
np.random.seed(123)
sns.scatterplot(data=debiased_test.sample(1000), x="price-Mt(X)", y="sales-My(X)", hue="weekday")
plt.vlines(0, debiased_test["sales-My(X)"].min(), debiased_test["sales-My(X)"].max(), linestyles='--', color="black");

이렇게 편향을 제거한 데이터에도 작은 단점이 하나 있습니다. 잔차는 원래와 다른 척도로 옮겨져 있어서 그 값이 무엇을 뜻하는지 해석하기 어렵습니다(가격 잔차가 -3이라는 건 무슨 뜻일까요?). 그래도 원래 무작위가 아니던 데이터에서 무작위 데이터를 만들어 내는 편리함에 비하면 작은 대가라고 생각합니다.

정리하면, 처치를 예측해서 편향이 없는 처치 역할을 하는 \(\hat{t}\)를 만들었고, 결과를 예측해서 처치를 써야만 더 설명할 수 있는 결과 \(\hat{y}\)를 만들었습니다. \(y\)를 \(\hat{y}\)로, \(t\)를 \(\hat{t}\)로 바꾼 이 데이터가 바로 원하던 편향 제거 데이터입니다. 앞에서 무작위 데이터로 했던 것과 똑같이 이 데이터로 인과 모델을 평가할 수 있습니다.

확인을 위해 이번에도 훈련 데이터로 가격 탄력성을 추정하는 인과 모델을 만들겠습니다.

m3 = smf.ols(f"sales ~ price*cost + price*C(weekday) + price*temp", data=train).fit()

그다음 편향을 제거한 테스트 셋에서 탄력성을 예측합니다.

def predict_elast(model, price_df, h=0.01):
    return (model.predict(price_df.assign(price=price_df["price"]+h))
            - model.predict(price_df)) / h

debiased_test_pred = debiased_test.assign(**{
    "m3_pred": predict_elast(m3, debiased_test),
})

debiased_test_pred.head()
temp weekday cost price sales price-Mt(X) sales-My(X) m3_pred
7791 20.8 3 1.5 6.3 187 -0.201769 1.441373 -0.073317
1764 26.6 3 1.5 6.3 201 -0.179506 4.737748 -2.139611
5785 24.0 4 1.0 5.8 186 -0.215107 -5.855171 -0.549798
3542 20.9 3 1.5 5.1 180 -1.401386 -5.743172 -0.108943
9250 26.7 5 1.0 7.0 201 0.978382 4.384885 -1.427230

누적 탄력성을 그릴 때 예측 탄력성을 기준으로 데이터셋을 정렬하는 것은 전과 같습니다. 다만 이번에는 편향을 제거한 처치와 결과로 탄력성을 구합니다. 이는 다음 회귀 모델에서 \(\beta_1\)을 추정하는 것과 같습니다.

\[ \hat{y}_i = \beta_0 + \beta_1 \hat{t}_i + e_i \]

여기서 잔차는 앞에서 설명한 그대로입니다.

코드
plt.figure(figsize=(10,6))

cumm_elast = cumulative_elast_curve_ci(debiased_test_pred, "m3_pred", "sales-My(X)", "price-Mt(X)", min_periods=50, steps=200)
x = np.array(range(len(cumm_elast)))
plt.plot(x/x.max(), cumm_elast, color="C0")

plt.hlines(elast(debiased_test_pred, "sales-My(X)", "price-Mt(X)"), 0, 1, linestyles="--", color="black", label="Avg. Elast.")
plt.xlabel("% of Top Elast. Customers")
plt.ylabel("Elasticity of Top %")
plt.title("Cumulative Elasticity")
plt.legend();

물론 누적 이득 곡선도 똑같이 그릴 수 있습니다.

코드
plt.figure(figsize=(10,6))

cumm_gain = cumulative_gain_ci(debiased_test_pred, "m3_pred", "sales-My(X)", "price-Mt(X)", min_periods=50, steps=200)
x = np.array(range(len(cumm_gain)))
plt.plot(x/x.max(), cumm_gain, color="C1")

plt.plot([0, 1], [0, elast(debiased_test_pred, "sales-My(X)", "price-Mt(X)")], linestyle="--", label="Random Model", color="black")

plt.xlabel("% of Top Elast. Customers")
plt.ylabel("Cumulative Gain")
plt.title("Cumulative Gain on Debiased Sample")
plt.legend();

앞 장의 그래프와 얼마나 비슷한지 보세요. 편향 제거가 제 몫을 톡톡히 해냈다는 신호입니다.

반대로 편향된 원래 데이터를 썼다면 누적 이득 그래프가 어떻게 나오는지 보겠습니다.

코드
plt.figure(figsize=(10,6))

cumm_gain = cumulative_gain_ci(debiased_test_pred, "m3_pred", "sales", "price", min_periods=50, steps=200)
x = np.array(range(len(cumm_gain)))
plt.plot(x/x.max(), cumm_gain, color="C1")

plt.plot([0, 1], [0, elast(debiased_test_pred, "sales", "price")], linestyle="--", label="Random Model", color="black")

plt.xlabel("% of Top Elast. Customers")
plt.title("Cumulative Gains on Biased Sample")
plt.ylabel("Cumulative Gains")
plt.legend();

가장 먼저 눈에 띄는 것은 평균 탄력성이 내려가지 않고 올라간다는 점입니다. 이미 본 현상입니다. 편향된 데이터에서는 가격이 오를수록 판매량이 늘어나는 것처럼 보입니다. 그래서 누적 이득 그래프의 끝점이 양수입니다. 아이스크림 가격을 올린다고 사람들이 더 사 먹지 않는다는 것을 알고 있으니 말이 안 되는 결과입니다. 평균 가격 탄력성부터 엉망이라면 그 안에서 매긴 순서도 의미가 없습니다. 결론적으로 이 데이터는 모델 평가에 써서는 안 됩니다.

머신러닝을 이용한 직교화

2016년 논문에서 Victor Chernozhukov 등은 머신러닝 모델로도 직교화를 할 수 있다는 것을 보였습니다. 아주 최근의 연구라서, 머신러닝 모델로 무엇을 할 수 있고 무엇을 할 수 없는지는 아직 밝혀야 할 것이 많습니다. 그래도 알아 둘 만한 매우 흥미로운 아이디어입니다.

기본 원리는 앞에서 다룬 내용과 거의 같습니다. 유일한 차이는 이제 편향 제거에 머신러닝 모델을 쓴다는 점입니다.

\[ \begin{align} \hat{y}_i &= y_i - M_y(X_i) \\ \hat{t}_i &= t_i - M_t(X_i) \end{align} \]

그런데 함정이 하나 있습니다. 잘 알다시피 머신러닝 모델은 무척 강력해서 데이터에 완벽하게 들어맞을 수 있습니다. 아니, 과적합(overfitting)될 수 있다고 해야겠습니다. 위 식만 봐도 그럴 때 무슨 일이 생길지 알 수 있습니다. \(M_y\)가 과적합되면 잔차가 모두 0에 아주 가까워집니다. 그러면 \(t\)가 잔차에 어떤 영향을 주는지 찾기 어려워집니다. 마찬가지로 \(M_t\)가 과적합되면 그 잔차도 0에 가까워집니다. 그러면 처치 잔차에 변동이 없어서 처치가 결과에 어떤 영향을 줄 수 있는지 알 수 없습니다.

이 문제를 해결하려면 표본 분할(sample splitting)을 해야 합니다. 데이터셋의 한 부분으로 모델을 추정하고, 다른 부분에서 예측하는 것입니다. 가장 간단한 방법은 테스트 표본을 반으로 나누고, 모델 두 개를 만들어 각각 한쪽 절반에서 추정하고 나머지 절반에서 예측하게 하는 것입니다.

조금 더 세련된 방법은 K-폴드 교차 검증(K-fold cross validation)을 쓰는 것입니다. 이렇게 하면 모든 모델을 테스트 셋의 절반보다 큰 표본으로 훈련할 수 있다는 장점이 있습니다.

다행히 이런 교차 예측(cross prediction)은 Sklearn의 cross_val_predict 함수로 아주 쉽게 구현할 수 있습니다.

from sklearn.model_selection import cross_val_predict
from sklearn.ensemble import RandomForestRegressor

X = ["cost", "weekday", "temp"]
t = "price"
y = "sales"

folds = 5

np.random.seed(123)
m_t = RandomForestRegressor(n_estimators=100)
t_res = test[t] - cross_val_predict(m_t, test[X], test[t], cv=folds)

m_y = RandomForestRegressor(n_estimators=100)
y_res = test[y] - cross_val_predict(m_y, test[X], test[y], cv=folds)

잔차를 구했으니 새 데이터셋의 열로 저장하겠습니다.

ml_debiased_test = test.assign(**{
    "sales-ML_y(X)": y_res,
    "price-ML_t(X)": t_res,
})
ml_debiased_test.head()
temp weekday cost price sales sales-ML_y(X) price-ML_t(X)
7791 20.8 3 1.5 6.3 187 -3.150833 -0.869267
1764 26.6 3 1.5 6.3 201 -0.418857 -0.192867
5785 24.0 4 1.0 5.8 186 -2.515667 0.790429
3542 20.9 3 1.5 5.1 180 -11.718500 -1.280460
9250 26.7 5 1.0 7.0 201 -1.214167 1.715117

마지막으로 편향을 제거한 데이터셋을 그려 보겠습니다.

코드
np.random.seed(123)
sns.scatterplot(data=ml_debiased_test.sample(1000),
                x="price-ML_t(X)", y="sales-ML_y(X)", hue="weekday");

이번에도 가격이 판매량에 미치는 음의 탄력성을 찾아냈습니다. 사실 이 그래프는 단순 선형회귀를 썼을 때 얻은 그래프와 놀라울 만큼 비슷합니다. 하지만 이는 아마 데이터셋이 아주 단순하기 때문일 것입니다. 머신러닝 직교화의 장점은 더 복잡한 함수를 추정할 수 있다는 데 있습니다. 선형회귀로는 표현하기 어려운 상호작용과 비선형성을 학습할 수 있습니다. 또 일부 머신러닝 모델(결정 트리 기반 모델)은 선형회귀보다 훨씬 간편하게 돌릴 수 있다는 장점도 있습니다. 범주형 데이터, 이상치, 심지어 결측값까지 처리할 수 있는데, 선형회귀만 쓴다면 이런 것들은 따로 신경을 써야 합니다.

마무리하기 전에, 데이터 과학자들이 이 아이디어를 처음 접할 때 흔히 저지르는 실수 하나만 짚고 넘어가겠습니다(저도 해 봤습니다). 처치나 결과가 이진 변수라면 머신러닝 회귀 모델을 분류 모델로 바꾸는 편이 낫다고 생각할 수 있습니다. 하지만 그렇게 하면 안 됩니다. 직교화 이론은 회귀 모델에서만 성립합니다. 오래전에 도구변수(instrumental variable)를 다룰 때 본 것과 비슷합니다. 솔직히 말하면 회귀를 분류로 바꾼다고 모델이 처참하게 실패하지는 않겠지만, 저라면 권하지 않겠습니다. 이론이 뒷받침하지 않는데 굳이 위험을 감수할 이유가 있을까요?

핵심 요약

이번 장은 인과 모델 평가 방법이 제대로 작동하려면 처치가 무작위로 배정되어야 한다는 점을 강조하면서 시작했습니다. 무작위 데이터가 없으면 이것이 문제가 됩니다. 분명히 해 두자면, 이럴 때 가장 안전한 해법은 실험을 해서 무작위 데이터를 얻는 것입니다. 그럴 수 없을 때에야 비로소 기발한 대안에 기댈 수 있습니다. 바로 처치가 무작위로 배정된 것처럼 보이도록 데이터를 변환하는 것입니다.

이번 장에서는 직교화의 원리로 이를 해내는 방법을 다뤘습니다. 먼저 특성 \(X\)로 처치 \(t\)를 예측하는 모델을 만들고 그 잔차를 구했습니다. 처치 잔차는 정의상 자신을 만드는 데 쓴 특성과 독립이라는 것이 핵심입니다. 다시 말해 처치 잔차는 특성과 직교합니다. 이 잔차는 \(X\)로 생긴 교란편향을 모두 걷어 낸 처치라고 볼 수 있습니다.

데이터를 무작위나 다름없게 만드는 데는 이것만으로 충분합니다. 하지만 한 걸음 더 나아갈 수 있습니다. 처치는 빼고 특성 \(X\)만으로 결과 \(y\)를 예측하는 모델을 만들고, 마찬가지로 그 잔차를 구하는 것입니다. 이번에도 직관은 아주 비슷합니다. 결과 잔차는 특성으로 생긴 분산을 모두 설명해서 걷어 낸 결과입니다. 이 과정에서 분산이 많이 설명되면 처치효과를 보기가 더 쉬워집니다.

이번 장에서는 모델 평가를 위해 데이터의 편향을 제거하려고 직교화를 썼습니다. 하지만 이 기법은 다른 목적으로도 쓰입니다. 많은 인과추론 모델이 인과 모델의 부담을 덜기 위해 첫 번째 전처리 단계로 직교화를 사용합니다. 직교화는 현대 인과추론 알고리즘 상당수의 토대라고 할 수 있습니다.

참고 자료

여기에 쓴 내용은 대부분 제 머릿속에서 나왔습니다. 경험으로 익힌 것들입니다. 그러니 좋은 과학이 보통 거치는 학계의 엄격한 검증을 통과하지 않았습니다. 실무에서 잘 통하는 방법을 이야기하면서도 왜 그런지 설명하는 데는 시간을 많이 들이지 않았다는 점을 눈치챘을 겁니다. 말하자면 길거리 과학인 셈입니다. 그래도 이 글을 공개해 누구나 검토할 수 있게 했으니, 터무니없는 내용을 발견하면 언제든 이슈를 열어 주세요. 최선을 다해 답하겠습니다.

이 장은 Victor Chernozhukov 등의 2016년 논문 Double/Debiased Machine Learning for Treatment and Causal Parameters를 바탕으로 했습니다. Frisch, Ragnar와 Waugh, Frederick V.의 1933년 원 논문 Partial Time Regressions as Compared with Individual Trends도 참고할 수 있습니다.

기여하기

Causal Inference for the Brave and True는 과학의 통계학이라 할 수 있는 인과추론을 다루는 오픈소스 자료입니다. 파이썬 기반의 무료 소프트웨어만 사용하며, 돈이 없어도, 배경지식이 많지 않아도 누구나 볼 수 있는 자료를 목표로 합니다. 이 책이 도움이 되었고 후원하고 싶다면 Patreon을 방문해 주세요. 금전적으로 기여하기 어렵다면 오타를 고치거나, 수정할 부분을 제안하거나, 이해하기 어려웠던 부분에 의견을 주는 것으로도 도울 수 있습니다. 책의 저장소에서 이슈를 열어 주세요. 마지막으로 이 내용이 마음에 들었다면 도움이 될 만한 사람들과 공유하고 GitHub에서 스타를 눌러 주세요.

힌트옮긴이

한국어판의 오역이나 오탈자는 한국어판 이슈 페이지에 알려 주세요. 한국어판이 도움이 되었다면 한국어판 저장소에도 스타를 부탁드립니다.

의견 나누기

번역이 어색한 곳, 더 쉬운 설명, 궁금한 점을 남겨 주세요. GitHub 계정으로 로그인하면 댓글을 달 수 있고, 남긴 의견은 GitHub Discussions에 장별로 모입니다.