inblog logo
|
hollyisyoon
    스터디

    [AB] 찐 ab테스트의 모든 것

    tds 아티클 번역 및 정리
    Aug 18, 2023
    [AB] 찐 ab테스트의 모든 것
    notion image
    해당 아티클에서 다루고 있는 목차는 다음과 같은데 가장 기본적인 건 제끼고 볼드 표시 챕터만 요약을 해봤습니다. 필요하시면 원문을 꼭 읽어보시길 추천합니다!
     
    • A/B 테스트란 무엇인가
    • A/B 테스트를 하기 전에 분명하게 해야 할 질문
    • 목표 지표 선택
    • 테스트의 가설
    • 테스트 디자인(power analysis)
    • 샘플 사이즈, 실험기간 계산
    • 통계적 테스트 (t-test, z-test, 카이제곱 검정)
    • 파이썬으로 A/B 테스트 결과 분석
    • 부트스트랩
    • 유의성(통계적vs실용적)
    • A/B테스트의 품질(신뢰도, 타당도, 효능)
    • A/B 테스트에서 흔한 문제
    • 윤리, 개인정보 보호 A/B 테스트
     

    1. 목표 지표(Success Metric) 선택

    • 모든 지표들이 일정하게 유지된다는 가정 하에, 해당 지표가 크게 증가하면 목표를 달성했다고 볼 수 있나(문제를 해결했나)??
    • but, 하나의 목표 지표만 바라보면 안되고 나머지 지표들엔 어떤 변화가 있는지 모니터링할 필요가 있다.
      • Sub Metric : 메인 지표를 보조할 수 있는 지표
      • Guardrail Metric : 떨어지면 안되는 지표(서비스에 악영향을 줬는지 파악 용도)
    • 보편적으로 많이 보는 A/B 테스트 지표
      • CTR
        • notion image
      • CTP
        • notion image
      • CVR
        • notion image

    2. 테스트의 가설

    • 가설의 기본 컨셉은 KPI에 영향을 미치는 제품의 잠재적 문제를 ‘fix’할 수 있는가
    • 가설의 예
      • KPI가 추천 시스템의 품질 개선. 기존 추천시스템에 XGBoost Re-ranker 모델 추가시 추천의 CTR이 증가할 것. 추천 품질이 향상 될 것이다. 측정 지표 : CTR
     

    3. A/B 테스트 디자인

    • 통계적 가설
    확률이 같다
    확률이 같다
    평균이 같다
    평균이 같다
    • 검증력 분석(Power Analysis)
      • 모집단에 대해 일반화할 수 있는지 확인을 할 수 있도록 ‘충분한’ 양의 데이터를 수집해야함
      • Power Analysis의 3가지 단계 : 검정력(power), 유의수준 결정, MDE(Minimum Detectable Effect)
      • 검정력 : 귀무가설을 올바르게 기각할 확률(거짓일 때 거짓으로 판단) = 1-beta = 2종 오류를 범하지 않을 확률 ; 일반적으로 80%로 설정함
      • 유의수준(Significance level) : 1종 오류의 확률로 일반적으로 0.05 사용 (비효율적인 기능에 대한 리소스 낭비를 방지하는 차원에서 유의수준을 엄격하게 정하기도 함)
      • MDE : 비즈니스 관점에서 투자 가치가 있다고 판단할 수 있는 최소 효과크기는 어느정도인지
    • 최소 샘플 사이즈 계산
      • 이진 지표(Binary Metric) : 베르누이 시행을 따르기 때문에, 이항분포를 따른다고 본다
        notion image
        notion image
        연속 지표(Continuous Metric) : 연속 지표의 평균의 샘플링은 중심극한정리에 따라 정규분포를 따른다고 본다
        notion image
        notion image
    • 기간
      • 일반적으로 앞에 고려한 최소 샘플 사이즈를 하루 방문자수로 나누어서 계산. 그러나 비즈니스 측면 고려 필요. 만약 코로나처럼 큰 external event 발생시, 실험 결과가 부정확해질 수 있음
        • notion image
      • 너무 짧으면 → Novelty Effect : 사용자는 초기 변화에 긍정적으로 반응하곤 한다. 시간이 지남에 따라 사라질 수 있다. A/B 테스트 외적 타당성에 영향을 줄 수 있음.
      • 너무 길면 → Maturation Effect : 너무 길어지면, 사용자의 반응이 다른 외부적 요인들에 의해 달라질 수 있다.
    • A/B 테스트 실행
      • 실험군, 대조군 사이에 무결성이 유지됬는지 보증 필요(engineering)
      • 최소 샘플 크기에 도달하지 않은 상태에서 통계적 유의성을 판단하면 안됨 (early stopping)
     

    4. 파이썬으로 A/B 테스트 결과 분석

    🛠
    -통계 분석방법 선택, 계산 -유의성 확인(p-value) -오차한계 계산(실험의 외부 유효성), 신뢰구간 계산(외적 타당도, 실제 유의성)
    • 분석방법에 대한 결정은 다음과 같은 요소에 따라 달라짐 :
      • 목표 지표의 형태 (PDF ; 연속 확률변수의 분포가 어떻게 되는지)
      • 샘플 사이즈
      • 통계적 가설의 성격
    • 모수적 방법 : 2 Sample T-test, 2 Sample Z-test
    • 비모수적 방법 : Fisher Exact test, Chi-Squared test, Wilcoxon Rank Sum/Mann Whitney test(Skewed 샘플링 분포, 중앙값 비교)
     
    T-test code
    import numpy as np from scipy.stats import t N_con = 20 df_con = N_con - 1 # degrees of freedom of Control N_exp = 20 df_exp = N_exp - 1 # degrees of freedom of Experimental # Significance level alpha = 0.05 # data of control group with t-distribution X_con = np.random.standard_t(df_con,N_con) # data of experimental group with t-distribution X_exp = np.random.standard_t(df_exp,N_exp) # mean of control mu_con = np.mean(X_con) # mean of experimental mu_exp = np.mean(X_exp) # variance of control sigma_sqr_con = np.var(X_con) #variance of control sigma_sqr_exp = np.var(X_exp) # pooled variance pooled_variance_t_test = ((N_con-1)*sigma_sqr_con + (N_exp -1) * sigma_sqr_exp)/(N_con + N_exp-2)*(1/N_con + 1/N_exp) # Standard Error SE = np.sqrt(pooled_variance_t_test) # Test Statistics T = (mu_con-mu_exp)/SE # Critical value for two sided 2 sample t-test t_crit = t.ppf(1-alpha/2, N_con + N_exp - 2) # P-value of the two sided T-test using t-distribution and its symmetric property p_value = t.sf(T, N_con + N_exp - 2)*2 # Margin of Error margin_error = t_crit * SE # Confidence Interval CI = [(mu_con-mu_exp) - margin_error, (mu_con-mu_exp) + margin_error] print("T-score: ", T) print("T-critical: ", t_crit) print("P_value: ", p_value) print("Confidence Interval of 2 sample Z-test: ", np.round(CI,2))
    Z-test(비율 비교) code
    import numpy as np from scipy.stats import norm X_con = 1242 #clicks control N_con = 9886 #impressions control X_exp = 974 #clicks experimental N_exp = 10072 #impressions experimetal # Significance Level alpha = 0.05 p_con_hat = X_con / N_con p_exp_hat = X_exp / N_exp p_pooled_hat = (X_con + X_exp)/(N_con + N_exp) pooled_variance = p_pooled_hat*(1-p_pooled_hat) * (1/N_con + 1/N_exp) # Standard Error SE = np.sqrt(pooled_variance) # test statsitics Test_stat = (p_con_hat - p_exp_hat)/SE # critical value usig the standard normal distribution Z_crit = norm.ppf(1-alpha/2) # Margin of error m = SE * Z_crit # two sided test and using symmetry property of Normal distibution so we multiple with 2 p_value = norm.sf(Test_stat)*2 # Confidence Interval CI = [(p_con_hat-p_exp_hat) - SE * Z_crit, (p_con_hat-p_exp_hat) + SE * Z_crit] if np.abs(Test_stat) >= Z_crit: print("reject the null") print(p_value) print("Test Statistics stat: ", Test_stat) print("Z-critical: ", Z_crit) print("P_value: ", p_value) print("Confidence Interval of 2 sample Z-test for proportions: ", np.round(CI,2)) import matplotlib.pyplot as plt z = np.arange(-3,3, 0.1) plt.plot(z, norm.pdf(z), label = 'Standard Normal Distribution',color = 'purple',linewidth = 2.5) plt.fill_between(z[z>Z_crit], norm.pdf(z[z>Z_crit]), label = 'Right Rejection Region',color ='y' ) plt.fill_between(z[z<(-1)*Z_crit], norm.pdf(z[z<(-1)*Z_crit]), label = 'Left Rejection Region',color ='y' ) plt.title("Two Sample Z-test rejection region") plt.legend() plt.show()
     
    Z-test(평균 비교) code
    import numpy as np from scipy.stats import norm N_con = 60 N_exp = 60 # Significance Level alpha = 0.05 X_A = np.random.randint(100, size = N_con) X_B = np.random.randint(100, size = N_exp) # Calculating means of control and experimental groups mu_con = np.mean(X_A) mu_exp = np.mean(X_B) variance_con = np.var(X_A) variance_exp = np.var(X_B) # Pooled Variance pooled_variance = np.sqrt(variance_con/N_con + variance_exp/N_exp) # Test statistics T = (mu_con-mu_exp)/np.sqrt(variance_con/N_con + variance_exp/N_exp) # two sided test and using symmetry property of Normal distibution so we multiple with 2 p_value = norm.sf(T)*2 # Z-critical value Z_crit = norm.ppf(1-alpha/2) # Margin of error m = Z_crit*pooled_variance # Confidence Interval CI = [(mu_con - mu_exp) - m, (mu_con - mu_exp) + m] print("Test Statistics stat: ", T) print("Z-critical: ", Z_crit) print("P_value: ", p_value) print("Confidence Interval of 2 sample Z-test for proportions: ", np.round(CI,2)) import matplotlib.pyplot as plt z = np.arange(-3,3, 0.1) plt.plot(z, norm.pdf(z), label = 'Standard Normal Distribution',color = 'purple',linewidth = 2.5) plt.fill_between(z[z>Z_crit], norm.pdf(z[z>Z_crit]), label = 'Right Rejection Region',color ='y' ) plt.fill_between(z[z<(-1)*Z_crit], norm.pdf(z[z<(-1)*Z_crit]), label = 'Left Rejection Region',color ='y' ) plt.title("Two Sample Z-test rejection region") plt.legend() plt.show()
    카이제곱 검정 code
    import numpy as np from scipy.stats import chi2 O = np.array([86, 83, 5810,3920]) T = np.array([105,65,5781, 3841]) # Squared_relative_distance def calculate_D(O,T): D_sum = 0 for i in range(len(O)): D_sum += (O[i] - T[i])**2/T[i] return(D_sum) D = calculate_D(O,T) p_value = chi2.sf(D, df = 1) import matplotlib.pyplot as plt # Step 1: pick a x-axis range like in case of z-test (-3,3,0.1) d = np.arange(0,5,0.1) # Step 2: drawing the initial pdf of chi-2 with df = 1 and x-axis d range we just created plt.plot(d, chi2.pdf(d, df = 1), color = "purple") # Step 3: filling in the rejection region plt.fill_between(d[d>D], chi2.pdf(d[d>D], df = 1), color = "y") # Step 4: adding title plt.title("Two Sample Chi-2 Test rejection region") # Step 5: showing the plt graph plt.show()
     
    • 비모수검정은 표준오차, 신뢰구간등에 대한 계산이 간단하지 않음. 두집단의 평균차를 부트스트랩 기법을 통해 신뢰구간을 검증해볼 수 있음. 이 때, 부트스트랩은 추정량의 오차 범위를 파악하기 위해 사용하는 방법임.
    부트스트랩 code
    import numpy as np import pandas as pd from scipy.stats import norm N = 100 X = pd.Series(np.random.binomial(300,0.7,size =N)) Y1 = np.repeat("Exp",N/2) N_exp = len(Y1) Y2 = np.repeat("Cont",N/2) N_con = len(Y2) Y = pd.Series(np.append(Y1,Y2)) data = pd.concat([X,Y],axis = 1) print(data) means_per_group = data.groupby(1, group_keys = False)[0].mean() medians_per_group = data.groupby(1)[0].median() alpha = 0.05 def Bootrapping_for_diff_means_medians(data,B): boot_mean_diff = [] boot_medians_diff = [] boot_means_con = [] boot_means_exp = [] count_num_positives_meandiff = 0 count_num_positives_mediandiff = 0 for i in range(B): boot_sample = data.sample(frac = 1, replace = True) #means of bootstrap sample for control and experimental group boot_means_per_group = boot_sample.groupby(1)[0].mean() boot_sample_mean_con = boot_means_per_group["Cont"] boot_sample_mean_exp = boot_means_per_group["Exp"] boot_means_con.append(boot_sample_mean_con) boot_means_exp.append(boot_sample_mean_exp) # calculating the difference in means per bootstrap sample diff_means = boot_sample_mean_exp - boot_sample_mean_con #counting number of times is the difference positive if diff_means > 0: count_num_positives_meandiff += 1 # medians of bootstrap sample for control and experimental group boot_medians_per_group = boot_sample.groupby(1)[0].median() # calculating the difference in medians per bootstrap sample diff_medians = boot_medians_per_group["Exp"] - boot_medians_per_group["Cont"] if diff_medians > 0: count_num_positives_mediandiff += 1 boot_mean_diff.append(diff_means) boot_medians_diff.append(diff_medians) return(boot_means_con,boot_means_exp,count_num_positives_meandiff,count_num_positives_mediandiff,boot_mean_diff) B = 10000 X_bars_con,X_bars_exp ,n_means, n_medians,boot_mean_diff = Bootrapping_for_diff_means_medians(data,B) **Z_mean = np.mean(X_bars_exp)- np.mean(X_bars_con) Z_sigma = np.sqrt((np.var(X_bars_exp)/N_exp + np.var(X_bars_con)/N_con)) CI = [Z_mean - norm.ppf(1-alpha/2)*Z_sigma, Z_mean + norm.ppf(1-alpha/2)*Z_sigma]** print("Mean of X_bar_exp - X_bar_con", Z_mean) print("Standard Error of X_bar_exp - X_bar_con", Z_sigma) print("CI of X_bar_exp - X_bar_con", CI) p_value_diff_means = n_means/B p_value_diff_medians = n_medians/B CI = np.percentile(boot_mean_diff, [2.5, 97.5]) import matplotlib.pyplot as plt counts,bins,ignored = plt.hist(boot_mean_diff,50,density = True,color = 'purple') plt.xlabel("mean difference") plt.title("Distribution of Bootstrapped samples mean difference") plt.show()
     

    5. 통계적 유의성 vs 실제적 유의성

    • 통계적 유의성만으로는 기능, 제품 출시에 대한 의사결정을 만들기에 충분하지 않음.
    • 따라서 두 그룹간의 차이(성과)가 투자를 정당화할만큼 충분히 큰지 이해해야함.
    • 신뢰구간의 하한과 MDE(경제적 유의성 추정치)를 비교. 예를 들어 CI = [5%, 7.5%] MDE = 3% 이면 5% > 3% 이니까 실용적인 의미가 있음.
    • CI의 너비가너무 넓으면 결과의 정확도가 낮음
     
     
     
    next step
    Fundamentals Of Statistics For Data Scientists and Data Analysts
    Key statistical concepts for your data science or data analytics journey
    Fundamentals Of Statistics For Data Scientists and Data Analysts
    https://towardsdatascience.com/fundamentals-of-statistics-for-data-scientists-and-data-analysts-69d93a05aae7
    Fundamentals Of Statistics For Data Scientists and Data Analysts
    Share article

    hollyisyoon

    RSS·Powered by Inblog