본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 2. · 14 Views
CUPED 실험 민감도 향상 기법 완벽 가이드
A/B 테스트의 정확도를 높이는 CUPED(Controlled-experiment Using Pre-Experiment Data) 기법을 초급자도 이해할 수 있도록 설명합니다. 실험 기간을 단축하면서도 더 정확한 결과를 얻는 통계 기법의 핵심을 다룹니다.
목차
- CUPED_기본_개념
- 분산_감소_원리
- 공변량_선택_전략
- 다중_공변량_CUPED
- 실험_그룹별_CUPED_적용
- CUPED_통계적_검정
- 검정력_향상_효과
- 실무_구현_파이프라인
- CUPED_한계와_대안
- 실전_케이스_스터디
1. CUPED 기본 개념
김개발 씨는 데이터 분석팀에 합류한 지 3개월 된 주니어 분석가입니다. 어느 날 팀장님이 다가와 말했습니다.
"이번 A/B 테스트, 2주 안에 결과를 내야 하는데 샘플 수가 부족해서 통계적 유의성이 안 나올 것 같아요." 김개발 씨는 고민에 빠졌습니다. 실험 기간을 늘릴 수도 없고, 그렇다고 부정확한 결과를 보고할 수도 없는 상황이었습니다.
CUPED는 Controlled-experiment Using Pre-Experiment Data의 약자로, 실험 전 데이터를 활용해 분산을 줄이는 기법입니다. 마치 시험을 볼 때 이전 모의고사 성적을 참고해 실력 변화를 더 정확히 측정하는 것과 같습니다.
이 기법을 사용하면 같은 샘플 수로도 더 정확한 실험 결과를 얻을 수 있어, 실험 기간을 최대 50%까지 단축할 수 있습니다.
다음 코드를 살펴봅시다.
import numpy as np
from scipy import stats
# 실험 전 데이터(X)와 실험 후 데이터(Y)
pre_experiment_data = np.array([100, 150, 200, 120, 180]) # 실험 전 구매액
post_experiment_data = np.array([110, 160, 210, 125, 190]) # 실험 후 구매액
# CUPED의 핵심: 공분산과 분산을 계산합니다
covariance = np.cov(pre_experiment_data, post_experiment_data)[0, 1]
variance_pre = np.var(pre_experiment_data)
# 세타(theta) 계산: 조정 계수
theta = covariance / variance_pre
# CUPED 조정된 메트릭 계산
mean_pre = np.mean(pre_experiment_data)
Y_cuped = post_experiment_data - theta * (pre_experiment_data - mean_pre)
print(f"원본 분산: {np.var(post_experiment_data):.2f}")
print(f"CUPED 분산: {np.var(Y_cuped):.2f}")
김개발 씨는 입사 3개월 차 주니어 데이터 분석가입니다. 오늘도 열심히 A/B 테스트 결과를 분석하던 중, 답답한 상황에 부딪혔습니다.
p-value가 0.08로 나왔는데, 통계적 유의성의 기준인 0.05에 미치지 못하는 것이었습니다. 분명 효과가 있어 보이는데, 숫자가 뒷받침해주지 않으니 보고서를 쓸 수가 없었습니다.
선배 분석가 박시니어 씨가 다가와 화면을 살펴봅니다. "아, 샘플 수가 부족해서 그래요.
그런데 우리 서비스 특성상 실험 전 데이터가 있잖아요. CUPED 써보셨어요?" 그렇다면 CUPED란 정확히 무엇일까요?
쉽게 비유하자면, CUPED는 마치 다이어트 효과를 측정할 때 이전 체중을 함께 고려하는 것과 같습니다. 단순히 다이어트 후 체중만 비교하면, 원래 체중이 다른 사람들 간의 차이 때문에 노이즈가 발생합니다.
하지만 "다이어트 전 체중 대비 변화량"을 보면 훨씬 정확한 비교가 가능해집니다. CUPED가 없던 시절에는 어땠을까요?
데이터 분석가들은 오직 실험 기간 동안 수집된 데이터만으로 분석해야 했습니다. 문제는 사용자마다 원래 행동 패턴이 다르다는 것이었습니다.
어떤 사용자는 원래 구매를 많이 하고, 어떤 사용자는 거의 구매하지 않습니다. 이런 개인차가 분산을 크게 만들어, 실험 효과를 정확히 측정하기 어렵게 만들었습니다.
바로 이런 문제를 해결하기 위해 2013년 Microsoft의 연구팀이 CUPED를 개발했습니다. CUPED를 사용하면 실험 전 데이터를 활용해 개인차로 인한 노이즈를 제거할 수 있습니다.
결과적으로 분산이 줄어들고, 같은 샘플 수로도 더 작은 효과를 탐지할 수 있게 됩니다. 실제로 많은 기업에서 CUPED를 적용해 실험 기간을 30~50% 단축하고 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 공분산을 계산하는 부분이 핵심입니다.
실험 전 데이터와 실험 후 데이터가 얼마나 함께 움직이는지를 측정합니다. 다음으로 **세타(theta)**를 계산하는데, 이것이 바로 조정 계수입니다.
세타 값이 클수록 실험 전 데이터가 실험 후 데이터를 잘 예측한다는 의미입니다. 마지막으로 Y_cuped를 계산합니다.
원래 실험 결과에서 실험 전 데이터의 영향을 빼주는 것입니다. 이렇게 조정된 메트릭은 원본보다 분산이 작아집니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 쇼핑몰에서 새로운 추천 알고리즘의 효과를 측정한다고 가정해봅시다.
실험 전 2주간의 구매 데이터를 공변량으로 사용하면, 원래 구매력이 높은 고객과 낮은 고객 간의 차이를 보정할 수 있습니다. Netflix, Uber, Airbnb 같은 기업들이 이 기법을 적극 활용하고 있습니다.
하지만 주의할 점도 있습니다. CUPED가 효과를 발휘하려면 실험 전 데이터와 실험 후 데이터 간에 상관관계가 있어야 합니다.
상관관계가 없다면 분산 감소 효과도 없습니다. 따라서 공변량 선택이 매우 중요합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.
"아, 실험 전 데이터를 활용하면 되는군요!" CUPED를 제대로 이해하면 더 빠르고 정확한 A/B 테스트를 수행할 수 있습니다. 여러분도 다음 실험에서 적용해 보세요.
실전 팁
💡 - 실험 전 데이터는 최소 1~2주 이상의 기간을 사용하는 것이 안정적입니다
- 공변량과 결과 변수의 상관관계가 0.5 이상일 때 효과가 큽니다
- 여러 공변량을 조합하면 더 큰 분산 감소를 얻을 수 있습니다
2. 분산 감소 원리
CUPED의 기본 개념을 이해한 김개발 씨는 한 가지 의문이 생겼습니다. "왜 실험 전 데이터를 빼주면 분산이 줄어드는 거죠?" 박시니어 씨는 화이트보드 앞으로 김개발 씨를 데려갔습니다.
"수학적 원리를 알면 더 잘 활용할 수 있어요. 어렵지 않으니 천천히 따라와 보세요."
CUPED의 분산 감소 원리는 조정된 변수의 분산 공식에서 비롯됩니다. 새로운 변수 Y_cuped = Y - theta * (X - mean_X)를 만들면, 이 변수의 분산은 원래 Y의 분산에서 상관관계의 제곱만큼 줄어듭니다.
마치 잡음이 섞인 라디오 신호에서 잡음을 제거하면 원래 음악이 더 선명하게 들리는 것과 같습니다.
다음 코드를 살펴봅시다.
import numpy as np
def calculate_variance_reduction(X, Y):
"""분산 감소율을 계산합니다"""
# 상관계수 계산
correlation = np.corrcoef(X, Y)[0, 1]
# 분산 감소율 = 상관계수의 제곱 (R-squared)
variance_reduction = correlation ** 2
# 원래 분산과 CUPED 분산 비교
original_variance = np.var(Y)
cuped_variance = original_variance * (1 - variance_reduction)
print(f"상관계수: {correlation:.3f}")
print(f"분산 감소율: {variance_reduction:.1%}")
print(f"원래 분산: {original_variance:.2f}")
print(f"CUPED 후 분산: {cuped_variance:.2f}")
return variance_reduction
# 예시 데이터
X = np.random.normal(100, 20, 1000) # 실험 전 데이터
Y = 0.8 * X + np.random.normal(10, 10, 1000) # 실험 후 데이터
reduction = calculate_variance_reduction(X, Y)
김개발 씨는 화이트보드 앞에 섰습니다. 박시니어 씨가 펜을 들고 수식을 적기 시작합니다.
"자, 우리가 만드는 새로운 변수를 Y_cuped라고 부르자. 이건 원래 결과 Y에서 실험 전 데이터 X의 영향을 빼준 거야." 수식으로 표현하면 Y_cuped = Y - theta * (X - mean_X)입니다.
여기서 theta는 공분산을 분산으로 나눈 값으로, 회귀 분석의 기울기와 같은 개념입니다. 그렇다면 왜 분산이 줄어들까요?
쉽게 비유하자면, 주식 투자 수익률을 분석할 때와 비슷합니다. 단순히 각 종목의 수익률만 보면 변동성이 큽니다.
하지만 시장 전체 움직임(베타)을 제거한 알파를 보면 해당 종목의 고유한 성과를 더 정확히 볼 수 있습니다. CUPED도 마찬가지로 개인차라는 "시장 움직임"을 제거해 실험 효과라는 "알파"를 선명하게 드러냅니다.
분산 공식을 풀어보면 흥미로운 사실을 발견할 수 있습니다. Var(Y_cuped) = Var(Y) * (1 - r^2) 여기서 r은 X와 Y의 상관계수입니다.
상관계수가 0.7이면 분산은 원래의 51%로 줄어듭니다. 상관계수가 0.9이면 무려 19%까지 줄어듭니다.
이것이 의미하는 바는 무엇일까요? 상관계수의 제곱인 R-squared가 바로 분산 감소율입니다.
즉, 실험 전 데이터가 실험 후 데이터를 얼마나 잘 설명하는지가 CUPED의 효과를 결정합니다. 좋은 공변량을 선택하는 것이 왜 중요한지 알 수 있는 대목입니다.
위의 코드를 살펴봅시다. 먼저 np.corrcoef 함수로 상관계수를 계산합니다.
이 값을 제곱하면 분산 감소율이 됩니다. 예시 데이터에서 Y는 X와 강한 상관관계를 가지도록 설계했습니다.
결과적으로 상당한 분산 감소를 확인할 수 있습니다. 실제 현업에서 상관계수는 얼마나 될까요?
일반적으로 같은 메트릭의 실험 전후 데이터는 0.5~0.8 정도의 상관계수를 보입니다. 예를 들어 "지난주 구매액"과 "이번 주 구매액"은 높은 상관관계를 가집니다.
원래 구매를 많이 하는 사람은 계속 많이 하고, 적게 하는 사람은 계속 적게 하기 때문입니다. 주의해야 할 점이 있습니다.
상관계수가 낮으면 CUPED를 적용해도 효과가 미미합니다. 상관계수가 0.3이면 분산이 겨우 9%만 줄어듭니다.
이런 경우 공변량을 다시 선택하거나 여러 공변량을 조합하는 방법을 고려해야 합니다. 박시니어 씨가 화이트보드를 정리하며 말했습니다.
"결국 핵심은 좋은 공변량 선택이야. 상관관계가 높을수록 효과가 커지니까." 김개발 씨는 고개를 끄덕였습니다.
이제 왜 분산이 줄어드는지 수학적으로 이해할 수 있게 되었습니다.
실전 팁
💡 - 상관계수가 0.5 이상인 공변량을 선택하세요
- R-squared = 0.5면 분산이 절반으로 줄어듭니다
- 여러 공변량을 조합하면 R-squared를 높일 수 있습니다
3. 공변량 선택 전략
김개발 씨는 CUPED를 실제 프로젝트에 적용하려고 합니다. 그런데 막상 시작하려니 어떤 데이터를 공변량으로 써야 할지 막막했습니다.
실험 전 데이터가 여러 개 있는데, 어떤 걸 선택해야 효과가 좋을까요? 박시니어 씨에게 조언을 구하기로 했습니다.
공변량 선택은 CUPED 성공의 핵심입니다. 좋은 공변량은 결과 변수와 높은 상관관계를 가지면서 실험 처리에 영향받지 않는 데이터입니다.
마치 학생의 실력 향상을 측정할 때, 이전 성적은 좋은 공변량이지만 수업 출석률은 실험 효과에 영향받을 수 있어 위험합니다. 일반적으로 같은 메트릭의 실험 전 기간 데이터가 가장 좋은 공변량입니다.
다음 코드를 살펴봅시다.
import numpy as np
import pandas as pd
def evaluate_covariate_candidates(df, target_col, candidate_cols):
"""여러 공변량 후보를 평가합니다"""
results = []
for col in candidate_cols:
# 상관계수 계산
correlation = df[target_col].corr(df[col])
# 분산 감소율 계산
variance_reduction = correlation ** 2
results.append({
'covariate': col,
'correlation': correlation,
'variance_reduction': f"{variance_reduction:.1%}"
})
result_df = pd.DataFrame(results)
result_df = result_df.sort_values('correlation', ascending=False)
print("공변량 후보 평가 결과:")
print(result_df.to_string(index=False))
return result_df
# 예시: 공변량 후보들
data = {
'purchase_amount': np.random.normal(100, 30, 1000), # 실험 후 구매액
'pre_purchase_1week': np.random.normal(100, 30, 1000), # 1주 전 구매액
'pre_purchase_2week': np.random.normal(100, 25, 1000), # 2주 전 구매액
'page_views': np.random.normal(50, 20, 1000), # 페이지뷰
'account_age_days': np.random.normal(365, 200, 1000) # 계정 나이
}
df = pd.DataFrame(data)
# 실제로는 상관관계가 있도록 데이터 생성 필요
evaluate_covariate_candidates(df, 'purchase_amount',
['pre_purchase_1week', 'pre_purchase_2week', 'page_views', 'account_age_days'])
김개발 씨가 자리로 돌아와 데이터베이스를 뒤지기 시작했습니다. 실험 전 데이터로 쓸 수 있는 것들이 꽤 있었습니다.
지난주 구매액, 지난달 방문 횟수, 계정 생성일, 마지막 로그인 시간... 어떤 걸 골라야 할까요?
박시니어 씨가 옆에 앉으며 말했습니다. "공변량 선택에는 두 가지 기준이 있어요.
첫째, 결과 변수와 상관관계가 높아야 해요. 둘째, 실험 처리에 영향받으면 안 돼요." 첫 번째 기준부터 살펴보겠습니다.
상관관계가 높아야 하는 이유는 앞서 배운 분산 감소 공식 때문입니다. 상관계수가 0.7이면 분산이 51% 줄지만, 0.3이면 고작 9%만 줄어듭니다.
따라서 가능한 한 높은 상관관계를 가진 공변량을 찾아야 합니다. 두 번째 기준은 더 중요합니다.
쉽게 비유하자면, 신약의 효과를 측정할 때 "복용 전 혈압"은 좋은 공변량이지만, "복용 중 운동량"은 위험합니다. 왜냐하면 신약이 체력을 좋게 만들어 운동량이 늘어났을 수 있기 때문입니다.
이렇게 실험 처리에 영향받는 변수를 공변량으로 쓰면 실험 효과가 왜곡됩니다. 그렇다면 어떤 공변량이 안전할까요?
가장 안전하고 효과적인 공변량은 같은 메트릭의 실험 전 기간 데이터입니다. 예를 들어 구매액을 측정한다면, 실험 전 1주간의 구매액이 가장 좋은 공변량입니다.
같은 메트릭이므로 상관관계가 높고, 실험이 시작되기 전 데이터이므로 처리 효과에 영향받지 않습니다. 위의 코드를 살펴보면 여러 공변량 후보를 평가하는 함수가 있습니다.
각 후보에 대해 상관계수와 분산 감소율을 계산합니다. 결과를 정렬하면 어떤 공변량이 가장 효과적인지 한눈에 볼 수 있습니다.
실무에서는 이런 탐색 과정을 거쳐 최적의 공변량을 선택합니다. 실무에서 자주 쓰이는 공변량 유형을 정리하면 이렇습니다.
첫째, 동일 메트릭의 과거 데이터입니다. 가장 효과적이고 안전합니다.
둘째, 사용자 속성 데이터입니다. 계정 나이, 디바이스 타입 등이 여기 해당합니다.
셋째, 집계된 행동 데이터입니다. 지난 30일 총 방문 횟수 같은 것들입니다.
피해야 할 공변량도 있습니다. 실험 기간 중에 측정된 데이터는 절대 쓰면 안 됩니다.
또한 실험 처리에 따라 달라질 수 있는 변수도 위험합니다. 예를 들어 "앱 사용 시간"은 실험 그룹에 따라 달라질 수 있어 공변량으로 부적합합니다.
김개발 씨가 물었습니다. "그럼 공변량을 여러 개 쓰면 더 좋을까요?" 박시니어 씨가 고개를 끄덕였습니다.
"네, 여러 개를 조합하면 분산 감소율을 더 높일 수 있어요. 다음에 그 방법을 알려줄게요."
실전 팁
💡 - 결과 변수와 같은 메트릭의 실험 전 데이터가 가장 효과적입니다
- 실험 기간 중 데이터는 절대 공변량으로 쓰지 마세요
- 여러 후보를 평가해 상관계수가 가장 높은 것을 선택하세요
4. 다중 공변량 CUPED
공변량 선택 방법을 배운 김개발 씨는 또 다른 궁금증이 생겼습니다. "지난주 구매액도 좋고, 지난달 방문 횟수도 상관관계가 높아요.
둘 다 쓰면 효과가 더 커질까요?" 박시니어 씨의 눈이 반짝였습니다. "좋은 질문이네요.
다중 공변량 CUPED를 배워볼까요?"
다중 공변량 CUPED는 여러 개의 실험 전 변수를 동시에 활용해 분산을 줄이는 기법입니다. 마치 GPS가 하나의 위성보다 여러 위성 신호를 조합할 때 위치를 더 정확히 파악하는 것과 같습니다.
다중 선형 회귀의 잔차를 구하는 방식으로 구현하며, 단일 공변량보다 더 큰 분산 감소 효과를 얻을 수 있습니다.
다음 코드를 살펴봅시다.
import numpy as np
from sklearn.linear_model import LinearRegression
def multi_covariate_cuped(Y, X_matrix):
"""다중 공변량 CUPED를 수행합니다"""
# 선형 회귀 모델 적합
model = LinearRegression()
model.fit(X_matrix, Y)
# 예측값 계산
Y_predicted = model.predict(X_matrix)
# 잔차가 CUPED 조정된 메트릭
Y_cuped = Y - Y_predicted + np.mean(Y)
# 분산 감소율 계산
original_var = np.var(Y)
cuped_var = np.var(Y_cuped)
reduction = 1 - (cuped_var / original_var)
print(f"원본 분산: {original_var:.2f}")
print(f"CUPED 분산: {cuped_var:.2f}")
print(f"분산 감소율: {reduction:.1%}")
print(f"R-squared: {model.score(X_matrix, Y):.3f}")
return Y_cuped, model
# 예시 데이터 생성
np.random.seed(42)
n = 1000
# 공변량들
X1 = np.random.normal(100, 20, n) # 지난주 구매액
X2 = np.random.normal(50, 15, n) # 지난달 방문 횟수
X3 = np.random.normal(30, 10, n) # 평균 세션 시간
# 결과 변수 (공변량들과 상관관계를 가지도록)
Y = 0.5 * X1 + 0.3 * X2 + 0.2 * X3 + np.random.normal(0, 15, n)
X_matrix = np.column_stack([X1, X2, X3])
Y_cuped, model = multi_covariate_cuped(Y, X_matrix)
김개발 씨는 흥미로운 발견을 했습니다. 지난주 구매액의 상관계수는 0.6, 지난달 방문 횟수의 상관계수는 0.4였습니다.
각각 사용하면 분산이 36%, 16%씩 줄어듭니다. 그렇다면 둘을 함께 쓰면 어떻게 될까요?
단순히 36% + 16% = 52%가 될 것 같지만, 실제로는 그렇지 않습니다. 두 공변량이 서로 상관관계가 있다면 중복되는 정보가 있기 때문입니다.
다중 공변량 CUPED는 이런 상황을 최적으로 처리합니다. 쉽게 비유하자면, 날씨를 예측할 때 온도만 보면 어느 정도 맞출 수 있습니다.
습도만 봐도 어느 정도 맞출 수 있습니다. 하지만 온도와 습도를 함께 고려하면 더 정확해집니다.
다만, 온도와 습도가 서로 관련이 있으므로 단순 합산은 아닙니다. 다중 공변량 CUPED는 다중 선형 회귀를 사용합니다.
여러 공변량으로 결과 변수를 예측하는 모델을 만들고, 그 잔차(실제값 - 예측값)를 조정된 메트릭으로 사용합니다. 잔차는 공변량들로 설명되지 않는 부분만 남긴 것이므로, 개인차로 인한 노이즈가 제거됩니다.
위의 코드를 살펴보겠습니다. sklearn의 LinearRegression을 사용해 Y를 X들로 예측합니다.
model.predict로 예측값을 구하고, 원래 Y에서 빼줍니다. 여기서 np.mean(Y)를 다시 더해주는 이유는 평균을 원래대로 유지하기 위함입니다.
이렇게 하지 않으면 조정된 메트릭의 평균이 0 근처가 되어 해석이 어려워집니다. 분산 감소율은 회귀 모델의 R-squared와 같습니다.
세 개의 공변량을 사용한 예시에서 R-squared가 0.7 정도라면, 분산이 70% 줄어든다는 의미입니다. 단일 공변량만 썼을 때보다 훨씬 큰 효과입니다.
실무에서 다중 공변량을 선택할 때 주의할 점이 있습니다. 첫째, 공변량들 간의 다중공선성을 확인해야 합니다.
공변량들이 서로 너무 높은 상관관계를 가지면 모델이 불안정해질 수 있습니다. 둘째, 공변량 수를 너무 많이 늘리면 과적합 위험이 있습니다.
일반적으로 3~5개 정도가 적당합니다. 박시니어 씨가 조언했습니다.
"실무에서는 같은 메트릭의 여러 기간 데이터를 조합하는 게 가장 효과적이에요. 예를 들어 지난 1주, 2주, 4주 구매액을 함께 쓰는 거죠." 김개발 씨는 노트에 열심히 적었습니다.
다중 공변량 CUPED로 분산을 더 크게 줄일 수 있다는 것을 알게 되었습니다.
실전 팁
💡 - 3~5개의 공변량을 조합하는 것이 일반적입니다
- 같은 메트릭의 여러 기간 데이터를 쓰면 효과적입니다
- 공변량 간 상관관계가 너무 높으면 하나를 제거하세요
5. 실험 그룹별 CUPED 적용
이론을 충분히 배운 김개발 씨는 드디어 실제 A/B 테스트에 CUPED를 적용하려 합니다. 그런데 막상 코드를 작성하려니 막히는 부분이 있었습니다.
"통제 그룹과 실험 그룹 데이터가 따로 있는데, CUPED를 어떻게 적용해야 하죠?" 이번에는 실전 코드를 작성해 보겠습니다.
실제 A/B 테스트에서 CUPED를 적용할 때는 전체 데이터로 세타를 계산한 후, 각 그룹에 동일하게 적용합니다. 이렇게 해야 그룹 간 비교가 공정해집니다.
마치 시험 점수를 조정할 때 전체 학생의 평균과 표준편차로 표준화하는 것과 같습니다. 그룹별로 다른 세타를 쓰면 비교 자체가 왜곡됩니다.
다음 코드를 살펴봅시다.
import numpy as np
from scipy import stats
def apply_cuped_to_ab_test(control_pre, control_post,
treatment_pre, treatment_post):
"""A/B 테스트에 CUPED를 적용합니다"""
# 전체 데이터로 세타 계산 (핵심!)
all_pre = np.concatenate([control_pre, treatment_pre])
all_post = np.concatenate([control_post, treatment_post])
theta = np.cov(all_pre, all_post)[0, 1] / np.var(all_pre)
mean_pre = np.mean(all_pre)
# 각 그룹에 동일한 세타 적용
control_cuped = control_post - theta * (control_pre - mean_pre)
treatment_cuped = treatment_post - theta * (treatment_pre - mean_pre)
# 원본 t-test
t_orig, p_orig = stats.ttest_ind(control_post, treatment_post)
# CUPED t-test
t_cuped, p_cuped = stats.ttest_ind(control_cuped, treatment_cuped)
print("=== 원본 분석 ===")
print(f"통제군 평균: {np.mean(control_post):.2f}")
print(f"실험군 평균: {np.mean(treatment_post):.2f}")
print(f"p-value: {p_orig:.4f}")
print("\n=== CUPED 분석 ===")
print(f"통제군 평균 (조정): {np.mean(control_cuped):.2f}")
print(f"실험군 평균 (조정): {np.mean(treatment_cuped):.2f}")
print(f"p-value: {p_cuped:.4f}")
return control_cuped, treatment_cuped
# 예시 데이터
np.random.seed(42)
n = 500
# 통제군: 실험 전후
control_pre = np.random.normal(100, 30, n)
control_post = 0.7 * control_pre + np.random.normal(35, 15, n)
# 실험군: 5% 상승 효과가 있다고 가정
treatment_pre = np.random.normal(100, 30, n)
treatment_post = 0.7 * treatment_pre + np.random.normal(35, 15, n) + 5
apply_cuped_to_ab_test(control_pre, control_post, treatment_pre, treatment_post)
김개발 씨 앞에 데이터가 펼쳐져 있습니다. 통제군 500명, 실험군 500명.
각각 실험 전 1주간의 구매액과 실험 기간 2주간의 구매액 데이터가 있습니다. 처음에는 그룹별로 따로 세타를 계산하려고 했습니다.
하지만 박시니어 씨가 말렸습니다. "잠깐, 그러면 안 돼요.
세타는 전체 데이터로 계산해야 해요." 왜 그럴까요? 쉽게 비유하자면, 두 반의 시험 성적을 비교할 때를 생각해봅시다.
성적을 표준화하려면 전체 학생의 평균과 표준편차를 써야 합니다. 만약 각 반의 평균과 표준편차를 따로 쓰면, 비교 자체가 의미 없어집니다.
모든 반이 평균 0, 표준편차 1이 되어버리니까요. CUPED도 마찬가지입니다.
전체 데이터로 세타를 계산해야 그룹 간 공정한 비교가 가능합니다. 그룹별로 다른 세타를 쓰면 각 그룹의 조정 기준이 달라져 비교가 왜곡됩니다.
위의 코드에서 핵심 부분을 살펴보겠습니다. 먼저 np.concatenate로 통제군과 실험군 데이터를 합칩니다.
이 전체 데이터로 세타와 mean_pre를 계산합니다. 그 다음, 계산된 세타를 각 그룹에 동일하게 적용합니다.
결과를 보면 흥미로운 점이 있습니다. 원본 분석에서 p-value가 0.05보다 크게 나왔다면, CUPED 적용 후에는 0.05보다 작아질 수 있습니다.
분산이 줄어들면서 같은 효과 크기도 더 명확하게 드러나기 때문입니다. 이것이 CUPED의 힘입니다.
실무에서는 몇 가지 추가 고려사항이 있습니다. 첫째, 무작위 배정 검증을 해야 합니다.
실험 전 데이터의 그룹 간 차이가 통계적으로 유의하지 않은지 확인합니다. 유의하다면 무작위 배정에 문제가 있는 것입니다.
둘째, 효과 크기도 CUPED 적용 전후로 비교해봅니다. 평균 차이 자체는 변하지 않아야 합니다.
CUPED는 분산만 줄이고 평균 차이(효과 크기)는 유지하기 때문입니다. 김개발 씨가 코드를 실행했습니다.
원본 분석에서 p-value가 0.12였는데, CUPED 적용 후 0.03으로 떨어졌습니다. "와, 통계적으로 유의해졌어요!" 박시니어 씨가 미소 지었습니다.
"네, 이제 자신 있게 보고서 쓸 수 있겠죠?"
실전 팁
💡 - 세타는 반드시 전체 데이터로 계산하세요
- 실험 전 데이터로 그룹 간 균형을 먼저 확인하세요
- 효과 크기(평균 차이)가 CUPED 전후로 동일한지 검증하세요
6. CUPED 통계적 검정
CUPED를 적용해 p-value가 개선된 것을 본 김개발 씨는 한 가지 의문이 들었습니다. "CUPED로 분산만 줄이면 가짜 양성(false positive)이 늘어나는 건 아닐까요?" 박시니어 씨가 고개를 저었습니다.
"좋은 질문이에요. CUPED의 통계적 성질을 정확히 이해해야 합니다."
CUPED는 분산을 줄이지만 Type I 오류율(가짜 양성률)을 증가시키지 않습니다. 이것이 가능한 이유는 CUPED가 실험 효과에 독립적인 노이즈만 제거하기 때문입니다.
마치 안경을 써서 시야가 선명해졌다고 해서 없는 물체가 보이는 것이 아닌 것과 같습니다. CUPED는 원래 있던 효과를 더 잘 볼 수 있게 해줄 뿐, 없는 효과를 만들어내지 않습니다.
다음 코드를 살펴봅시다.
import numpy as np
from scipy import stats
def simulate_type1_error(n_simulations=1000, n_per_group=500):
"""Type I 오류율을 시뮬레이션합니다 (효과 없는 경우)"""
original_significant = 0
cuped_significant = 0
for _ in range(n_simulations):
# 효과가 없는 데이터 생성 (두 그룹 동일)
pre_control = np.random.normal(100, 30, n_per_group)
pre_treatment = np.random.normal(100, 30, n_per_group)
# 실험 후: 효과 없음 (두 그룹 동일한 분포)
post_control = 0.7 * pre_control + np.random.normal(30, 15, n_per_group)
post_treatment = 0.7 * pre_treatment + np.random.normal(30, 15, n_per_group)
# 원본 t-test
_, p_orig = stats.ttest_ind(post_control, post_treatment)
if p_orig < 0.05:
original_significant += 1
# CUPED 적용
all_pre = np.concatenate([pre_control, pre_treatment])
all_post = np.concatenate([post_control, post_treatment])
theta = np.cov(all_pre, all_post)[0, 1] / np.var(all_pre)
mean_pre = np.mean(all_pre)
cuped_control = post_control - theta * (pre_control - mean_pre)
cuped_treatment = post_treatment - theta * (pre_treatment - mean_pre)
_, p_cuped = stats.ttest_ind(cuped_control, cuped_treatment)
if p_cuped < 0.05:
cuped_significant += 1
print(f"시뮬레이션 횟수: {n_simulations}")
print(f"원본 Type I 오류율: {original_significant/n_simulations:.1%}")
print(f"CUPED Type I 오류율: {cuped_significant/n_simulations:.1%}")
simulate_type1_error()
김개발 씨의 걱정은 충분히 합리적이었습니다. 분산을 줄이면 더 작은 차이도 유의하게 나올 수 있으니, 실제로는 효과가 없는데 있다고 잘못 판단하는 경우가 늘어나지 않을까요?
결론부터 말하면, 그렇지 않습니다. CUPED의 핵심 원리를 다시 생각해봅시다.
CUPED가 제거하는 것은 실험 처리와 무관한 노이즈입니다. 개인마다 원래 구매 성향이 다르다는 것은 실험과 무관합니다.
이런 개인차를 제거해도 실험 효과 자체에는 영향이 없습니다. 쉽게 비유하자면, 청진기의 잡음 제거 기능과 같습니다.
주변 소음을 제거하면 심장 소리가 더 잘 들립니다. 하지만 심장 소리가 없는데 있다고 들리지는 않습니다.
CUPED도 마찬가지로 실험 효과와 무관한 잡음만 제거합니다. 위의 코드는 이를 시뮬레이션으로 검증합니다.
효과가 없는 데이터를 1000번 생성합니다. 두 그룹은 완전히 동일한 분포에서 나왔습니다.
이 데이터에 원본 t-test와 CUPED t-test를 각각 적용합니다. 결과를 보면 두 가지가 확인됩니다.
첫째, 원본 Type I 오류율은 약 5%입니다. 이는 유의수준 0.05에서 기대되는 값입니다.
둘째, CUPED Type I 오류율도 약 5%입니다. 분산이 줄었지만 가짜 양성이 늘어나지 않았습니다.
왜 이런 결과가 나올까요? CUPED가 분산을 줄이는 동시에 검정 통계량의 분포도 함께 조정되기 때문입니다.
t-test의 검정 통계량은 평균 차이를 표준오차로 나눈 값입니다. CUPED는 분산을 줄여 표준오차를 작게 만들지만, 효과가 없으면 평균 차이도 0에 가깝습니다.
결과적으로 t값의 분포는 동일하게 유지됩니다. 다만 주의할 점이 있습니다.
CUPED를 잘못 적용하면 문제가 생길 수 있습니다. 예를 들어 실험 기간 중 데이터를 공변량으로 쓰거나, 그룹별로 다른 세타를 적용하면 통계적 성질이 깨집니다.
올바른 방법으로 적용하는 것이 중요합니다. 김개발 씨가 안도의 한숨을 쉬었습니다.
"그럼 CUPED는 통계적으로도 안전한 방법이군요." 박시니어 씨가 고개를 끄덕였습니다. "네, 그래서 많은 기업에서 표준으로 사용하는 거예요."
실전 팁
💡 - CUPED는 Type I 오류율을 증가시키지 않습니다
- 올바른 적용 방법을 지키는 것이 중요합니다
- 시뮬레이션으로 직접 검증해보면 더 확신을 가질 수 있습니다
7. 검정력 향상 효과
Type I 오류율이 증가하지 않는다는 것을 확인한 김개발 씨는 이제 궁금한 게 있습니다. "그럼 CUPED의 진짜 장점은 뭐예요?" 박시니어 씨가 설명합니다.
"Type II 오류율이 줄어들어요. 다시 말해, 검정력(power)이 높아집니다.
실제 효과가 있을 때 이를 탐지할 확률이 높아지는 거죠."
CUPED의 핵심 장점은 검정력 향상입니다. 검정력이란 실제 효과가 있을 때 이를 유의하다고 판단할 확률입니다.
분산이 줄어들면 같은 효과 크기도 더 명확하게 드러나므로 검정력이 높아집니다. 마치 고해상도 카메라로 사진을 찍으면 작은 물체도 더 잘 보이는 것과 같습니다.
CUPED는 실험의 해상도를 높여줍니다.
다음 코드를 살펴봅시다.
import numpy as np
from scipy import stats
def simulate_power(effect_size, n_simulations=1000, n_per_group=500):
"""검정력을 시뮬레이션합니다"""
original_detected = 0
cuped_detected = 0
for _ in range(n_simulations):
# 실험 전 데이터
pre_control = np.random.normal(100, 30, n_per_group)
pre_treatment = np.random.normal(100, 30, n_per_group)
# 실험 후: 실험군에 effect_size만큼 효과 추가
post_control = 0.7 * pre_control + np.random.normal(30, 15, n_per_group)
post_treatment = 0.7 * pre_treatment + np.random.normal(30, 15, n_per_group) + effect_size
# 원본 t-test
_, p_orig = stats.ttest_ind(post_control, post_treatment)
if p_orig < 0.05:
original_detected += 1
# CUPED 적용
all_pre = np.concatenate([pre_control, pre_treatment])
all_post = np.concatenate([post_control, post_treatment])
theta = np.cov(all_pre, all_post)[0, 1] / np.var(all_pre)
mean_pre = np.mean(all_pre)
cuped_control = post_control - theta * (pre_control - mean_pre)
cuped_treatment = post_treatment - theta * (pre_treatment - mean_pre)
_, p_cuped = stats.ttest_ind(cuped_control, cuped_treatment)
if p_cuped < 0.05:
cuped_detected += 1
print(f"효과 크기: {effect_size}")
print(f"원본 검정력: {original_detected/n_simulations:.1%}")
print(f"CUPED 검정력: {cuped_detected/n_simulations:.1%}")
print(f"검정력 향상: +{(cuped_detected-original_detected)/n_simulations:.1%}")
# 작은 효과 크기로 테스트
simulate_power(effect_size=3)
통계 검정에는 두 가지 오류가 있습니다. Type I 오류는 효과가 없는데 있다고 판단하는 것입니다.
앞서 확인했듯이 CUPED는 이 오류율을 증가시키지 않습니다. Type II 오류는 효과가 있는데 없다고 판단하는 것입니다.
CUPED는 이 오류율을 줄여줍니다. 검정력은 1에서 Type II 오류율을 뺀 값입니다.
검정력이 80%라면, 실제 효과가 있을 때 80%의 확률로 이를 탐지한다는 의미입니다. 나머지 20%는 효과가 있는데도 놓치는 경우입니다.
CUPED를 적용하면 이 검정력이 높아집니다. 왜 검정력이 높아질까요?
쉽게 비유하자면, 라디오에서 잡음이 줄어들면 작은 소리의 음악도 더 잘 들립니다. 분산은 일종의 잡음입니다.
잡음이 줄어들면 작은 신호(효과)도 더 잘 감지할 수 있습니다. 위의 코드에서 효과 크기를 3으로 설정했습니다.
이는 실험군이 통제군보다 평균적으로 3만큼 높다는 의미입니다. 시뮬레이션 결과를 보면, 원본 검정력보다 CUPED 검정력이 상당히 높게 나옵니다.
실무적으로 이것이 의미하는 바는 무엇일까요? 첫째, 더 작은 효과를 탐지할 수 있습니다.
기존에는 5%의 효과만 탐지할 수 있었다면, CUPED로 2%의 효과도 탐지할 수 있게 됩니다. 둘째, 더 적은 샘플로 같은 효과를 탐지할 수 있습니다.
검정력 80%를 달성하는 데 필요한 샘플 수가 줄어듭니다. 셋째, 더 짧은 기간에 실험을 완료할 수 있습니다.
같은 트래픽이라면 필요한 샘플에 더 빨리 도달합니다. 김개발 씨가 계산해봤습니다.
"분산이 50% 줄면 필요 샘플 수도 50% 줄어드니까... 2주 걸릴 실험을 1주에 끝낼 수 있겠네요!" 박시니어 씨가 고개를 끄덕였습니다.
"맞아요. 그래서 실험 속도가 중요한 회사에서 CUPED를 적극 활용하는 거예요."
실전 팁
💡 - 검정력 80%를 목표로 샘플 크기를 설계하세요
- CUPED로 분산이 X% 줄면 필요 샘플도 약 X% 줄어듭니다
- 작은 효과를 탐지해야 할 때 CUPED가 특히 유용합니다
8. 실무 구현 파이프라인
이론과 검증을 모두 마친 김개발 씨는 이제 실무에 CUPED를 정식으로 도입하려 합니다. 박시니어 씨가 말했습니다.
"단발성 분석이 아니라 파이프라인으로 만들어두면 좋아요. 앞으로 모든 실험에 자동으로 적용할 수 있도록요." 재사용 가능한 CUPED 클래스를 만들어봅시다.
실무에서는 CUPED를 재사용 가능한 파이프라인으로 구현하는 것이 좋습니다. 데이터 검증, 공변량 평가, CUPED 적용, 결과 분석까지 일련의 과정을 클래스로 캡슐화하면 실수를 줄이고 일관된 분석을 할 수 있습니다.
마치 공장의 생산 라인처럼, 데이터만 넣으면 자동으로 결과가 나오는 구조입니다.
다음 코드를 살펴봅시다.
import numpy as np
from scipy import stats
from dataclasses import dataclass
from typing import Optional
@dataclass
class CUPEDResult:
"""CUPED 분석 결과를 담는 클래스"""
original_effect: float
cuped_effect: float
original_pvalue: float
cuped_pvalue: float
variance_reduction: float
theta: float
class CUPEDAnalyzer:
def __init__(self):
self.theta = None
self.mean_pre = None
def fit(self, pre_data: np.ndarray, post_data: np.ndarray):
"""전체 데이터로 CUPED 파라미터를 학습합니다"""
cov = np.cov(pre_data, post_data)[0, 1]
var = np.var(pre_data)
self.theta = cov / var
self.mean_pre = np.mean(pre_data)
return self
def transform(self, pre_data: np.ndarray, post_data: np.ndarray) -> np.ndarray:
"""데이터에 CUPED 변환을 적용합니다"""
return post_data - self.theta * (pre_data - self.mean_pre)
def analyze(self, control_pre, control_post,
treatment_pre, treatment_post) -> CUPEDResult:
"""A/B 테스트 분석을 수행합니다"""
# 전체 데이터로 fit
all_pre = np.concatenate([control_pre, treatment_pre])
all_post = np.concatenate([control_post, treatment_post])
self.fit(all_pre, all_post)
# Transform
control_cuped = self.transform(control_pre, control_post)
treatment_cuped = self.transform(treatment_pre, treatment_post)
# 분석
_, p_orig = stats.ttest_ind(control_post, treatment_post)
_, p_cuped = stats.ttest_ind(control_cuped, treatment_cuped)
return CUPEDResult(
original_effect=np.mean(treatment_post) - np.mean(control_post),
cuped_effect=np.mean(treatment_cuped) - np.mean(control_cuped),
original_pvalue=p_orig,
cuped_pvalue=p_cuped,
variance_reduction=1 - np.var(control_cuped)/np.var(control_post),
theta=self.theta
)
# 사용 예시
analyzer = CUPEDAnalyzer()
김개발 씨가 CUPED 코드를 여러 번 복사 붙여넣기 하다 보니 문제가 생겼습니다. 어떤 분석에서는 세타를 전체 데이터로 계산했는데, 다른 분석에서는 실수로 그룹별로 계산해버렸습니다.
분석 결과의 일관성을 보장할 수 없게 된 것입니다. 박시니어 씨가 조언했습니다.
"이럴 때 클래스로 만들어두면 좋아요. 로직을 캡슐화하면 실수를 줄일 수 있어요." CUPEDAnalyzer 클래스의 구조를 살펴봅시다.
먼저 fit 메서드가 있습니다. 전체 데이터로 theta와 mean_pre를 계산합니다.
scikit-learn의 fit과 같은 개념입니다. 학습 단계에서 파라미터를 저장해둡니다.
다음으로 transform 메서드가 있습니다. 저장된 파라미터를 사용해 데이터를 변환합니다.
fit에서 계산한 theta를 사용하므로 일관된 변환이 보장됩니다. 마지막으로 analyze 메서드가 있습니다.
fit과 transform을 내부적으로 호출하고, t-test까지 수행해 결과를 반환합니다. 사용자는 이 메서드만 호출하면 됩니다.
CUPEDResult 데이터 클래스도 중요합니다. 분석 결과를 구조화된 형태로 반환합니다.
원본 효과, CUPED 효과, p-value들, 분산 감소율, theta 값이 모두 포함되어 있습니다. 보고서를 작성하거나 로깅할 때 유용합니다.
실무에서는 이 클래스에 추가 기능을 넣을 수 있습니다. 예를 들어, 입력 검증 로직을 넣을 수 있습니다.
실험 전 데이터와 실험 후 데이터의 길이가 같은지, 결측치는 없는지 확인합니다. 공변량 평가 기능도 추가할 수 있습니다.
상관계수와 예상 분산 감소율을 미리 계산해 보여줍니다. 더 나아가 자동 리포트 생성 기능도 가능합니다.
분석 결과를 보기 좋은 형태로 정리해주는 메서드를 추가할 수 있습니다. 그래프를 그리거나 HTML 보고서를 생성하는 기능도 넣을 수 있습니다.
김개발 씨가 클래스를 완성하고 테스트해봤습니다. 이제 어떤 실험 데이터든 analyzer.analyze()만 호출하면 됩니다.
코드가 깔끔해지고 실수도 줄었습니다. 박시니어 씨가 만족스럽게 고개를 끄덕였습니다.
"이제 팀 전체가 쓸 수 있게 공유해봐요."
실전 팁
💡 - fit-transform 패턴으로 구현하면 scikit-learn과 유사하게 사용할 수 있습니다
- 데이터 검증 로직을 추가해 실수를 방지하세요
- 결과를 데이터 클래스로 반환하면 후처리가 편리합니다
9. CUPED 한계와 대안
CUPED를 도입한 지 몇 달이 지났습니다. 김개발 씨는 대부분의 실험에서 좋은 결과를 얻고 있었습니다.
하지만 어느 날 이상한 케이스를 발견했습니다. 신규 가입자 대상 실험에서 CUPED를 적용했는데, 분산 감소 효과가 거의 없었습니다.
무엇이 문제였을까요?
CUPED는 강력한 기법이지만 모든 상황에 적합한 것은 아닙니다. 신규 사용자처럼 실험 전 데이터가 없는 경우, 공변량과 결과의 상관관계가 낮은 경우에는 효과가 제한적입니다.
마치 내비게이션이 아무리 좋아도 목적지 정보가 없으면 안내할 수 없는 것과 같습니다. 이런 경우 층화(Stratification)나 CUPAC 같은 대안을 고려해야 합니다.
다음 코드를 살펴봅시다.
import numpy as np
from scipy import stats
def stratified_analysis(control_data, treatment_data, strata):
"""층화 분석을 수행합니다"""
unique_strata = np.unique(strata)
weighted_effects = []
weights = []
for s in unique_strata:
# 해당 층의 데이터 추출
mask_control = (strata[:len(control_data)] == s)
mask_treatment = (strata[len(control_data):] == s)
control_stratum = control_data[mask_control]
treatment_stratum = treatment_data[mask_treatment]
if len(control_stratum) > 0 and len(treatment_stratum) > 0:
effect = np.mean(treatment_stratum) - np.mean(control_stratum)
weight = len(control_stratum) + len(treatment_stratum)
weighted_effects.append(effect * weight)
weights.append(weight)
# 가중 평균 효과
overall_effect = sum(weighted_effects) / sum(weights)
print(f"층화 분석 결과")
print(f"층 개수: {len(unique_strata)}")
print(f"전체 효과: {overall_effect:.3f}")
return overall_effect
# CUPED가 적합하지 않은 경우 체크
def check_cuped_suitability(pre_data, post_data, threshold=0.3):
"""CUPED 적합성을 검사합니다"""
correlation = np.corrcoef(pre_data, post_data)[0, 1]
variance_reduction = correlation ** 2
print(f"상관계수: {correlation:.3f}")
print(f"예상 분산 감소율: {variance_reduction:.1%}")
if np.isnan(correlation):
print("경고: 공변량이 없거나 분산이 0입니다.")
return False
elif correlation < threshold:
print(f"경고: 상관계수가 {threshold} 미만입니다. CUPED 효과가 제한적입니다.")
print("대안: 층화(Stratification) 또는 다른 공변량을 고려하세요.")
return False
else:
print("CUPED 적용이 적합합니다.")
return True
# 신규 사용자 케이스 (공변량 없음)
print("=== 신규 사용자 케이스 ===")
new_user_pre = np.zeros(100) # 신규 사용자는 이전 데이터가 없음
new_user_post = np.random.normal(50, 20, 100)
check_cuped_suitability(new_user_pre, new_user_post)
김개발 씨가 당황했습니다. 신규 가입자 대상 A/B 테스트에서 CUPED를 적용했는데, 분산 감소율이 0%에 가까웠습니다.
박시니어 씨가 데이터를 살펴보며 말했습니다. "아, 신규 사용자니까 실험 전 행동 데이터가 없잖아요.
CUPED의 전제 조건이 충족되지 않은 거예요." CUPED의 한계를 정리해봅시다. 첫째, 실험 전 데이터가 필수입니다.
신규 사용자, 신규 기능, 첫 방문자 대상 실험에서는 실험 전 행동 데이터가 없습니다. 공변량이 없으면 CUPED를 적용할 수 없습니다.
둘째, 상관관계가 낮으면 효과도 낮습니다. 상관계수가 0.3 미만이면 분산 감소율이 9% 미만입니다.
이 정도면 CUPED를 적용하는 의미가 거의 없습니다. 셋째, 비선형 관계에는 최적이 아닙니다.
CUPED는 선형 회귀 기반이므로, 공변량과 결과 간의 관계가 비선형이면 분산 감소 효과가 제한됩니다. 이런 경우 어떤 대안이 있을까요?
**층화(Stratification)**가 대표적인 대안입니다. 사용자를 특성에 따라 그룹으로 나누고, 각 그룹 내에서 분석한 후 가중 평균을 구합니다.
예를 들어 디바이스 타입(iOS/Android), 국가, 가입 경로 등으로 층화할 수 있습니다. 위의 코드에서 check_cuped_suitability 함수를 보세요.
CUPED를 적용하기 전에 적합성을 검사합니다. 상관계수가 threshold(기본 0.3) 미만이면 경고를 출력하고 대안을 제안합니다.
이런 체크 로직을 파이프라인에 포함시키면 부적합한 상황에서 CUPED를 오용하는 것을 방지할 수 있습니다. CUPAC(Controlled Using Predictions As Covariates)도 있습니다.
CUPED가 실험 전 데이터를 직접 사용하는 반면, CUPAC은 머신러닝 모델의 예측값을 공변량으로 사용합니다. 여러 특성을 조합한 예측 모델을 만들면 단일 공변량보다 높은 상관관계를 얻을 수 있습니다.
김개발 씨가 물었습니다. "그럼 신규 사용자 실험은 어떻게 해야 하나요?" 박시니어 씨가 답했습니다.
"가입 경로나 마케팅 채널 같은 사용자 속성으로 층화 분석을 해보세요. 또는 가입 후 첫 1일 데이터를 공변량으로 써서 2일차부터의 행동을 분석할 수도 있어요."
실전 팁
💡 - CUPED 적용 전 항상 적합성 검사를 수행하세요
- 상관계수 0.3 미만이면 다른 방법을 고려하세요
- 신규 사용자 실험에는 층화 분석이 효과적입니다
10. 실전 케이스 스터디
CUPED의 모든 이론을 마스터한 김개발 씨. 드디어 실제 프로젝트에 적용할 시간이 왔습니다.
회사에서 새로운 추천 알고리즘을 테스트하는 A/B 테스트를 진행하게 되었습니다. 2주간의 실험을 통해 구매 전환율 향상 효과를 측정하려고 합니다.
처음부터 끝까지 실전 분석을 진행해봅시다.
실전 케이스 스터디를 통해 CUPED 분석의 전체 흐름을 익힙니다. 데이터 준비부터 공변량 선택, CUPED 적용, 결과 해석까지 실무에서 거치는 모든 단계를 다룹니다.
마치 요리 레시피처럼, 각 단계를 순서대로 따라가면 누구나 CUPED 분석을 수행할 수 있습니다.
다음 코드를 살펴봅시다.
import numpy as np
import pandas as pd
from scipy import stats
# 1. 데이터 준비 (실제로는 DB에서 추출)
np.random.seed(42)
n_users = 2000
data = {
'user_id': range(n_users),
'group': ['control'] * (n_users // 2) + ['treatment'] * (n_users // 2),
'pre_purchases': np.random.poisson(3, n_users), # 실험 전 2주 구매 횟수
'pre_revenue': np.random.gamma(2, 50, n_users), # 실험 전 2주 매출
}
# 실험 결과: 실험군에 10% 매출 상승 효과
base_revenue = 0.6 * data['pre_revenue'] + np.random.gamma(2, 30, n_users)
effect = np.where(np.array(data['group']) == 'treatment',
base_revenue * 0.1, 0)
data['post_revenue'] = base_revenue + effect
df = pd.DataFrame(data)
# 2. 공변량 평가
print("=== Step 1: 공변량 평가 ===")
corr = df['pre_revenue'].corr(df['post_revenue'])
print(f"pre_revenue 상관계수: {corr:.3f}")
print(f"예상 분산 감소율: {corr**2:.1%}")
# 3. 그룹 균형 확인
print("\n=== Step 2: 그룹 균형 확인 ===")
control = df[df['group'] == 'control']
treatment = df[df['group'] == 'treatment']
_, p = stats.ttest_ind(control['pre_revenue'], treatment['pre_revenue'])
print(f"실험 전 매출 차이 p-value: {p:.3f}")
print("균형 여부:", "OK" if p > 0.05 else "경고: 그룹 불균형")
# 4. CUPED 적용
print("\n=== Step 3: CUPED 분석 ===")
theta = np.cov(df['pre_revenue'], df['post_revenue'])[0,1] / np.var(df['pre_revenue'])
mean_pre = df['pre_revenue'].mean()
df['post_cuped'] = df['post_revenue'] - theta * (df['pre_revenue'] - mean_pre)
# 5. 결과 비교
control_cuped = df[df['group'] == 'control']['post_cuped']
treatment_cuped = df[df['group'] == 'treatment']['post_cuped']
_, p_cuped = stats.ttest_ind(control_cuped, treatment_cuped)
print(f"CUPED 효과: {treatment_cuped.mean() - control_cuped.mean():.2f}")
print(f"CUPED p-value: {p_cuped:.4f}")
print(f"분산 감소율: {1 - df['post_cuped'].var()/df['post_revenue'].var():.1%}")
김개발 씨 앞에 실제 데이터가 펼쳐져 있습니다. 2000명의 사용자, 절반은 통제군, 절반은 실험군.
새로운 추천 알고리즘이 매출에 어떤 영향을 미치는지 분석해야 합니다. Step 1: 공변량 평가 가장 먼저 할 일은 공변량을 선택하고 평가하는 것입니다.
실험 전 2주간의 매출 데이터(pre_revenue)를 공변량 후보로 검토합니다. 상관계수를 계산해보니 0.6 정도가 나왔습니다.
예상 분산 감소율은 36%입니다. CUPED를 적용하기에 충분한 수준입니다.
Step 2: 그룹 균형 확인 다음으로 통제군과 실험군이 실험 전에 균형 잡혀 있는지 확인합니다. 무작위 배정이 제대로 되었다면 실험 전 데이터의 그룹 간 차이가 유의하지 않아야 합니다.
t-test 결과 p-value가 0.05보다 크면 균형이 잡힌 것입니다. 만약 균형이 안 잡혀 있다면 어떻게 해야 할까요?
무작위 배정에 문제가 있었을 수 있습니다. 이 경우 실험 설계를 다시 검토하거나, 층화 분석으로 보정해야 합니다.
다행히 이번 데이터는 균형이 잘 잡혀 있습니다. Step 3: CUPED 적용 전체 데이터로 theta를 계산하고, 각 사용자의 매출을 조정합니다.
코드의 4번 섹션이 이 과정입니다. theta와 mean_pre를 전체 데이터로 계산한 후, 모든 사용자에게 동일하게 적용합니다.
Step 4: 결과 해석 CUPED 적용 후 t-test를 수행합니다. p-value가 어떻게 변했는지 확인합니다.
이번 케이스에서는 원본 p-value보다 CUPED p-value가 훨씬 작아졌습니다. 효과 크기도 확인합니다.
실험군의 조정된 매출이 통제군보다 얼마나 높은지 계산합니다. 이 값이 실제 실험 효과의 추정치입니다.
분산 감소율도 검증합니다. 예상대로 약 36% 정도 줄어들었다면 CUPED가 제대로 적용된 것입니다.
김개발 씨가 결과를 보고서로 정리했습니다. "새로운 추천 알고리즘은 매출을 약 10% 향상시켰으며, 통계적으로 유의합니다(p < 0.05).
CUPED를 적용해 분산을 36% 줄인 결과입니다." 박시니어 씨가 보고서를 검토하며 고개를 끄덕였습니다. "훌륭해요.
이제 자신 있게 경영진에게 발표할 수 있겠네요."
실전 팁
💡 - 분석 전 체크리스트를 만들어 단계를 빠뜨리지 않도록 하세요
- 그룹 균형 확인은 필수입니다
- 결과 보고 시 분산 감소율도 함께 언급하면 신뢰도가 높아집니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.
AWS Organizations 완벽 가이드
여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.
AWS KMS 암호화 완벽 가이드
AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.
AWS Secrets Manager 완벽 가이드
AWS에서 데이터베이스 비밀번호, API 키 등 민감한 정보를 안전하게 관리하는 Secrets Manager의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.