EP.17 で の設計を扱いました。今回は 実際に動かす。EC サイトの CTA 文言を題材に、データ生成 → 仮説検定 → 信頼区間 → ベイジアン → 可視化 まで一気通貫で示します。コードは で動きます。
シナリオ:CTA 文言テスト
- A 案(コントロール): ボタン文言「カートに追加」
- B 案(テスト): ボタン文言「今すぐ買う」
- 仮説: 「今すぐ買う」の方が 意思決定が明確 で、購入率が改善するはず
- Target: 購入率(クリック→購入の転換率)
- Guardrail: 平均購入額、返品率、サポート問合せ
- サンプルサイズ: 各群 5,000 人(合計 10,000 人)
- 期間: 14 日間
1. シミュレーションデータの生成
import numpy as npimport pandas as pdimport matplotlib.pyplot as pltimport japanize_matplotlib # noqa: F401import seaborn as sns
np.random.seed(42)
# 真の購入率(実際にはこれが知りたい)TRUE_RATE_A = 0.050 # 5.0%TRUE_RATE_B = 0.063 # 6.3% (B 案が +1.3pt 良い、という仮の真実)
n_a, n_b = 5000, 5000
a_purchases = np.random.binomial(1, TRUE_RATE_A, n_a)b_purchases = np.random.binomial(1, TRUE_RATE_B, n_b)
# データフレーム化df = pd.DataFrame({ "variant": ["A"] * n_a + ["B"] * n_b, "purchase": np.concatenate([a_purchases, b_purchases]),})print(df.groupby("variant")["purchase"].agg(["count", "sum", "mean"]))「正解」を知っているデータで各手法を試せるのが利点。実データでは「本当の真実」が分からないので、各手法の挙動を比較する材料になる。
2. チェック(最初に必ず)
from scipy import stats
a_count = (df["variant"] == "A").sum()b_count = (df["variant"] == "B").sum()expected = (a_count + b_count) / 2chi2, p_srm = stats.chisquare([a_count, b_count], [expected, expected])print(f"A: {a_count}, B: {b_count}")print(f"chi2 = {chi2:.4f}, p = {p_srm:.4f}")print("✓ SRM なし" if p_srm > 0.001 else "⚠️ SRM 検出 - 結果無効")3. 頻度論:χ² 検定(カテゴリ変数)
購入したか / しなかったか の 2x2 分割表を作り、カイ二乗検定で「群と購入の独立性」を検定。p < 0.05 なら「群によって購入確率が違う」と判定。
# 2x2 分割表contingency = pd.crosstab(df["variant"], df["purchase"])print(contingency)# purchase 0 1# variant# A 4763 237# B 4685 315
chi2, p_value, dof, expected = stats.chi2_contingency(contingency)print(f"\nchi2 = {chi2:.2f}, p-value = {p_value:.4f}")
rate_a = a_purchases.mean()rate_b = b_purchases.mean()print(f"\nA の購入率: {rate_a:.2%}")print(f"B の購入率: {rate_b:.2%}")print(f"絶対差: {(rate_b - rate_a) * 100:+.2f} pt")print(f"相対差: {(rate_b - rate_a) / rate_a * 100:+.1f}%")4. 信頼区間の計算と可視化
p 値だけでは伝わらない。信頼区間付きの棒グラフで経営層・PM に見せるのが定石。
from statsmodels.stats.proportion import proportion_confint
# Wilson 法(小サンプルにも安全)ci_a_low, ci_a_high = proportion_confint(a_purchases.sum(), n_a, alpha=0.05, method="wilson")ci_b_low, ci_b_high = proportion_confint(b_purchases.sum(), n_b, alpha=0.05, method="wilson")
print(f"A: {rate_a:.2%} 95%CI [{ci_a_low:.2%}, {ci_a_high:.2%}]")print(f"B: {rate_b:.2%} 95%CI [{ci_b_low:.2%}, {ci_b_high:.2%}]")
fig, ax = plt.subplots(figsize=(7, 5))variants = ["A: カートに追加", "B: 今すぐ買う"]rates = [rate_a, rate_b]errors = [ [rate_a - ci_a_low, rate_b - ci_b_low], # 下振れ [ci_a_high - rate_a, ci_b_high - rate_b], # 上振れ]ax.bar(variants, rates, yerr=errors, capsize=12, color=["#1f77b4", "#ff7f0e"], alpha=0.85, edgecolor="black")for i, r in enumerate(rates): ax.text(i, r + 0.003, f"{r:.2%}", ha="center", fontweight="bold", fontsize=12)ax.set_ylabel("購入率"); ax.set_title("CTA文言A/Bテスト結果(95% CI)")ax.grid(alpha=0.3, axis="y"); ax.set_ylim(0, 0.085)plt.tight_layout(); plt.show()経営層・PM は p 値より棒グラフの重なり で理解する。95% CI のエラーバーが重ならない → 有意差あり、と覚えておけば十分。
5. ベイジアン A/B:「B が A より良い確率は何%か」
頻度論の p 値は 「両者が同じだとしたら、観測差がこれくらい極端になる確率」 で、直感的に解釈しにくい。 は 「B が A より良い確率」 を直接出してくれる。
from scipy import stats as ss
# 事前分布: Beta(1, 1) = uniform(弱情報事前)# 事後分布: Beta(1 + 成功数, 1 + 失敗数)post_a = ss.beta(1 + a_purchases.sum(), 1 + n_a - a_purchases.sum())post_b = ss.beta(1 + b_purchases.sum(), 1 + n_b - b_purchases.sum())
# モンテカルロで P(B > A) を推定n_sim = 100_000samples_a = post_a.rvs(n_sim)samples_b = post_b.rvs(n_sim)p_b_better = (samples_b > samples_a).mean()
# 期待損失(B を選んで実は A の方が良かった場合の損失期待値)expected_loss_a = np.maximum(samples_b - samples_a, 0).mean()expected_loss_b = np.maximum(samples_a - samples_b, 0).mean()
print(f"P(B > A) = {p_b_better:.1%}")print(f"Expected loss if B chosen: {expected_loss_b:.5f}")print(f"Expected loss if A chosen: {expected_loss_a:.5f}")
# 可視化: 事後分布xs = np.linspace(0.03, 0.085, 500)fig, ax = plt.subplots(figsize=(10, 5))ax.fill_between(xs, post_a.pdf(xs), alpha=0.4, color="#1f77b4", label="A 事後分布")ax.fill_between(xs, post_b.pdf(xs), alpha=0.4, color="#ff7f0e", label="B 事後分布")ax.set_xlabel("購入率"); ax.set_ylabel("密度")ax.set_title(f"A/B 事後分布 P(B > A) = {p_b_better:.1%}")ax.legend(); ax.grid(alpha=0.3); plt.tight_layout(); plt.show()P(B > A) > 95% で「B 採用」、< 5% で「A 採用」、その間は 「結論保留 → サンプル増やす」 の3択判定が定石。Expected Loss < 閾値 で打ち切る方法もある(GrowthBook の判定ロジック)。
6. 連続値メトリクス:Mann-Whitney U
平均購入額(連続値) の比較なら t 検定 か Mann-Whitney U 検定。金額分布は強く右に裾を引く(log-normal) ため、t検定の正規性仮定が崩れるケースが多い。Mann-Whitney(順位ベース、ノンパラ)の方が安全。
# 購入者だけの平均購入額を比較a_amounts = np.random.lognormal(mean=8.5, sigma=0.6, size=int(a_purchases.sum()))b_amounts = np.random.lognormal(mean=8.4, sigma=0.6, size=int(b_purchases.sum()))
t_stat, t_p = ss.ttest_ind(a_amounts, b_amounts)u_stat, u_p = ss.mannwhitneyu(a_amounts, b_amounts, alternative="two-sided")
print(f"t-test: p = {t_p:.4f}")print(f"Mann-Whitney: p = {u_p:.4f}")print(f"A 中央値: {np.median(a_amounts):,.0f} 円")print(f"B 中央値: {np.median(b_amounts):,.0f} 円")7. 判定とアクション
| 指標 | A | B | 差 | p値 | 判定 |
|---|---|---|---|---|---|
| 購入率(Target) | 4.74% | 6.30% | +1.56pt | 0.0009 | B 勝ち(有意) |
| 平均購入額(Guardrail) | 5,200円 | 5,150円 | -50円 | 0.31 | 差なし(OK) |
| 返品率(Guardrail) | 2.1% | 2.3% | +0.2pt | 0.62 | 差なし(OK) |
| P(B > A) ベイジアン | — | — | — | 99.9% | B 採用 |
8. Decision Log(EP.16 と接続)
id: H-2026-Q4-024title: CTA文言テスト「カートに追加」vs「今すぐ買う」period: 2026-12-15 〜 2026-12-28(14日)sample: A=5,000 / B=5,000(SRM check OK)result: status: success primary: 購入率 +1.56pt (95% CI +0.65〜+2.47, p=0.0009) bayesian: P(B > A) = 99.9% guardrails: - 平均購入額: 差なし - 返品率: 差なしdecision: B 全展開learning: | 「次のアクション」が明示的な文言の方が転換率が高い。 他の CTA(「お気に入り」「予約」など)でも同様の検証を行う価値ありnext_hypothesis: H-2026-Q4-031(リスト一覧画面の「詳細を見る」を「在庫を確認」へ)ふくふくの進め方
「A/B テストを始めたいけどどこから手を付けるか分からない」というご相談には、ツール選定 + 設計フレームワーク を 1 ヶ月、最初の 3 件の実験を伴走しながら 2〜3 ヶ月で社内に運用フローを定着させる、というロードマップ。自社 ( / )と の連携で「自社データ完結・ ベース」の実験基盤を組むのが定型パターンです。
ここまでのまとめ
EP.01〜18 の「指標設計の教科書」、 設計の基本から A/Bテスト実践(本記事) までを通すと、指標を作る → 育てる → 学習に繋げる → 因果を確かめる という一連の工程が見えてきます。応用編として、業界別 KPI(小売・・ゲーム・B2B) や / 時代の KPI(出力品質、ハルシネーション率)、サンプル → 全体への拡大推計などのバリエーションを今後追加していきます。続編は読者リアクションに応じて随時。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。