はじめに
仕事で、ベイジアンABテストを利用するのが良さそうだなと思い、調べてみましたのでまとめてみます。
ベイジアンABテスト
通常のABテストの懸念点
頻度論に基づく仮説検定では、帰無仮説における母数に対して得られた標本のがどの程度起こりうるかを算出する。
また、A/Bテストを開始する前に有意水準、検出力、効果量を予め定め、サンプルサイズを決定しておかなけらばならない。
それぞれの指標の決定は、少なからず統計の知識が必要であり、万人には受け入れられにくい。
そして、設定したサンプルサイズが集まるまで、検定を行うまで待たなければならない。
ビジネスの現場において通常のABテストを適切に運用しようとすると、上記のようなことが制約になることが少なくない。
ベイズ統計を用いた検定
ベイズ統計では自体を確率変数とし、データxを得た上でのの事後分布を次のベイズの定理を用いて推定する。
ここで、はを条件付けたの確率分布(尤度)、は事前分布、p(x)は周辺尤度である。
ベイジアンABテストでは、A群とB群それぞれのイベント発生率の事後分布を算出する。
そして、これら二つの分布を比較することで、A群とB群に差があるかどうかを決定する。
イベント発生率の推定
ここであるイベントが発生するかどうかを考えます。
ある群におけるイベントの発生は、ベルヌーイ分布で表現することができる。
この時の尤度は次にようになります。
事前分布としてベータ分布を用いる。
ベータ分布分布はベルヌーイ分布の共役事前分布である。
共役事前分布とは、事前分布を元に事後分布を計算した際に、それらが一致する分布のこと。
この共役事前分布を用いることで、ベイズ更新が行えるようになる。
なお、ベータ分布のパタメータをぞれぞれ1とすると、一様分布に一致する。
これらより事後分布を計算すると次のようにある。
事後分布はとなる。 なお、は次のようになる。
事前分布の設定
事前分布を適切に設定することでサンプルサイズを少なくすることができる。
事前分布の設定の考え方について、こちらでは次にように書いていました。
We do not recommend using a prior distribution so strong that it overwhelms any data that is observed. It’s generally good practice to choose priors that are a bit weaker than what the historical data suggest.
確かに、テストを行う前に得られている事前情報を表現するのもであるため、確信度が高いものを設定するのは適切ではなさそう。
RでベイジアンA/Bテスト
Rで手軽にABテストを行うには、bayesAB
packageが便利です。
二つのあるパラメータを指定したベルヌーイ分布から得られたデータを取得します。
A_binom <- rbinom(2000, 1, .62) B_binom <- rbinom(4000, 1, .60)
bayesTest関数を用いてベイジアンA/Bテストが実行できます。 distribution引数で検定のパターン(分布)を、prior引数には事前分布のパラメータを指定します。
AB1 <- bayesTest(A_binom, B_binom, priors = c('alpha' = 1, 'beta' = 1), n_samples = 1e5, distribution = 'bernoulli')
Bの分布のパラメータよりAのパラメータの方が大きい確率を確認する。
Aの方の比率が高いと言える可能性はありますが、確信度としては高くありません。
> summary(AB1)[2]$probability$Probability [1] 0.68993
結果を可視化してみます。 以下のようにすると3種類の結果が出力されます。
plot(AB1)
一つ目は、事前分布の形状です。
二つ目は、それぞれの群の推定された事後分布です。
三つ目は、(A-B)/Bのデータの分布(相対効果値のヒストグラム)です。
AとBに差があるとは主張できないと考えられます。また、差があることを検出するにはサンプルサイズが足りないかもしれません。
事前分布を確信度が高いものにしてみました。
AB1 <- bayesTest(A_binom, B_binom, priors = c('alpha' = 90, 'beta' = 80), n_samples = 1e5, distribution = 'bernoulli')
Aの方の比率が高いと言える可能性は低くなった。
AとBに差があることを主張するにはサンプルサイズが足りない可能性がある方向により傾いたことになる。
事前分布を適切に設定することにより、よりよい意思決定に繋がりそうです。(言ってることが怪しい...)
> summary(AB1)[2]$probability$Probability [1] 0.59808
サンプルサイズと確率
サンプルサイズと推定されるパラメータの変化を確認します。
set.seed(1234) N <- 10000 A_all_binom <- rbinom(N, 1, .61) B_all_binom <- rbinom(N, 1, .60) data.frame(samplesize = seq(100, N, 100)) %>% mutate(A = map(samplesize, ~mean(A_all_binom[1:(.)])) %>% unlist(), B = map(samplesize, ~mean(B_all_binom[1:(.)])) %>% unlist()) %>% pivot_longer(col = -samplesize, names_to = "data", values_to = "rate" ) %>% ggplot(aes(x = samplesize, y = rate, group = data, col = data))+ #geom_point()+ geom_line()+ theme(legend.position=c(0.8,0.8))
次に、サンプルサイズと推定されたA群のの方が高い確率の変化も確認しました。
test_list <- lapply(1:100, A = A_all_binom, B = B_all_binom, FUN = loop_bayesTest) %>% unlist() data.frame(p = test_list, samplesize = 1:N) %>% ggplot(aes(x=samplesize, y=p))+ geom_line()+ geom_point()
5000サンプルぐらい集まれば確信を持ってA群の方が良いとして、テストを停止することができそうです。
テストの停止基準
上記の状態では、明確なサンプルサイズに関する設定がない場合ため、いつテストを停止するかの決定が行いにくい。
調べてみると次にような記事がありました。
参考2:medium.com
ここでは、テストの停止を判断するための損失関数を紹介していました。
ここで、はA群とB群のメトリックである。
は選択する群を意味する。
がより小さい時にAを選択すると、損失は、が より大きい時にAを選択すると損失は0となる。
の真の値を観測できないため、事後密度の予想損失を計算する。
これは精度が悪い選択肢を間違って選択した場合の損失を表現している。
AとBのどちらかの群の予測損失が閾値を下回ると、実験を停止する。
この閾値は精度が悪い選択肢を間違って選択した場合の損失の許容度を表現している。
また、サンプルサイズと検出精度のトレードオフになってる。
損失のシミュレーション
この損失関数を利用したベイジアンABテストの停止がどのような挙動を示すのかを確認しました。
基本的にはこちらのコードをそのまま動かしただけです。
シミュレーションの設定は次にようにしました。
- A群の真値:0.015
- B群の真値:0.018
- 最大サンプルサイズ:20000
- 試行回数:500
- 停止基準:
sim1 = run.sims(a.cvr = 0.015, b.cvr = 0.018, max.obs = 10000, obs.increments = 100, n.simulations = 500) sim1 = getloss(sim1)
はじめに、ある試行におけるサンプルサイズに対するA群とB群の損失の変化を確認しました。
threshold = 0.001 sim1 %>% filter(n.sim==3) %>% pivot_longer(cols = c("loss.a","loss.b"), names_to="group", values_to ="loss") %>% ggplot(aes(x = checkin, y = loss, group=group, col=group))+ geom_line()+ xlab("サンプルサイズ")+ geom_hline(yintercept = threshold, linetype = 2, aes(colour = "Threshold")) + theme_gray(base_family = "HiraKakuPro-W3")+ theme(legend.position=c(0.8,0.85))
Bの損失が1000付近でを切っていました。
次に、サンプルサイズが増えていったときにテストが停止する割合を確認してみました。
outcomes %>% ggplot(aes(x = checkin * 2)) + # Multiply by two because both variants had this many obs stat_ecdf() + ggtitle("テストの停止に関する経験分布関数") + labs(x = "サンプルサイズ", y = "テストを停止した割合")+ theme_gray(base_family = "HiraKakuPro-W3")
サンプルサイズが5000程度で全ての試行は停止しています。
小さい効果量の割に少ないサンプルサイズでテストを停止することができているようです。
500回の試行のうちどれだけB群を採択してしたのかを確認しました。
outcomes <- sim1 %>% mutate(threshold.met = (loss.a < threshold | loss.b < threshold)) %>% filter(threshold.met | checkin == max(checkin)) %>% group_by(n.sim) %>% filter(checkin == min(checkin)) %>% mutate(selection = case_when( loss.a < loss.b ~ "A", loss.b < loss.a ~ "B" )) outcomes %>% group_by(selection) %>% count(name = "count") %>% ungroup %>% mutate(freq = count / sum(count)) %>% ggplot(aes(x = selection, fill = selection, y = freq, label = round(freq, 2))) + geom_bar(stat = "identity") + geom_text(vjust = -0.75, size = 4) + ggtitle("各群の採択割合") + labs(x = "", y = "採択割合") + coord_cartesian(ylim = c(0, 1)) + theme_gray(base_family = "HiraKakuPro-W3") + theme(legend.position = "none")
B群の方が採択される可能性が高い状態になっています。が、7割弱は少ないです。
やはり、ビジネス要件に合うようなを設定することが必要になります。
その意思決定のために参考1の記事では、以下のようなシミュレーションを行っていました。
参考にしたいです。
(疲れたので雑に締めます)