Tae Hyun Kim (Lowell)

CUPED

3분 읽기 #experiments#ab-testing

정의

**CUPED (Controlled-experiment Using Pre-Experiment Data)**는 사전 실험 데이터로 A/B 테스트 추정량의 분산(variance)을 줄여 검정 민감도를 높이는 분산 감소(variance reduction) 기법이다.

Y^cuped=Yθ(XE[X])\hat{Y}_{cuped} = Y - \theta(X - E[X])

여기서:

  • YY: 실험 중 관측된 결과
  • XX: 사전 실험 데이터 (예: 실험 전 2주간의 행동)
  • θ=Cov(Y,X)Var(X)\theta = \frac{Cov(Y, X)}{Var(X)}: 조정 계수

Microsoft Research의 Deng et al. (2013)이 제안했다.

직관적 이해

고객마다 기본 구매 성향이 다르다. 어떤 고객은 원래 많이 구매하고, 어떤 고객은 적게 구매한다.

CUPED는 “이 고객이 원래 얼마나 구매하는 편인지”를 이용해 실험 결과의 노이즈를 줄인다. 사전 행동으로 예측 가능한 변동을 제거하면 처치효과(treatment effect)를 더 정확하게 추정한다.

핵심 성질

분산 감소

Var(Y^cuped)=Var(Y)(1ρXY2)Var(\hat{Y}_{cuped}) = Var(Y)(1 - \rho^2_{XY})

  • ρXY\rho_{XY}: XXYY의 상관계수
  • 상관이 높을수록 분산 감소 효과가 크다

불편성 유지

무작위 배정일 때: E[Y^cupedT=1]E[Y^cupedT=0]=E[YT=1]E[YT=0]E[\hat{Y}_{cuped}|T=1] - E[\hat{Y}_{cuped}|T=0] = E[Y|T=1] - E[Y|T=0]

CUPED로 조정해도 추정량(estimator)은 처치효과의 불편 추정량으로 남는다.

효율성 향상

분산이 줄면 더 적은 샘플로 같은 통계적 검정력(statistical power)을 달성한다.

유효 샘플 증가=11ρXY2\text{유효 샘플 증가} = \frac{1}{1 - \rho^2_{XY}}

예: ρ=0.5\rho = 0.5이면 유효 샘플이 33% 증가한다.

예시

Python 구현

import numpy as np
from scipy import stats

class CUPEDEstimator:
    def __init__(self, pre_period_days=14):
        self.pre_period_days = pre_period_days

    def fit(self, Y, X, treatment):
        """
        Y: 실험 중 결과
        X: 사전 실험 데이터 (공변량)
        treatment: 처리 지시자 (0/1)
        """
        # theta 계산 (전체 데이터)
        self.theta = np.cov(Y, X)[0, 1] / np.var(X)
        self.X_mean = np.mean(X)

        # CUPED 조정된 결과
        Y_cuped = Y - self.theta * (X - self.X_mean)

        # 처리 효과 추정
        Y_cuped_treatment = Y_cuped[treatment == 1]
        Y_cuped_control = Y_cuped[treatment == 0]

        self.effect = np.mean(Y_cuped_treatment) - np.mean(Y_cuped_control)
        self.effect_se = np.sqrt(
            np.var(Y_cuped_treatment) / len(Y_cuped_treatment) +
            np.var(Y_cuped_control) / len(Y_cuped_control)
        )

        # 비교: 조정 전
        self.effect_raw = np.mean(Y[treatment == 1]) - np.mean(Y[treatment == 0])
        self.effect_raw_se = np.sqrt(
            np.var(Y[treatment == 1]) / sum(treatment == 1) +
            np.var(Y[treatment == 0]) / sum(treatment == 0)
        )

        # 분산 감소율
        self.variance_reduction = 1 - (self.effect_se / self.effect_raw_se)**2

        return self

    def summary(self):
        return {
            'effect_cuped': self.effect,
            'se_cuped': self.effect_se,
            'effect_raw': self.effect_raw,
            'se_raw': self.effect_raw_se,
            'variance_reduction': self.variance_reduction,
            'theta': self.theta
        }

사용 예시

# 시뮬레이션 데이터
np.random.seed(42)
n = 10000

# 개인별 기본 성향 (미관측)
baseline = np.random.randn(n) * 10 + 50

# 사전 실험 데이터 (실험 전 2주 매출)
X_pre = baseline + np.random.randn(n) * 5

# 처리 배정
treatment = np.random.binomial(1, 0.5, n)

# 실험 중 결과 (처리 효과 = 2)
true_effect = 2
Y = baseline + true_effect * treatment + np.random.randn(n) * 5

# CUPED 적용
cuped = CUPEDEstimator()
cuped.fit(Y, X_pre, treatment)
results = cuped.summary()

print(f"진짜 효과: {true_effect}")
print(f"원시 추정치: {results['effect_raw']:.3f} ± {results['se_raw']:.3f}")
print(f"CUPED 추정치: {results['effect_cuped']:.3f} ± {results['se_cuped']:.3f}")
print(f"분산 감소: {results['variance_reduction']:.1%}")

다중 공변량(covariate) 확장

from sklearn.linear_model import LinearRegression

def cuped_multiple_covariates(Y, X_covariates, treatment):
    """
    여러 사전 실험 변수를 활용한 CUPED
    """
    # 선형 모델로 theta 추정
    model = LinearRegression()
    model.fit(X_covariates, Y)

    # 예측된 값 빼기
    Y_pred = model.predict(X_covariates)
    Y_cuped = Y - Y_pred + np.mean(Y_pred)

    # 처리 효과
    effect = np.mean(Y_cuped[treatment == 1]) - np.mean(Y_cuped[treatment == 0])

    return effect, Y_cuped

관련 개념

참고 문헌

  • Deng, A., Xu, Y., Kohavi, R., & Walker, T. (2013). “Improving the Sensitivity of Online Controlled Experiments by Utilizing Pre-Experiment Data.”
  • Comprehensive Personalized Pricing Guide, Part V, §14.3

연결 그래프