해당 포스트는 Uber사에서 개발한 파이썬 오픈소스 패키지 CausalML 에 구현된 소스코드들을 다루고 있습다.
* Introduction
일반적인 머신러닝 프로젝트와 다르게 Uplift modeling에서 가장 어려운 부분은 모델의 성능을 평가하는 방법이 매우 까다롭다는 것이다.
(인과추론의 근본문제에 의해)
이러한 평가를 할 때 문헌에서 가장 많이 사용되는 Uplift Curve(and AUUC)와 Qini measure에 대해, Causal에 구현되어 있는 함수들을 이용해서 설명하고자 한다.
* 데이터의 준비
먼저 '치료할당', '전환(conversion)', '모델에 의한 score'의 column을 갖는 DataFrame을 준비한다.
모델에 의한 score가 uplift를 제대로 포착했는지 알아보기 위해 Evaluation을 하는 것이 목적이다.
* 기본 package import
1 2 3 4 5 6 7 8 9 10 from matplotlib import pyplot as pltimport loggingimport numpy as npimport pandas as pdimport seaborn as snsfrom lightgbm import LGBMRegressorplt.style.use('fivethirtyeight' ) sns.set_palette("Paired" ) logger = logging.getLogger('causalml' )
1. GET evaluation metrics
1-1. get_cumlift 함수
1 2 3 4 5 6 7 from causalml.metrics import get_cumliftdf = df_raw.copy() get_cumlift(df, outcome_col='conversion' , treatment_col='treatment_assign' )
모델 추정치의 상위 인구 t 에서 cumulative lift 를 가져온다. 이는 치료군과 통제군의 반응률 차이에 해당한다. 만약 가공의 데이터에서 실제 처치효과를 안다고 할 때는 그 값을 이용하지만, 실제 데이터에서 모르는 경우는 치료군과 통제군의 차이를 이용한다. 모델에 의한 score로 내림차순 정렬했을 때, 상위 \(t\) 명에 대해 \(f(t)\) 를 계산해서 반환한다.
또한 Benchmark를 위해 랜덤하게 할당을 했을 경우 기대할 수 있는 uplift도 함께 반환한다.
\(f_{cumulative\_lift}(t) = \cfrac{Y_t^T}{N_t^T} - \cfrac{Y_t^C}{N_t^C}\)
[코드의 작동원리]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 df = df_raw.copy() np.random.seed(42 ) random_cols = [] for i in range (10 ): random_col = '__random_{}__' .format (i) df[random_col] = np.random.rand(df.shape[0 ]) random_cols.append(random_col) model_names = [x for x in df.columns if x not in ['conversion' , 'treatment_assign' , 'tau' ]] lift = [] for i, col in enumerate (model_names): df = df.sort_values(col, ascending=False ).reset_index(drop=True ) df.index = df.index + 1 df['cumsum_tr' ] = df['treatment_assign' ].cumsum() df['cumsum_ct' ] = df.index.values - df['cumsum_tr' ] df['cumsum_y_tr' ] = (df['conversion' ] * df['treatment_assign' ]).cumsum() df['cumsum_y_ct' ] = (df['conversion' ] * (1 - df['treatment_assign' ])).cumsum() lift.append(df['cumsum_y_tr' ] / df['cumsum_tr' ] - df['cumsum_y_ct' ] / df['cumsum_ct' ]) lift = pd.concat(lift, join='inner' , axis=1 ) lift.loc[0 ] = np.zeros((lift.shape[1 ], )) lift = lift.sort_index().interpolate() lift.columns = model_names lift['Random' ] = lift[random_cols].mean(axis=1 ) lift.drop(random_cols, axis=1 , inplace=True ) lift
1-2. get_cumgain 함수
1 2 3 4 5 6 7 from causalml.metrics import get_cumgaindf = df_raw.copy() get_cumgain(df, outcome_col='conversion' , treatment_col='treatment_assign' )
모델 추정치의 상위 인구 t 에서 cumulative gains 를 가져온다. cumulative gain은 cumulative lift에 상위인구수 전체를 곱한 값이다. 반응률의 증가분에 사람수를 곱해주었기 때문에 Benchmark가 45도선처럼 나타난다.
\(f_{cumulative\_gain}(t) = (\cfrac{Y_t^T}{N_t^T} - \cfrac{Y_t^C}{N_t^C})(N_t^T + N_t^C)\)
normalize=True
를 통해 정규화 할 수 있다.
[코드 작동원리]
1 2 3 4 5 6 7 8 gain = lift.mul(lift.index.values, axis=0 ) normalize = False if normalize: gain = gain.div(np.abs (gain.iloc[-1 , :]), axis=1 ) gain
1-3. get_qini 함수
1 2 3 4 5 6 7 from causalml.metrics import get_qinidf = df_raw.copy() get_qini(df, outcome_col='conversion' , treatment_col='treatment_assign' )
모델 추정치의 상위 인구 t 에서 Qini 를 가져온다. 상위 인구의 치료군 중 반응수와 인구조정을 한 통제군의 반응수를 비교한다.
\(Qini(t) = Y_t^T - Y_t^C\cdot\cfrac{N_t^T}{N_t^C}\)
\(Qini(t) = f_{cumulative\_gain}(t)(\cfrac{N_t^T}{N_t^T+N_t^C})\)
또한 Qini는 culmulative gain에 통제군 비율을 곱한 값인데, gain에서 통제군 비율만큼만 효과로 생각한다고 볼 수 있다. 역시 normalize=True
를 통해 정규화 할 수 있다.
[코드 작동원리]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 df = df_raw.copy() normalize=False np.random.seed(42 ) random_cols = [] for i in range (10 ): random_col = '__random_{}__' .format (i) df[random_col] = np.random.rand(df.shape[0 ]) random_cols.append(random_col) model_names = [x for x in df.columns if x not in ['conversion' , 'treatment_assign' , 'tau' ]] qini = [] for i, col in enumerate (model_names): df = df.sort_values(col, ascending=False ).reset_index(drop=True ) df.index = df.index + 1 df['cumsum_tr' ] = df['treatment_assign' ].cumsum() df['cumsum_ct' ] = df.index.values - df['cumsum_tr' ] df['cumsum_y_tr' ] = (df['conversion' ] * df['treatment_assign' ]).cumsum() df['cumsum_y_ct' ] = (df['conversion' ] * (1 - df['treatment_assign' ])).cumsum() l = df['cumsum_y_tr' ] - df['cumsum_y_ct' ] * df['cumsum_tr' ] / df['cumsum_ct' ] qini.append(l) qini = pd.concat(qini, join='inner' , axis=1 ) qini.loc[0 ] = np.zeros((qini.shape[1 ], )) qini = qini.sort_index().interpolate() qini.columns = model_names qini['Random' ] = qini[random_cols].mean(axis=1 ) qini.drop(random_cols, axis=1 , inplace=True ) if normalize: qini = qini.div(np.abs (qini.iloc[-1 , :]), axis=1 ) qini
2. Plot evaluation metrics
2-1. plot_lift 함수
cumulative lift를 plot 한다.
1 2 3 4 5 6 from causalml.metrics import plot_liftdf = df_raw.copy() plot_lift(df, outcome_col='conversion' , treatment_col='treatment_assign' )
[코드 작동원리]
1 2 3 4 5 6 7 8 9 10 11 12 df = df_raw.copy() df = get_cumlift(df, outcome_col='conversion' , treatment_col='treatment_assign' ) if (n is not None ) and (n < df.shape[0 ]): df = df.iloc[np.linspace(0 , df.index[-1 ], 100 , endpoint=True )] df.plot(figsize=(8 ,8 )) plt.xlabel('Population' ) plt.ylabel('Lift' ) plt.show()
2-2. plot_gain 함수
cumulative gain을 plot한다. \(\rightarrow\) Uplift curve
1 2 3 4 5 6 from causalml.metrics import plot_gaindf = df_raw.copy() plot_gain(df, outcome_col='conversion' , treatment_col='treatment_assign' )
[코드 작동원리]
1 2 3 4 5 6 7 8 9 10 11 12 df = df_raw.copy() df = get_cumgain(df, outcome_col='conversion' , treatment_col='treatment_assign' ) if (n is not None ) and (n < df.shape[0 ]): df = df.iloc[np.linspace(0 , df.index[-1 ], 100 , endpoint=True )] df.plot(figsize=(8 ,8 )) plt.xlabel('Population' ) plt.ylabel('Gain' ) plt.title('Uplift curve' ) plt.show()
2-3. plot_qini 함수
Qini를 plot 한다. \(\rightarrow\) Qini curve
1 2 3 4 5 6 from causalml.metrics import plot_qinidf = df_raw.copy() plot_qini(df, outcome_col='conversion' , treatment_col='treatment_assign' )
[코드 작동원리]
1 2 3 4 5 6 7 8 9 10 11 12 df = df_raw.copy() df = get_qini(df, outcome_col='conversion' , treatment_col='treatment_assign' ) if (n is not None ) and (n < df.shape[0 ]): df = df.iloc[np.linspace(0 , df.index[-1 ], 100 , endpoint=True )] df.plot(figsize=(8 ,8 )) plt.xlabel('Population' ) plt.ylabel('Qini' ) plt.title('Qini curve' ) plt.show()
3. Scoring
3-1. auuc_score 함수
AUUC (Area Under the Uplift Curve) score를 계산한다.
'Uplift Curve의 아래면적'과 'Benchmark의 아래면적'에서 총 인구수를 나눈 값을 반환한다.
1 2 3 4 5 6 from causalml.metrics import auuc_scoredf = df_raw.copy() auuc_score(df, outcome_col='conversion' , treatment_col='treatment_assign' )
[Output]
score_by_model 0.704089
Random 0.500895
3-2. qini_score 함수
랜덤과 Qini curve 사이의 면적으로 점수를 계산한다.
'Qini curve에서 랜덤을 뺀 값'과 '랜덤에서 랜덤을 뺀 값'을 총 인구수로 나눈 값을 반환한다.
1 2 3 4 5 6 from causalml.metrics import qini_scoredf = df_raw.copy() qini_score(df, outcome_col='conversion' , treatment_col='treatment_assign' )
[Output]
score_by_model 5.637753e-02
Random 2.348062e-15