paiza開発日誌

IT/Webエンジニア向け総合求人・学習サービス「paiza」の開発者が、プログラミングやITエンジニアの転職などについて書いています。

Webサービスでよく使うA/Bテストの評価方法5種をコード付きで解説

f:id:paiza:20191029171322p:plain

f:id:paiza:20151217152725j:plainこんにちは、吉岡([twitter:@yoshiokatsuneo])です。

Webサービスを運営していると、いくつかの機能やページのデザイン・文面・などから、どれを選ぶか迷うことがありますよね。

これがいいんじゃないか、いやあれがいいんじゃないか…などとみんなで考えたり議論したりしても結論が出ないとき、使われる手法の1つがA/Bテストです。

A/Bテストは、A案・B案の機能やデザインをユーザーによって出し分けて、クリック率などの反応を見ることで、どちらの案がよかったかを確認します。

ただ、単純にクリック数やクリック率で比べるだけでは、たまたまその時クリックされただけなのか、有意差があるのかどうかがわかりません。たとえば、A案が10回表示して1クリック、B案が10回表示して2クリック、という場合、B案の方が2倍クリックされているわけですが、1回しか差がないので、偶然多くなっただけな可能性が高いです。

そのため、A/Bテストを実施したあとは、どれくらい確実な差がついているかを評価する必要があります。

今回は、A/Bテストでよく使われる評価方法をまとめてみました。 A/Bテストでよく使われカイ二乗検定から、A/B/C...のように3項目以上の多重比較する方法、ベイズ推定でグラフを描く方法などを紹介します。

実際にPythonとR言語で実行する方法もあわせて書いています。実際にオンライン実行環境paiza.IOで実行することもできますので、興味のある方はぜひ試してみてください。

また、今回紹介するの評価方法では確率分布を利用しています。確率分布については以前こちらの記事で書いていますので、興味のある方はこちらも読んでみてください。

paiza.hatenablog.com

【目次】

カイ二乗検定

A/Bテストで、最も一般的な評価方法がカイ二乗検定(独立性検定)です。

クリック数の観測結果は二項分布で数が大きいと正規分布になり、また正規分布の二乗和がカイ二乗分布になることを利用しています。カイ二乗分布は以下のようなグラフです。グラフに右に進むと確率がゼロに近くなるので、カイ二乗値が大きいとクリック数の差が偶然ではありえないほど大きくAとBで有意な差があると判断できます。

f:id:paiza:20191015171722p:plain
カイ二乗分布

たとえば、A/BテストでページAは100回表示して10回クリックされ(90回クリックされなかった)、ページBは120回表示して20回クリックされた(100回クリックされなかった)場合、A/Bそれぞれのクリック数は以下の表(分割表)になります。

クリック数の測定結果

クリックされた クリックされなかった 計(表示回数)
A案 10 90 100
B案 20 100 120
30 190 220

クリック率を計算すると、A案は10%(10/100=0.1)、B案は17%(20/120=0.17)で、一見B案の方が優れているように見えますが、この程度の差は偶然に起こることかもしれませんので検定してみましょう。

検定では、A案とB案で差がない場合に、測定結果にこのような偏り(またはそれ以上の偏り)が起こるかを調べます。

まず、A案とB案でクリック率に差がない場合のクリック数(理論度数)を調べます。 例えば、A案とB案をあわせた全体のクリック率は13.63%(30/220)なので、A案のクリック数の理論度数は100 * 0.1363 = 13.63となります。

他の項目についても計算すると、理論度数の分割表は以下のようになります。

クリック数の理論度数

クリックされた クリックされなかった 計(表示回数)
A案 13.63 86.36 100
B案 16.36 103.64 120
30 190 220

カイ二乗値は、理論度数と測定結果の差の理論度数に対する比の和になり

 \frac{(10 - 13.63) ^ 2}{13.63}  + \frac{(90 - 86.36) ^ 2}{86.36} + \frac{(20 - 16.36) ^ 2}{16.36} + \frac{(100 - 103.64) ^ 2}{103.64} = 2.058

となります。

