RandomForestベースのUplift-modelingを試してみる

2020.05.01

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

最初に

causalmlを使って「RandomForest」ベースの手法でUplift-modelingを試してみました。
実際にやっていく過程を通して、本手法に対する理解を深めていくことを目的としています。
具体的にやっていくことは「データの準備」、「モデルの学習」、「結果の可視化」です。

目次

1.最初に

基本的にはcausalmlのドキュメントやサンプルノートブックファイル、実装内容を元に確認を進めていきます。
ドキュメントは下記を、

サンプルノートブックファイルは下記の2つを

実装内容はGitを確認しました。

2.データとやることの概要

まずは、下記の通りにサンプルデータを用意します。

import numpy as np
import pandas as pd
from IPython.display import Image
from sklearn.model_selection import train_test_split

from causalml.dataset import make_uplift_classification
from causalml.inference.tree import UpliftTreeClassifier, UpliftRandomForestClassifier, uplift_tree_string, uplift_tree_plot
from causalml.metrics import plot_gain


# データの取得
df, x_names = make_uplift_classification()

# 列名を変更
x_names_new = ['feature_%s'%(i) for i in range(len(x_names))]
rename_dict = {x_names[i]:x_names_new[i] for i in range(len(x_names))}
df = df.rename(columns=rename_dict)
x_names = x_names_new

# データの確認
df.head()

こんな感じで特徴量を保持しているデータで、

「conversion」を目的変数としたデータです。
(0 or 1の値が入ります)

注意しておきたい点として、「介入群」を示す列(treatment_group_key)を見ると、「4種類の値」が入っています。
1つのcontrol群と、3種類のtreatment群です。
今回はこのような「複数のtreatment群」を取り扱うデータを取り扱っていきます。

df.pivot_table(values='conversion',
               index='treatment_group_key',
               aggfunc=[np.mean, np.size],
               margins=True)

今回はこのデータを「何らかの施策を実施した結果のデータ(会員ID単位でデータを保持、とする)」とみなして、「次の施策対象者」を選定することを想定したフローを実施してみようと思います。

3.やってみた

まずはいきなりですが、モデルを学習させてみます。
後ほど簡単な検証をするためにデータを分割した後に、fitさせています。
Uplift modeling特有のパラメータがある点に注意が必要です。
(本来なら、前処理やデータの確認等をもっと丁寧にやるべきですが、今回は全体のフローを確認することを目的としているため省略します)

# データの分割
df_train, df_test = train_test_split(df, test_size=0.2, random_state=111)
df_train, df_test = df_train.reset_index(drop=True), df_test.reset_index(drop=True)

# 必要列のみに絞り込み
x_names = ['feature_0', 'feature_1', 'feature_2', 'feature_3', 'feature_4', 'feature_5', 'feature_11', 'feature_12', 'feature_13', 'feature_14', 'feature_15', 'feature_16', 'feature_17', 'feature_18']

# 学習
uplift_model = UpliftRandomForestClassifier(n_estimators=500, 
                                            max_depth = 5, 
                                            min_samples_leaf = 50, 
                                            min_samples_treatment = 20, 
                                            evaluationFunction='ED', # Choose from one of the models: 'KL', 'ED', 'Chi', 'CTS'.
                                            control_name='control'
                                           )
uplift_model.fit(X=df_train[x_names].values,
                 treatment=df_train['treatment_group_key'].values,
                 y=df_train['conversion'].values)

続いて、簡単に検証をしてみましょう。
今回は上記で学習したモデルに「検証用データ」を投入し、学習データで過学習していないか、を確認します。

# RFの中の木のうちの一つについて、中身を表示する
uplift_tree = uplift_model.uplift_forest[0]
uplift_tree.fill(X=df_test[x_names].values, 
                 treatment=df_test['treatment_group_key'].values, 
                 y=df_test['conversion'].values)

# Plot
graph = uplift_tree_plot(uplift_tree.fitted_uplift_tree,x_names)
Image(graph.create_png())

こんな画像が出力されるかと思いますが、こちらはRandomForestの複数ある木のうちの一本を抜き出して、学習用データと検証用データを投入し推論した際の各指標が表示されています。

これだとわかりづらいので、一部をクローズアップしてみます。

「uplift score」と「validation uplift score」を比較して、過学習していないかを確認します。
(あくまでも「複数ある木のうちの一本」なのでモデル全体の評価とはならないので、参考程度ですが...)
一応、下記のように画像ファイルとして出力しておけます。

# ファイルに出力して保存
graph.write_png("tbc.png")

続いて、検証用データでITEを推論し、その結果を見てみます。

# 推論 
pred_test = uplift_model.predict(X=df_test[x_names].values,
                                 full_output=False)
# どの列がどの介入群かわかるようにする
pred_test = pd.DataFrame(pred_test,columns=uplift_model.classes_)

# 確認
print(pred_test.head())
print('='*40)
print(pred_test.describe())

下記のように、複数の介入群それぞれ別々にITEの推論値が取得できます。

   treatment1  treatment2  treatment3
0    0.013529    0.068299    0.009561
1    0.008413    0.096323    0.043545
2    0.001077    0.019802   -0.014936
3    0.080017    0.069163    0.486557
4    0.105540    0.103907    0.399410
========================================
       treatment1  treatment2  treatment3
