🫨

開発生産性の次の「評価効率」とベイジアンA/Bテストを試してみた話

に公開

はじめに

こんにちは、ランサーズでエンジニアをしている岡田です。

早速ですが、タイトルにもなっている 「評価効率」 という言葉、聞いたことはあるでしょうか?
私はありません。なぜなら、私が考えた造語だからです。(既にあったら、先人の方すみません)

昨今のAIで「開発生産性」が2倍3倍と騒がれる中、次に出てくるであろう問題について考えてみませんか?

開発生産性を上げた先に見えてくる「評価効率」

「開発生産性」という言葉が広くどこの組織・チームでも使われるようになって、「AIを使って開発生産性をあげよう」や「このツールを使って開発生産性がX倍に」など聞かない・見ない日はないくらいです。
実際、自分の組織・チームでも「開発生産性」なるものを導入して業務改善が進むことで、リリースの速度自体はかなり上がってきたと思います。

ただ、「開発生産性」が2倍3倍となった時

  • 実装は速くなったけど、中々プロダクトが良くなっている実感が湧かない
  • 作ってリリースしたはいいものの、ユーザーに刺さっているのか良く分からない

という方、多いのではないでしょうか?

これは「プロダクトの良し悪し」「ユーザーの反応」という評価は、結果が出るまで時間のかかるものだからです。
簡単に言うと開発スピードに対して、評価するスピードが釣り合いが取れなくなってきたから、上手くいってるのか分からなくなってきたんですね。

そこで出てくるのが 「評価効率」 という考え方です。
ここであえて「速度」ではなく「効率」という言葉を使っているのは、評価では必ず精度も意識して欲しいからです。

速度だけ追ってると?

例えば、とにかく評価の速度を上げようと、ABテストのCVRの数字をパッと見て即断するとします。

  1. 現行のAより新案のBがCVRが5%高かった
  2. 新案Bを採用する
  3. 本当はAとBの母数はそれぞれ20件しかなかった

こうなると、 5% = 1件の違い でしかありません。
こうやって評価において 速度だけ追っていると、意思決定が曖昧になり、進んでいるようで迷走して後退してしまう。
「あの時は確かに良いはずだったのに。。。」 となりかねません。

「評価効率」の重要性

だからと言って、厳密な統計的検定で評価するために何週間も待ってると、AIでどんどん速くなっていく社会の変化についていけません。
いくらAIを使って1週間で新機能を開発・既存機能を改善しても、意思決定に1ヶ月かかっていれば、そこがボトルネックになっていきます。

だから、「開発生産性」だけじゃなくて、施策の効果を評価するときの速度精度を掛け合わせた 「評価効率」 も意識していこうよ、という話です。

「開発生産性も広義では評価まで含めて」という考えもあるかも知れませんが、狭義(特に昨今のAIで流行ってる)では「作って送り出す」までを指して使われることが多いと思います。
ここはひとつ、バズワードが一人歩きして抽象的で曖昧なものにならない内に、定義を固めて「開発生産性」は作る指標とし、測る指標に「評価効率」という言葉を使っていきませんか?
(正直、「開発生産性」を聞きすぎてウンザリしてきたから、対抗できる言葉が欲しくなったのもあります😒)

現状のABテスト、こうなりがち問題

うちのチームでもABテストはやっていて、施策AとBを出し分けて、CVRを比較することはよくあります。

で、ありがちなのがこういうパターン

  1. とりあえず関係する数値をモニタリング
  2. 良くなればそのまま受け入れて、悪くなれば他の色んな観点で調べてみちゃう
  3. 結果、よく分からなくなって根拠は曖昧だけど、定性的に良さそうと感じる方で決めてしまう

でもこれ、都合の良いデータを見ただけだったり、ユーザーのためじゃなくて運営都合になったりして。。。
「じゃあちゃんとやろうよ」となると、統計的検定の出番。帰無仮説を立てて、サンプルサイズを事前に決めて、p値を計算して〜〜〜