より正確にするため、連続性の補正(イェーツの補正)で理論度数と測定度数の差から0.5を引くと、カイ二乗値は

 \frac{(|10 - 13.63| - 0.5) ^ 2}{13.63}  + \frac{(|90 - 86.36| - 0.5) ^ 2}{86.36} + \frac{(|20 - 16.36| - 0.5) ^ 2}{16.36} + \frac{(|100 - 103.64| - 0.5) ^ 2}{103.64} = 1.531

となります。

自由度は、表の(列の数 - 1) * (行の数 - 1) なので、 1 * 1 = 1となります。自由度1でカイ二乗値が2.058より大くなる確率は p = 0.2159 になります。

pが0.2付近なので、5回に1回程度はこのような差は発生するということになります。

よく使われる有意水準は0.05より大きいので有意な差はありません。つまり、一見クリック率に大きな差があったA案とB案ですが、実際には差があるとは言えない、ということになります。

Rでこのカイ二乗検定を行う場合、以下のようになります。

# カイ二乗検定
x<-matrix(c(10, 90, 20, 100), ncol=2, byrow=T)
chisq.test(x)
    Pearson's Chi-squared test with Yates' continuity correction

data:  x
X-squared = 1.5313, df = 1, p-value = 0.2159

https://paiza.io/projects/34czf_ugwz-ALC6TOL9Qhw

カイ二乗値が"X-squared = 1.5313"で、自由度が"df = 1", p値は"p-value = 0.2159"と結果が得られました。

Pythonでカイ二乗検定を行う場合、以下のようになります。

# カイ二乗検定
from scipy import stats
chi2, p, dof, expected = stats.chi2_contingency([[10, 20],[90, 100]])
print(f'Chi-squared={chi2}')
print(f'p={p}')
print(f'degree of freedom={dof}')
print(f'expected frequency={expected}')https://paiza.io/projects/gyyRnC-SQAYl0xaQK-5e-Q?language=r
Chi-squared=1.5313157894736849
p=0.21591507557963685
degree of freedom=1
expected frequency=[[ 13.63636364  16.36363636]
 [ 86.36363636 103.63636364]]

https://paiza.io/projects/gyyRnC-SQAYl0xaQK-5e-Q?language=r

カイ二乗値が"Chi-squared=1.531..."で、自由度が"degree of freedom=1", p値は"p=0.215.."と結果が得られました。期待度数は"expected frequency=[[ 13.63636364 16.36363636] [ 86.36363636 103.63636364]]"です。

フィッシャーの正確検定

カイ二乗検定は分布が正規分布と仮定していますが、実際には二項分布なので、特に標本数が少ない場合(いずれかの期待値が10未満の場合)は誤差が大きくなります。

このような場合、フィッシャーの正確検定が利用できます。この方法では、分割表の値をa,b,c,dとした場合、その値になる確率を、全ての組み合わせからその値になる割合として以下のように求めます。

 p = \frac{ _ {a+b}C _ {a}  \cdot _ {c+d} C _ {c} }{     _ {n} C _ {a+c}   }  = \frac{(a+b)!(c+d)!(a+c)!(b+d)!}{n!a!b!c!d!}   ( n = a+b+c+d )

検定を行うには、これより極端な場合を含めて計算します。

カイ二乗分布と同じデータについて計算すると、p = 0.1712 になり、カイ二乗検定の結果(p=0.2159)よりやや小さくなりました。

Rでフィッシャーの正確検定(両側検定)を行う場合、以下のようになります。

# フィッシャーの正確検定
x<-matrix(c(10, 90, 20, 100), ncol=2, byrow=T)
fisher.test(x)
    Fisher's Exact Test for Count Data

data:  x
p-value = 0.1712
alternative hypothesis: true odds ratio is not equal to 1
95 percent confidence interval:
 0.2204248 1.3256619
sample estimates:
odds ratio 
 0.5570042 

https://paiza.io/projects/jqKJPJPbtfmruK5NAAMzCQ?language=python3

95%信頼区間(95 percent confidence interval)が約0.2〜約1.3となっていることから、B案が5倍程度いい〜A案が1.3倍程度いい、の間で分布していることがわかります。

