파이썬으로 업리프트 모델링
출처: 아리가 미치아키, 머신러닝 실무 프로젝트, 2018, 한빛미디어
업리프트 모델링(Uplift modeling)은 역학통계에서 사용되는 기법으로, 무작위 대조시험의 결과를 분석합니다. 이를 응용해 신약 개발에서 환자의 연령 및 성별, 유전자등을 특성으로 머신러닝을 수행하고, 부작용이 예상되는 환자를 미리 제외해 개인화된 의료 서비스 제공을 기대할 수 있습니다.
- 무작위 대조시험: 모집단을 무작위로 실험군과 대조군으로 나누는 것을 말합니다. 예를 들어 신약 개발에서 실험군에는 신약을 투여하고 대조군에는 가짜 약을 투여하는 것 입니다.
0. 데이터 설명¶
과거 12개월 동안 구매이력이 있는 고객을 대상으로 '남성 타겟 판촉 메일', '여성 타겟 판촉 메일', '메일을 발송하지 않음'을 수행하고 이 행동이 사이트 방문으로 이어졌는지 조사한 것입니다.
필드명 | 내용 |
---|---|
recency | 마지막 구매 기록으로부터 경과한 기간 |
history_segment | 최근 1년 구매 금액에 따라 분류한 유형 |
history | 최근 1년 구매 금액 |
mens | 최근 1년 동안 남성용 제품 구매 여부 |
womens | 최근 1년 동안 여성용 제품 구매 여부 |
zip_code | 우편번호 |
newbies | 신규고객 여부 |
channel | 최근 1년동안 구매한 경로 |
segment | 고객에게 보낸 판촉 메일 유형 |
visit | 메일 수신후 2주 내 사이트 방문여부 |
conversion | 메일 수신후 2주내 상품 구매 여부 |
spend | 메일 수신후 2주내 구매 금액 |
1. 데이터 불러오기¶
먼저 데이터 파일을 pandas
를 이용해 읽어 들이고 구조를 확인합니다.
import pandas as pd
source_df = pd.read_csv("./data/Kevin_Hillstrom_MineThatData.csv")
source_df.tail()
mailed_df = source_df[source_df["segment"] != "No E-mail"].reset_index(drop=True)
mailed_df.tail()
데이터의 형태를 확인해 봅니다.
mailed_df.dtypes
위의 결과에서 zip_code, channe, segment가 object인것을 볼 수 있습니다. segment는 label로 사용될 것입니다.
2.2. 원핫인코딩(one-hot encoding)¶
zip_code, channel 값은 범주형임으로 one-hot encoding을 수행합니다.
dummied_df = pd.get_dummies(mailed_df[["zip_code", "channel"]], drop_first=True)
# 필요없는 행을 지웁니다.
feature_vector_df = mailed_df.drop(
[
"history_segment",
"zip_code",
"channel",
"segment",
"visit",
"conversion",
"spend",
],
axis=1,
)
feature_vector_df = feature_vector_df.join(dummied_df)
feature_vector_df.head()
남성 타겟 메일을 받은 고객들 중 사이트에 방문한 데이터를 분리해 줍니다.
is_treat_list = list(mailed_df["segment"] == "Mens E-Mail")
is_cv_list = list(mailed_df["visit"] == 1)
3. 학습하기¶
먼저, 데이터를 학습용과 테스트 데이터로 나누어 줍니다.
from sklearn.model_selection import train_test_split
(
train_is_cv_list,
test_is_cv_list,
train_is_treat_list,
test_is_treat_list,
train_feature_vector_df,
test_feature_vector_df,
) = train_test_split(
is_cv_list, is_treat_list, feature_vector_df, test_size=0.5, random_state=42
)
이 예제에서는 전송받은 이메일에 의한 사이트 방문여부를 예측하는 것이므로 LogisticRegression을 이용해 학습을 하겠습니다.
from sklearn.linear_model import LogisticRegression
treat_model = LogisticRegression(C=0.01)
control_model = LogisticRegression(C=0.01)
train_sample_num = len(train_is_cv_list)
treat_is_cv_list = [
train_is_cv_list[i]
for i in range(train_sample_num)
if train_is_treat_list[i] is True
]
treat_feature_vector_list = train_feature_vector_df[train_is_treat_list]
control_is_cv_list = [
train_is_cv_list[i]
for i in range(train_sample_num)
if train_is_treat_list[i] is False
]
control_feature_vector_list = train_feature_vector_df[
list(map(lambda a: a is False, train_is_treat_list))
]
treat_model.fit(treat_feature_vector_list, treat_is_cv_list)
control_model.fit(control_feature_vector_list, control_is_cv_list)
4. 모델 평가하기¶
학습한 모델을 평가하기 위한 점수 시스템을 만들어보겠습니다. 업리프트 모델링에서는 treat_model, control_model로 부터 두개의 예측값을 얻게 됩니다 . 업리프트 모델링 점수는 실험군 예측값을 대조군 예측값으로 나눈 비율로 사용하겠습니다. sklearn
의 predict_proba
함수를 통해 아래와 같이 간단히 할 수 있습니다.
from operator import itemgetter
treat_score = treat_model.predict_proba(test_feature_vector_df) # 실험군 예측값
control_score = control_model.predict_proba(test_feature_vector_df) # 대조군 예측값
score_list = treat_score[:, 1] / control_score[:, 1] # 업리프트 모델링 점수계산
result = list(zip(test_is_cv_list, test_is_treat_list, score_list))
result.sort(key=itemgetter(2), reverse=True) # 점수가 높은 순으로 정렬
4.1. 백분위수 단위로 나타낸 전환율¶
먼저 점수가 큰 순서대로 정렬하고 백분위수마다 점수를 계산해봅니다.
import matplotlib.pyplot as plt
%matplotlib inline
qdf = pd.DataFrame(columns=("treat_cvr", "control_cvr"))
quantile_data = []
for n in range(10):
start = int(n * len(result) / 10) # 결과를 10% 단위로 나눈다
end = int((n + 1) * len(result) / 10) - 1
quantiled_result = result[start:end]
# 실험군과 대조군에서 결과 모으기
treat_uu = list(map(lambda item: item[1], quantiled_result)).count(True)
control_uu = list(map(lambda item: item[1], quantiled_result)).count(False)
# 실험군과 대조군의 전환건수 세기
treat_cv = [item[0] for item in quantiled_result if item[1] is True].count(True)
control_cv = [item[0] for item in quantiled_result if item[1] is False].count(True)
treat_cvr = treat_cv / treat_uu
control_cvr = control_cv / control_uu
quantile_data.append(
[treat_uu, control_uu, treat_cv, control_cv, treat_cvr, control_cvr]
)
label = "{}%~{}%".format(n * 10, (n + 1) * 10)
qdf.loc[label] = [treat_cvr, control_cvr]
qdf.plot.bar()
plt.xlabel("percentile")
plt.ylabel("conversion rate")
백분위수 단위로 전환율을 시각화해보니 점수가 상위 60% 까지는 남성 타깃 메일을 보낸 쪽이 여성 타깃 메일을 보낸 쪽보다 반응이 좋음을 알 수 있습니다. 반대로 하위 40%까지는 여성 타깃 메일이 반응이 좋습니다. 그러므로 상위 60%는 남성 타깃 메일을 보내고 하위 40%는 여성 타깃 메일을 보내는 것이 좋음을 알 수 있습니다.
4.2. AUC로 평가하기¶
- 점수가 높은 순서대로 각 대상을 훑으면서 매 시점의 점수를 측정합니다.
- 점수의 값 차이로부터 개입이 일으킨 증가 건수(lift)를 계산합니다.
- lift의 원점과 끝점을 지나는 직선을 베이스라인으로 삼습니다.
- lift와 베이스라인 사이의 면적을 계산합니다.
treat_uu = 0
control_uu = 0
treat_cv = 0
control_cv = 0
treat_cvr = 0.0
control_cvr = 0.0
lift = 0.0
stat_data = []
for is_cv, is_treat, score in result:
if is_treat:
treat_uu += 1
if is_cv:
treat_cv += 1
treat_cvr = treat_cv / treat_uu
else:
control_uu += 1
if is_cv:
control_cv += 1
control_cvr = control_cv / control_uu
lift = (treat_cvr - control_cvr) * treat_uu
stat_data.append(
[
is_cv,
is_treat,
score,
treat_uu,
control_uu,
treat_cv,
control_cv,
treat_cvr,
control_cvr,
lift,
]
)
df = pd.DataFrame(stat_data)
df.columns = [
"is_cv",
"is_treat",
"score",
"treat_uu",
"control_uu",
"treat_cv",
"control_cv",
"treat_cvr",
"control_cvr",
"lift",
]
df["base_line"] = df.index * df["lift"][len(df.index) - 1] / len(df.index)
f, ([ax0, ax1], [ax2, ax3]) = plt.subplots(
nrows=2, ncols=2, sharex=True, figsize=(10, 8)
)
df.plot(y=["treat_cv", "control_cv"], ax=ax0)
ax0.set_xlabel("uplift score rank")
ax0.set_ylabel("conversion count")
df.plot(y=["treat_cvr", "control_cvr"], ylim=[0, 0.3], ax=ax1)
ax1.set_xlabel("uplift score rank")
ax1.set_ylabel("conversion rate")
df.plot(y=["lift", "base_line"], ax=ax2)
ax2.set_xlabel("uplift score rank")
ax2.set_ylabel("lift count")
ax3.axis("off")
plt.tight_layout()
데이터를 정규화한 그림을 다시 그립니다.
f, ([ax0, ax1], [ax2, ax3]) = plt.subplots(
nrows=2, ncols=2, sharex=True, figsize=(10, 8)
)
df.plot(y=["treat_cv", "control_cv"], x="score", title="conversion count", ax=ax0)
df.plot(
y=["treat_cvr", "control_cvr"],
ylim=[0, 0.3],
x="score",
title="conversion rate",
ax=ax1,
)
df.plot(y=["lift", "base_line"], x="score", title="lift", ax=ax2)
ax3.axis("off")
plt.tight_layout()
업리프트 모델링의 결과가 정확할 수록 방문하는 실험군의 고객과 방문하지 않은 대조군의 고객이 높은 점수대로 모입니다. 그리고 낮은 점수대에서는 반대현상이 일어납니다. 이 때문에 lift곡선은 실험군에서 방문하는 고객에 해당하는 초반 부분에서 양의 기울기를 가지며 정확도가 높을 수록 이 기울기가 가파릅니다. 같은 이유로 곡선의 후반 부분은 음의 기울기를 갖습니다. 또한 업리프트의 모델링의 정확도가 높을수록 이 기울기도 가팔라집니다. 결국 업리프트 모델링의 결과가 정확할수록 곡선의 형태가 볼록해지며 lift와 베이스라인 사이의 면적이 넓어지기 때문에 AUC값이 커지게 됩니다.
이 점수를 기초로 개입여부를 결정할 수 있습니다. lift점수가 최고점일때 개입하면 됩니다.
5. 마치며¶
업리프트 모델링을 소개하고 예를 살펴보았습니다. 업리프트 모델링은 무작위 대조시험과 고객 정보로 부터 추출한 특징을 조합하여, 효과가 좋은 고객을 예측해주는 모델을 구축하는 기법입니다. 이 기법을 사용하면 비용을 절감하고 효과를 최대화 할 수 있습니다.