PythonでA/Bテストを実装する完全ガイド:SciPy・statsmodelsで統計的検定を実践

PythonとSciPy・statsmodelsでA/Bテストを実装する完全ガイド。サンプルサイズ計算、t検定、カイ二乗検定、効果量、信頼区間、多重比較補正、ベイズ的A/Bテスト、SRM・ピーキングなど実務の落とし穴まで、実コード付きで2026年版のベストプラクティスを解説します。

Python A/Bテスト実装ガイド SciPy 2026

最終更新: 2026年5月28日

PythonでA/Bテストを実装する最も信頼できる方法は、scipy.statsで頻度論的検定(t検定・カイ二乗検定)を行い、statsmodelsで検出力分析・サンプルサイズ計算・多重比較補正を実施することです。本ガイドでは、データサイエンティストとして実務で使えるA/Bテストの統計パイプラインを、コードとともに解説します。標本設計から効果量の評価、ベイズ的手法との比較まで、2026年時点で私が現場で実際に使っているベストプラクティスを共有します。

  • SciPy 1.14以降のscipy.stats.ttest_indchi2_contingencyは、A/Bテストにおける連続値・カテゴリ値の比較で標準的な検定手段である。
  • サンプルサイズはテスト前にstatsmodels.stats.power.TTestIndPowerで計算し、α=0.05・検出力0.8・最小検出効果量(MDE)を明示する。
  • p値が0.05を下回ることは「効果がある」ことを意味せず、効果量(Cohen's d、相対リフト)と信頼区間を併記して判断する。
  • 複数指標を同時検証する場合は、Benjamini-Hochberg法による偽発見率(FDR)制御が実務的に最も妥当。
  • ベイズ的A/Bテスト(PyMC)は、逐次的な意思決定と「効果が存在する確率」の直接推定に有用。
  • SRM(Sample Ratio Mismatch)チェック、ピーキング問題、ノベルティ効果は実務での3大落とし穴である。

A/Bテストとは何か:統計的検定としての定義

A/Bテスト(オンライン制御実験、Online Controlled Experiment)とは、ユーザーを無作為に2群(対照群A・処置群B)に割り当て、特定の指標における処置の因果効果を統計的に推定する手法です。Kohavi、Tang、Xu(2020)の『Trustworthy Online Controlled Experiments』では、A/Bテストを「無作為化比較試験(RCT)のWebアプリケーション版」と位置づけています。

さて、本論に入る前に用語を整理します。帰無仮説 H₀ は「処置効果はゼロ」、対立仮説 H₁ は「処置効果はゼロでない」とします。観測されたデータの下で H₀ が真であった場合に、観測値以上の極端な統計量が得られる確率が p値 です。閾値 α(通常0.05)を下回れば、H₀ を棄却します。ここで重要なのは、「p値が0.04だから処置効果が95%の確率で存在する」という解釈は誤りであり、これは 頻度論的 な手続きの長期的な誤り率を意味するに過ぎません(この誤解は私自身も新人時代に何度かやらかしました)。

2026年現在、PythonのA/Bテスト実装は事実上 scipy.stats(検定)、statsmodels(検出力分析・回帰・多重比較)、PyMC または numpyro(ベイズ)の3点セットで構成されます。本記事ではこれらを順番に取り上げます。

A/Bテストに必要なサンプルサイズはどう決める?

A/Bテストを始める前に最も重要な作業、それがサンプルサイズ計算です。検出したい最小効果量(Minimum Detectable Effect、MDE)、有意水準 α、検出力 1−β(通常0.8)を決め、必要な被験者数を逆算します。サンプルサイズが小さすぎれば真の効果を見逃し(第II種の過誤)、大きすぎれば運用コストが無駄になる、そんな話です。

連続値指標(例:1ユーザーあたりの売上)の場合、statsmodels.stats.power.TTestIndPower を使います。

import numpy as np
from statsmodels.stats.power import TTestIndPower, NormalIndPower

# 連続値指標:平均5,000円、標準偏差2,000円、MDE=200円(4%リフト)
mean_a = 5000
sd = 2000
mde = 200
effect_size = mde / sd  # Cohen's d = 0.1

analyzer = TTestIndPower()
n_per_group = analyzer.solve_power(
    effect_size=effect_size,
    alpha=0.05,
    power=0.80,
    alternative="two-sided",
)
print(f"群あたり必要サンプル数: {int(np.ceil(n_per_group))}")  # 約1,571

コンバージョン率のような比率指標では、標準正規近似に基づく NormalIndPowerproportion_effectsize を組み合わせます。

from statsmodels.stats.proportion import proportion_effectsize

p1, p2 = 0.10, 0.11  # コンバージョン率10% → 11%(相対リフト10%)
effect_size = proportion_effectsize(p2, p1)
n = NormalIndPower().solve_power(
    effect_size=effect_size, alpha=0.05, power=0.80, alternative="two-sided"
)
print(f"群あたり必要サンプル数: {int(np.ceil(n))}")  # 約14,744

SciPyによるt検定の実装(連続値指標)

連続値指標(売上、滞在時間、ページビュー数など)の比較には、Welchのt検定(等分散を仮定しない)を使います。正直なところ、これがデフォルトでよい理由は単純で、等分散性の仮定を満たすかどうかを毎回検証するより、最初から満たさない前提で計算する方が安全だからです。SciPy 1.14以降では scipy.stats.ttest_ind がデフォルトで equal_var=False をサポートし、信頼区間も返します。

import numpy as np
from scipy import stats

rng = np.random.default_rng(42)
# シミュレーションデータ:A群は平均5,000、B群は平均5,200
group_a = rng.normal(loc=5000, scale=2000, size=1600)
group_b = rng.normal(loc=5200, scale=2050, size=1600)

result = stats.ttest_ind(group_b, group_a, equal_var=False, alternative="two-sided")
ci = result.confidence_interval(confidence_level=0.95)

print(f"t統計量 = {result.statistic:.3f}")
print(f"p値    = {result.pvalue:.4f}")
print(f"平均差の95%信頼区間 = [{ci.low:.1f}, {ci.high:.1f}]")

出力は概ね t≈2.6、p≈0.009、信頼区間は約[51, 354] となります。p < 0.05 なので H₀ を棄却しますが、信頼区間の幅が広いことに注目してください。点推定値だけを報告すると、「実際には30円のリフトかもしれないし、350円のリフトかもしれない」という不確実性を見落とします。これは経営層への報告で何度も指摘された経験があります。

分布が極端に歪んでいる場合(例:ロングテールの売上分布)、t検定の前提が崩れることがあります。その場合は対数変換、Mann-Whitney U検定(stats.mannwhitneyu)、または分散安定化のためにブートストラップ(stats.bootstrap)を検討します。データクリーニングのパイプライン化については、pandas pipe()によるデータクリーニングパイプラインを参照してください。

カイ二乗検定でコンバージョン率を比較する

コンバージョン率・クリック率・離脱率のような2値指標の場合は、2×2分割表に対してカイ二乗検定(独立性の検定)またはZ検定を適用します。

import numpy as np
from scipy.stats import chi2_contingency
from statsmodels.stats.proportion import proportions_ztest, confint_proportions_2indep

# A群:14,700人中1,470人がコンバージョン(10.0%)
# B群:14,700人中1,617人がコンバージョン(11.0%)
conv_a, n_a = 1470, 14700
conv_b, n_b = 1617, 14700

table = np.array([[conv_b, n_b - conv_b],
                  [conv_a, n_a - conv_a]])

chi2, p_chi, dof, expected = chi2_contingency(table, correction=False)
z_stat, p_z = proportions_ztest([conv_b, conv_a], [n_b, n_a])
ci_low, ci_high = confint_proportions_2indep(conv_b, n_b, conv_a, n_a, method="wald")

print(f"カイ二乗統計量 = {chi2:.3f}, p値 = {p_chi:.4f}")
print(f"Z統計量       = {z_stat:.3f}, p値 = {p_z:.4f}")
print(f"絶対差の95%CI = [{ci_low:.4f}, {ci_high:.4f}]")

カイ二乗検定とZ検定は数学的に等価で(χ² = Z²)、得られるp値は同一です。correction=False としているのは、サンプルサイズが十分大きい場合のYates連続性補正は過度に保守的になるためです。

効果量と信頼区間:p値だけに頼らない判定

p値はサンプルサイズに強く依存します。十分大きなサンプルでは、ビジネス的に無意味な小さな差でも「統計的に有意」になり得ます。American Statistical Associationの2016年声明以降、p値単独での意思決定は推奨されていません。実務では 効果量信頼区間 を併記します。

連続値の効果量にはCohen's d(標準化平均差)を使います。慣例として d=0.2 を小、0.5 を中、0.8 を大とします。

def cohens_d(x, y):
    nx, ny = len(x), len(y)
    var_x, var_y = np.var(x, ddof=1), np.var(y, ddof=1)
    pooled_sd = np.sqrt(((nx - 1) * var_x + (ny - 1) * var_y) / (nx + ny - 2))
    return (np.mean(x) - np.mean(y)) / pooled_sd

d = cohens_d(group_b, group_a)
print(f"Cohen's d = {d:.3f}")  # 例:0.092(小さい効果)

比率指標では、リフト率(相対差)と絶対差の両方を信頼区間付きで報告するのが標準です。経営層への報告では「コンバージョン率が10.0% → 11.0%に上昇(絶対差 +1.0pp、95%CI [+0.3pp, +1.7pp]、相対リフト +10%)」のような形式が最も誤解を生みません。私のチームでは、この書式をテンプレ化してSlackボットから自動投稿させています。

多重比較補正:BonferroniとBenjamini-Hochberg

同一の実験で複数の指標(売上・滞在時間・離脱率…)を同時にテストする、あるいは複数のセグメント(年齢層、地域)でサブグループ分析を行う場合、家族単位の誤り率(FWER)または偽発見率(FDR)が膨れ上がります。20指標で各α=0.05だと、少なくとも1つで偽陽性が出る確率は1−0.95²⁰≈64%です。これは本当に怖い数字です。

statsmodels.stats.multitest.multipletests を使えば、主要な補正手法を1行で適用できます。

from statsmodels.stats.multitest import multipletests

pvals = [0.001, 0.012, 0.034, 0.048, 0.061, 0.10, 0.21, 0.55]

# Bonferroni補正(保守的だがFWERを厳密制御)
reject_b, p_adj_b, _, _ = multipletests(pvals, alpha=0.05, method="bonferroni")

# Benjamini-Hochberg法(FDR制御、実務で最も使われる)
reject_bh, p_adj_bh, _, _ = multipletests(pvals, alpha=0.05, method="fdr_bh")

print("Bonferroni 棄却:", reject_b)
print("BH (FDR)   棄却:", reject_bh)

探索的分析ではBenjamini-Hochberg(FDR)、確認的なローンチ判定ではBonferroniというのが、私が実務で採用している使い分けです。詳しい背景はBenjamini & Hochberg(1995)の原論文に当たってください。

ベイズ的A/Bテストとの比較

頻度論の枠組みでは「効果が存在する確率」を直接計算できません。一方ベイズ的A/Bテストでは、事前分布と尤度から事後分布 P(θ_B > θ_A | data) を推定します。これは「Bが優れている確率は93%」のように、ステークホルダーが直感的に理解できる形で報告できる利点があります(実際、PdMからの質問が激減しました)。

import numpy as np
from scipy.stats import beta

# コンバージョン率に対する共役事前分布(Beta(1,1) = 一様)
alpha_prior, beta_prior = 1, 1

# A群:1,470/14,700, B群:1,617/14,700
post_a = beta(alpha_prior + 1470, beta_prior + 14700 - 1470)
post_b = beta(alpha_prior + 1617, beta_prior + 14700 - 1617)

rng = np.random.default_rng(0)
samples_a = post_a.rvs(200_000, random_state=rng)
samples_b = post_b.rvs(200_000, random_state=rng)

p_b_better = np.mean(samples_b > samples_a)
expected_lift = np.mean((samples_b - samples_a) / samples_a)
print(f"P(B > A) = {p_b_better:.3f}")
print(f"期待リフト = {expected_lift:.4f}")

2×2分割表のように共役性が成立する場合は、サンプリングなしで解析的に処理できます。より複雑なモデル(階層モデル、CUPED調整)が必要であればPyMC 5.x を使います。scikit-learn 1.8の機械学習パイプラインと組み合わせて予測モデルを評価したい場合は、scikit-learn 1.8 完全ガイドも参考になります。

実務での落とし穴:SRM・ピーキング・ノベルティ効果

A/Bテストの実装そのものは数行で書けます。が、結果の信頼性を損なう落とし穴が複数存在します。実務で繰り返し見てきた(そして自分でも踏んだ)3つを挙げます。

SRM(Sample Ratio Mismatch)

50:50で割り当てたはずなのに、観測されたサンプル比が50.4:49.6など想定からずれている場合、ロギングのバイアス・bot流入・割当ロジックのバグが疑われます。テスト結果を見る前に必ずSRMをチェックします。これを怠って3週間後に「実は片方の群でJSが正しくロードされていなかった」ことが発覚した事例を、私は2回見ています。

from scipy.stats import chisquare
observed = [14820, 14580]  # A, B
expected = [sum(observed) / 2] * 2
chi2, p = chisquare(observed, expected)
if p < 0.001:
    raise ValueError(f"SRM検知: p={p:.4f}, 結果を採用しないこと")

ピーキング問題

毎日p値を見て「有意になった瞬間に停止」する運用は、第I種の過誤を3〜10倍に膨らませます。事前に決めたサンプルサイズに達するまで結果を見ない、あるいはalpha-spending関数や逐次検定(mSPRT、statsmodels.stats.proportionのZ検定の逐次拡張)を使います。

ノベルティ効果と季節性

初日にスパイクして数日で減衰する効果は「新しいUIへの好奇心」かもしれず、長期的な真の効果ではありません。最低でも完全な週次サイクル(7日間)、できれば2週間以上の運用期間を確保します。時系列の挙動を可視化したい場合は、Python時系列データ分析ガイドのテクニックが応用できます。

これらに加え、Microsoft Research の ExP Platform文献集、SciPyの statsモジュール公式ドキュメント、および statsmodels の統計検定リファレンス を実務の傍らに置いておくことをお勧めします。

よくある質問

t検定とz検定の違いは何ですか?

z検定は母集団の標準偏差が既知(または十分大きなサンプルで標本標準偏差で近似できる)場合に使い、t検定は標本標準偏差を使う場合に使います。サンプルサイズが概ね30以上であれば両者の結果はほぼ一致するため、実務のA/Bテストではどちらを使っても結論はほとんど変わりません。

p値が0.05を少し上回った場合、どう判断すればよいですか?

p=0.06は「効果なし」を意味しません。効果量の点推定値と信頼区間を見て、ビジネス的に意味のある幅に収まっているかを確認します。検出力不足が疑われる場合は、サンプルサイズを増やす、あるいは事前のサンプルサイズ計算が妥当だったかを再確認します。

A/Bテストにベイズ法と頻度論法のどちらを使うべきですか?

事前に明確な意思決定基準(α、検出力、MDE)を設計できる確認的実験では頻度論が標準的で、再現性とガバナンスに優れます。一方、逐次的な意思決定、複雑な階層構造、不確実性を確率として直接表現したい場面ではベイズが有利です。両者は対立ではなく補完関係として使ってください。

複数の指標をA/Bテストで同時に評価しても問題ありませんか?

多重比較補正なしでは偽陽性率が膨らみます。北極星指標を1つ事前定義し、補助指標にはBenjamini-Hochberg法でFDRを制御するのが実務の標準です。statsmodels.stats.multitest.multipletests で簡単に実装できます。

どのくらいのテスト期間を取れば十分ですか?

サンプルサイズ計算から導かれる被験者数に達するのが必要条件ですが、それに加え、最低でも完全な週次サイクル(7日間)を含めることが推奨されます。曜日効果やノベルティ効果を平均化するため、通常は2週間以上の運用が安全な目安です。

Dr. Elena Vasquez
著者について Dr. Elena Vasquez

Data scientist with a PhD in computational statistics. Translates papers into pandas one notebook at a time.