Pythonでは以下のようになります。

# フィッシャーの正確検定
from scipy import stats
oddsratio, p = stats.fisher_exact([[10, 20],[90, 100]])
print(f'Odds ratio={oddsratio}')
print(f'p={p}')
Odds ratio=0.5555555555555556
p=0.17119203514834436

Odds ratioはオッズ比でA案とB案のオッズ(クリックされた数/クリックされてない数)の比になります。Rではやや計算結果が異なるので少し違う数字が出ています。

https://paiza.io/projects/xaQX8ohb3A3mei3UHWJT7A?language=r

二項検定

同じページに、2個の選択肢(リンク、ボタンなど)がある場合、二項検定が利用できます。

2つから1つを選ぶときの、選ばれた数の分布は二項分布で以下のようなグラフになります。 二項検定では、この二項分布を使って偏りがあるかどうかを調べます。 グラフの右の方では確率が小さくなるので、このような結果がでた場合、有意な差があると判断します。

f:id:paiza:20191011162502p:plain
二項分布: Bi(20, 1/6)

例えば、35回中20回はA、15回はBが選ばれる確率は、両側検定を行うと

  p = 2 \cdot \frac{ {} _ {35} C _ {15} + {} _ {100} C _ {14} + ... +  _ {35} C _{0} }{35!} = 0.4996

で 5%程度とかなり低い確率となり、有意な差があると言っていい場合も多いかと思います。

Rで二項検定を行う場合、以下のように計算できます。

# 二項検定
binom.test(c(20, 15), p=0.5)
    Exact binomial test

data:  c(20, 15)
number of successes = 20, number of trials = 35, p-value = 0.4996
alternative hypothesis: true probability of success is not equal to 0.5
95 percent confidence interval:
 0.3935309 0.7367728
sample estimates:
probability of success 
             0.5714286 

https://paiza.io/projects/V8UOd9uVEc6ydMCZQFjBPg

95%信頼区間は 0.3935309〜0.7367728 となっており、Aが選ばれる確率が39%〜73%の間で分布していることがわかります。

Pythonでは以下のようになります。

# 二項検定
from scipy import stats
stats.binom_test(20, 20+15, 0.5)
0.49955983320251085

https://paiza.io/projects/2CGAcBHbEf9gvrnE6EXD5g?language=r

A/B/nテスト(多重比較)でのカイ二乗検定の補正 

A/Bテストでは2つのケースについて比較しますが、3つ以上のケースについて比較したい場合もありますよね。

たとえば、LPを3種類以上用意して試したい場合や、A/Bパターンと現状を比較したい場合などもあると思います。 

A/B/C/D...のように比較対象が3つ以上ある場合を、A/B/nテストと言います。

A/B/nテストでもカイ二乗検定を行うことができますが、A↔B、B↔C、C↔Aのように複数の比較を行った場合、p値が同じでも偶然どれかの2つでそのp値になる可能性が増えます。

たとえば、A↔B、B↔C、C↔A それぞれの独立性検定で p=0.05 だとしても、3つの検定が独立とすると、全体としてはいずれかが偶然発生する可能性(FWER, familywise error rate)は 1 - (1 - 0.05)3 = 0.14 程度となります。

そのため、3つ以上のケースについて多重比較する場合、多重比較補正を行った調整p値を使います。多重比較補正には、ボンフェローニ補正、シダック補正、ホルム補正などがあります。

  • ボンフェローニ補正

各項目のp値を、比較項目数mで掛けます。p=0.05、項目数が3の場合、調整p値は0.05 * 3 = 0.15 とします。

  • シダック補正

FWERがp値になるように調整します。p=0.05、項目数が3の場合 1 - (1 - 0.05)3 = 0.15のように調整します。

  • ホルム法 (ホルム=ボンフェローニ法)

p値が小さいものから順に、割る数を1ずつ減らしていきます。

たとえば、p=0.03, p=0.04, p=0.05の項目があった場合、0.03 * 3 = 0.09, 0.04 * 2 = 0.08, 0.05 * 1 = 0.05 とします。

