Machine Learning: A/Bテストと実験デザイン — 統計に基づくオンライン実験ガイド
最終更新:2026-08-26
A/Bテストなしでモデルをリリースするのは、安全ネットなしで綱渡りをするようなものです。実際に改善されたのか、それとも単なる運なのか、まったく分かりません。
1. 学習内容
- A/Bテストの基礎:仮説検定、p値、信頼区間、統計的検出力
- 実験デザイン:サンプルサイズ計算、実験期間、トラフィック分割戦略
- 機械学習モデルのA/Bテスト:新旧モデルのオンライン比較とビジネスメトリクスの評価
- マルチアームドバンディット(MAB):Thompson Sampling / UCBと探索・活用のトレードオフ
- AliceのSaaSシナリオ:新しいレコメンデーションモデルのコンバージョン率向上をA/Bテストで検証する
2. プロダクトマネージャーの実話
(1) 課題:新しいモデルが本当に優れているか分からない
AliceのSaaSプラットフォームは新しいレコメンデーションモデルをリリースし、ダッシュボードではコンバージョン率が3.2%から3.5%に上昇しました。しかしCEOは反論しました。「これは自然な変動かもしれない。モデルが原因だとどう証明するんだ?」Aliceには答えがありませんでした。対照実験なしでは、いかなる改善も帰属させることはできません。
(2) A/Bテストという解決策
A/Bテストでは、ユーザーをグループA(旧モデル)とグループB(新モデル)にランダムに分割し、両方を同時に実行して、統計的にその差が有意かどうかを判定します。
PYTHON
from scipy import stats
# Group A: old model, Group B: new model
conversions_a, n_a = 320, 10000 # 3.2%
conversions_b, n_b = 350, 10000 # 3.5%
z_stat, p_value = stats.proportions_ztest(
[conversions_a, conversions_b], [n_a, n_b]
)
print(f"p-value: {p_value:.4f}")
print(f"Significant: {p_value < 0.05}")
(3) 成果:3週間の実験でコンバージョン率2.3%向上を証明し、年間50万ドルの増収を実現
Aliceは3週間のA/Bテストで新モデルの改善を科学的に検証しました。p=0.03で統計的に有意です。この確信を持って全ユーザーに展開したところ、年間約50万ドルの増収につながりました。
3. A/Bテストの背後にある統計学
(1) 仮説検定のフレームワーク
sequenceDiagram
participant Design as Design Experiment
participant Split as Random Split
participant Collect as Collect Data
participant Test as Statistical Test
participant Decision as Decision
Design->>Split: Define H0/H1, sample size
Split->>Collect: Group A (control) vs Group B (treatment)
Collect->>Test: After experiment period
Test->>Decision: p-value calculation
Decision->>Decision: p < α → Reject H0 (B is better)
Decision->>Decision: p ≥ α → Cannot reject H0
(2) 主要な統計概念
▶ サンプル:仮説検定の計算
PYTHON
from scipy import stats
import numpy as np
# Two-proportion z-test
def ab_test_proportions(conversions_a, n_a, conversions_b, n_b, alpha=0.05):
p_a = conversions_a / n_a
p_b = conversions_b / n_b
p_pool = (conversions_a + conversions_b) / (n_a + n_b)
se = np.sqrt(p_pool * (1 - p_pool) * (1/n_a + 1/n_b))
z_stat = (p_b - p_a) / se
p_value = 1 - stats.norm.cdf(z_stat)
ci_low = (p_b - p_a) - 1.96 * se
ci_high = (p_b - p_a) + 1.96 * se
return {
"conversion_a": f"{p_a:.4f}",
"conversion_b": f"{p_b:.4f}",
"lift": f"{(p_b - p_a) / p_a * 100:.2f}%",
"z_stat": f"{z_stat:.3f}",
"p_value": f"{p_value:.4f}",
"ci_95": f"({ci_low:.4f}, {ci_high:.4f})",
"significant": p_value < alpha,
}
result = ab_test_proportions(320, 10000, 350, 10000)
for k, v in result.items():
print(f"{k:15s}: {v}")
出力:
TEXT
📖 参照専用
# Functions defined successfully
| 概念 | 意味 | 一般的な閾値 |
|---|---|---|
| H0(帰無仮説) | AとBに差はない | — |
| H1(対立仮説) | BはAより優れている | — |
| α(有意水準) | 第一種過誤(偽陽性)の確率 | 0.05 |
| p値 | H0の下で現在の結果、またはそれより極端な結果が観測される確率 | <0.05 |
| 検出力 | H0を正しく棄却する確率(1-β) | 0.8 |
| 信頼区間 | 真の差の尤もらしい範囲 | 95% CI |
4. 実験デザイン
(1) サンプルサイズの計算
▶ サンプル:A/Bテストに必要なサンプルサイズの計算
PYTHON
from statsmodels.stats.power import zt_ind_solve_power
from statsmodels.stats.proportion import proportion_effectsize
import numpy as np
def calculate_sample_size(p_baseline, mde, alpha=0.05, power=0.8):
"""Calculate required sample size per group.
Args:
p_baseline: Baseline conversion rate
mde: Minimum Detectable Effect (relative lift)
alpha: Significance level
power: Statistical power
"""
p_new = p_baseline * (1 + mde)
effect_size = proportion_effectsize(p_new, p_baseline)
n = zt_ind_solve_power(effect_size=effect_size, alpha=alpha, power=power)
return int(np.ceil(n))
# Alice's SaaS: baseline 3.2%, wants to detect 10% relative lift
baseline = 0.032
for mde in [0.05, 0.10, 0.15, 0.20]:
n = calculate_sample_size(baseline, mde)
print(f"MDE={mde*100:.0f}%: Need {n:,} users per group "
f"(total {n*2:,}, ~{n*2/baseline/1000:.0f}k visitors)")
出力:
TEXT
📖 参照専用
# Functions defined successfully
| MDE(最小検出可能効果) | 1グループあたりのサンプルサイズ | 合計サンプルサイズ | 実験期間(DAU 5万人の場合) |
|---|---|---|---|
| 相対リフト5% | 152,000 | 304,000 | 6日間 |
| 相対リフト10% | 38,000 | 76,000 | 1.5日間 |
| 相対リフト15% | 17,000 | 34,000 | 1日未満 |
| 相対リフト20% | 9,600 | 19,200 | 1日未満 |
📌 ポイント: MDEが小さい(検出したいリフトが小さい)ほど、必要なサンプルサイズは大きくなります。ベースラインがわずか3.2%の場合、相対リフト5%(つまり3.2%→3.36%)を検出するには30万人以上のユーザーが必要です。
(2) トラフィック分割戦略
| 戦略 | 方法 | メリット | デメリット |
|---|---|---|---|
| ユーザー単位 | user_idによるハッシュ | 一貫したユーザー体験 | ログインが必要 |
| リクエスト単位 | request_idによる分割 | シンプル | 同じユーザーが異なる結果を見る可能性 |
| デバイス単位 | device_idによる分割 | 未ログインユーザーもカバー | マルチデバイスユーザーで矛盾の可能性 |
▶ サンプル:一貫性のあるハッシュ分割
PYTHON
import hashlib
def assign_group(user_id, salt="experiment_1", num_groups=2):
"""Consistent hash assignment for A/B testing."""
hash_input = f"{salt}_{user_id}".encode()
hash_val = int(hashlib.md5(hash_input).hexdigest(), 16)
return hash_val % num_groups # 0=A, 1=B
# Verify equal split
user_ids = range(100000)
groups = [assign_group(uid) for uid in user_ids]
print(f"Group A: {groups.count(0)} ({groups.count(0)/len(groups)*100:.1f}%)")
print(f"Group B: {groups.count(1)} ({groups.count(1)/len(groups)*100:.1f}%)")
出力:
TEXT
📖 参照専用
# Functions defined successfully
5. 機械学習モデルのA/Bテスト
(1) モデルのオンライン比較
▶ サンプル:AliceのレコメンデーションモデルA/Bテスト
PYTHON
import numpy as np
from scipy import stats
rng = np.random.default_rng(42)
# Simulate 2-week A/B test for recommendation model
n_users = 20000 # Per group
# Group A: Old model (baseline conversion 3.2%)
conversions_a = rng.binomial(1, 0.032, n_users)
revenue_a = conversions_a * rng.exponential(200, n_users) # Avg 200 USD per conversion
# Group B: New model (true conversion 3.5%, 9.4% lift)
conversions_b = rng.binomial(1, 0.035, n_users)
revenue_b = conversions_b * rng.exponential(200, n_users)
# Analyze conversion rate difference
conv_rate_a = conversions_a.mean()
conv_rate_b = conversions_b.mean()
z_stat, p_value = stats.proportions_ztest(
[conversions_a.sum(), conversions_b.sum()],
[n_users, n_users]
)
# Analyze revenue difference
rev_per_user_a = revenue_a.mean()
rev_per_user_b = revenue_b.mean()
t_stat, t_pvalue = stats.ttest_ind(revenue_a, revenue_b)
print("=" * 50)
print("A/B TEST RESULTS: Recommendation Model")
print("=" * 50)
print(f"Conversion: A={conv_rate_a:.3%} vs B={conv_rate_b:.3%}")
print(f" Lift: {(conv_rate_b - conv_rate_a) / conv_rate_a * 100:.1f}%")
print(f" p-value: {p_value:.4f} {'✅ Significant' if p_value < 0.05 else '❌ Not significant'}")
print(f"\nRevenue/User: A={rev_per_user_a:.2f} vs B={rev_per_user_b:.2f} USD")
print(f" Lift: {(rev_per_user_b - rev_per_user_a) / rev_per_user_a * 100:.1f}%")
print(f" p-value: {t_pvalue:.4f}")
# Business impact
annual_lift = (rev_per_user_b - rev_per_user_a) * 50000 * 12 # 50k users * 12 months
print(f"\nProjected annual revenue lift: {annual_lift:,.0f} USD")
出力:
TEXT
📖 参照専用
=
A/B TEST RESULTS: Recommendation Model
=
(2) A/Bテストでよくある落とし穴
| 落とし穴 | 説明 | 対策 |
|---|---|---|
| 早期判定(Peeking) | 期間終了前に有意差を宣言してしまう | 実験期間を固定し、終了時のみ分析する |
| 多重検定 | 多数のメトリクス・多数のグループを検定する | Bonferroni補正 |
| 新奇性効果 | 新しいUIが最初は良い結果を出す | 実験期間を延長する |
| コンタミネーション | グループAのユーザーがグループBのコンテンツを見てしまう | ユーザー単位分割+ログ検証 |
| サンプルサイズ不足 | 統計的検出力に達していない | 事前にサンプルサイズを計算する |
6. マルチアームドバンディット(MAB)
(1) 探索と活用のバランス
従来のA/Bテストは固定のトラフィック分割(50/50)を使用しますが、MABは動的に調整します。パフォーマンスの良いモデルにより多くのトラフィックが割り当てられます。
▶ サンプル:Thompson Sampling
PYTHON
import numpy as np
rng = np.random.default_rng(42)
# 3 model variants with true conversion rates
true_rates = [0.030, 0.035, 0.028] # Model B is best
n_arms = len(true_rates)
n_rounds = 10000
# Thompson Sampling
successes = np.zeros(n_arms)
trials = np.zeros(n_arms)
total_reward = 0
for t in range(n_rounds):
# Sample from Beta distribution for each arm
samples = [rng.beta(successes[i] + 1, trials[i] - successes[i] + 1) for i in range(n_arms)]
chosen = np.argmax(samples)
# Simulate user interaction
reward = rng.binomial(1, true_rates[chosen])
successes[chosen] += reward
trials[chosen] += 1
total_reward += reward
print(f"Thompson Sampling after {n_rounds} rounds:")
for i in range(n_arms):
print(f" Model {i}: chosen {int(trials[i]):5d} times, "
f"observed rate={successes[i]/max(trials[i],1):.4f} (true: {true_rates[i]:.4f})")
print(f"Total conversions: {int(total_reward)}")
出力:
TEXT
📖 参照専用
# Executed successfully
| MABアルゴリズム | 戦略 | 探索方法 | 複雑度 |
|---|---|---|---|
| ε-Greedy | 確率εでランダムに探索 | 固定割合 | 低 |
| UCB | 信頼上限が最大のアームを選択 | 不確実性駆動 | 中 |
| Thompson Sampling | 事後分布からサンプリングしてアームを選択 | 確率駆動 | 中 |
| 観点 | A/Bテスト | MAB |
|---|---|---|
| トラフィック配分 | 固定(50/50) | 動的に調整 |
| 探索コスト | 高い(50%が劣る方に流れる) | 低い(適応的) |
| 統計的厳密性 | 高い(仮説検定) | 低い(漸近的) |
| ユースケース | 統計的証明が必要な場合 | 高速イテレーション |
❓ よくある質問
Q A/Bテストはどのくらいの期間実行すべきですか?
A 少なくとも2週間(週末の違いなどフルサイクルの影響をカバーするため)、かつ計画したサンプルサイズに達するまで実行してください。長い方を採用します。早期に終了しないでください(p<0.05であっても)。
Q p値 < 0.05だけで十分ですか?
A いいえ。他にも次の点が必要です。1)統計的検出力(power ≥ 0.8)、2)十分なサンプルサイズ、3)ビジネス上の有意性(リフトが投資に見合うか)、4)サイクル効果の排除。
Q A/BテストとB/Aテストの結果が矛盾したらどうすればいいですか?
A 次の点を確認してください。1)時期効果の有無、2)分割が均等かどうか、3)交互作用の有無(新モデルがあるユーザーには良く、別のユーザーには悪い場合)。層別分析で深掘りしましょう。
Q MABはA/Bテストを置き換えられますか?
A 完全には置き換えられません。MABは高速な探索(レコメンデーションのランキングなど)に適し、A/Bテストは統計的証明が必要な意思決定(価格戦略、モデルの全面展開など)に適しています。両者は補完関係にあります。
Q A/Aテスト(両グループとも旧モデルを使用)はどう扱いますか?
A A/Aテストは分割システムの検証に使用します。A/Aテストで有意差が出た場合、分割に問題があります。A/Bテストの前に毎回A/A検証を行うのが望ましいです。
Q 複数のメトリクスを同時に検定する場合はどうすればいいですか?
A Bonferroni補正を使用します。k個のメトリクスがある場合、有意水準をα/kにします。あるいは、1つの主要メトリクスを事前に指定し、残りを副次的メトリクスとして扱います。
📖 まとめ
- A/Bテストの核心:ランダム分割+同時実行+統計的検定により、改善を科学的に帰属させる
- 統計上の3つの要点:α(偽陽性率)< 0.05、検出力 ≥ 0.8、十分なサンプルサイズ
- サンプルサイズはベースラインのコンバージョン率とMDE(最小検出可能効果)に依存する。小さいリフトの検出にはより多くのサンプルが必要
- 分割戦略:ユーザー単位(推奨)> デバイス単位 > リクエスト単位。一貫性ハッシュで安定性を確保
- 機械学習モデルのA/Bテスト:コンバージョン率や売上などのビジネスメトリクスを比較し、モデル指標(AUC / R²)で比較しない
- MABはトラフィックを動的に配分し、探索コストを削減して高速イテレーションのシナリオに適している
📝 練習問題
- 基礎(難易度 ⭐):
proportions_ztestを使って、2つのコンバージョン率(3.0% vs 3.5%、各1万ユーザー)に有意差があるかを検定し、p値と95%信頼区間を計算してください。ヒント:stats.proportions_ztest。 - 中級(難易度 ⭐⭐):異なるMDEに対するサンプルサイズを計算し、「MDE vs サンプルサイズ」の曲線をプロットしてください。ヒント:
zt_ind_solve_power+ matplotlib。 - 応用(難易度 ⭐⭐⭐):Thompson Samplingのシミュレーターを構築してください。真のコンバージョン率が異なる3つのアームを用意し、10,000ラウンド後にThompson Samplingとε-Greedyを総リワードおよび最良アーム選択率で比較してください。ヒント:Beta分布からのサンプリング+ランダム探索との比較。