これ自体は正しいやり方で、ちゃんと勉強して使いこなせるに越したことはないんですが、正直、自分もまだ全部ちゃんと理解しきれているわけではないし、チーム全員が統計の知識を正しく身につけられるかと言われると。。。
評価の専門チームがあればいいんですが、いろんな事情でなかなか実現はできない。それに加えて、厳密に運用しようとすると現場で引っかかるポイントがいくつかあるみたいです。

現場で引っかかるポイント

途中で結果を見ちゃう問題
頻度論的検定は、事前に決めたサンプルサイズに達してから1回だけ判定する前提。
でも、リリースしてサンプルサイズが集まるまで、ただ待つだけ?ユーザーに悪影響を与えていたら、すぐにでも戻したいのが多くのプロダクトオーナーの本音でしょう。でも、途中で様子を見てしまえば検定の前提が崩れて、ピーハッキングになりかねない。だから、評価者はいつも正確さと報告義務に板挟み状態。。。

「有意差なし」がよくわからない問題
検定で有意差が出なかったとき、「差がない」のか「まだわからない」のかがはっきりしない。
報告しても「で、結局どうするの?」となりがち。

サンプルサイズがなかなか集まらない問題
効果が小さい施策だと必要なサンプルサイズがとても大きくなってしまう。
利用者数が少ない機能やサービスだと、最低限必要なサンプルサイズを集めるだけでも数週間〜数ヶ月。。。

まとめると、統計的検定は精度は高いけど、速度面のコストが重い
さっきの 「評価効率」の話で言えば、精度寄りに振り切った手法 ということになります。

そこで、もうちょっとバランスの良い方法はないかな〜と探していて、偶然見つけたのがベイジアンABテストでした。

ベイジアンABテストについて

ざっくりした解説

ここでは、CVRのような「した/しなかった」の二値データで、ベイジアンABテストと従来の頻度論的な統計的検定(母比率の差の検定)との違いをざっくり整理します。

頻度論的な統計的検定では、AにもBにも「本当のCVR」(母集団)がそれぞれ1つだけ存在していて、今回集めたデータはそこからたまたまサンプリングされた標本だと考えます。

  1. それぞれのデータからCVRを計算する(A: 5%、B: 6%)
  2. 「この差は本当のCVRの差なのか、たまたまブレただけなのか?」を検定し、p値で判断する
    ※「何人分データを集めるか」は事前に決めておく必要があり、途中で結果を覗き見して判断してはいけない。

ベイジアンABテストでは「本当のCVR」は1つの固定値として扱うのではなく、不確実さを含んだ確率分布として表現する。
得られたデータと事前分布(事前の仮定)をもとに、CVRがどのあたりにありそうかを確率分布として計算する。

  1. それぞれのデータからCVRの事後分布を計算する(「CVRは3%〜7%のあたりにありそうで、5%あたりが一番確からしい」)
  2. 分布同士を重ねて比較し、「Bが勝つ確率」や「間違えた場合の損失」を直接計算する

事後分布から意思決定の指標を作る

具体的な使い方は、事前知識がない状態(\text{Beta}(1, 1)、一様分布)のベータ分布からスタートして、データが集まるたびに分布を更新していきます。
サンプル数 n、コンバージョン数 k のとき、事後分布は \text{Beta}(1 + k, 1 + n - k) になっていきます。
(事前分布がわかっている場合は変わりますが)
事後分布は、サンプルが少ないときには分布の幅が広くて不確実性が高く、サンプルが増えてくると分布がキュッと尖って推定の精度が上がっていきます。

ここまでは頻度論でも同じですが、ベイジアンアプローチではこの分布そのものを意思決定に使えることが大きな違いになってきます。
得られたAとBのそれぞれの事後分布から、以下の指標を計算して意思決定に使います。

  • 勝率: AとBの分布からBがAを上回る割合 → 「Bが勝つ確率は85%」みたいな数字で、p値よりずっと直感的に理解しやすいです
  • 期待損失: 間違った方を選んだ場合に、どれくらいCVRを損するかの期待値 → 「Bを選ばなかった場合の期待損失は0.3%ポイント」みたいな感じで、リスクの大きさを定量化できます

ベイジアンABテストが良さそうに思った理由

このベイジアンABテストの考え方は、前述した現場で引っかかるポイントに対して以下のように効いてきます。

