Tae Hyun Kim (Lowell)

CUPED

3분 읽기 #experiments#ab-testing

Definition

**CUPED (Controlled-experiment Using Pre-Experiment Data)**는 사전 실험 데이터를 활용하여 A/B 테스트의 분산을 줄이는 기법입니다.

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)이 제안했습니다.

Intuitive Understanding

고객마다 기본적인 구매 성향이 다릅니다. 어떤 고객은 원래 많이 구매하고, 어떤 고객은 적게 구매합니다.

CUPED는 “이 고객이 원래 얼마나 구매하는 편인지”를 활용하여 실험 결과의 노이즈를 줄입니다. 사전 행동으로 예측 가능한 변동을 제거하면, 처리 효과를 더 정확하게 추정할 수 있습니다.

Key Properties

분산 감소

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 조정 후에도 처리 효과의 불편 추정량입니다.

효율성 향상

분산이 줄면 더 적은 샘플로 같은 Statistical Power를 달성합니다:

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

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

Example

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%}")

다중 공변량 확장

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

References

  • 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

연결 그래프