count  800.000000  800.000000  800.000000
mean     0.006893    0.047674    0.098753
std      0.075594    0.058028    0.194619
min     -0.195904   -0.120387   -0.135766
25%     -0.035603    0.013585   -0.022576
50%      0.009756    0.047892    0.007120
75%      0.048577    0.080463    0.154632
max      0.228215    0.265345    0.664184

この後の処理のために、この推論結果をちょっと整形します。
「どの介入群に割り当てても効果が負」と推定された場合は「control群」、そうでないなら一番効果が大きい介入群、という形で割り当てるための列を計算します。

# どの介入群に割り当てても負の効果しかもたらさない場合は「Control」とするために列を追加
pred_test = pred_test.assign(Control=0.0)

max_treatment_key = pd.DataFrame(pred_test.idxmax(axis=1))
max_treatment_key.rename(columns={0:'max_treatment_key'}, inplace=True)
max_treatment_value = pd.DataFrame(pred_test.max(axis=1))
max_treatment_value.rename(columns={0:'max_treatment_value'}, inplace=True)

max_treatment = pd.concat([max_treatment_key, max_treatment_value], axis=1)

# 元のDFと上記までの計算結果を結合
result_test = pd.concat([df_test, pred_test, max_treatment], axis=1)
# 必要列だけに絞り込み
result_test = result_test.loc[:,['treatment_group_key','conversion', 
                                 'treatment1', 'treatment2', 'treatment3', 
                                 'max_treatment_key', 'max_treatment_value']]
# 結果の確認
result_test.head(10)

これで、検証用データに対して、各介入群ごとのITE推論値と、どの群に割り当てると良さそうか、というところまでが計算できました。
続いて、ここまでの結果を可視化してみます。
上記の整形した推論値と、介入したか否か、コンバージョンしたか否か、を示す列を使って「uplift-curve」を描画します。

from causalml.metrics import plot,auuc_score

# 介入したか否か、を列として追加
result_test['treatment_flg'] = 1
result_test.loc[(result_test['treatment_group_key'] == 'control'), 'treatment_flg'] = 0

# 可視化
plot(df=result_test.loc[:,['conversion','treatment_flg', 'max_treatment_value']], 
     outcome_col='conversion', 
     treatment_col='treatment_flg'
    )

上記では、最大の介入効果が予想できる介入群に割り当てた際(青色)と、Randomに割り当てた際(赤色)とを比較してLiftの累積値を計算しています。
(X軸:サンプルサイズ、Y軸:累積でのLift値)

青い曲線が少しギザギザしてしまっています...。
理想としては、下記の赤/青色のような曲線にしたかったのですが、これはモデリングをもう少し頑張る必要がありそうです。
(ex.前処理/パラメータチューニング/モデルを変える)

一旦フローを最後まで確認することを優先するため、このまま最後まで進めます。
上記の結果から、施策対象者を選定するための閾値を決めたら、下記のように値を抽出して、
(今回は、上位300人目のScoreを閾値としました)

result_test.max_treatment_value.sort_values(ascending=False)[300]
0.37130275199999974

今回作成したモデルで推論して、この閾値より大きい対象者を施策対象者にする、というような使い方ができそうです。
(この時、どの介入群に割り当てるか、も推論できる)
また、定量評価指標として、AUUC(Area Under the Uplift Curve)も簡単に計算できます。
(今回は複数のTreatment群の中のMAX値を使っている点に注意してください)

# AUUCの確認
auuc_score(df=result_test.loc[:,['conversion','treatment_flg', 'max_treatment_value']], 
          outcome_col='conversion', 
          treatment_col='treatment_flg')
max_treatment_value    0.819895
Random                 0.446675
dtype: float64

本当に使うとしたら、まだまだ検証や考察が必要(ex.他のモデルとの比較、Scoreが高いレコードの内容を確認)ですが、本エントリーではざっくり雰囲気をつかむことを目標とするため、一旦ここまでとします。

4.最後に

今回、全体的なフローを確認してみたのですが、まだまだ「モデルや推論値の検証」といった点を勉強しないといけないと感じました...。
「因果推論では検証が難しい」ものだとは思いますが、実践で使うためには「検証」はとても重要な要素だと個人的に感じているので、もっと検証方法について勉強しないといけませんね...。

また、今回はロジックの詳細には触れていないのですが、詳細部分が気になる方も多いと思います。
causalmlにおける「RandomForestClassifier」は「Ensemble methods for uplift modeling」と「Decision trees for uplift modeling with single and multiple treatments」に基づいている、とのことですので、もし詳細を確認したい方は、まずはTree-Based Algorithmsを読んで次に「Decision trees for uplift modeling with single and multiple treatments」や「Ensemble methods for uplift modeling」を読んでいくといいかもしれません。

UpliftRandomForestClassifier is based on Ensemble methods for uplift modeling by Sołtys (2015) and its base learner, UpliftTreeClassifier is based on Decision trees for uplift modeling with single and multiple treatments by Rzepakowski and Jaroszewicz (2012). More references for other methods are available at the API documentation site.

参照:Issues

最後まで読んでいただきありがとうございます。
変なところがあればドシドシご指摘いただけると幸いです。