Machine Learning: A/Bテストと実験デザイン — 統計に基づくオンライン実験ガイド

最終更新:2026-08-26

A/Bテストなしでモデルをリリースするのは、安全ネットなしで綱渡りをするようなものです。実際に改善されたのか、それとも単なる運なのか、まったく分かりません。

1. 学習内容


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) 仮説検定のフレームワーク

100%
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つの主要メトリクスを事前に指定し、残りを副次的メトリクスとして扱います。

📖 まとめ


📝 練習問題

  1. 基礎(難易度 ⭐)proportions_ztestを使って、2つのコンバージョン率(3.0% vs 3.5%、各1万ユーザー)に有意差があるかを検定し、p値と95%信頼区間を計算してください。ヒント:stats.proportions_ztest
  2. 中級(難易度 ⭐⭐):異なるMDEに対するサンプルサイズを計算し、「MDE vs サンプルサイズ」の曲線をプロットしてください。ヒント:zt_ind_solve_power + matplotlib。
  3. 応用(難易度 ⭐⭐⭐):Thompson Samplingのシミュレーターを構築してください。真のコンバージョン率が異なる3つのアームを用意し、10,000ラウンド後にThompson Samplingとε-Greedyを総リワードおよび最良アーム選択率で比較してください。ヒント:Beta分布からのサンプリング+ランダム探索との比較。

← 前へ:モデルのデプロイ | 次へ:本番環境のモニタリングとモデルドリフト →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%