シミュレーションで理解するランダム化比較試験:(1)ランダム化の効果を確認する

はじめに

異なる処置の間に効果の違いがあるかどうかを評価するための方法として、「ランダム化(Randomization)」と呼ばれる方法が広く知られています。ランダム化は、処置の効果を歪める*1さまざまな要因の影響を取り除くことができ、処置と結果との因果関係を実証するための最も有効な方法とされています。

例えば新治療の効果が標準治療を上回るかどうかを確かめたい場合には、患者さんを新治療を受けるグループと標準治療を受けるグループにランダムに分け、患者さんの属性(年齢、性別、重症度など・・・)が両グループ間でバランスが取れるようにします*2。例えば重症の患者さんばかりが標準治療を受け、軽症の方ばかりが新治療を受けてしまうと、新治療のほうが良好な結果が得られてしまう可能性があります。このように、属性の違いが結果に影響することを防ぐことが目的です。

また別の例として、Webページに表示する広告をランダムに出し分けることで、広告AとBとの間でサイト閲覧や商品購入に至る割合に違いがあるかを検討するなどの応用も知られています*3

さて、ではランダム化を行うと、本当にグループ間で属性のバランスが取れるのでしょうか?
今回は、Rを使ってシミュレーションしてみることにします。


問題設定

こんな事例を考えて見たいと思います。
とあるアパレルショップでは、新規にアプリ会員登録をしたお客さんを対象に、次回の来店時に使用できるクーポンを配布することにしました。ただし、クーポンの内容をどうするかについては社内で議論があり、以下の2つのどちらが次回の購入金額アップにつながるかを確かめたいということになりました。

(クーポンA):5,000円以上の買い物をした場合に1,000円を割引
(クーポンB):10,000円以上の買い物をした場合に購入金額の10%を割引

さらに、一度の購入単価はお客さんの属性(年齢、性別、収入、など・・・)によってかなり違いそうだという意見が出たため、どちらのクーポンを配布するかをランダムに決めることでクーポンA/Bそれぞれの配布されるお客さんの属性が偏らないよう配慮し、平均的にどちらのクーポンを配布したグループの売り上げが高くなるかを評価しよう、と会議で決定しました。

f:id:mstour:20210402212611j:plain

さて、どちらのクーポンを配布するかは会員登録時点でランダムに決まるものとした場合、クーポンAを渡されたグループとクーポンBを渡されたグループの属性は似たようなものになるでしょうか?

話を簡単にするため、今回は以下の3つの属性に注目することにします。
(1) 性別(男性、女性)
(2) 年齢カテゴリー(20代以下、30代、40代以上)
(3) 会員登録時の購入金額(単位:円)

今回はクーポンAグループとクーポンBグループの平均的な購入金額を比較することになるので、(3)は評価したい結果そのものの処置前*4の値となります*5
血圧のように測定の度に変化しやすいような数値をイメージするとわかりやすいのですが、たまたま高い値が出たとしても、次回測定した時にはそこまで高い値にはならず、何度も測定を繰り返すとその人の本来の値に落ち着くということが往々にしてあります*6。このような現象は「平均への回帰(Regression to the mean)」と呼ばれます。

今回の例では、会員登録時点ではあまり大きな買い物をしなかった場合、次回来店時には比較的購入金額が大きくなる可能性があります(逆に、前回大量の買い物をした場合には、次回は購入金額がそれほど大きくならないことが予想されます)。そうすると仮に登録時点の購入金額が少ない人ばかりにクーポンAが配布された場合、本当はクーポンAとBとで効果に違いがなかったとしても、クーポンAはクーポンBよりも次回の購入金額が大きくなってしまうかもしれません。
平均への回帰を処置の効果と混同しないよう、ランダム化によって比較するグループの間で処置前の値が偏らないようにすることが重要です。

少し話が脇道にそれてしまいました。続いて、今回のシミュレーションの説明に移ります。


シミュレーション方法

クーポンA、クーポンBをそれぞれ500人ずつに配布するという企画を実施するものとします。

まずは、新規会員登録したユーザーの仮想的なデータを作ってみましょう。
以下のように、ユーザーの属性データを作成します。

library(tidyverse)
library(TruncatedNormal)

n_half = 500
set.seed(1234)

# 乱数発生
gender = c(rbinom(n_half, 1, 0.7), rbinom(n_half, 1, 0.3))
agecat = rbind(t(rmultinom(n_half, 1, c(0.6, 0.3, 0.1))),
               t(rmultinom(n_half, 1, c(0.1, 0.3, 0.6))))
buy_bl = c(round(rtnorm(n = n_half, mu = 20000, sd = 5000, lb = 3000, ub = 50000)),
           round(rtnorm(n = n_half, mu = 10000, sd = 5000, lb = 3000, ub = 50000)))

users_0 = data.frame(GENDER = gender,
                   AGE20 = agecat[, 1],
                   AGE30 = agecat[, 2],
                   AGE40 = agecat[, 3],
                   BUY_BL = buy_bl)
# 文字型の変数も作る
users = mutate(users_0,
               GENDERC = factor(case_when(
                 GENDER == 1 ~ "F",
                 GENDER == 0 ~ "M"
               )),
               AGEC = factor(case_when(
                 AGE20 == 1 ~ "20s or younger",
                 AGE30 == 1 ~ "30s",
                 AGE40 == 1 ~ "40s or older"
               )))

