ジョンとヨーコのイマジン日記

想像してください。「あなたはぼくをプラグマティストだと言うかもしれない」と歌う、逆イマジンです。

検定いらずのABテスト:ポアソン分布とベータ分布によるサンプルサイズ設計

多くのABテストではクリック数やコンバージョン数などのカウントデータを比較します。

ABテストで問題になるのが、

  • どのくらい差があれば、十分「Bのほうが良さそう」と判断できるのか
  • どのくらいのデータ(クリック数やコンバージョン数)がたまるまでまてばいいのか

などです。

そこで仮説検定の枠組みを用いて、例数(サンプルサイズ)設計をきっちり行ってABテストを実施しようと主張する人もいます。

が、たぶん無理だと思います。

その理由は次の通り:
伝統的な仮説検定の枠組みで例数設計を行うには以下を想定(設定)する必要がある

  • 有意水準
  • 検出力
  • A の 期待CV(コンバージョン数)
  • B の 期待CV

しかし AB テストの多くは探索的なものなので、A のCV、B のCVを想定するのは難しいし、有意水準とか検出力とかの設定も難しい。
また「検定して有意にならなかった場合、どうすればいいのか」という指針が与えられません。
(「確実な結果が欲しいので有意になるまで検定を続けます」とやるなら検定とはいえません)

ぼくは、ABテストで興味があるのは「有意差」ではないと思い、問題設定をこのように置きかえます。

『本当は A のほうがCVしやすいのに、誤って B の方を選んでしまう確率を \alpha くらいに抑えたい場合、どのくらい試行回数が必要か』

今回はCVの比を使ってABテストの試行回数を見積もる方法を考えます。

用意するものは以下の2つ。

  • 「AのCVに対してのBのCVの比がこれくらいあれば実質的に意味があるとみなそう」と決めた値(\betaと呼ぶことにする)
  • 「本当は A のほうがCVしやすいのに、誤って B の方を選んでしまう確率を \alpha くらいに抑えたい」という \alpha

AとBのCV数のモデルとしてそれぞれ次のポアソン分布を仮定します。

 a \sim \operatorname{Poisson}(\lambda)
 b \sim \operatorname{Poisson}(\beta \lambda)

B の CV数の期待値は A のほうの \beta 倍です。

「本当は A のほうがCVしやすいのに、誤って B の方を選んでしまう確率」はb/(a+b) が0.5以下になる確率と同値です。

ポアソン分布のモデルが正しいとして、r= b/(a+b) の分布はベータ分布で近似できます。

 s \sim \operatorname{Beta}(\beta \lambda, \lambda)

なので、ベータ分布の分布関数より、s が0.5以下になる確率が\alpha を下回るように  \lambda を定めることができます。

求めた  \lambda 以上のCV数がたまったら、コンバージョンが少しでも多い方を選べば、「本当は A のほうがCVしやすいのに、誤って B の方を選んでしまう確率」は \alpha 以下に抑えられます。

\beta\alpha を小さくとるほど必要な \lambda は大きくなります。

求めた  \lambda が、予算や時間の制約から見て大きすぎる場合は、より大きな \alpha を許容するか、 \beta をより大きくして、「この程度の差なら微差だから気にしないことにする」という範囲を広げます。

実装例

R です。

target <- function(lambda,beta,alpha){
  pbeta(0.5,lambda*beta,lambda)-alpha  
}

sol1 <- uniroot(target,beta=1.03,alpha=0.05,interval = c(0,1e+05))
print(sol1$root)
#6101.871

3%のCV上昇(\beta=1.03 )を、誤りの確率5%(\alpha=0.05)で検知したい場合、必要なCV数は約6102でした。


こういう風にすればシミュレーションができます。

simfunc <- function(lambda,beta){
  a <- rpois(100000,lambda)
  b <- rpois(100000,lambda*beta)
  data.frame(a=a,b=b)
}

sol1 <- uniroot(target,beta=1.03,alpha=0.05,interval = c(0,1e+05))
print(sol1$root)
#6101.871
set.seed(1);dat <- simfunc(round(sol1$root),1.03)
print(round(mean(dat$a>dat$b),2))
#0.05

sol2 <- uniroot(target,beta=1.02,alpha=0.05,interval = c(0,100000))
print(sol2$root)
#13662.36
set.seed(2);dat2 <- simfunc(round(sol2$root),1.03)
print(round(mean(dat$a>dat$b),2))
#0.05

なんパターンかためしてみれば、誤りの確率が設定した \alpha とほぼ同じになることがわかると思います。

\alpha を 0.05 に固定して、\beta を変化させて、必要な  \lambda の値をプロットしてみました。

f:id:abrahamcow:20210104062401p:plain

おしまい。