import numpy as npimport pandas as pdfrom toolz import curryimport seaborn as snsfrom matplotlib import pyplot as pltimport statsmodels.formula.api as smfimport cvxpy as cpimport toolz as ffrom sklearn.linear_model import Lassoimport warningswarnings.filterwarnings('ignore')from matplotlib import stylestyle.use("ggplot")
통제집단합성법 복습
통제집단합성법(synthetic control, SC)은 실험군이 하나뿐이고 대조군도 아주 적을 때 특히 유용한 인과추론 기법입니다. 대신 각 대상을 시간에 걸쳐 반복해서 관측한 데이터가 있어야 합니다(빅데이터 환경에 맞춘 확장 기법도 많이 있습니다). 대표적인 사용 사례는 한 지역(예를 들어 한 주)에 가한 처치의 효과를 알고 싶을 때 처치를 받지 않은 다른 주들을 대조군으로 쓰는 경우입니다. 통제집단합성법 장에서는 발의안 99호(1988년 캘리포니아에서 통과된, 담배세를 올린 법안)가 담배 판매량에 미친 효과를 추정하는 문제로 이 기법을 소개했습니다.
이 효과를 추정하려면 캘리포니아가 발의안 99호를 통과시키지 않았다면 어떻게 됐을지 추정해야 합니다. 결국 반사실(counterfactual) \(Y_{t}(0)\)를 추정해서 개입 이후 기간에 관측된 결과와 비교하는 문제로 귀결됩니다.
\[
ATT = Y_{t}(1) - Y_{t}(0) = Y_{t} - Y_{t}(0) \text{ for } t \geq 1988
\]
이를 위한 방법은 여러 가지인데, 그중 하나가 통제집단합성법입니다. 통제집단합성법은 여러 대조군 대상을 조합해 실험군 대상의 처치 전 움직임을 흉내 내도록 만들어서 실험군의 \(Y(0)\)를 모델링합니다. 이 예에서는 발의안 99호 이전 캘리포니아의 담배 판매 추세를 함께 근사하는 주들의 조합을 찾는다는 뜻입니다. 이렇게 하는 이유는 실험군과 같은 패턴을 따르는 대조군 대상이 거의 없기 때문입니다. 여러 주의 담배 판매 추세를 그려 보면 바로 알 수 있습니다. 캘리포니아와 추세가 비슷한 주가 하나도 없습니다.
data = pd.read_csv("data/smoking.csv")data = data.pivot("year", "state", "cigsale")data = data.rename(columns={c: f"state_{c}"for c in data.columns}).rename(columns={"state_3": "california"})data.shape
통제집단합성법 장에서는 대조군 대상을 실험군인 것처럼 가정하고 대상을 바꿔 가며 효과를 구하는 추론 절차를 보여 드렸습니다. 처치를 받지 않은 대상의 효과를 확인한다는 점에서 위약 검정(placebo test)이라고도 부릅니다. 실험군 대상의 효과 추정값이 대부분의 위약 효과보다 크면 이 효과 추정값이 유의하다고 말합니다.
이 예에서는 캘리포니아의 처치 후 차이가 다른 주들에 비해 꽤 극단적입니다. 하지만 처치 전 적합이 형편없는 주도 몇 개 있고, 이런 주는 개입 이후 기간에 오차가 아주 커집니다. 처치 전 오차가 큰 대상은 빼라는 것이 일반적인 지침이지만, 얼마나 커야 큰 것인지는 조금 복잡한 문제입니다. 게다가 이 절차는 개입이 무작위로 배정됐다고 가정하는데, 이런 종류의 정책 개입에서는 믿기 어려운 가정입니다(Abadie, 2021 참고).
다른 추론 방법으로, 효과 추정 문제를 반사실 예측 문제로 바꿔서 볼 수 있습니다. 생각해 보면 지금 하려는 일은 결국 반사실 \(Y_{i, t}(0)\)를 예측하는 것입니다. 여기서 \(i\)는 실험군 대상이고 \(t \geq T1\), 즉 개입 이후 기간입니다. 이렇게 보면 추론을 위한 컨포멀 예측(conformal prediction) 문헌을 활용할 수 있습니다. 흥미롭게도 이 방법은 꽤 일반적이어서 \(Y_{i, t}(0)\)에 대한 다른 모델에도 적용할 수 있지만, 여기서는 통제집단합성법에만 집중하겠습니다.
이 절차를 이해하기 위해, 먼저 가설검정을 하고 p 값을 구하는 방법부터 살펴봅시다.
가설검정과 p 값
처치 이후 기간의 효과 궤적 \(\theta = (\theta_{T0+1}, ..., \theta_{T})\)에 대한 가설을 검정하고 싶다고 합시다.
\[
H_0 : \theta = \theta^0
\]
예를 들어 효과가 전혀 없는지 검정하고 싶다면 \(\theta^0 = (0, ..., 0)\)으로 두면 됩니다. 이 가설이 처치가 없을 때의 반사실적 결과를 완전히 결정한다는 점에 주목하세요.
\[
Y_t(0) = Y_t(1) - \theta_t = Y_t - \theta_t
\]
핵심 아이디어는 검정하려는 귀무가설을 따르는 데이터를 만들고, 이렇게 만든 데이터에서 \(Y(0)\) 모델의 잔차를 확인하는 것입니다. 잔차가 너무 극단적이면, 이 데이터가 가정한 귀무가설에서 나왔을 가능성이 낮다고 말합니다. 처음에는 절차 전체가 모호하게 들리더라도 걱정하지 마세요. 단계별로 구현해 보면 분명해질 것입니다.
첫 단계는 귀무가설 아래의 데이터를 만드는 것입니다. 위 식처럼 실험군 대상의 결과에서 가정한 귀무가설 값을 빼기만 하면 됩니다. 코드는 다음과 같습니다.
plt.plot(with_effect(data, "california", 0, 1988, 2000-1988+1)["california"], label="H0: 0")plt.plot(with_effect(data, "california", -4, 1988, 2000-1988+1)["california"], label="H0: -4")plt.ylabel("Y0 Under the Null")plt.legend();
효과가 없다는 귀무가설을 가정하면, 그 귀무가설 아래의 데이터는 \(Y(0) = Y(1) = Y\)입니다. 즉 실험군인 캘리포니아에서 관측한 결과의 궤적 그대로입니다. 이번에는 귀무가설을 -4, 즉 발의안 99호가 담배 판매량을 4갑 줄인다고 가정하면 \(Y(0) = Y(1) - (-4)\)가 되어 처치 후 결과의 궤적이 +4만큼 위로 이동합니다. 아주 직관적입니다. 법안이 담배 판매량을 줄인다고 생각한다면, 법안이 없었을 때는 관측 데이터보다 담배 판매량이 더 높아야 하니까요.
추론 절차의 다음 단계는 방금 만든 함수로 얻은 반사실 \(Y(0)\)에 대한 모델을 처치 전과 처치 후 기간을 아우르는 전체 데이터로 적합하는 것입니다. 평소 가상의 대조군을 적합할 때와 크게 다른 점입니다. 개입 이후 잔차가 지나치게 커지지 않도록, 가정한 귀무가설 아래에서 전체 데이터로 모델을 추정해야 한다는 것이 이 방법의 아이디어입니다. 그다음 이 모델로 모든 기간 \(t\)에 대해 잔차 \(\hat{u_t} = Y_t - \hat{Y}_t(0)\)를 계산합니다.
이 일을 하는 함수는 먼저 앞에서 만든 with_effect 함수로 귀무가설 아래의 데이터를 만듭니다. 그다음 이 데이터로 모델을 적합합니다. 이어서 방금 적합한 모델로 예측해서 \(Y(0)\)를 추정합니다. 마지막으로 잔차 \(\hat{u}_t\)를 계산하고 모든 결과를 데이터프레임에 담습니다.
이 데이터에서 \(H_0 : 0\), 즉 발의안 99호가 효과가 없었다는 귀무가설의 잔차를 구하려면 함수의 귀무가설 인자에 0을 넘기기만 하면 됩니다.
model = SyntheticControl()residuals_df = residuals(data,"california", null=0.0, intervention_start=1988, window=2000-1988+1, model=model)residuals_df.head()
y0
y0_est
residuals
post_intervention
year
1970
123.000000
112.529475
10.470525
False
1971
121.000000
114.315723
6.684277
False
1972
123.500000
119.302289
4.197711
False
1973
124.400002
121.265554
3.134447
False
1974
126.699997
124.356696
2.343301
False
결과는 기간마다 추정한 잔차를 담은 데이터프레임이며, 앞으로 계속 이 결과를 사용합니다. 개입 이후 기간의 잔차가 너무 큰지 확인하는 것이 목적이라는 점을 기억하세요. 잔차가 너무 크다면 이 데이터가 효과가 0이라는 귀무가설에서 나왔을 가능성이 낮습니다. 무슨 이야기인지 눈으로 확인하기 위해 개입 이후 기간의 모델 오차를 살펴봅시다.
코드
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 4))residuals_df[["y0", "y0_est"]].plot(ax=ax1)ax1.set_title("Y0 under H0: 0");residuals_df[["residuals"]].plot(ax=ax2);ax2.set_title("Residuals under H0: 0");
\(H_0: 0\) 아래에서 적합한 모델의 잔차가 음수 쪽으로 꽤 크다는 것이 벌써 보입니다. 효과가 없다는 귀무가설을 기각해야 할 수도 있다는 증거입니다.
검정통계량
이런 시각적 증거는 직관적으로 이해하는 데는 도움이 되지만, 조금 더 엄밀하게 따질 필요가 있습니다. 그래서 검정통계량(test statistic) S를 정의합니다. 검정통계량은 잔차가 얼마나 큰지, 따라서 귀무가설 아래에서 관측한 데이터가 나올 가능성이 얼마나 낮은지를 요약합니다.
검정통계량이 크면 개입 이후 적합이 나쁘다는 뜻이고, 따라서 귀무가설을 기각합니다. 하지만 \(H_0\)가 참이더라도 모델이 잘 적합되지 않았다면 개입 이후 기간의 검정통계량이 꽤 클 수 있습니다. 그러니 ’크다’를 절대적인 기준으로 정할 수는 없습니다. 대신 개입 이후의 잔차와 검정통계량이 개입 이전의 잔차에 비해 얼마나 큰지를 따져야 합니다.
p 값
p 값을 계산하려면 잔차를 블록 순열(block permutation)하고, 순열마다 검정통계량을 계산합니다. 다음 그림을 보면 이 절차를 더 쉽게 이해할 수 있습니다.
이제 \(H_0: 0\)의 p 값을 계산할 수 있습니다. 보다시피 p 값이 낮긴 하지만 아주 낮지는 않습니다. \(\alpha=0.1\)에서는 효과가 없다는 이 귀무가설을 기각하지 않습니다.
p_value(residuals_df)
0.16129032258064516
이 p 값은 모든 기간의 효과가 0이라는 귀무가설 \(\theta = (\theta_{T0+1}=0, ..., \theta_{T}=0)\)에 대한 것이라는 점을 기억하세요. 통제집단합성법으로 그린 효과 그래프를 보면 발의안 99호의 효과가 고정된 값이 아닌 것 같습니다. 처음에는 -5 정도로 작게 시작하지만 점점 커져서 -25까지 갑니다. 그래서 효과 궤적 전체에 대한 귀무가설 하나만 검정하기보다는, 처치 이후 기간마다 효과의 신뢰구간을 따로 그려 보는 편이 흥미로울 수 있습니다.
신뢰구간
처치 이후 각 기간의 효과에 신뢰구간을 두는 방법을 이해하기 위해, 먼저 한 기간의 신뢰구간을 어떻게 정의할지 생각해 봅시다. 기간이 하나뿐이라면 \(H_0\)는 궤적 벡터 \(\theta\)가 아니라 스칼라 값 하나로 정의됩니다. 따라서 \(H_0s\)를 촘촘하게 늘어놓고 귀무가설마다 p 값을 계산할 수 있습니다. 예를 들어 발의안 99호가 통과된 1988년의 효과가 -20에서 20 사이 어딘가라고 생각한다고 합시다. 그러면 -20부터 20까지 여러 \(H_0\)와 각각의 p 값을 담은 표를 만들 수 있습니다.
앞에서 정의한 함수로 이 표를 만들려면, 먼저 관심 있는 기간(이 예에서는 1988년)을 개입 이전 기간 끝에 이어 붙여 증강 데이터셋(augmented dataset)이라는 것을 만듭니다. 그다음 촘촘하게 늘어놓은 귀무가설들을 차례로 돌면서, 관심 기간에서 시작하는 길이 1짜리 개입 이후 구간의 p 값을 계산합니다.
신뢰구간을 만들려면 p 값이 낮게 나온 \(H_0\)들을 걸러 내기만 하면 됩니다. p 값이 낮다는 것은 지금 가진 데이터가 그 귀무가설에서 나왔을 가능성이 낮다는 뜻입니다. 예를 들어 유의수준 \(\alpha\)를 0.1로 정했다면 p 값이 0.1보다 낮은 \(H_0\)들을 제거합니다.
\(H_0\)별 p 값을 그래프로 그려 보면 이 신뢰구간이 어떻게 나왔는지 더 잘 이해할 수 있습니다. 아래 그림에서 점선은 앞에서 정한 \(\alpha\)인 0.1을 나타냅니다. 파란 선은 신뢰구간의 경계입니다. 이 선 바깥의 \(H_0\)는 p 값이 0.1보다 낮습니다.
코드
plt.plot(p_values_df[1988], p_values_df.index)plt.xlabel("P-Value")plt.ylabel("H0")plt.vlines(0.1, nulls.min(), nulls.max(), color="black", ls="dotted", label="0.1")plt.hlines(confidence_interval_from_p_values(p_values_df)["90_ci_upper"], 0, 1, color="C1", ls="dashed")plt.hlines(confidence_interval_from_p_values(p_values_df)["90_ci_lower"], 0, 1, color="C1", ls="dashed", label="90% CI")plt.legend()plt.title("Confidence Interval for the Effect in 1988");
이제 남은 일은 위 절차를 기간마다 반복하는 것뿐입니다. 즉, 개입 이후 각 연도를 개입 이전 기간 끝에 이어 붙여 증강 데이터셋을 만들고, 앞에서 한 것과 똑같이 신뢰구간을 계산합니다.
이 부록은 Victor Chernozhukov, Kaspar Wüthrich, Yinchu Zhu의 논문 An Exact and Robust Conformal Inference Method for Counterfactual and Synthetic Controls를 바탕으로 썼습니다. 제가 가졌던 많은 궁금증을 풀어 준 Kaspar에게 특별히 감사드립니다.
통제집단합성법에 관한 추가 자료로는 Alberto Abadie(2021)의 Using Synthetic Controls: Feasibility, Data Requirements, and Methodological Aspects를 참고하세요.
기여하기
Causal Inference for the Brave and True는 과학의 통계학이라 할 수 있는 인과추론을 다루는 오픈소스 자료입니다. 파이썬 기반의 무료 소프트웨어만 사용하며, 돈이 없어도, 배경지식이 많지 않아도 누구나 볼 수 있는 자료를 목표로 합니다. 이 책이 도움이 되었고 후원하고 싶다면 Patreon을 방문해 주세요. 금전적으로 기여하기 어렵다면 오타를 고치거나, 수정할 부분을 제안하거나, 이해하기 어려웠던 부분에 의견을 주는 것으로도 도울 수 있습니다. 책의 저장소에서 이슈를 열어 주세요. 마지막으로 이 내용이 마음에 들었다면 도움이 될 만한 사람들과 공유하고 GitHub에서 스타를 눌러 주세요.
의견 나누기
번역이 어색한 곳, 더 쉬운 설명, 궁금한 점을 남겨 주세요. GitHub 계정으로 로그인하면 댓글을 달 수 있고, 남긴 의견은 GitHub Discussions에 장별로 모입니다.