途中で結果を見ちゃう問題
途中で見ていい
事後分布は常に「今のデータに基づくベストな推定」なので、頻度論とは違って途中でチェックしても問題ない。

「有意差なし」がよくわからない問題サンプルサイズがなかなか集まらない問題
「差がなさそう」も判断できる
勝率が50%付近で期待損失も小さければ、「これは差がなさそうだから次の施策に切り替えよう」という判断が合理的にできる。

さらに結果が説明しやすく、「Bが勝つ確率85%で、Bを選ばないと0.3%ポイント損しそうです」とエンジニアじゃないメンバーにも伝わりやすいです。

もちろん万能な手法ではないし、事前分布の設定をどうするかとか注意点はあります。
ただ何よりも、「数字を見て定性で決めて終わり」より精度高く、「統計的検定」よりも意思決定を速くしやすい、 精度と速度を兼ねた「評価効率」の高い十分良いアプローチ だと思います。
また、他に逐次検定[1]なども調べていたのですが、ベイジアンABテストは数年前から様々な企業での採用実績があることも知って、お試し感覚で使ってみることにしました。

https://speakerdeck.com/ak_iyama/b-testing-truly-effective
https://engineering.mercari.com/blog/entry/20221110-bayesian-testing-for-souzoh/
https://www.ai-shift.co.jp/techblog/4710
https://developers.cyberagent.co.jp/blog/archives/29088/

実際に使ってみた

実装

「ベイジアンABテスト」は初耳で少し身構えたのですが、AIに聞きながらやると基本的な実装は意外にもシンプルでした。

Pythonで簡易実装

以下が実際に使っているコードです。
試行数と成功数を入力すると、事後分布のヒストグラム、勝率、期待損失がリアルタイムに算出されます。
(今回は marimo で使って分布のグラフを表示するようにしてみました。同期のエンジニアに教えてもらったPythonのノートブックで、パラメータを変えると分布がリアルタイムに更新されるので母数の違いによる分布の変化を確認するのにちょうどよかったです😊)

https://docs.marimo.io/

bayesian_ab_test.py
import marimo

__generated_with = "0.19.6"
app = marimo.App(width="medium")


@app.cell
def imports():
    import marimo as mo
    import numpy as np
    import matplotlib.pyplot as plt
    import japanize_matplotlib
    return mo, np, plt


@app.cell
def input_section(mo):
    mo.md(r"""
    # 📊 Bayesian A/B Test Calculator (Monte Carlo)

    ベルヌーイ分布(CVR比較)におけるベイジアンABテストツールです。
    モンテカルロシミュレーションにより各統計量を算出します。
    """)
    return


@app.cell
def ui_controls(mo):
    # パターンAの入力
    trials_a = mo.ui.number(value=1000, label="Pattern A: 試行数 (Trials)", step=1)
    success_a = mo.ui.number(value=100, label="Pattern A: 成功数 (CV)", step=1)

    # パターンBの入力
    trials_b = mo.ui.number(value=1000, label="Pattern B: 試行数 (Trials)", step=1)
    success_b = mo.ui.number(value=120, label="Pattern B: 成功数 (CV)", step=1)

    # サンプル数の入力
    n_samples = mo.ui.number(value=100000, label="モンテカルロ サンプル数", step=10000)

    # 分析ボタン
    run_button = mo.ui.run_button(label="🔍 分析する")

    # 入力フォームのレイアウト
    mo.vstack([
        mo.md("### 1. データ入力"),
        mo.hstack([
            mo.vstack([trials_a, success_a]),
            mo.vstack([trials_b, success_b])
        ], justify="start", gap=2),
        n_samples,
        run_button
    ])
    return n_samples, run_button, success_a, success_b, trials_a, trials_b


