Open5
BigQuery Python UDF

PythonでA/Bテストの有意差を判定する「A/Bテスト信頼度判定ツール」の簡易スクリプトです。入力として Visitors(母数) と Conversions(成功数) を受け取り、コンバージョン率の差に統計的に有意な差があるか を計算します。
✅ A/Bテスト信頼度判定ツール(Python)
import math
from scipy.stats import norm
def ab_test_confidence(a_visitors, a_conversions, b_visitors, b_conversions):
# コンバージョン率
a_cr = a_conversions / a_visitors
b_cr = b_conversions / b_visitors
# プールされたコンバージョン率
pooled_cr = (a_conversions + b_conversions) / (a_visitors + b_visitors)
# 標準誤差(standard error)
se = math.sqrt(pooled_cr * (1 - pooled_cr) * (1 / a_visitors + 1 / b_visitors))
# Zスコア
z = (b_cr - a_cr) / se
# p値(片側→両側に)
p_value = 2 * (1 - norm.cdf(abs(z)))
# 信頼度
confidence = (1 - p_value) * 100
# 信頼度レベルのメッセージ
if confidence >= 99:
msg = "99%以上の信頼度で有意な差である"
label = "99% confident"
elif confidence >= 95:
msg = "95%以上の信頼度で有意な差である"
label = "95% confident"
elif confidence >= 90:
msg = "90%以上の信頼度で低い。傾向は出ているが差が出ているとは言えない"
label = "90% confident"
elif confidence >= 80:
msg = "80%以上の信頼度で低い。差が出ているとは言えない"
label = "80% confident"
else:
msg = "信頼度は80%以下。差や傾向は出ていない"
label = "–"
# 結果出力
return {
"A_conversion_rate": round(a_cr * 100, 2),
"B_conversion_rate": round(b_cr * 100, 2),
"confidence_level": round(confidence, 2),
"confidence_label": label,
"message": msg
}
# 例:使用方法
result = ab_test_confidence(
a_visitors=1000, a_conversions=100,
b_visitors=1000, b_conversions=130
)
for key, value in result.items():
print(f"{key}: {value}")
🔍 出力例(100/1000 vs 130/1000 の場合)
A_conversion_rate: 10.0
B_conversion_rate: 13.0
confidence_level: 96.73
confidence_label: 95% confident
message: 95%以上の信頼度で有意な差である
🛠 補足
- これは 2標本の比率の差の検定(Z検定) に基づいています。
-
scipy.stats.norm
を使って正規分布の累積確率から p値 を計算しています。 - コンバージョン率が非常に小さい場合や、サンプル数が小さい場合は注意が必要です(その場合はFisher検定などが必要)。
GUI付きで動かしたい場合(StreamlitやGradioなど)も対応可能です。希望があればご相談ください。

動いた
create or replace function work.ab_test_confidence_only(
a_visitors int64,
a_conversions int64,
b_visitors int64,
b_conversions int64
)
returns float64
language python
options (
runtime_version = "python-3.11",
entry_point = "main",
packages=["scipy"]
)
as r'''
import pandas as pd
import numpy as np
from scipy.stats import norm
def main(df: pd.DataFrame) -> pd.Series:
a_v = df["a_visitors"].astype(float)
a_c = df["a_conversions"].astype(float)
b_v = df["b_visitors"].astype(float)
b_c = df["b_conversions"].astype(float)
# ゼロ除算を防ぐマスク
valid = (a_v > 0) & (b_v > 0)
# 初期化(無効な行は NaN)
confidence = pd.Series(np.nan, index=df.index)
# 有効な行のみ計算
if valid.any():
a_cr = a_c[valid] / a_v[valid]
b_cr = b_c[valid] / b_v[valid]
# プールされたコンバージョン率
pooled_cr = (a_c[valid] + b_c[valid]) / (a_v[valid] + b_v[valid])
# 標準誤差
se = np.sqrt(pooled_cr * (1 - pooled_cr) * (1 / a_v[valid] + 1 / b_v[valid]))
# z値とp値
z = (b_cr - a_cr) / se
p = 2 * (1 - norm.cdf(np.abs(z)))
# 信頼度(パーセント)
confidence[valid] = (1 - p) * 100
return confidence.round(2)
''';
with test_data as (
select 1000 as a_visitors, 100 as a_conversions, 1000 as b_visitors, 130 as b_conversions
),
scored as (
select
a_visitors,
a_conversions,
b_visitors,
b_conversions,
a_conversions / a_visitors as a_conversion_rate,
b_conversions / b_visitors as b_conversion_rate,
work.ab_test_confidence_only(a_visitors, a_conversions, b_visitors, b_conversions) as confidence_level
from test_data
)
select
a_visitors,
a_conversions,
b_visitors,
b_conversions,
round(a_conversion_rate, 2) as a_conversion_rate,
round(b_conversion_rate, 2) as b_conversion_rate,
case
when confidence_level >= 99 then '99% confident'
when confidence_level >= 95 then '95% confident'
when confidence_level >= 90 then '90% confident'
when confidence_level >= 80 then '80% confident'
else '-'
end as confidence_label,
case
when confidence_level >= 95 and b_conversion_rate > a_conversion_rate then 'Bが勝者です。'
when confidence_level >= 95 and a_conversion_rate > b_conversion_rate then 'Aが勝者です。'
else '勝者は決定していません。'
end as winner,
case
when confidence_level >= 99 then '99%以上の信頼度で有意な差である'
when confidence_level >= 95 then '95%以上の信頼度で有意な差である'
when confidence_level >= 90 then '90%以上の信頼度で低い。傾向は出ているが差が出ているとは言えない'
when confidence_level >= 80 then '80%以上の信頼度で低い。差が出ているとは言えない'
else '信頼度は80%以下。差や傾向は出ていない'
end as message
from scored;