【python】A/Bテストにおける統計検定-離散値編-

はじめに

A/BテストといえばWebマーケティング戦略で最も使われる手法の一つです。一方でA/Bテストの際にしっかりと統計的な考え方を用いない人も多いのではないでしょうか?今回はpythonを用いてA/Bテストに対してどのように統計を応用していくかを紹介します。

また、今回はCTRCVRといった0,1(離散値)で評価するものを対象としています。ARPUといった連続値で表されるようなものは別記事で紹介する予定です。

統計を使わないとなにが困るのか?

具体的な話に入る前にA/Bテストにおいて統計を使わない場合一体何が困るのか?ということに対して簡単に言及していきます。

A/Bテストを実施してCTRを測定したと仮定します。測定の結果、以下の数値が得られたとします。

click no click
A 8 10
B 16 4

この数値をみるとBのデザインの方がCTRが高いと考えられそうです。ではそれは本当にそうなのでしょうか?もしかしたらたまたまBの方が高い数値になっているだけで、もう一回測定をやり直したらAのデザインの方が数値が高くなったりしないでしょうか?というような不確実性がうまれます。この悩みを解決するのが統計検定であり、得られた測定結果がどれだけ信用できるかを示してくれます。

統計検定

A/Bテストの特にCTRやCVRといった0,1(離散値)で表現するものに対しては主に、以下の統計検定が用いられます。

  • χ2検定
  • 二項検定
  • t検定

データの用意

今回は2つのA/Bテストを実施したと仮定し、2つの測定結果を用意します。データは以下の通り。

測定結果1 click no click
A 200 50
B 100 200
測定結果2 click no click
A 200 120
B 300 200
import panda as pd

df1 = pd.DataFrame([[200,50], [100,200]])
df2 = pd.DataFrame([[200,120], [300,200]])

χ2検定

χ2検定はscipyを用いて、以下のように検定することができます。

import scipy.stats as stats

chi2, p, dof, ef = stats.chi2_contingency(df, correction=False)

それでは実際に計算してみます。2つのデータに対して実際に当てはめていきます。今回はp値のみ取得して確認します。

import scipy.stats as stats

_, p1, _, _ = stats.chi2_contingency(df1, correction=False)
_, p2, _, _ = stats.chi2_contingency(df2, correction=False)

print("結果1におけるp値:{0}".format(p1))
print("結果2におけるp値:{0}".format(p2))

### 出力
# 結果1におけるp値:7.075951756644897e-28
# 結果2におけるp値:0.4740586233069075

この結果より、p値が5%未満となっている結果1においてAとBに有意差があるといえます。一方で結果2は5%を大きく上回っているので有意差があるとはいえないということになります。

二項検定

続いて二項検定です。

import scipy.stats as stats
import numpy as np

data = df.values
p_a = np.sum(data, axis=1)[0]/data.sum()
conversion_total = np.sum(data, axis=0)[0]
conversion_a  = data[0][0]
p_value = scipy.stats.binom_test(x=conversion_a, n=conversion_total, p=p_a, alternative="two-sided")

それでは計算してみます。

import scipy.stats as stats
import numpy as np

data = df1.values
p_a = np.sum(data, axis=1)[0]/data.sum()
conversion_total = np.sum(data, axis=0)[0]
conversion_a  = data[0][0]
p1 = scipy.stats.binom_test(x=conversion_a, n=conversion_total, p=p_a, alternative="two-sided")

data = df2.values
p_a = np.sum(data, axis=1)[0]/data.sum()
conversion_total = np.sum(data, axis=0)[0]
conversion_a  = data[0][0]
p2 = scipy.stats.binom_test(x=conversion_a, n=conversion_total, p=p_a, alternative="two-sided")

print("結果1におけるp値:{0}".format(p1))
print("結果2におけるp値:{0}".format(p2))

### 出力
# 結果1におけるp値:1.876563987554196e-13
# 結果2におけるp値:0.6799649131835366

t検定

最後にt検定です。

import scipy.stats as stats
import numpy as np

data = df.values
a = np.hstack((np.repeat(1, data.item(0,0)),  np.repeat(0, data.item(0, 1))))
b = np.hstack((np.repeat(1, data.item(1,0)),  np.repeat(0, data.item(1, 1))))
t, p = scipy.stats.ttest_ind(a, b, equal_var=False)

計算してみます。

import scipy.stats as stats
import numpy as np

data = df1.values
a = np.hstack((np.repeat(1, data.item(0,0)),  np.repeat(0, data.item(0, 1))))
b = np.hstack((np.repeat(1, data.item(1,0)),  np.repeat(0, data.item(1, 1))))
_, p1 = scipy.stats.ttest_ind(a, b, equal_var=False)

data = df2.values
a = np.hstack((np.repeat(1, data.item(0,0)),  np.repeat(0, data.item(0, 1))))
b = np.hstack((np.repeat(1, data.item(1,0)),  np.repeat(0, data.item(1, 1))))
_, p2 = scipy.stats.ttest_ind(a, b, equal_var=False)

print("結果1におけるp値:{0}".format(p1))
print("結果2におけるp値:{0}".format(p2))

### 出力
# 結果1におけるp値:7.136060577866148e-32
# 結果2におけるp値:0.4736068472487046

検定結果のまとめ

最後に行ってきた検定の結果をまとめてみます。測定結果1が有意であるといえそうですね。

測定結果1 測定結果2
χ2検定 p<0.05 0.05<=p
二項検定 p<0.05 0.05<=p
t検定 p<0.05 0.05<=p

終わりに

A/Bテストは奥が深い。

参考文献