2カテゴリーの変数である「性別」は二項分布からのサンプリング(rbinom関数)、3カテゴリーの「年齢カテゴリー」は多項分布(rmultinom関数)を利用して作成しています。
また、連続値である「会員登録時の購入金額」は切断正規分布(Truncated normal distribution)*7から生成しました。洋服の一度の購入金額ということなので、ここでは値が3,000円から50,000円の範囲になるように設定しています。切断正規分布からのサンプリングにはTruncatedNormalパッケージのrtnorm関数を使用しました。
ここで、前半の500サンプルは女性が出やすく、後半は逆に男性が出やすく設定しているのがポイントです。その他の属性も同様に設定しています。

次に、クーポンA・Bの割り当て方法を2通り用意します。
1つめの方法では、ランダム化をせず、単純に前半の500人にはクーポンAを、後半の500人にはクーポンBを渡すことにします。

# 割り当て方法1:ランダム化ではない
alloc1 = c(rep("A", n_half), rep("B", n_half))

2つめの方法では、クーポンAとクーポンBのどちらを配布するかをランダムに決定します(ランダム化です)。これは、単純にAとBがランダムに並ぶベクトルを作ればよいので、先に作成したalloc1をランダムに並べ替えます。

# 割り当て方法2:ランダム化
alloc2 = sample(alloc1)

最後にこの両方をユーザー属性のデータセットに結合しましょう(性別、年齢カテゴリーは、文字型のみ残しました)。

users_alloc = mutate(users[, 5:7], ALLOC1 = factor(alloc1), ALLOC2 = factor(alloc2))

では、クーポンの割り当て方法1(ランダム化なし)と2(ランダム化)のそれぞれで、ユーザーの属性がどのようになったかを確認してみましょう。


ランダム化の結果

属性の集計には、gtsummaryパッケージを使用してみます。

library(gtsummary)

割り当て方法1(ランダム化なし)を行った場合の、クーポンAを配布されたグループとBを配布されたグループそれぞれのユーザー属性を集計してみましょう。
なお、ここではgtsummaryの詳細は割愛します。記法については以下のサイトが参考になります。
note.com
qiita.com

tbl_summary(users_alloc[, -5], by = ALLOC1,
            statistic = list(BUY_BL ~ "{mean}({sd})"),
            digits = list(BUY_BL ~ 1),
            label = list(BUY_BL ~ "Previous sell(Yen)",
                         GENDERC ~ "Gender",
                         AGEC ~ "Age category")
)

結果は以下の通り、クーポンAは女性や若い人に多く配布され、クーポンBは男性や高年齢の人に多く配布されてしまっています。また前回の購入金額の平均にも大きな開きがあります。

f:id:mstour:20210402200153p:plain
もし男性の方が購入金額が高かったり、年齢が高いほど購入金額が高かったりする傾向にある場合、仮にクーポンBのグループの平均的な売り上げがクーポンAより高かったとしても、その効果は本当にクーポンBによるものなのかはっきりしません。この企画ではきっとうまくいかないでしょう。


では、どちらのクーポンを配布するかをランダムに決める割り当て方法2ではどうでしょうか?

tbl_summary(users_alloc[, -4], by = ALLOC2,
            statistic = list(BUY_BL ~ "{mean}({sd})"),
            digits = list(BUY_BL ~ 1),
            label = list(BUY_BL ~ "Previous sell(Yen)",
                         GENDERC ~ "Gender",
                         AGEC ~ "Age category")
)

結果は以下のようになりました。

f:id:mstour:20210402200401p:plain
割り当て方法1に比べて、どの属性も2グループ間でバランスが取れていることがわかります。
したがって公平な比較が可能になり、グループ間の売り上げの違いはクーポンの効果によるものであると考えることができます。

さらに重要なのは、ランダム化を行うことにより、ここでは見ていないその他のあらゆる属性についてもバランスが取れるという点です。
頑張ればデータを集められるような属性であれば、それらの影響を分析の際に調整することも可能ですが*8、データを集められないような属性についてもランダム化によって均等になることが期待できます。


まとめ

今回は、異なる処置を対象者へランダムに割り当てること(ランダム化)により、対象者の属性が処置のグループ間でバランスが取れることを簡単なシミュレーションでみていきました。
上記のような単純なランダム化で十分なこともありますが、場合によってはさらに込み入った方法を用いなければならないこともあります。
次回はもう少し難しい例題を使って考えてみたいと思います。

今回の記事作成では以下の資料を参考にしました。

[1] 岩崎学(2015), "統計的因果推論", 朝倉書店.
[2] 大城信晃 他(2020), "AI・データ分析プロジェクトのすべて", 技術評論社.
[3] J.L.Fleiss(2004), "臨床試験のデザインと解析", アーム.
[4] 「gtsummary」を使って論文に載せる表を作成してみた
https://note.com/exerciseandbrain/n/n755df620d42a
[5] 【R】データ要約ガチ勢のためのgtsummaryで表を書こう
https://qiita.com/yanami/items/117851de49024f5980d0

*1:本当は効果がないのに数値上に見せかけの効果が現れてしまう場合もありますし、逆に本当は効果があるのにその効果が隠されてしまうような場合もあります。

*2:医学分野では、このような研究を「ランダム化比較試験(Randomized controlled trial; RCT)」と呼びます。

*3:Webの分野では「A/Bテスト」という呼ばれ方が多いようです。

*4:ここでの「処置」とは、クーポンAまたはBを配布することです。

*5:医学分野では「ベースライン」と呼ぶことが多くあります。

*6:ここでは健康な人を念頭において考えています。

*7:とりうる値の範囲を制限した正規分布のことです。

*8:例えば線形モデルの説明変数に含めることによって、それらの属性の影響を取り除いた評価を行うことができます。しかしながら、データを取ることが困難であったり、そもそも観察すらできないような何かの影響についてはモデルによる分析では対処のしようがありません。