EconMLケーススタディ - レコメンデーション A/Bテスト: 不完全コンプライアンスを伴う実験

この投稿は[このノートブック](https://github.com/microsoft/EconML/blob/master/notebooks/CustomerScenarios/Case%20Study%20-%20Recommendation%20AB%20Testing%20at%20An%20Online%20Travel%20Company.ipynb)の和訳です。

<img src="https://cdn.pixabay.com/photo/2014/04/03/10/49/bowling-311395_960_720.png" width="300">

# レコメンデーション A/Bテスト: 不完全コンプライアンスを伴う実験

オンラインビジネスの世界では、自社のウェブサイトの新機能やサービスをテストし、下流の収益に与える影響を知りたいと考えています。さらに、どのようなユーザーが新バージョンに最もよく反応するかを知りたいと考えています。このようなユーザーに特化した効果を **heterogeneous treatment effect（異質処置効果）** と呼びます。

理想的なのは、ウェブサイトの新旧バージョン間でA/Bテストを実行することです。しかし、企業は顧客に新しい提供価値を取ることを強制することはできないので、直接のA/Bテストはうまくいかないかもしれません。この方法で効果を測定することは、新しい提供価値に触れたすべての顧客がそれを受け入れるわけではないため、誤解を招くでしょう。

また、既存のデータを直接見ることはできません。最新のウェブサイト機能を使用しているユーザーは、ウェブサイトに非常に興味を持っている可能性が高く、その結果、最初に会社の製品に多くの時間を費やすことになります。このような方法で効果を推定するのは楽観的すぎるでしょう。

このカスタマー・シナリオ・ウォークスルーでは、[EconML](https://aka.ms/econml)ライブラリのツールがどのようにして直接A/Bテストを使用し、これらの欠点を軽減することができるのかを紹介します。

### 要約

1. [背景](#背景)
2. [データ](#データ)
3. [EconMLによる因果関係の取得](#EconMLによる因果関係の取得)
4. [EconMLによる処置効果の理解](#EconMLによる処置効果の理解)
5. [EconMLを用いた方針決定](#EconMLを用いた方針決定)
6. [結論](#結論)

# 背景

<img src="https://cdn.pixabay.com/photo/2013/07/13/12/18/boeing-159589_640.png" width="450">

このシナリオでは、とある旅行会社のウェブサイトにおいて、会員制プログラムに参加することで、ユーザーがウェブサイトを利用する時間をより長くし、より多くの商品を購入するようにすることができるかどうかを知りたいと考えています。

ウェブサイトはユーザーに強制的に会員になってもらうことができないため、直接A/Bテストを行うことはできません。また、旅行会社は、会員になることを選択した顧客は、他のユーザーよりもすでにエンゲージメントが高い可能性が高いため、会員と非会員を比較する既存のデータを直接見ることができません。

**解決策:** この旅行会社は、以前にも、新しいより迅速なサインアップ・プロセスの価値をテストするための実験を行っていました。EconMLのIV推定器は、会員になる可能性の確率にランダムな変動を発生させる手段として、この実験的な会員への後押しを利用することができます。これは**intent-to-treat**設定として知られています。この意図は、ランダムなグループのユーザに「処置」（より簡単なサインアッププロセスへのアクセス）を与えることですが、すべてのユーザが実際に処置を受けるわけではありません。

EconMLの `IntentToTreatDRIV` 推定モデルは、簡単なサインアップを提供されたすべての顧客が会員になったわけではないという事実を利用して、簡単なサインアップを受けた効果ではなく、会員になることの効果を学習します。


```python
# Some imports to get us started
# Utilities
import os
import urllib.request
import numpy as np
import pandas as pd

# Generic ML imports
import lightgbm as lgb
from sklearn.preprocessing import PolynomialFeatures

# EconML imports
from econml.ortho_iv import LinearIntentToTreatDRIV
from econml.cate_interpreter import SingleTreeCateInterpreter, \
                                    SingleTreePolicyInterpreter

import matplotlib.pyplot as plt
%matplotlib inline
```

# データ

データ*は以下のもので構成されています。
 * 実験の28日前に収集された特徴(接尾辞 `_pre` で示される)
 * 実験変数（簡単なサインアップにユーザーがさらされたかどうか -> 操作変数、利用者が会員になったかどうか -> 処置）
 * 実験後28日以内に収集された変数(接尾辞 `_post` で示される)


特徴量名 | 詳細 
:--- |: --- 
**days_visited_exp_pre** |#ユーザーがアトラクションページを訪問した日数 
**days_visited_free_pre** | #ユーザーが無料のチャンネル（例：ドメインダイレクト）でウェブサイトを訪問した日数 
**days_visited_fs_pre** | #ユーザーがフライトページを訪問した日数 
**days_visited_hs_pre** | #ユーザーがホテルのページを訪問した日数 
**days_visited_rs_pre** | #ユーザーがレストランのページを訪問した日数 
**days_visited_vrs_pre** | #ユーザーがバケーションレンタルのページを訪問した日数 
**locale_en_US** | ユーザーが米国からウェブサイトにアクセスしているかどうか 
**os_type** | ユーザーのオペレーティングシステム (windows, osx, その他) 
**revenue_pre** | pre-period期間にユーザーがWebサイトを利用した金額 
**easier_signup** | ユーザーがより簡単なサインアッププロセスにさらされたかどうか 
**became_member** | ユーザがメンバーになったかどうか 
**days_visited_post** | #実験後28日以内にユーザーがサイトを訪れた日数 

**旅行会社の利用者のプライバシーを保護するため、本シナリオで使用するデータは合成的に生成されたものであり、特徴量の分布は実際の分布とは一致しません。ただし、特徴量名はその名称と意味を保持しています。*


```python
# Import the sample AB data
file_url = "https://msalicedatapublic.blob.core.windows.net/datasets/RecommendationAB/ab_sample.csv"   
ab_data = pd.read_csv(file_url)
```


```python
# Data sample
ab_data.head()
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>days_visited_exp_pre</th>
      <th>days_visited_free_pre</th>
      <th>days_visited_fs_pre</th>
      <th>days_visited_hs_pre</th>
      <th>days_visited_rs_pre</th>
      <th>days_visited_vrs_pre</th>
      <th>locale_en_US</th>
      <th>revenue_pre</th>
      <th>os_type_osx</th>
      <th>os_type_windows</th>
      <th>easier_signup</th>
      <th>became_member</th>
      <th>days_visited_post</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>1</td>
      <td>9</td>
      <td>7</td>
      <td>25</td>
      <td>6</td>
      <td>3</td>
      <td>1</td>
      <td>0.01</td>
      <td>0</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
    </tr>
    <tr>
      <th>1</th>
      <td>10</td>
      <td>25</td>
      <td>27</td>
      <td>10</td>
      <td>27</td>
      <td>27</td>
      <td>0</td>
      <td>2.26</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>15</td>
    </tr>
    <tr>
      <th>2</th>
      <td>18</td>
      <td>14</td>
      <td>8</td>
      <td>4</td>
      <td>5</td>
      <td>2</td>
      <td>1</td>
      <td>0.03</td>
      <td>0</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>17</td>
    </tr>
    <tr>
      <th>3</th>
      <td>17</td>
      <td>0</td>
      <td>23</td>
      <td>2</td>
      <td>3</td>
      <td>1</td>
      <td>1</td>
      <td>418.77</td>
      <td>0</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>6</td>
    </tr>
    <tr>
      <th>4</th>
      <td>24</td>
      <td>9</td>
      <td>22</td>
      <td>2</td>
      <td>3</td>
      <td>18</td>
      <td>1</td>
      <td>1.54</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>12</td>
    </tr>
  </tbody>
</table>
</div>




```python
# Define estimator inputs
Z = ab_data['easier_signup'] # nudge, or instrument
T = ab_data['became_member'] # intervention, or treatment
Y = ab_data['days_visited_post'] # outcome of interest
X_data = ab_data.drop(columns=['easier_signup', 'became_member', 'days_visited_post']) # features
```

データは、次の処置効果関数を用いて生成されました。

$$
\text{treatment_effect} = 0.2 + 0.3 \cdot \text{days_visited_free_pre} - 0.2 \cdot \text{days_visited_hs_pre} + \text{os_type_osx}
$$

これは、実験前にWebサイトにアクセスしたユーザーやiPhoneを使用しているユーザーはメンバーシッププログラムの恩恵を受ける傾向があるのに対し、ホテルのページにアクセスしたユーザーはメンバーシップによって害を受ける傾向があるという解釈ができます。 **このような関係性をデータから学ぼうとしています。**


```python
# Define underlying treatment effect function 
TE_fn = lambda X: (0.2 + 0.3 * X['days_visited_free_pre'] - 0.2 * X['days_visited_hs_pre'] + X['os_type_osx']).values
true_TE = TE_fn(X_data)

# Define the true coefficients to compare with
true_coefs = np.zeros(X_data.shape[1])
true_coefs[[1, 3, -2]] = [0.3, -0.2, 1]
```

# EconMLによる因果関係の取得

処置効果の線形射影を学習するには、`LinearIntentToTreatDRIV` EconML 推定器を用います。より柔軟な処置効果関数を求めるには、代わりに `IntentToTreatDRIV` 推定器を使用します。

モデルは、いくつかのニュアンスモデル（つまり、我々が実際には気にしていないが分析には重要なモデル）を定義する必要があります：結果 $Y$ が特徴量 $X$ にどのように依存するかのモデル（`model_Y_X`）と、処置 $T$ が手段 $Z$ と特徴量 $X$ にどのように依存するかのモデル（`model_T_XZ`）です。これらのモデルには事前分布がないため、一般的なブースティング決定木推定器を使用してモデルを学習します。


```python
# Define nuissance estimators
lgb_T_XZ_params = {
    'objective' : 'binary',
    'metric' : 'auc',
    'learning_rate': 0.1,
    'num_leaves' : 30,
    'max_depth' : 5
}

lgb_Y_X_params = {
    'metric' : 'rmse',
    'learning_rate': 0.1,
    'num_leaves' : 30,
    'max_depth' : 5
}
model_T_XZ = lgb.LGBMClassifier(**lgb_T_XZ_params)
model_Y_X = lgb.LGBMRegressor(**lgb_Y_X_params)
flexible_model_effect = lgb.LGBMRegressor(**lgb_Y_X_params)
```


```python
# Train EconML model
model = LinearIntentToTreatDRIV(
    model_Y_X = model_Y_X,
    model_T_XZ = model_T_XZ,
    flexible_model_effect = flexible_model_effect,
    featurizer = PolynomialFeatures(degree=1, include_bias=False)
)
model.fit(Y.values, T, Z, X_data.values, inference="statsmodels")
```




    <econml.ortho_iv.LinearIntentToTreatDRIV at 0x1a1152e3588>




```python
# Compare learned coefficients with true model coefficients
coef_indices = np.arange(model.coef_.shape[0])
# Calculate error bars
coef_error = np.asarray(model.coef__interval()) # 90% confidence interval for coefficients
coef_error[0, :] = model.coef_ - coef_error[0, :]
coef_error[1, :] = coef_error[1, :] - model.coef_
```


```python
plt.errorbar(coef_indices, model.coef_, coef_error, fmt="o", label="Learned coefficients\nand 90% confidence interval")
plt.scatter(coef_indices, true_coefs, color='C1', label="True coefficients")
plt.xticks(coef_indices, X_data.columns, rotation='vertical')
plt.legend()
plt.show()
```


![output_14_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449916/60c4552b-fadb-5339-2cef-437387934493.png)


係数の推定値が線形の処置効果関数の真の係数にかなり近いことがわかります。

また、点推定値、p値、信頼区間を得るために `model.summary` 関数を使うこともできます。下の表から、**days_visited_free_pre**, **days_visited_hs_pre**, **os_type_osx** 特徴量だけが、処置効果について統計的に有意であることがわかります（信頼区間は $0$ を含まず、p値 < 0.05）。


```python
model.summary(feat_name=X_data.columns)
```




<table class="simpletable">
<caption>Coefficient Results</caption>
<tr>
            <td></td>            <th>point_estimate</th> <th>stderr</th>  <th>zstat</th>  <th>pvalue</th> <th>ci_lower</th> <th>ci_upper</th>
</tr>
<tr>
  <th>days_visited_exp_pre</th>       <td>0.001</td>      <td>0.007</td>  <td>0.157</td>   <td>0.875</td>   <td>-0.01</td>    <td>0.012</td> 
</tr>
<tr>
  <th>days_visited_free_pre</th>      <td>0.285</td>      <td>0.007</td> <td>38.361</td>    <td>0.0</td>    <td>0.273</td>    <td>0.297</td> 
</tr>
<tr>
  <th>days_visited_fs_pre</th>       <td>-0.009</td>      <td>0.007</td> <td>-1.257</td>   <td>0.209</td>   <td>-0.02</td>    <td>0.003</td> 
</tr>
<tr>
  <th>days_visited_hs_pre</th>       <td>-0.191</td>      <td>0.007</td> <td>-28.274</td>   <td>0.0</td>   <td>-0.202</td>    <td>-0.18</td> 
</tr>
<tr>
  <th>days_visited_rs_pre</th>        <td>-0.0</td>       <td>0.007</td> <td>-0.051</td>   <td>0.959</td>  <td>-0.011</td>    <td>0.011</td> 
</tr>
<tr>
  <th>days_visited_vrs_pre</th>      <td>-0.001</td>      <td>0.007</td> <td>-0.143</td>   <td>0.887</td>  <td>-0.012</td>    <td>0.01</td>  
</tr>
<tr>
  <th>locale_en_US</th>              <td>-0.043</td>      <td>0.113</td> <td>-0.381</td>   <td>0.703</td>  <td>-0.229</td>    <td>0.143</td> 
</tr>
<tr>
  <th>revenue_pre</th>                <td>-0.0</td>        <td>0.0</td>  <td>-1.551</td>   <td>0.121</td>   <td>-0.0</td>      <td>0.0</td>  
</tr>
<tr>
  <th>os_type_osx</th>                <td>0.964</td>      <td>0.139</td>  <td>6.949</td>    <td>0.0</td>    <td>0.736</td>    <td>1.192</td> 
</tr>
<tr>
  <th>os_type_windows</th>            <td>0.034</td>      <td>0.138</td>  <td>0.244</td>   <td>0.807</td>  <td>-0.194</td>    <td>0.262</td> 
</tr>
</table>
<table class="simpletable">
<caption>Intercept Results</caption>
<tr>
      <td></td>      <th>point_estimate</th> <th>stderr</th> <th>zstat</th> <th>pvalue</th> <th>ci_lower</th> <th>ci_upper</th>
</tr>
<tr>
  <th>intercept</th>      <td>0.539</td>      <td>0.27</td>  <td>1.996</td>  <td>0.046</td>   <td>0.095</td>    <td>0.983</td> 
</tr>
</table>




```python
test_customers = X_data.iloc[:1000]
true_customer_TE = TE_fn(test_customers)
model_customer_TE = model.effect(test_customers)
```


```python
# How close are the predicted treatment effect to the true treatment effects for 1000 users?
plt.scatter(true_customer_TE, model.effect(test_customers), label="Predicted vs True treatment effect")
plt.xlabel("True treatment effect")
plt.ylabel("Predicted treatment effect")
plt.legend()
plt.show()
```



![output_18_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449916/9ed9b414-e4f5-5441-248b-42d8fb599feb.png)


# EconMLによる処置効果の理解

EconMLには、処置効果をよりよく理解するための解釈ツールが含まれています。 処置効果は複雑になる可能性がありますが、多くの場合、積極的に反応するユーザー、中立を維持するユーザー、提案された変更に否定的に反応するユーザーを区別できる単純なルールに私たちは関心があります。

EconML `SingleTreeCateInterpreter`は、EconML推定器のいずれかによって出力された処置効果について単一の決定木をトレーニングすることにより、相互運用性を提供します。 下の図では、メンバーシッププログラムに否定的な反応を示す濃い赤のユーザーと、肯定的な反応を示す濃い緑のユーザーを示しています。


```python
intrp = SingleTreeCateInterpreter(include_model_uncertainty=True, max_depth=2, min_samples_leaf=10)
intrp.interpret(model, test_customers)
plt.figure(figsize=(25, 5))
intrp.plot(feature_names=X_data.columns, fontsize=12)
```



![output_20_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449916/9cc991e0-24c6-1975-1aab-c5c644bac1b5.png)


# EconMLを用いた方針決定

介入には通常コストがかかります：ユーザーに会員になってもらうにはコストがかかります（割引を提供するなど）。したがって、ユーザーのエンゲージメントを高めることで利益を最大化するためには、どのような顧客をターゲットにすればよいかを知りたいと思います。これが**処置方針**です。

EconMLライブラリには、`SingleTreePolicyInterpreter`のような方針解釈ツールが用意されており、処置コストと処置効果を考慮して、どの顧客をターゲットにして利益を最大化するかという簡単なルールを学習することができます。


```python
intrp = SingleTreePolicyInterpreter(risk_level=0.05, max_depth=2, min_samples_leaf=10)
intrp.interpret(model, test_customers, sample_treatment_costs=0.2)
plt.figure(figsize=(25, 5))
intrp.plot(feature_names=X_data.columns, fontsize=12)
```



![output_22_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449916/b1fe72c0-b9cc-3509-4de0-f8e7e8c64bcc.png)


# 結論

このノートでは、EconMLを使って、以下のようなことができることを実証しました。

* 一見不可能に見えるシナリオにおいて、有効な因果関係の洞察を得る
* 結果として得られる個人レベルの処置効果を計算する
* 学んだ効果を軸に方針を構築する

EconMLがあなたのためにできることの詳細については、[ウェブサイト](https://aka.ms/econml)、[GitHubページ](https://github.com/microsoft/EconML)、[ドキュメント](https://econml.azurewebsites.net/)を参照してください。