UPDATE: 2023-08-26 22:29:27
ここでは、以前おさらいしたベイジアンフレームワークのもとでのABテストをrstanパッケージを利用して再現する。RStanはStanをRから使うためのインターフェースで、Stanは確率的プログラミング言語である。密度関数を指定して、MCMCサンプリングによるベイジアン統計推論による結果が得ることが可能。詳しい話は良き書籍やWebサイトがあるので、そちらにお任せする。
ABテストで使用するサンプルデータはBayesABパッケージのまとめの時に使用したサンプルデータを利用する。日付は14日に限定し、aパターンの方がコンバージョンレートが高くなるようにしている。
library(tidyverse)
library(bayesAB)
library(rstan)
library(scales)
# exploratory社のデータカタログからお借りする
<- read_csv('https://exploratory.io/public/api/GMq1Qom5tS/A-B-IJp6BcB2/data')
df_ab # uniquePageViewとconversion_rateから集計前を再現するための関数
<- function(x, y){
vec_gen map2(
.x = x,
.y = y,
.f = function(x, y){rbinom(n = x, size = 1, prob = y)}
%>% unlist()
)
}<- df_ab %>%
df_a ::filter(
dplyr== '/post?id=11' &
landingPagePath == TRUE &
is_signup >= '2017-06-01' &
date '2017-06-15' > date
)<- df_ab %>%
df_b ::filter(
dplyr== '/post?id=12' &
landingPagePath == TRUE &
is_signup >= '2017-06-01' &
date '2017-06-15' > date
)
<- seq(as.Date('2023-08-01'), as.Date('2023-08-14'), by = "day")
dt <- rep(dt, times = df_a$uniquePageView)
dt_a <- rep(dt, times = df_b$uniquePageView)
dt_b
set.seed(1989)
<- vec_gen(x = df_a$uniquePageView, y = df_a$conversion_rate+0.015)
cv_a <- vec_gen(x = df_b$uniquePageView, y = df_b$conversion_rate)
cv_b
<- union_all(
df tibble(dt = dt_a, cv = cv_a, flag = 'a'),
tibble(dt = dt_b, cv = cv_b, flag = 'b')
)
<- df %>%
df_summary group_by(flag) %>%
summarise(
cnt = n(),
cv = sum(cv),
not_cv = cnt - cv,
cvr = cv / cnt
)
df_summary
## # A tibble: 2 × 5
## flag cnt cv not_cv cvr
## <chr> <int> <int> <int> <dbl>
## 1 a 15575 1737 13838 0.112
## 2 b 15182 1586 13596 0.104
サンプルデータの準備が整ったので、これからベイジアンフレームワークのABテストを行う。
事前分布として前回同様Beta(2, 10)
を利用する。これは、コンバージョンレートは10%前後であると考えており、この確信度合いを観察されたデータから計算できる尤度に反映する。可視化するとこんな感じのベータ分布である。
<- 2
prior_alpha <- 10
prior_beta
tibble(
x = seq(0, 1, 0.01),
y = dbeta(x, prior_alpha, prior_beta)
%>%
) ggplot(., aes(x, y)) +
geom_line() +
geom_vline(xintercept = 0.10, linetype = "dashed") +
scale_x_continuous(breaks = seq(0, 1, 0.05)) +
labs(title = "Beta Distribution with alpha = 2, beta = 10", x = "Conversion Rate", y = "Density") +
theme_bw()
rstanでMCMCを行って、パラメタを推定する前に、モデルを定義しておく必要がある。ここでのモデルを説明する。sA(sB)
はパラメータrateA(rateB)
を成功確率として持つ二項分布に従うと仮定しており、nA(nB)
回の試行中でrateA(rateB)
の確率で成功する回数がsA(sB)
回となる確率を計算している。また、rateA
とrateB
に対して、Beta(2, 10)
を事前分布として設定。これは、成功確率が低いことを反映した分布。
<- "
model data {
// Number of imp
int nA;
int nB;
// Number of cv
int sA;
int sB;
}
parameters {
real<lower=0, upper=1> rateA;
real<lower=0, upper=1> rateB;
}
model {
// 前回の事前分布の設定
rateA ~ beta(2, 10);
rateB ~ beta(2, 10);
sA ~ binomial(nA, rateA);
sB ~ binomial(nB, rateB);
}
generated quantities {
real rate_diff;
rate_diff = (rateA - rateB) / rateB;
}
"
あとは観測されたデータを渡し、
<- list(
data nA = df_summary$cnt[1],
nB = df_summary$cnt[2],
sA = df_summary$cv[1],
sB = df_summary$cv[2]
)
data
## $nA
## [1] 15575
##
## $nB
## [1] 15182
##
## $sA
## [1] 1737
##
## $sB
## [1] 1586
コンパイルして、事後分布を推定する。チェインは4回、試行は1万回とする。
<- stan(
stan_samples model_code = model,
data = data,
chains = 4,
iter = 10000,
seed = 1989
)
## Running /Library/Frameworks/R.framework/Resources/bin/R CMD SHLIB foo.c
## clang -arch arm64 -I"/Library/Frameworks/R.framework/Resources/include" -DNDEBUG -I"/Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/Rcpp/include/" -I"/Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/RcppEigen/include/" -I"/Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/RcppEigen/include/unsupported" -I"/Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/BH/include" -I"/Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/StanHeaders/include/src/" -I"/Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/StanHeaders/include/" -I"/Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/RcppParallel/include/" -I"/Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/rstan/include" -DEIGEN_NO_DEBUG -DBOOST_DISABLE_ASSERTS -DBOOST_PENDING_INTEGER_LOG2_HPP -DSTAN_THREADS -DBOOST_NO_AUTO_PTR -include '/Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/StanHeaders/include/stan/math/prim/mat/fun/Eigen.hpp' -D_REENTRANT -DRCPP_PARALLEL_USE_TBB=1 -I/opt/R/arm64/include -fPIC -falign-functions=64 -Wall -g -O2 -c foo.c -o foo.o
## In file included from <built-in>:1:
## In file included from /Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/StanHeaders/include/stan/math/prim/mat/fun/Eigen.hpp:13:
## In file included from /Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/RcppEigen/include/Eigen/Dense:1:
## In file included from /Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/RcppEigen/include/Eigen/Core:88:
## /Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/RcppEigen/include/Eigen/src/Core/util/Macros.h:628:1: error: unknown type name 'namespace'
## namespace Eigen {
## ^
## /Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/RcppEigen/include/Eigen/src/Core/util/Macros.h:628:16: error: expected ';' after top level declarator
## namespace Eigen {
## ^
## ;
## In file included from <built-in>:1:
## In file included from /Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/StanHeaders/include/stan/math/prim/mat/fun/Eigen.hpp:13:
## In file included from /Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/RcppEigen/include/Eigen/Dense:1:
## /Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/library/RcppEigen/include/Eigen/Core:96:10: fatal error: 'complex' file not found
## #include <complex>
## ^~~~~~~~~
## 3 errors generated.
## make: *** [foo.o] Error 1
##
## SAMPLING FOR MODEL '4e512128d358f3d12936fb6a0b4cb873' NOW (CHAIN 1).
## Chain 1:
## Chain 1: Gradient evaluation took 6e-06 seconds
## Chain 1: 1000 transitions using 10 leapfrog steps per transition would take 0.06 seconds.
## Chain 1: Adjust your expectations accordingly!
## Chain 1:
## Chain 1:
## Chain 1: Iteration: 1 / 10000 [ 0%] (Warmup)
## Chain 1: Iteration: 1000 / 10000 [ 10%] (Warmup)
## Chain 1: Iteration: 2000 / 10000 [ 20%] (Warmup)
## Chain 1: Iteration: 3000 / 10000 [ 30%] (Warmup)
## Chain 1: Iteration: 4000 / 10000 [ 40%] (Warmup)
## Chain 1: Iteration: 5000 / 10000 [ 50%] (Warmup)
## Chain 1: Iteration: 5001 / 10000 [ 50%] (Sampling)
## Chain 1: Iteration: 6000 / 10000 [ 60%] (Sampling)
## Chain 1: Iteration: 7000 / 10000 [ 70%] (Sampling)
## Chain 1: Iteration: 8000 / 10000 [ 80%] (Sampling)
## Chain 1: Iteration: 9000 / 10000 [ 90%] (Sampling)
## Chain 1: Iteration: 10000 / 10000 [100%] (Sampling)
## Chain 1:
## Chain 1: Elapsed Time: 0.022791 seconds (Warm-up)
## Chain 1: 0.024821 seconds (Sampling)
## Chain 1: 0.047612 seconds (Total)
## Chain 1:
##
## SAMPLING FOR MODEL '4e512128d358f3d12936fb6a0b4cb873' NOW (CHAIN 2).
## Chain 2:
## Chain 2: Gradient evaluation took 2e-06 seconds
## Chain 2: 1000 transitions using 10 leapfrog steps per transition would take 0.02 seconds.
## Chain 2: Adjust your expectations accordingly!
## Chain 2:
## Chain 2:
## Chain 2: Iteration: 1 / 10000 [ 0%] (Warmup)
## Chain 2: Iteration: 1000 / 10000 [ 10%] (Warmup)
## Chain 2: Iteration: 2000 / 10000 [ 20%] (Warmup)
## Chain 2: Iteration: 3000 / 10000 [ 30%] (Warmup)
## Chain 2: Iteration: 4000 / 10000 [ 40%] (Warmup)
## Chain 2: Iteration: 5000 / 10000 [ 50%] (Warmup)
## Chain 2: Iteration: 5001 / 10000 [ 50%] (Sampling)
## Chain 2: Iteration: 6000 / 10000 [ 60%] (Sampling)
## Chain 2: Iteration: 7000 / 10000 [ 70%] (Sampling)
## Chain 2: Iteration: 8000 / 10000 [ 80%] (Sampling)
## Chain 2: Iteration: 9000 / 10000 [ 90%] (Sampling)
## Chain 2: Iteration: 10000 / 10000 [100%] (Sampling)
## Chain 2:
## Chain 2: Elapsed Time: 0.023059 seconds (Warm-up)
## Chain 2: 0.026099 seconds (Sampling)
## Chain 2: 0.049158 seconds (Total)
## Chain 2:
##
## SAMPLING FOR MODEL '4e512128d358f3d12936fb6a0b4cb873' NOW (CHAIN 3).
## Chain 3:
## Chain 3: Gradient evaluation took 2e-06 seconds
## Chain 3: 1000 transitions using 10 leapfrog steps per transition would take 0.02 seconds.
## Chain 3: Adjust your expectations accordingly!
## Chain 3:
## Chain 3:
## Chain 3: Iteration: 1 / 10000 [ 0%] (Warmup)
## Chain 3: Iteration: 1000 / 10000 [ 10%] (Warmup)
## Chain 3: Iteration: 2000 / 10000 [ 20%] (Warmup)
## Chain 3: Iteration: 3000 / 10000 [ 30%] (Warmup)
## Chain 3: Iteration: 4000 / 10000 [ 40%] (Warmup)
## Chain 3: Iteration: 5000 / 10000 [ 50%] (Warmup)
## Chain 3: Iteration: 5001 / 10000 [ 50%] (Sampling)
## Chain 3: Iteration: 6000 / 10000 [ 60%] (Sampling)
## Chain 3: Iteration: 7000 / 10000 [ 70%] (Sampling)
## Chain 3: Iteration: 8000 / 10000 [ 80%] (Sampling)
## Chain 3: Iteration: 9000 / 10000 [ 90%] (Sampling)
## Chain 3: Iteration: 10000 / 10000 [100%] (Sampling)
## Chain 3:
## Chain 3: Elapsed Time: 0.022928 seconds (Warm-up)
## Chain 3: 0.027642 seconds (Sampling)
## Chain 3: 0.05057 seconds (Total)
## Chain 3:
##
## SAMPLING FOR MODEL '4e512128d358f3d12936fb6a0b4cb873' NOW (CHAIN 4).
## Chain 4:
## Chain 4: Gradient evaluation took 2e-06 seconds
## Chain 4: 1000 transitions using 10 leapfrog steps per transition would take 0.02 seconds.
## Chain 4: Adjust your expectations accordingly!
## Chain 4:
## Chain 4:
## Chain 4: Iteration: 1 / 10000 [ 0%] (Warmup)
## Chain 4: Iteration: 1000 / 10000 [ 10%] (Warmup)
## Chain 4: Iteration: 2000 / 10000 [ 20%] (Warmup)
## Chain 4: Iteration: 3000 / 10000 [ 30%] (Warmup)
## Chain 4: Iteration: 4000 / 10000 [ 40%] (Warmup)
## Chain 4: Iteration: 5000 / 10000 [ 50%] (Warmup)
## Chain 4: Iteration: 5001 / 10000 [ 50%] (Sampling)
## Chain 4: Iteration: 6000 / 10000 [ 60%] (Sampling)
## Chain 4: Iteration: 7000 / 10000 [ 70%] (Sampling)
## Chain 4: Iteration: 8000 / 10000 [ 80%] (Sampling)
## Chain 4: Iteration: 9000 / 10000 [ 90%] (Sampling)
## Chain 4: Iteration: 10000 / 10000 [100%] (Sampling)
## Chain 4:
## Chain 4: Elapsed Time: 0.02178 seconds (Warm-up)
## Chain 4: 0.026929 seconds (Sampling)
## Chain 4: 0.048709 seconds (Total)
## Chain 4:
パラメータの事後分布の収束を確認しておく。サンプリングチェーンが安定して、トレースが描かれているので、収束していると思われる。また、自己相関なども確認できないので問題はなさそう。rate_diff
のトレースプロットをみるとわかるが、パターンAとパターンBの成功率の相対比にグループAに偏っていることがわかる。
traceplot(stan_samples)
相対比のところは、トレースプロットでみなくても事後分布のプロットを見たほうが早い。
plot(stan_samples)
最後に、前回もまとめているAとBの相対的な優秀さ度合いを可視化しておく。まずは、サンプリング結果を取り出す。
<- as.data.frame(stan_samples)
df_posterior head(df_posterior)
## rateA rateB rate_diff lp__
## 1 0.1108897 0.1057191 0.04890879 -10540.51
## 2 0.1085480 0.1047004 0.03674785 -10541.09
## 3 0.1116404 0.1058727 0.05447805 -10540.51
## 4 0.1069296 0.1055698 0.01288031 -10542.18
## 5 0.1123825 0.1037077 0.08364578 -10540.46
## 6 0.1125160 0.1000795 0.12426630 -10542.07
rate_diff
はstanのモデルのgenerated
quantitiesブロックで定義しておいたので、これが0より大きい回数を計算して平均すれば割合がわかる。結果として97%でパターンAが優れていることがわかる。
<- mean(sign(df_posterior$rate_diff > 0))
ab_result ab_result
## [1] 0.9769
事後分布の信用区間も計算しておく。5%:0.01
はAをBと比較した際に、Aが101%以下の効果を出す確率が5%で、95%:0.12
は、AをBと比較した際に、Aが112%以上の効果を出す確率が5%。つまり、AをBと比較した際に、Aが90%の確率で101%から112%の効果を出すだろうと解釈できる。
# 90% Credible Interval
quantile(df_posterior$rate_diff, probs = c(0.05, 0.95))
## 5% 95%
## 0.01131337 0.12629568
最後に可視化しておく。
%>%
df_posterior mutate(col = if_else(rate_diff > 0 , 'high', 'low')) %>%
ggplot(aes(rate_diff, fill = col)) +
geom_histogram(bins = 100, alpha = 1/2) +
geom_vline(xintercept = 0, linetype = "dashed") +
geom_text(
data = data.frame(),
aes(x = 0.03, y = 50, label = percent(ab_result, accuracy = 0.01)), inherit.aes = FALSE, size = 5
+
) scale_x_continuous(labels = percent_format(accuracy = 0.01)) +
scale_fill_brewer(palette = "Set1") +
labs(subtitle = "Histogramn of (A - B) / B Samples: Probability", x = "(A-B)/B", y = "Density") +
theme_bw()
(x - y) / y
の数式のお気持ちをおまけとしてまとめておく。突然ベイジアンABテストの文脈で出てくるので、どういうことなんだろうかと疑問に思ったが、よく見ると、この式自体は売上高成長率(Sales
Growth
Rate)と同じである。x
とy
の差をy
で割ることによって、x
とy
の相対的な大きさや関係性を表現しようとしているだけではあるが、おさらいを兼ねてメモ書きを残しておく。
式の分子(x - y)
は、x
とy
の差を表している。これにより、x
がy
よりも大きい場合は正の値が、x
がy
よりも小さい場合は負の値が得られる。つまり、この差が意味しているのは、どれだけの大きさの差があるか、またその方向性は正なのか負なのかを意味する。
次に、分母y
は除数で、差をどれだけの大きさで割るかを決める。y
の値が大きいほど、差をより小さな比率で表すことになり、y
の値が小さいと、差の影響がより強く反映される。
式全体で見ると(x - y) / y
は、x
とy
の相対的な関係を示すことになる。例としてx
が100でy
が50の場合、絶対的な差は50。しかし、相対的な比率を計算すると、(100 - 50)/50 = 1
となる。この場合、x
はy
の倍の値を持っていることが分かる。(x - y)/y = -0.5
は、x
がy
の半分以下であることを示す。このように、相対的な比率を用いることで、数値間の関係性を相対的に評価できる。