また、比較項目を重要度で階層化するゲートキーピング法も利用できます。

具体的な例を見てみましょう。A/B/C案の3つのページについて、クリック数が以下のようになったとします。

クリックされた クリックされなかった 計(表示回数)
A案 10 90 100
B案 15 100 115
B案 25 120 145
50 310 360

この場合の多重比較をR言語で実行すると以下のようになります。

# 多重検定

x<-matrix(c(10, 90, 15, 100, 35, 120)
    , ncol=2
    , byrow=T
    , dimnames=list(c('A', 'B', 'C'), c('Yes', 'No')) )

# カイ二乗検定
r<-chisq.test(x)
r

# 標準化残差
print('Residual:')
r$residuals

# 調整済み残差
print('Stadardized residuals:')
r$stdres

# 残差のp値
print('P:')
pm <- pnorm(abs(r$stdres), lower.tail=FALSE)*2
pm[,'Yes']

# 残差のp値の多重比較補正
print('Adjusted p-value for multiple comparisons:')
Dim <- dim(x)
p.adjust(pm[,'Yes'], "bonf")

# 多重比較の比較対ごとの多重比較補正p値
print('Adjusted p-value for pairwise multiple comparisons:')
pairwise.prop.test(x, c('a', 'a', 'b', 'b', 'c', 'c'), p.adj = "bonf")  # "bonf", "holm"
 Pearson's Chi-squared test

data:  x
X-squared = 8.3172, df = 2, p-value = 0.01563

[1] "Residual:"
         Yes         No
A -1.5436589  0.6791194
B -0.8449059  0.3717091
C  1.9676621 -0.8656559
[1] "Stadardized residuals:"
        Yes        No
A -1.974196  1.974196
B -1.111883  1.111883
C  2.820020 -2.820020
[1] "P:"
          A           B           C 
0.048359421 0.266188395 0.004802067 
[1] "Adjusted p-value for multiple comparisons:"
        A         B         C 
0.1450783 0.7985652 0.0144062 
[1] "Adjusted p-value for pairwise multiple comparisons:"

    Pairwise comparisons using Pairwise comparison of proportions 

data:  x 

  A     B    
B 1.000 -    
C 0.049 0.199

P value adjustment method: bonferroni 

https://paiza.io/projects/HLywpko_yPv9upSqNGc5Ww?language=python3

最後の"Pairwise comparisons using Pairwise comparison of proportions"の部分で、組み合わせでの補正p値が表示されています。 A=C間については、補正後のp値が0.049となっており、有意な差があると言えます。

Pythonでは以下のように計算できます。

# 多重検定
#Ref: http://cup.sakura.ne.jp/prast/prast03.htm

from scipy import stats
import numpy as np

x = np.array([[10, 90], [15, 100], [35, 120]])
x2, p, dof, expected = stats.chi2_contingency(x)
res = x - expected  # 残差
residuals = res / np.sqrt(expected)  # 標準化残差
tsum = x.sum()*1.0  # 観測度数の計
relm = 1.0 - np.sum(x, axis=1) / tsum  # 行要素
celm = 1.0 - np.sum(x, axis=0) / tsum  # 列要素
mouter = np.outer(relm, celm)  # 行要素×列要素:行列の外積
stdres = residuals / np.sqrt(mouter)  # 調整済み残差

print('p-value:')
print(p)

pvals = stats.norm.cdf(- abs(stdres))*2
print('p-values for residuals:')
print(pvals)

print('Standardized residuals:')
print(stdres)

import statsmodels.stats.multitest as multitest

print()
print('Redisual p-value for multiple comparisons:')
reject, pvals_corrected, alphacSidak, alphacBonf = multitest.multipletests(pvals.T[0], method='bonf')
print('Hypothesis:', reject)
print('p-values corrected:', pvals_corrected)
print('alphacSidak:', alphacSidak)
print('alphacBonf:', alphacBonf)

pvals = []
pairs = []
for i in range(3):
    for j in range(3):
        if j<=i:
            continue
        # print(f'i={i}, j={j}')
        pairs.append([i, j])
        pair_x = x[[i,j],:]
        # print(pair_observed)
        x2, p, dof, expected = stats.chi2_contingency(pair_x)
        pvals.append(p)



print()
print('Pairwise p-value for multiple comparisons:')
reject, pvals_corrected, alphacSidak, alphacBonf = multitest.multipletests(pvals, method='bonf')
print('Hypothesis:', reject)
print('pairs:', pairs)
print('p-values corrected:', pvals_corrected)
print('alphacSidak:', alphacSidak)
print('alphacBonf:', alphacBonf)
p-value:
0.015629625899179732
p-values for residuals:
[[0.04835942 0.04835942]
 [0.2661884  0.2661884 ]
 [0.00480207 0.00480207]]
Standardized residuals:
[[-1.97419635  1.97419635]
 [-1.11188315  1.11188315]
 [ 2.82001992 -2.82001992]]

Redisual p-value for multiple comparisons:
Hypothesis: [False False  True]
p-values corrected: [0.14507826 0.79856519 0.0144062 ]
alphacSidak: 0.016952427508441503
alphacBonf: 0.016666666666666666

Pairwise p-value for multiple comparisons:
Hypothesis: [False  True False]
pairs: [[0, 1], [0, 2], [1, 2]]
p-values corrected: [1.         0.04856094 0.19886214]
alphacSidak: 0.016952427508441503
alphacBonf: 0.016666666666666666

"p-values corrected"で、それぞれの組み合わせでのp値が表示されていて、こちらもA=C間での補正p値が0.04856094と、有意といっていい差があることがわかります。

ベイジアンA/Bテスト

ベイズ推定を利用して評価することもできます。単純なA/Bテスト以外の複雑なモデルも扱うことが可能で、分布をグラフで確認して理解を深めることができます。

A/Bテストの場合、事前分布を一様分布(無条件事前分布)として、事後分布としてベータ分布で推定することができます。

Aが100回中10回、Bが100回中20回選ばれた場合についてみてみます。

Rでは以下のように計算できます。

# ベイジアンA/Bテスト
# Ref: https://tech.leverages.jp/entry/2019/04/24/113000

install.packages("bayesAB")
A<-append(numeric(100), numeric(10)+1)
B<-append(numeric(100), numeric(20)+1)
# A<-rbinom(100,1,10/100)
# B<-rbinom(100,1,20/100)
AB <- bayesTest(A,B,priors = c('alpha' = 1, 'beta' = 1),distribution = 'bernoulli') 
summary(AB)
plot(AB)
Quantiles of posteriors for A and B:

$Probability
$Probability$A
        0%        25%        50%        75%       100% 
0.01691176 0.07806591 0.09564285 0.11540097 0.26245213 

$Probability$B
       0%       25%       50%       75%      100% 
0.0602807 0.1481494 0.1701715 0.1937633 0.3297785 


--------------------------------------------

P(A > B) by (0)%: 

$Probability
[1] 0.04585

--------------------------------------------

Credible Interval on (A - B) / B for interval length(s) (0.9) : 

$Probability
         5%         95% 
-0.69299563 -0.01378291 

--------------------------------------------

Posterior Expected Loss for choosing B over A:

$Probability
[1] 0.9163009

"P(A > B) by (0)%: "の部分が 0.04585 となっていて、AがBより大きい確率が約5%、BがAより大きい確率が約95%となっています。

またAとBの差の90%信用区間("Credible Interval on (A - B) / B for interval length(s) (0.9)")が-0.693〜 -0.013となっています。グラフを見たほうがわかりやすいかもしれません。

f:id:paiza:20191029170311p:plain
事後分布

A案、B案でのクリック率の分布です。全体的にB案のクリック率が高そうですが、重なっている部分もあります。ベイズ推定では、確率分布のグラフとして確認できることが特徴です。

f:id:paiza:20191029170413p:plain
(A-B)/Bの分布

AとBの差も確率分布として確認できます。0.5付近(クリック率が倍)程度で山となっていることがわかります。

Pythonでは以下のように実行できます。

