준실험적 설계: ITS, RDD, Stepped Wedge

무작위 배정 없이 인과 추론 — 자연 실험의 활용

무작위 배정이 불가능할 때 자연 실험(Natural Experiment)을 활용하는 준실험적 설계를 다룬다. Interrupted Time Series(ITS), Regression Discontinuity Design(RDD), Stepped Wedge Design의 원리, 수식, 가정, 그리고 IT/이커머스에의 적용을 정리한다.

Statistics
Epidemiology
Experimentation
저자

Kwangmin Kim

공개

2026년 03월 08일

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. 개입 시점의 명확성: 정확한 개입 시점을 알아야 함
  3. 동시 사건 없음: 개입과 동시에 다른 사건이 발생하지 않았을 것
  4. 독립적 관측 또는 자기상관 처리: 시계열 자기상관 고려

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.2 Sharp RDD vs Fuzzy RDD

유형 조건 추정
Sharp RDD \(D_i = \mathbf{1}(X_i \geq c_0)\) 임계값에서 결과의 불연속
Fuzzy RDD \(P(D_i=1 \mid X_i)\)\(c_0\)에서 불연속적으로 점프 IV/2SLS로 LATE 추정

Sharp RDD: 임계값을 넘으면 반드시 처치 (100% 순응)

Fuzzy RDD: 임계값을 넘으면 처치 확률이 증가 (불완전 순응)

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 가정

  1. 연속성 가정 (Continuity): 반사실 기대값 \(E[Y(0) \mid X=x]\)\(E[Y(1) \mid X=x]\)\(c_0\)에서 연속
  2. 비조작 가정 (No Manipulation): 개인이 배정 변수를 조작하여 임계값을 의도적으로 넘을 수 없음
  3. 국소 무작위화: 임계값 근방에서 다른 공변량이 불연속적으로 변하지 않음

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 패키지 활용

# pip install rdrobust
from rdrobust import rdrobust

# 최적 bandwidth + 편향 보정 추정
result = rdrobust(y=df["satisfaction"].values,
                  x=df["score_centered"].values,
                  c=0)
print(result)

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면 조작 없음
library(rddensity)
test <- rddensity(X = df$score_centered)
summary(test)

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 — 인과 추론 프레임워크 총정리

Subscribe

Enjoy this blog? Get notified of new posts by email: