BigQuery上でABテスト結果の検定（母比率の差の検定）を行なう

# 概要とモチベーション

ユーザーを2つ以上の群に分け、それぞれの群に施策を行なって反応の違いを見る[ABテスト](https://ja.wikipedia.org/wiki/A/B%E3%83%86%E3%82%B9%E3%83%88)では、得られた結果を統計検定にかけて、それぞれの群の反応が統計的に有意に異なっていたかを確かめる事が重要です。

引用：UnityAnalyticsABTestingの統計検定結果画面( `significant=yes` なら統計的有意差有り)
![53bdd0b9-097f-4212-a6ff-aa13016128e3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54305/a973fc15-4e6f-420f-a4b3-c6fd0820d336.png)
https://docs.unity3d.com/ja/2020.1/Manual/UnityAnalyticsABTesting.html


ABテストのうち、コンバージョンしたかしていないかをテストの対象とする場合、以下のような表に対して「[母比率の差の検定](https://bellcurve.jp/statistics/course/18227.html)」を実施することになります。

|  | CVした | CVしてない |
| --- | --- | --- |
| control群(統制群) | 300 | 2000 |
| treatment群(介入群)1 | 380 | 2200 |
| treatment群(介入群)2 | 450 | 1800 |

↑のような表をBigQueryで出した後、これまでは母比率の差の検定ができるツール上に数値を入力して有意差があるかどうかを検証していましたが、**BigQuery内で検定させることができると便利**なので実際にやってみます。


# ユーザー定義関数(UDF)の実装
BigQueryでは任意のJavascript関数をUDFとして定義できます。

- https://cloud.google.com/bigquery/docs/reference/standard-sql/user-defined-functions
  - もうUDF機能の[GAリリースから1年以上経った](https://cloud.google.com/bigquery/docs/release-notes#November_22_2019)んですね…

母比率の差の検定を行なうJavascript実装は世の中にいくつか存在するので、今回は https://github.com/thumbtack/abba を使って、母比率の差の検定を行なうBigQuery UDFを実装しようと思います。

[UDFのドキュメント](https://cloud.google.com/bigquery/docs/reference/standard-sql/user-defined-functions#temporary-udf-syntax)に従い、必要なjsライブラリをGCS bucketに置いた上で、以下のようにUDFを定義します。

```sql
CREATE OR REPLACE FUNCTION `util.abba_test`(
  baseSuccess FLOAT64,
  baseTotal FLOAT64,
  variationSuccess FLOAT64,
  variationTotal FLOAT64,
  numOfGroups FLOAT64
) 
RETURNS STRUCT<
  pValue FLOAT64, 
  proportion FLOAT64,
  lowerBound FLOAT64,
  upperBound FLOAT64,
  relativeLowerBound FLOAT64,
  relativeUpperBound FLOAT64
  >
DETERMINISTIC
LANGUAGE js
OPTIONS (
  library=[
    "gs://udf-bucket/path/to/jstat.min.js",
    "gs://udf-bucket/path/to/stats.js"
  ])
AS """
  const ALPHA = 0.05  // 有意水準は5%とする
  const experiment = new Abba.Experiment(numOfGroups-1, baseSuccess, baseTotal, ALPHA, jStat)
  const result = experiment.getResults(variationSuccess, variationTotal)
  
  return {
    "pValue": result.pValue,
    "proportion": result.proportion.value,
    "lowerBound": result.proportion.lowerBound,
    "upperBound": result.proportion.upperBound,
    "relativeLowerBound": result.relativeImprovement.lowerBound,
    "relativeUpperBound": result.relativeImprovement.upperBound,
  }
"""
```

引数と返り値は以下のとおりです。

```markdown

# 引数:
  - baseSuccess: 基準となる母集団の成功人数
  - baseTotal: 基準となる母集団の人数
  - variationSuccess: 比較する母集団の成功人数
  - variationTotal: 比較する母集団の人数
  - numOfGroups: 母集団の数。2集団の比較のときは2 （Bonferroni調整に使用する）
# 返り値:
  - 以下の要素を含んだ構造体
    - pValue: p値
    - proportion: 母集団から推定される成功者の割合
    - lowerBound: 母集団から推定される成功者の割合の、信頼区間の下限
    - upperBound: 母集団から推定される成功者の割合の、信頼区間の上限
    - relativeLowerBound: 基準となる母集団からの、比較する母集団の改善率の下限
    - relativeUpperBound: 基準となる母集団からの、比較する母集団の改善率の上限
```

スクリーンショット：
![36cb89ac-78b4-4908-b572-1c04df6c7105.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54305/13addc8b-7d76-18d5-03c9-d7d916f07c7d.png)


# UDFの利用

冒頭で示したABCテスト結果に対して検定を行なうには、以下のようなクエリを叩きます。

```sql
WITH data AS (
SELECT 'control' _label, 300 success, 2000 total
UNION ALL
SELECT 'treatment1' _label, 380 success, 2200 total
UNION ALL
SELECT 'treatment2' _label, 450 success, 1800 total
)

SELECT 
  _label, 
  success, 
  total,
  util.abba_test(
    first_value(success) over (order by _label = 'control' desc), -- control群のCV数
    first_value(total) over (order by _label = 'control' desc),  -- control群の人数
    success,
    total,
    count(1) over() -- 群の数（今回はABCテストなので3）
  ).*,
FROM data
GROUP BY 1,2,3
ORDER BY 1
```

![5355a73c-5ef0-4e1d-afc2-b7d23942c474-1920x401r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54305/4193272c-6a5d-8261-4789-1580021d01cb.png)


↓小数が見づらいのでフォーマット

_label | success | total | pValue | proportion | lowerBound | upperBound | relativeLowerBound | relativeUpperBound
--- | --- | --- | --- | --- | --- | --- | --- | ---
control | 300 | 2000 | 1.000 | 0.150 | 0.133 | 0.169 | -0.168 | 0.168
treatment1 | 380 | 2200 | 0.083 | 0.173 | 0.155 | 0.192 | -0.019 | 0.318
treatment2 | 450 | 1800 | 0.000 | 0.250 | 0.228 | 0.274 | 0.469 | 0.854

このABCテストにおいては、

- control群のCV率は15.0%(95%信頼区間13.3%〜16.9％)
- treatment1群のCV率は17.3%(95%信頼区間は15.5%〜19.2%)で、control群と比べて有意差がなく(p値=0.083>0.05)、改善率は-1.9%〜+31.8%
- treatment2群のCV率は25.0%(95%信頼区間は22.8%〜27.4%)で、control群と比べて有意差が有り(p値<0.0001)、改善率は+46.9%〜+85.4%

と言ったことが検定からわかりました。

# まとめ

UDF上でABテストでの検定（母比率の差の検定）を実装することで、これまで人力でツールにコピペして実施していたABテストの統計検定をBigQueryのみで実施することができるようになりました。
Lookerダッシュボード等各種BIツールと組み合わせると、人力で計算する部分のない強力なダッシュボードができそうです。

# 備考

## 統計検定手法の詳細について

https://github.com/thumbtack/abba では母比率の差の検定の内部で使う信頼区間に"Agresti-Coull信頼区間"を採用し、多群ABテスト（すなわちABCテスト）時には"Bonferroni調整"によって有意水準の調整を行なっています。

詳細を知りたい方は、以下の参考文献を参照してください。 <span style="font-size: 80%">_私もあんまり細かい部分までちゃんと説明できる自信ないです_</span>


- 母比率の差の検定について： https://bellcurve.jp/statistics/course/18227.html
- ABBAが元にしている統計検定手法について
  - http://thumbtack.github.io/abba/demo/abba.html
- Agresti-Coull信頼区間(もしくは調整Wald法)について
  - https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval#Agresti%E2%80%93Coull_interval
  - https://towardsdatascience.com/five-confidence-intervals-for-proportions-that-you-should-know-about-7ff5484c024f#a837
  - Agresti-Coull信頼区間の元となっているwilson score信頼区間について: https://ja.wikipedia.org/wiki/%E3%82%A6%E3%82%A3%E3%83%AB%E3%82%BD%E3%83%B3%E3%81%AE%E4%BF%A1%E9%A0%BC%E5%8C%BA%E9%96%93
- Bonferroni調整(多群ABテスト(ABC...テスト)手法)について
  - https://www.med.osaka-u.ac.jp/pub/kid/clinicaljournalclub1.html
  - 要するに、比較したい母集団の数Nに対して、有意水準αを `α/N` に変更するというもの。

## ライセンス表記

#### [abba](https://github.com/thumbtack/abba)
```
Copyright (c) 2014 Thumbtack
BSD 3-Clause "New" or "Revised" License
```
#### [jStat](https://github.com/jstat/jstat)
```
Copyright (c) 2013 jStat
MIT License
```