A/B ํ ์คํธ
์ค๊ธ๊ณ ๊ธ
ํ์ต ๋ชฉํ
- A/B ํ ์คํธ ์ค๊ณ
- ํ๋ณธ ํฌ๊ธฐ ์ฐ์
- ์ ํ์จ/ํ๊ท ๋น๊ต ๊ฒ์
- ๊ฒฐ๊ณผ ํด์
0. ์ฌ์ ์ค๋น (Setup)
๋ฐ์ดํฐ ์ค์ต์ ์ํด CSV ํ์ผ์ ๋ก๋ํฉ๋๋ค.
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from scipy import stats
# Load Data
orders = pd.read_csv('src_orders.csv', parse_dates=['created_at'])
items = pd.read_csv('src_order_items.csv')
products = pd.read_csv('src_products.csv')
users = pd.read_csv('src_users.csv')
# Merge for Analysis
df = orders.merge(items, on='order_id').merge(products, on='product_id').merge(users, on='user_id')
# Simulate AB Test Data for Examples
np.random.seed(42)
df['experiment_group'] = np.random.choice(['control', 'treatment'], size=len(df))
# Add slight effect to treatment
df.loc[df['experiment_group'] == 'treatment', 'sale_price'] *= 1.05
df['order_amount'] = df['sale_price'] # Alias for example1. A/B ํ ์คํธ๋?
์ ์
A/B ํ ์คํธ๋ ๋ ๊ฐ์ง ๋ฒ์ (A: ๋์กฐ๊ตฐ, B: ์คํ๊ตฐ)์ ๋น๊ตํ์ฌ ์ด๋ ๊ฒ์ด ๋ ํจ๊ณผ์ ์ธ์ง ๊ฒ์ฆํ๋ ์คํ์ ๋๋ค.
ํ์ฉ ์์
- ์น์ฌ์ดํธ ๋ฒํผ ์์
- ์ด๋ฉ์ผ ์ ๋ชฉ
- ๊ฐ๊ฒฉ ์ ์ฑ
- ์ถ์ฒ ์๊ณ ๋ฆฌ์ฆ
2. ํ๋ณธ ํฌ๊ธฐ ์ฐ์
๊ฒ์ ๋ ฅ ๋ถ์
์คํ ์ ์ ํ์ํ ํ๋ณธ ํฌ๊ธฐ๋ฅผ ๊ณ์ฐํฉ๋๋ค.
from statsmodels.stats.power import TTestIndPower
# ํ๋ผ๋ฏธํฐ
effect_size = 0.2 # ํจ๊ณผ ํฌ๊ธฐ (์์: 0.2, ์ค๊ฐ: 0.5, ํผ: 0.8)
alpha = 0.05 # ์ ์์์ค
power = 0.8 # ๊ฒ์ ๋ ฅ
# ํ๋ณธ ํฌ๊ธฐ ๊ณ์ฐ
analysis = TTestIndPower()
sample_size = analysis.solve_power(
effect_size=effect_size,
power=power,
alpha=alpha,
alternative='two-sided'
)
print(f"ํ์ ํ๋ณธ ํฌ๊ธฐ (๊ทธ๋ฃน๋น): {int(sample_size)}")
print(f"์ด ํ์ ํ๋ณธ: {int(sample_size * 2)}")์คํ ๊ฒฐ๊ณผ
ํ์ ํ๋ณธ ํฌ๊ธฐ (๊ทธ๋ฃน๋น): 393 ์ด ํ์ ํ๋ณธ: 786
3. ์ ํ์จ ๋น๊ต (๋น์จ ๊ฒ์ )
Z-๊ฒ์
from statsmodels.stats.proportion import proportions_ztest
# ๋ฐ์ดํฐ
# A๊ทธ๋ฃน: 1000๋ช
์ค 50๋ช
์ ํ
# B๊ทธ๋ฃน: 1000๋ช
์ค 65๋ช
์ ํ
conversions = [50, 65]
n_observations = [1000, 1000]
# Z-๊ฒ์
z_stat, p_value = proportions_ztest(conversions, n_observations, alternative='two-sided')
# ์ ํ์จ
rate_a = conversions[0] / n_observations[0]
rate_b = conversions[1] / n_observations[1]
lift = (rate_b - rate_a) / rate_a * 100
print(f"A๊ทธ๋ฃน ์ ํ์จ: {rate_a:.2%}")
print(f"B๊ทธ๋ฃน ์ ํ์จ: {rate_b:.2%}")
print(f"์๋์ ์ฆ๊ฐ: {lift:.1f}%")
print(f"p-value: {p_value:.4f}")
if p_value < 0.05:
print("โ B๊ทธ๋ฃน์ด ์ ์๋ฏธํ๊ฒ ๋ ์ข์!")
else:
print("โ ์ ์๋ฏธํ ์ฐจ์ด ์์")์คํ ๊ฒฐ๊ณผ
A๊ทธ๋ฃน ์ ํ์จ: 5.00% B๊ทธ๋ฃน ์ ํ์จ: 6.50% ์๋์ ์ฆ๊ฐ: 30.0% p-value: 0.1496 โ ์ ์๋ฏธํ ์ฐจ์ด ์์
4. ํ๊ท ๋น๊ต (t-๊ฒ์ )
์ฃผ๋ฌธ ๊ธ์ก ๋น๊ต
from scipy import stats
# ๊ทธ๋ฃน๋ณ ๋ฐ์ดํฐ
group_a = df[df['experiment_group'] == 'control']['order_amount']
group_b = df[df['experiment_group'] == 'treatment']['order_amount']
# t-๊ฒ์
t_stat, p_value = stats.ttest_ind(group_a, group_b)
print(f"A๊ทธ๋ฃน ํ๊ท : ${group_a.mean():.2f}")
print(f"B๊ทธ๋ฃน ํ๊ท : ${group_b.mean():.2f}")
print(f"์ฐจ์ด: ${group_b.mean() - group_a.mean():.2f}")
print(f"p-value: {p_value:.4f}")
# ํจ๊ณผ ํฌ๊ธฐ (Cohen's d)
pooled_std = np.sqrt((group_a.std()**2 + group_b.std()**2) / 2)
cohens_d = (group_b.mean() - group_a.mean()) / pooled_std
print(f"Cohen's d: {cohens_d:.3f}")์คํ ๊ฒฐ๊ณผ
A๊ทธ๋ฃน ํ๊ท : $59.49 B๊ทธ๋ฃน ํ๊ท : $62.97 ์ฐจ์ด: $3.48 p-value: 0.0000 Cohen's d: 0.050
5. ๊ฒฐ๊ณผ ํด์
์์ฌ๊ฒฐ์ ํ๋ ์์ํฌ
1. p < 0.05 ์ธ๊ฐ?
- No โ ์ ์๋ฏธํ ์ฐจ์ด ์์, ์ถ๊ฐ ์คํ ํ์
- Yes โ ๋ค์ ๋จ๊ณ๋ก
2. ํจ๊ณผ ํฌ๊ธฐ๊ฐ ์ค๋ฌด์ ์ผ๋ก ์๋ฏธ์๋๊ฐ?
- ์ ํ์จ 0.1% ์ฆ๊ฐ vs 10% ์ฆ๊ฐ
- ๋น์ฆ๋์ค ์ํฉํธ ๊ณ์ฐ
3. ๋น์ฉ ๋๋น ํจ๊ณผ๊ฐ ์๋๊ฐ?
- ๊ตฌํ ๋น์ฉ
- ์์ ์์ต ์ฆ๊ฐ์ฃผ์์ฌํญ
โ ๏ธ
A/B ํ
์คํธ ์ฃผ์์ฌํญ
- ํผํน(Peeking): ์คํ ๋์ค ๊ฒฐ๊ณผ ํ์ธ ํ ์กฐ๊ธฐ ์ข ๋ฃ ๊ธ์ง
- ๋ค์ค ๋น๊ต: ์ฌ๋ฌ ์งํ ๋์ ๊ฒ์ ์ ๋ณด์ ํ์ (Bonferroni)
- ๋ ธ์ถ ํธํฅ: ๊ทธ๋ฃน ๊ฐ ํน์ฑ ๋ถ๊ท ํ ํ์ธ
- ์ธ๋ถ ์์ธ: ์์ฆ, ํ๋ก๋ชจ์ ๋ฑ ์ํฅ ๊ณ ๋ ค
ํด์ฆ
๋ฌธ์
A/B ํ ์คํธ ๊ฒฐ๊ณผ๊ฐ ๋ค์๊ณผ ๊ฐ์ ๋, ์ ๋์์ธ(B)์ ์ ์ฉํด์ผ ํ ๊น์?
- A๊ทธ๋ฃน: 5000๋ช , ์ ํ 150๋ช
- B๊ทธ๋ฃน: 5000๋ช , ์ ํ 175๋ช
์ ๋ต ๋ณด๊ธฐ
from statsmodels.stats.proportion import proportions_ztest
conversions = [150, 175]
n = [5000, 5000]
z_stat, p_value = proportions_ztest(conversions, n)
rate_a = 150/5000
rate_b = 175/5000
lift = (rate_b - rate_a) / rate_a * 100
print(f"A ์ ํ์จ: {rate_a:.2%}")
print(f"B ์ ํ์จ: {rate_b:.2%}")
print(f"์๋์ ์ฆ๊ฐ: {lift:.1f}%")
print(f"p-value: {p_value:.4f}")
if p_value < 0.05:
print("\n๊ฒฐ๋ก : B๋ฅผ ์ ์ฉํ์ธ์!")
print(f"- ์ ํ์จ {lift:.1f}% ์ฆ๊ฐ")
print(f"- ํต๊ณ์ ์ผ๋ก ์ ์๋ฏธํจ")
else:
print("\n๊ฒฐ๋ก : ์ถ๊ฐ ์คํ์ด ํ์ํฉ๋๋ค")์คํ ๊ฒฐ๊ณผ
A ์ ํ์จ: 3.00% B ์ ํ์จ: 3.50% ์๋์ ์ฆ๊ฐ: 16.7% p-value: 0.1586 ๊ฒฐ๋ก : ์ถ๊ฐ ์คํ์ด ํ์ํฉ๋๋ค
์ ๋ฆฌ
A/B ํ ์คํธ ์ฒดํฌ๋ฆฌ์คํธ
- ๊ฐ์ค ๋ช ํํ ์ค์
- ํ์ ํ๋ณธ ํฌ๊ธฐ ๊ณ์ฐ
- ๋ฌด์์ ๋ฐฐ์ ํ์ธ
- ์ถฉ๋ถํ ๊ธฐ๊ฐ ์คํ
- ์ ์ ํ ํต๊ณ ๊ฒ์ ์ ํ
- ํจ๊ณผ ํฌ๊ธฐ์ ๋น์ฆ๋์ค ์ํฉํธ ํจ๊ป ๊ณ ๋ ค
๋ค์ ๋จ๊ณ
ํต๊ณ๋ถ์ ์น์ ์ ์๋ฃํ์ต๋๋ค! ML ๊ธฐ์ด ์น์ ์์ ๋จธ์ ๋ฌ๋ ๊ธฐ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.
Last updated on