해당 포스트는 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)\) 를 계산해서 반환한다.
\(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]
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]