1 준실험적 설계: ITS, RDD, Stepped Wedge
이 파일은 20번 연구 설계 대분류의 준실험적 설계 섹션을 확장한 것이다. 이전 파일: 33 — 관찰 연구 설계
1.1 1. 준실험적 설계가 필요한 이유
무작위 배정이 불가능하거나 비윤리적이지만, 순수 관찰 연구보다 강한 인과 추론이 필요한 상황:
| 상황 | 의학 예시 | IT 예시 |
|---|---|---|
| 정책 변경 | 금연법 도입 전후 | 알고리즘/가격 정책 변경 |
| 자연 실험 | 자연재해 후 건강 영향 | 경쟁사 서비스 중단으로 인한 유입 |
| 윤리적 제약 | 위험 노출을 배정할 수 없음 | 의도적 서비스 저하 불가 |
| 임계값 기반 | 혈압 기준 투약 결정 | 등급 기준 혜택 부여 |
| 순차 도입 | 병원별 순차 프로토콜 적용 | 지역별 단계적 기능 롤아웃 |
준실험의 핵심: 무작위 배정 없이, 설계의 구조적 특성을 이용하여 교란 변수를 통제한다.
인과 추론 강도 스펙트럼:
RCT/A/B Test > 준실험(DiD, ITS, RDD) > 관찰 연구(코호트, 케이스-컨트롤)
★★★★★ ★★★★☆ ★★~★★★
1.2 2. Interrupted Time Series (ITS)
1.2.1 개념
처치(개입) 전후의 시계열을 비교하여, 개입이 결과의 수준(level)과 추세(trend)에 변화를 주었는지 검정한다.
결과
│ ●
│ ● ← 개입 전 추세
│ ●
│ ● 처치 시점
│ ● ─ ─ ─ ─ ─ ← 반사실 (개입 없었더라면)
│────────┼──────────
│ │ ●●●
│ │ ●● ← 개입 후 실제
│ │●
│ ↑
│ 개입 시점
└────────────────────→ 시간
두 가지 효과:
- 수준 변화 (Level change): 개입 시점에서 결과의 즉각적 변화
- 추세 변화 (Trend change): 개입 후 시간 흐름에 따른 기울기 변화
1.2.2 ITS 회귀 모델
\[Y_t = \beta_0 + \beta_1 t + \beta_2 D_t + \beta_3 (t \times D_t) + \epsilon_t\]
| 모수 | 의미 |
|---|---|
| \(\beta_0\) | 개입 전 절편 (초기 수준) |
| \(\beta_1\) | 개입 전 추세 (시간당 변화율) |
| \(\beta_2\) | 수준 변화 (개입의 즉각 효과) |
| \(\beta_3\) | 추세 변화 (개입 후 기울기 변화) |
| \(D_t\) | 개입 지시 변수 (\(t \geq t_0\)이면 1, 아니면 0) |
해석:
- \(\beta_2 > 0\): 개입 직후 결과가 즉시 증가
- \(\beta_3 > 0\): 개입 후 결과의 증가 속도가 빨라짐
- \(\beta_2 = \beta_3 = 0\): 개입 효과 없음
1.2.3 가정
- 추세의 연속성: 개입이 없었다면 기존 추세가 계속되었을 것
- 개입 시점의 명확성: 정확한 개입 시점을 알아야 함
- 동시 사건 없음: 개입과 동시에 다른 사건이 발생하지 않았을 것
- 독립적 관측 또는 자기상관 처리: 시계열 자기상관 고려
1.2.4 자기상관(Autocorrelation) 처리
시계열 데이터는 \(\epsilon_t\)가 독립이 아닐 가능성이 높다.
Newey-West 표준 오차: HAC(Heteroskedasticity and Autocorrelation Consistent) SE
\[\hat{V}(\hat{\beta}) = (X'X)^{-1} \hat{S} (X'X)^{-1}\]
여기서 \(\hat{S}\)는 Newey-West 추정량으로, 자기상관을 반영한 공분산 행렬.
1.2.5 IT 적용: 알고리즘 변경 전후 메트릭 분석
import numpy as np
import pandas as pd
import statsmodels.api as sm
# --- 시뮬레이션: 추천 알고리즘 변경 효과 ---
np.random.seed(42)
n_pre = 52 # 개입 전 52주
n_post = 26 # 개입 후 26주
T = n_pre + n_post
# 시간 변수
time = np.arange(1, T + 1)
intervention = (time > n_pre).astype(int)
time_after = np.maximum(time - n_pre, 0)
# 결과: 주간 전환율 (%)
# 개입 전: 기저 수준 5%, 주당 0.02%p 증가
# 개입 후: 수준 +0.8%p, 추세 +0.05%p/주 추가
y = (5.0
+ 0.02 * time
+ 0.8 * intervention
+ 0.05 * time_after
+ np.random.normal(0, 0.3, T))
df = pd.DataFrame({
"week": time,
"conversion_rate": y,
"intervention": intervention,
"time_after": time_after
})
# --- ITS 회귀 (OLS) ---
X = df[["week", "intervention", "time_after"]]
X = sm.add_constant(X)
model_ols = sm.OLS(df["conversion_rate"], X).fit()
print("=== OLS 결과 ===")
print(model_ols.summary().tables[1])
# --- Newey-West SE로 자기상관 보정 ---
model_nw = sm.OLS(df["conversion_rate"], X).fit(
cov_type="HAC",
cov_kwds={"maxlags": 4} # 4주 lag까지 고려
)
print("\n=== Newey-West SE 결과 ===")
print(model_nw.summary().tables[1])
# --- 시각화 ---
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10, 5))
ax.scatter(df["week"], df["conversion_rate"], alpha=0.5, s=20)
ax.axvline(x=n_pre, color="red", linestyle="--", label="Intervention")
# 적합 직선
y_pred = model_ols.predict(X)
ax.plot(df["week"], y_pred, color="blue", linewidth=2, label="ITS Fit")
# 반사실
X_cf = X.copy()
X_cf["intervention"] = 0
X_cf["time_after"] = 0
y_cf = model_ols.predict(X_cf)
ax.plot(df["week"][n_pre:], y_cf[n_pre:], color="gray",
linestyle="--", label="Counterfactual")
ax.set_xlabel("Week")
ax.set_ylabel("Conversion Rate (%)")
ax.set_title("ITS: Algorithm Change Effect on Conversion Rate")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()1.2.6 R 코드
library(nlme)
# GLS with autocorrelation
model <- gls(
conversion_rate ~ week + intervention + time_after,
data = df,
correlation = corARMA(p = 1, form = ~ week) # AR(1)
)
summary(model)
# Newey-West SE
library(sandwich)
library(lmtest)
model_ols <- lm(conversion_rate ~ week + intervention + time_after, data = df)
coeftest(model_ols, vcov = NeweyWest(model_ols, lag = 4))1.3 3. Regression Discontinuity Design (RDD)
1.3.1 개념
연속적인 배정 변수(running variable)가 특정 임계값(cutoff)을 넘는지에 따라 처치 여부가 결정될 때, 임계값 근방의 관측치는 사실상 무작위 배정된 것과 유사하다.
결과 Y
│
│ ●●●● ← 처치군 (cutoff 이상)
│ ●●●
│ ──────── ← 불연속 (처치 효과 τ)
│ ●●●
│ ●●● ← 대조군 (cutoff 미만)
│ ●●●
└──────────┼──────────→ 배정 변수 X
c₀
(cutoff)
직관: 99점인 사람과 101점인 사람은 거의 동일한 특성을 가진다. 유일한 차이는 “임계값을 넘었는가”이다.
1.3.3 RDD 추정량
Sharp RDD:
\[\tau_{RDD} = \lim_{x \downarrow c_0} E[Y_i \mid X_i = x] - \lim_{x \uparrow c_0} E[Y_i \mid X_i = x]\]
Fuzzy RDD (Wald Estimator):
\[\tau_{LATE} = \frac{\lim_{x \downarrow c_0} E[Y_i \mid X_i = x] - \lim_{x \uparrow c_0} E[Y_i \mid X_i = x]}{\lim_{x \downarrow c_0} E[D_i \mid X_i = x] - \lim_{x \uparrow c_0} E[D_i \mid X_i = x]}\]
1.3.4 가정
- 연속성 가정 (Continuity): 반사실 기대값 \(E[Y(0) \mid X=x]\)와 \(E[Y(1) \mid X=x]\)가 \(c_0\)에서 연속
- 비조작 가정 (No Manipulation): 개인이 배정 변수를 조작하여 임계값을 의도적으로 넘을 수 없음
- 국소 무작위화: 임계값 근방에서 다른 공변량이 불연속적으로 변하지 않음
1.3.5 Bandwidth 선택 문제
임계값 근방의 “얼마나 좁은 범위”를 사용할지가 핵심:
- 좁은 bandwidth: 편향 낮음, 분산 높음 (사용할 관측치 적음)
- 넓은 bandwidth: 편향 높음 (먼 관측치 포함), 분산 낮음
- 최적 bandwidth: Imbens-Kalyanaraman (IK) 또는 Calonico-Cattaneo-Titiunik (CCT) 방법
1.3.6 국소 다항 회귀 (Local Polynomial Regression)
임계값 좌우에서 각각 다항 회귀를 적합:
\[Y_i = \alpha_l + \beta_{1l}(X_i - c_0) + \beta_{2l}(X_i - c_0)^2 + \cdots + \epsilon_i \quad \text{(좌측)}\] \[Y_i = \alpha_r + \beta_{1r}(X_i - c_0) + \beta_{2r}(X_i - c_0)^2 + \cdots + \epsilon_i \quad \text{(우측)}\]
처치 효과: \(\hat{\tau} = \hat{\alpha}_r - \hat{\alpha}_l\)
1.3.7 IT 적용: 등급 기반 프리미엄 기능 효과
import numpy as np
import pandas as pd
import statsmodels.api as sm
# --- 시뮬레이션: 활동 점수 100점 이상이면 프리미엄 기능 부여 ---
np.random.seed(42)
n = 2000
cutoff = 100
# 배정 변수: 활동 점수 (연속)
score = np.random.normal(100, 20, n)
# 처치: 100점 이상이면 프리미엄
treatment = (score >= cutoff).astype(int)
# 결과: 만족도 (처치 효과 = 0.5)
# Y = f(score) + treatment_effect + noise
y = (3.0
+ 0.01 * (score - cutoff)
+ 0.5 * treatment
+ np.random.normal(0, 0.8, n))
df = pd.DataFrame({
"score": score,
"treatment": treatment,
"satisfaction": y,
"score_centered": score - cutoff
})
# --- 방법 1: 단순 Sharp RDD (선형) ---
# bandwidth 내 관측치만 사용
bw = 15
df_bw = df[abs(df["score_centered"]) <= bw].copy()
X = sm.add_constant(df_bw[["score_centered", "treatment"]])
model = sm.OLS(df_bw["satisfaction"], X).fit(cov_type="HC1")
print("=== Sharp RDD (linear, bw=15) ===")
print(f"처치 효과: {model.params['treatment']:.4f}")
print(f"SE: {model.bse['treatment']:.4f}")
print(f"p-value: {model.pvalues['treatment']:.4f}")
print(f"95% CI: ({model.conf_int().loc['treatment', 0]:.4f}, "
f"{model.conf_int().loc['treatment', 1]:.4f})")
# --- 방법 2: 좌우 별도 기울기 ---
df_bw["score_treat"] = df_bw["score_centered"] * df_bw["treatment"]
X2 = sm.add_constant(df_bw[["score_centered", "treatment", "score_treat"]])
model2 = sm.OLS(df_bw["satisfaction"], X2).fit(cov_type="HC1")
print("\n=== Sharp RDD (separate slopes) ===")
print(model2.summary().tables[1])1.3.8 rdrobust 패키지 활용
1.3.9 R 코드
library(rdrobust)
# 최적 bandwidth + 편향 보정
result <- rdrobust(y = df$satisfaction, x = df$score_centered, c = 0)
summary(result)
# 시각화
rdplot(y = df$satisfaction, x = df$score_centered, c = 0,
title = "RDD: Premium Feature Effect",
x.label = "Activity Score (centered at cutoff)",
y.label = "Satisfaction")1.3.10 조작 검정 (McCrary Test)
배정 변수가 임계값 부근에서 조작되었는지 확인:
from rddensity import rddensity
# 밀도 검정: 임계값 부근에서 밀도가 불연속이면 조작 의심
test = rddensity(X=df["score_centered"].values, c=0)
print(f"McCrary-type test p-value: {test.p:.4f}")
# p > 0.05면 조작 없음1.4 4. Stepped Wedge Design
1.4.1 개념
모든 클러스터(집단)가 순차적으로 처치를 받는 설계. 시간이 지남에 따라 대조 → 처치로 전환.
클러스터 \ 기간
T1 T2 T3 T4 T5
───────────────────────────────────
A C T T T T
B C C T T T
C C C C T T
D C C C C T
C = 대조, T = 처치
→ 각 클러스터가 서로 다른 시점에 처치로 전환
→ 전환 시점은 무작위 배정 (어떤 순서로 전환할지)
1.4.2 윤리적/실무적 이점
- 모든 클러스터가 결국 처치를 받음: 윤리적으로 처치를 보류할 필요 없음
- 순차 도입이 실무적으로 자연스러움: 서버 배포, 지역별 출시 등
- 자체 대조군: 같은 클러스터의 전환 전/후를 비교
1.4.3 모델
\[Y_{ij} = \mu + \alpha_j + \beta X_{ij} + u_i + \epsilon_{ij}\]
- \(i\): 클러스터, \(j\): 시간
- \(\alpha_j\): 시간 고정 효과 (secular trend)
- \(X_{ij}\): 처치 지시 변수 (클러스터 \(i\)가 시간 \(j\)에 처치 중이면 1)
- \(\beta\): 처치 효과
- \(u_i\): 클러스터 랜덤 효과
- \(\epsilon_{ij}\): 잔차
1.4.4 IT 적용: 단계적 기능 롤아웃
import numpy as np
import pandas as pd
import statsmodels.api as sm
from statsmodels.regression.mixed_linear_model import MixedLM
# --- 시뮬레이션: 4개 지역, 5개 월 ---
np.random.seed(42)
clusters = ["Region_A", "Region_B", "Region_C", "Region_D"]
periods = range(1, 6)
switch_times = [1, 2, 3, 4] # 각 지역의 전환 시점
data = []
for i, (cluster, switch) in enumerate(zip(clusters, switch_times)):
for t in periods:
treatment = int(t >= switch)
# 결과: 기저 + 시간 효과 + 처치 효과 + 클러스터 효과 + 노이즈
y = (50
+ 2 * t # secular trend
+ 5 * treatment # treatment effect = 5
+ np.random.normal(0, 3) # cluster effect
+ np.random.normal(0, 2)) # noise
# 각 클러스터에서 여러 관측치 (예: 100명)
for _ in range(100):
y_individual = y + np.random.normal(0, 8)
data.append({
"cluster": cluster,
"period": t,
"treatment": treatment,
"outcome": y_individual
})
df = pd.DataFrame(data)
# --- Mixed Model ---
model = MixedLM.from_formula(
"outcome ~ C(period) + treatment",
groups="cluster",
data=df
)
result = model.fit()
print("=== Stepped Wedge: Mixed Model ===")
print(f"처치 효과: {result.params['treatment']:.3f}")
print(f"SE: {result.bse['treatment']:.3f}")
print(f"p-value: {result.pvalues['treatment']:.4f}")
print(f"95% CI: ({result.conf_int().loc['treatment', 0]:.3f}, "
f"{result.conf_int().loc['treatment', 1]:.3f})")1.4.5 R 코드
library(lme4)
# Stepped Wedge: LMM
model <- lmer(outcome ~ factor(period) + treatment + (1 | cluster), data = df)
summary(model)
confint(model, parm = "treatment")
# 또는 GEE
library(geepack)
model_gee <- geeglm(
outcome ~ factor(period) + treatment,
id = cluster,
data = df,
corstr = "exchangeable"
)
summary(model_gee)1.4.6 Stepped Wedge의 주의사항
| 주의점 | 설명 |
|---|---|
| 시간 효과와 처치 효과의 혼재 | 시간 고정 효과를 반드시 포함 |
| 학습 효과 (Learning Curve) | 처치 직후 효과가 낮을 수 있음 |
| 비가역성 가정 | 한번 전환하면 대조로 돌아가지 않음 |
| 클러스터 간 이질성 | 랜덤 효과로 처리 |
| 표본 크기 계산 복잡 | 전통적 power analysis와 다름 |
1.5 5. DiD와의 관계
1.5.1 ITS와 DiD
ITS: 처치 전/후 시계열 비교 (대조군 없음)
DiD: 처치 전/후 × 처치군/대조군 (대조군 있음)
ITS ⊂ DiD?
→ ITS는 "대조군 없는 DiD"로 볼 수 있음
→ DiD에 대조군을 추가하면 시간적 교란을 더 잘 통제
| 특성 | ITS | DiD |
|---|---|---|
| 대조군 | 없음 (자체 비교) | 있음 |
| 반사실 | 기존 추세 연장 | 대조군의 변화 패턴 |
| 가정 | 추세 연속성 | 평행 추세 |
| 교란 통제 | 시간에 따른 교란에 취약 | 시불변 + 공통 시변 교란 통제 |
CITS (Controlled ITS): ITS + 대조군 = DiD와 유사
ITS의 가장 큰 약점은 “개입 시점에 다른 외부 사건이 동시에 발생했다면 효과를 구분할 수 없다”는 것이다. 예를 들어 알고리즘을 변경한 주에 마침 경쟁사가 서비스를 중단했다면, 전환율 상승이 알고리즘 때문인지 경쟁사 이탈 때문인지 분리 불가능하다. CITS는 대조군(개입을 받지 않은 집단)을 추가하여 이 문제를 해결한다.
\[Y_{it} = \beta_0 + \beta_1 t + \beta_2 G_i + \beta_3 D_t + \beta_4 (t \times D_t) + \beta_5 (G_i \times D_t) + \beta_6 (G_i \times t \times D_t) + \epsilon_{it}\]
| 모수 | 의미 |
|---|---|
| \(G_i\) | 집단 지시 변수 (처치군=1, 대조군=0) |
| \(D_t\) | 개입 후 지시 변수 (\(t \geq t_0\)이면 1) |
| \(\beta_5\) | 핵심: 처치군만의 수준 변화 (대조군 대비) |
| \(\beta_6\) | 핵심: 처치군만의 추세 변화 (대조군 대비) |
\(\beta_5\)와 \(\beta_6\)이 처치 효과의 추정량이다. 대조군에서도 동일한 외부 충격(\(\beta_3\), \(\beta_4\))이 반영되므로, 처치군 고유의 효과만 분리된다.
1.5.2 RDD의 시간적 변형
- RDD: 배정 변수의 공간적 임계값
- RD in Time (RDiT): 시간이 배정 변수 → 정책 시행 시점 근방 비교
- DiD와 RDiT의 관계: 둘 다 시간적 불연속을 이용하지만 가정이 다름
1.6 6. 비교 표: ITS vs RDD vs Stepped Wedge vs DiD
| 특성 | ITS | RDD | Stepped Wedge | DiD |
|---|---|---|---|---|
| 아이디어 | 개입 전/후 추세 비교 | 임계값 근방 비교 | 순차 전환 | 처치/대조 × 전/후 |
| 대조군 | 없음 (자체 비교) | 임계값 반대편 | 전환 전 시점 | 별도 대조군 |
| 반사실 | 기존 추세 연장 | 임계값 좌측 추세 | 전환 전 + 시간 효과 | 평행 추세 가정 |
| 핵심 가정 | 추세 연속성 | 비조작, 연속성 | 시간 효과 통제 | 평행 추세 |
| 인과 강도 | ★★★☆☆ | ★★★★☆ | ★★★★☆ | ★★★★☆ |
| 추정 효과 | 전체 효과 | LATE (국소) | ATE (전체) | ATT |
| 데이터 요구 | 긴 시계열 | 임계값 근방 밀도 | 클러스터 × 시간 | 처치/대조 × 시간 |
| 자기상관 | 반드시 처리 | 보통 무시 | 클러스터 내 상관 | 클러스터 SE |
| IT 적용 | 알고리즘 변경 | 등급 기반 혜택 | 단계적 롤아웃 | 지역별 출시 |
1.6.1 선택 가이드
대조군이 있는가?
├── Yes → DiD (평행 추세 가정 성립?)
│ ├── Yes → DiD
│ └── No → Synthetic Control + DiD
└── No
├── 임계값이 있는가?
│ ├── Yes → RDD
│ └── No
│ ├── 시계열이 충분한가?
│ │ ├── Yes → ITS
│ │ └── No → 관찰 연구 (매칭 등)
│ └── 순차 도입인가?
│ └── Yes → Stepped Wedge
1.7 7. 실무 예시 통합
1.7.1 AI Agent 서비스: 추천 알고리즘 변경
상황: 추천 알고리즘을 2026년 3월 1일에 변경
A/B 테스트 없이 전체 적용 (비즈니스 결정)
효과를 사후적으로 평가해야 함
방법 1: ITS
→ 변경 전 12주 + 변경 후 6주의 주간 전환율 비교
→ 수준 변화 + 추세 변화 검정
방법 2: CITS (대조군 있는 ITS)
→ 한국 서비스는 3/1 변경, 일본 서비스는 미변경
→ 한국/일본의 DiD + 추세 비교
1.7.2 이커머스: VIP 등급 효과 (RDD)
상황: 구매 금액 100만원 이상이면 VIP 등급 자동 부여
VIP 혜택이 추가 구매를 유발하는지 알고 싶음
방법: RDD
→ 배정 변수: 누적 구매 금액
→ 임계값: 100만원
→ 95만원~105만원 근방의 고객 비교
→ 주의: 100만원 근처에서 의도적 구매 조작 가능 → McCrary 검정 필수
1.7.3 SaaS: 단계적 기능 롤아웃 (Stepped Wedge)
상황: 새 대시보드를 4개 지역에 순차 적용
모든 지역이 결국 새 대시보드를 받음
윤리적/비즈니스적으로 일부 지역을 영구 대조군으로 둘 수 없음
방법: Stepped Wedge
→ 전환 순서를 무작위로 결정
→ 각 지역의 전환 전/후 비교 + 시간 효과 통제
→ Mixed Model로 분석
1.8 8. 요약
| 설계 | 핵심 | 가정 | IT 적용 |
|---|---|---|---|
| ITS | 개입 전/후 추세 | 추세 연속성 | 알고리즘/정책 변경 |
| RDD | 임계값 불연속 | 비조작, 연속성 | 등급/점수 기반 혜택 |
| Stepped Wedge | 순차 전환 | 시간 효과 통제 | 단계적 롤아웃 |
| DiD | 처치/대조 × 전/후 | 평행 추세 | 지역별 출시 |
핵심: 준실험적 설계는 “무작위 배정이 없으니 인과를 말할 수 없다”는 관점을 넘어, 설계의 구조적 특성을 활용하여 상당히 강한 인과 추론을 가능하게 한다. 단, 각 설계의 가정이 성립하는지 반드시 검증해야 한다.
다음 파일: 35 — 인과 추론 프레임워크 총정리