【Uplift Modeling】모델의 평가 AUUC & Qini measure

해당 포스트는 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 plt
import logging
import numpy as np
import pandas as pd
import seaborn as sns
from lightgbm import LGBMRegressor

plt.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_cumlift

df = df_raw.copy()

get_cumlift(df, outcome_col='conversion', treatment_col='treatment_assign')

# treatment_effect_col='tau', random_seed=42

모델 추정치의 상위 인구 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

# When treatment_effect_col is not given, use outcome_col and treatment_col
# to calculate the average treatment_effects of cumulative population.
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], )) # 제일 뒤에 인덱스가 0 이고 모든 값이 0인 행 삽입
lift = lift.sort_index().interpolate() # 인덱스0 을 가장 앞으로 빼고 각 열에 대해 선형보간
#선형보간은 밑에 있는 결측치에 대해 결측치가 아닌 마지막 값을 전부 넣는것을 반복하는 것

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_cumgain

df = df_raw.copy()

get_cumgain(df, outcome_col='conversion', treatment_col='treatment_assign')

# treatment_effect_col='tau', normalize=False, random_seed=42

모델 추정치의 상위 인구 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
# cumulative gain = cumulative lift x (상위 t 명)
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_qini

df = df_raw.copy()

get_qini(df, outcome_col='conversion', treatment_col='treatment_assign')

# treatment_effect_col='tau', normalize=False, random_seed=42

모델 추정치의 상위 인구 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_lift

df = df_raw.copy()
plot_lift(df, outcome_col='conversion', treatment_col='treatment_assign')

#treatment_effect_col='tau', random_seed=42, n=100, figsize=(8, 8))

[코드 작동원리]

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)]
#0 과 총 인원수(마지막 인덱스) 사이를 n개로 나누는 등차수열 반환

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_gain

df = df_raw.copy()
plot_gain(df, outcome_col='conversion', treatment_col='treatment_assign')

#treatment_effect_col='tau', random_seed=42, n=100, figsize=(8, 8))

[코드 작동원리]

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)]
#0 과 총 인원수(마지막 인덱스) 사이를 n개로 나누는 등차수열 반환

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_qini

df = df_raw.copy()
plot_qini(df, outcome_col='conversion', treatment_col='treatment_assign')

#treatment_effect_col='tau', random_seed=42, n=100, figsize=(8, 8))

[코드 작동원리]

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)]
#0 과 총 인원수(마지막 인덱스) 사이를 n개로 나누는 등차수열 반환

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_score

df = df_raw.copy()
auuc_score(df, outcome_col='conversion', treatment_col='treatment_assign')

# treatment_effect_col='tau', normalize=True

[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_score

df = df_raw.copy()
qini_score(df, outcome_col='conversion', treatment_col='treatment_assign')

# treatment_effect_col='tau', normalize=True

[Output]
score_by_model 5.637753e-02
Random 2.348062e-15

Comments