Open5

BigQuery Python UDF

YuichiYuichi

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など)も対応可能です。希望があればご相談ください。

YuichiYuichi

動いた

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;