# ベイジアンA/Bテスト
# Ref: https://medium.com/hockey-stick/tl-dr-bayesian-a-b-testing-with-python-c495d375db4d

from scipy.stats import beta
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns

fig, ax = plt.subplots(1, 1) 

a_distribution = beta(10, 100)
b_distribution = beta(20, 100)

x = np.linspace(0., 0.5, 1000) 
ax.plot(x, c_distribution.pdf(x))
ax.plot(x, e_distribution.pdf(x))

ax.set(xlabel='conversion rate', ylabel='density')

print('A: 95% interval:', a_distribution.ppf([0.25, 0.975]))
print('B: 95% interval:', b_distribution.ppf([0.25, 0.975]))

sample_size = 100000
a_samples = pd.Series([a_distribution.rvs() for _ in range(sample_size)])
b_samples = pd.Series([b_distribution.rvs() for _ in range(sample_size)])

p_ish_value = 1.0 - sum(a_samples > b_samples)/sample_size
print('p=', p_ish_value)

fig, ax = plt.subplots(1, 1)

ser = pd.Series(e_samples/c_samples)
# Make the CDF
ser = ser.sort_values()
ser[len(ser)] = ser.iloc[-1] 
cum_dist = np.linspace(0., 1., len(ser))
ser_cdf = pd.Series(cum_dist, index=ser)

ax.plot(ser_cdf)
ax.set(xlabel='B / A', ylabel='CDF')
plt.show()
A: 90% interval: [0.07130929 0.13965963]
B: 90% interval: [0.11425129 0.22541755]
p= 0.96001
90% interval:  1.0354072346851797 3.544092374791344

BがAより大きい確率が"p= 0.96001"で96%程度とわかります。90%信用区間(90% interval)を見ると、90%の確率で、Bが1.035〜3.544倍大きいことがわかります。 グラフにすると以下のようになります。 

f:id:paiza:20191029170607p:plain
事後分布

f:id:paiza:20191029170627p:plain
B/Aの分布

まとめ

というわけで、Webサービスの運営でよく利用されているA/Bテストの評価方法について紹介しました。

A/Bテストではカイ二乗検定を使うことが多いですが、状況によって他の検定を利用することで、より正確で柔軟な分析ができるようになります。実際に検証を行う中での気づきも多いので、ぜひ試してみてください。


PaizaCloud」は、環境構築に悩まされることなく、ブラウザだけで簡単にウェブサービスやサーバアプリケーションの開発や公開ができます。 https://paiza.cloud


paizaラーニング」では、未経験者でもブラウザさえあれば、今すぐプログラミングの基礎が動画で学べるレッスンを多数公開しております。

そして、paizaでは、Webサービス開発企業などで求められるコーディング力や、テストケースを想定する力などが問われるプログラミングスキルチェック問題も提供しています。

スキルチェックに挑戦した人は、その結果によってS・A・B・C・D・Eの6段階のランクを取得できます。必要なスキルランクを取得すれば、書類選考なしで企業の求人に応募することも可能です。「自分のプログラミングスキルを客観的に知りたい」「スキルを使って転職したい」という方は、ぜひチャレンジしてみてください。

paizaのスキルチェック





※このブログで紹介しているキャンペーンやイベント、およびサイト内の情報については、すべて記事公開時の情報となります。閲覧されたタイミングによっては状況が変わっている場合もございますのでご了承ください。

ITプログラマー・エンジニア転職・就活・学習のpaiza

プログラミング入門講座|paizaラーニング

PHP入門編Ruby入門編Python入門編Java入門編JavaScript入門編C言語入門編C#入門編アルゴリズム入門編AI機械学習入門

エンジニアのためのプログラミング転職サイト|paiza転職

プログラミング スキルチェックエンジニア求人一覧

未経験からエンジニアを目指す人の転職サイト|EN:TRY

プログラミング スキルチェックエンジニア未経験可求人一覧

エンジニアを目指す学生の就活サイト|paiza新卒

プログラミング スキルチェックエンジニア求人一覧

ブラウザを開くだけで エディタ、Webサーバ、DB等の開発環境が整う|PaizaCloud