@app.cell
def calculation_logic(mo, np, run_button, success_a, success_b, trials_a, trials_b, n_samples):
    mo.stop(not run_button.value)

    # 値の取得
    nA, sA = trials_a.value, success_a.value
    nB, sB = trials_b.value, success_b.value
    n = int(n_samples.value)

    # 事前分布 Beta(1, 1) を加えた事後分布のパラメータ
    alpha_a, beta_a = 1 + sA, 1 + nA - sA
    alpha_b, beta_b = 1 + sB, 1 + nB - sB

    # --- モンテカルロシミュレーション ---

    # 再現性のためシード固定(シミュレーションをやり直したい場合はコメントアウト)
    rng = np.random.default_rng(seed=42)

    # 事後分布からサンプリング
    samples_a = rng.beta(alpha_a, beta_a, size=n)
    samples_b = rng.beta(alpha_b, beta_b, size=n)

    # 1. BがAに勝つ確率 P(B > A)
    prob_b_better = np.mean(samples_b > samples_a)

    # 2. 期待損失 (Expected Loss)
    # Aを選んだ場合のリスク = Bの方が実は良かった場合に失う改善幅の期待値
    expected_loss_a = np.mean(np.maximum(samples_b - samples_a, 0))
    # Bを選んだ場合のリスク
    expected_loss_b = np.mean(np.maximum(samples_a - samples_b, 0))

    return (
        alpha_a,
        alpha_b,
        beta_a,
        beta_b,
        expected_loss_a,
        expected_loss_b,
        prob_b_better,
        samples_a,
        samples_b,
    )


@app.cell
def visualization(
    alpha_a,
    alpha_b,
    beta_a,
    beta_b,
    expected_loss_a,
    expected_loss_b,
    mo,
    np,
    plt,
    prob_b_better,
    samples_a,
    samples_b,
):
    # --- グラフ描画 ---

    # 表示範囲の自動調整 (平均 ± 4標準偏差 をカバーする範囲)
    mean_a = alpha_a / (alpha_a + beta_a)
    std_a = np.sqrt(mean_a * (1 - mean_a) / (alpha_a + beta_a + 1))
    mean_b = alpha_b / (alpha_b + beta_b)
    std_b = np.sqrt(mean_b * (1 - mean_b) / (alpha_b + beta_b + 1))

    # グラフのX軸範囲
    lower_lim = max(0, min(mean_a, mean_b) - 4 * max(std_a, std_b))
    upper_lim = min(1, max(mean_a, mean_b) + 4 * max(std_a, std_b))

    fig, ax = plt.subplots(figsize=(8, 4), constrained_layout=True)

    # Pattern A (ヒストグラム)
    ax.hist(samples_a, bins=100, density=True, alpha=0.5,
            label=f'Pattern A (Mean: {mean_a:.2%})', color='#1f77b4',
            range=(lower_lim, upper_lim))

    # Pattern B (ヒストグラム)
    ax.hist(samples_b, bins=100, density=True, alpha=0.5,
            label=f'Pattern B (Mean: {mean_b:.2%})', color='#ff7f0e',
            range=(lower_lim, upper_lim))

    ax.set_title("事後分布の比較 (Posterior Distributions)", fontsize=12)
    ax.set_xlabel("Conversion Rate (CVR)")
    ax.set_ylabel("Density")
    ax.legend(loc='upper right')
    ax.grid(True, alpha=0.3, linestyle='--')

    # 不要な枠線を消す
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)

    # --- 結果表示UI ---
    mo.vstack([
        mo.md("### 2. 分析結果"),
        mo.hstack([
            mo.stat(
                label="BがAより優れている確率",
                value=f"{prob_b_better:.2%}",
                caption="Probability B > A"
            ),
            mo.stat(
                label="Aを選んだ場合の期待損失",
                value=f"{expected_loss_a:.3%}",
                caption="Risk of choosing A"
            ),
            mo.stat(
                label="Bを選んだ場合の期待損失",
                value=f"{expected_loss_b:.3%}",
                caption="Risk of choosing B"
            ),
        ]),
        mo.mpl.interactive(fig)
    ])
    return


if __name__ == "__main__":
    app.run()

手元で動かしてみる

Dockerで環境を用意すればすぐに試せるようにしました。
以下の3ファイルを用意してください。

bayesian-ab-test/
├── Dockerfile
├── requirements.txt
└── bayesian_ab_test.py
requirements.txt
marimo>=0.19.6
numpy
matplotlib
japanize-matplotlib
Dockerfile
FROM python:3.13-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY bayesian_ab_test.py .

EXPOSE 2718

CMD ["marimo", "run", "bayesian_ab_test.py", "--host", "0.0.0.0", "--port", "2718"]

実行コマンド

環境構築が終われば、以下のコマンドを実行するだけで http://localhost:2718 を開けば、ベイジアンABテストのツールが直ぐにでも使えます。

# 初回のみビルドが必要
docker build -t bayesian-ab-test .
# 2回目以降はビルド済みなので docker run だけでOK。
docker run --rm -p 2718:2718 bayesian-ab-test

試行数と成功数を入力して「分析する」を押すと、事後分布のグラフ、勝率、期待損失がリアルタイムに確認できます。

モンテカルロシミュレーションを使って勝率と期待損失を出しているので、より正確なものが良ければシミュレーション回数を増やすか、理論分布を表示するのもありだと思います。

使ってみた感想

細かい施策や数字は伏せさせていただくのですが、ある機能のABテストで利用しました。
ABそれぞれの母数とCV数を集計し、ベイジアンABテストで分布として可視化し勝率・期待損失を合わせて報告します。
1週間ごとに経過を報告して、当初4週に渡って計測を続ける予定でしたが、3週目の段階で意思決定に至りました。

うまくいった点

  • 根拠を持って報告できた: 可視化で理解しやすく、AIに投げれば勝率・期待損失も含めて良い感じにまとめてくれるので、「今どうなってるの?」という問いに対して根拠を持って答えられました
  • 意思決定を1週間前倒しできた: データが集まるまで判断を待たなくて良いので、別要因で速く意思決定をする必要ができても対応でき、精度高く判断できたと思います
  • 次のアクションに速く移れた: 早い段階で見え始めた傾向や問題点は、詳細分析や次の開発に活かすのも速く始められました

課題・改善点

  • 指標の説明に手間取った: 初めて導入したので「勝率?期待損失?」となり、自分でも「結局、この勝率で決めてしまっていいんだっけ?」「この期待損失が実際どれくらいの問題になるだろう?」と調べながら理解することが多かったです
  • 判断基準を事前に決めていなかった: 複数の指標で見てしまったせいで、こっちでは勝って、こっちでは負けて、が発生して判断に迷いました。これはベイジアンABテストに限らずですが、初めの段階で「何を最優先とするか」「勝率・期待損失が幾つになったら終了するか」を決めておくべきでした。
  • CVRが低い指標では速度の恩恵が薄い: 元からCVRが低いものだと、圧倒的な勝率や問題ないレベルの期待損失になるまでに必要なサンプルサイズが大きくなることは手法を変えても変わらないものでした。もっとCVRの高い指標(購入率であれば、その前段階のカート追加率)を計測するなど、そもそも根本の評価設計で考えないといけない問題でしたね。

まとめ

開発生産性を上げていくと、リリース速度ではなくリリース後の「評価」がボトルネックになるフェーズが来ると思ってます。(というか、もう来てるところは来てる気がします)

ベイジアンABテストは、そのバランスを取るためのひとつの選択肢として良いアプローチではないでしょうか?

  • 途中で見ていい
  • 勝率・期待損失で判断できる

これは意思決定のスピードを上げるのにとても有効に思います。

もちろん、統計的検定が不要になるわけではなく、複雑に絡み合った条件やABテストできず時系列で比較しないといけない場合は「外部影響はないか」「交絡因子がないか」「因果なのか相関なのか」など考えていくのが理想です。
ただ、そこでも大事になるのが、評価の精度を保ちつつ速度を上げる、すなわち 「評価効率」 をどう高めていくか、ということだと思ってます。

同じような課題を感じている組織・チームは、一緒に「評価効率」を上げていく方法を模索して欲しいです!

今回は定量評価の話をしましたが、一番時間が掛かって精度が高い定性評価でも

  • データからインタビュースクリプトをAIに書かせる
  • もうインタビューもAIにしてもらう
  • なんならAI自身にペルソナを与えて、仮のインタビュイーになってもらう

色々と考えられることがありそうですね😏

「評価効率」、バズワードになってくれればいいなぁ。。。

脚注
  1. https://buildersbox.corp-sansan.com/entry/2023/12/12/110000 ↩︎

ランサーズ株式会社

Discussion