파이썬으로 업리프트 모델링

출처: 아리가 미치아키, 머신러닝 실무 프로젝트, 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를 이용해 읽어 들이고 구조를 확인합니다.

In [1]:
import pandas as pd

source_df = pd.read_csv("./data/Kevin_Hillstrom_MineThatData.csv")
source_df.tail()
Out[1]:
recency history_segment history mens womens zip_code newbie channel segment visit conversion spend
63995 10 2) $100 - $200 105.54 1 0 Urban 0 Web Mens E-Mail 0 0 0.0
63996 5 1) $0 - $100 38.91 0 1 Urban 1 Phone Mens E-Mail 0 0 0.0
63997 6 1) $0 - $100 29.99 1 0 Urban 1 Phone Mens E-Mail 0 0 0.0
63998 1 5) $500 - $750 552.94 1 0 Surburban 1 Multichannel Womens E-Mail 0 0 0.0
63999 1 4) $350 - $500 472.82 0 1 Surburban 0 Web Mens E-Mail 0 0 0.0

2. 데이터 정리하기

2.1. 불필요한 데이터 제거

메일을 받지 않은 고객의 데이터는 필요하지 않음으로 제거합니다.

In [2]:
mailed_df = source_df[source_df["segment"] != "No E-mail"].reset_index(drop=True)
mailed_df.tail()
Out[2]:
recency history_segment history mens womens zip_code newbie channel segment visit conversion spend
63995 10 2) $100 - $200 105.54 1 0 Urban 0 Web Mens E-Mail 0 0 0.0
63996 5 1) $0 - $100 38.91 0 1 Urban 1 Phone Mens E-Mail 0 0 0.0
63997 6 1) $0 - $100 29.99 1 0 Urban 1 Phone Mens E-Mail 0 0 0.0
63998 1 5) $500 - $750 552.94 1 0 Surburban 1 Multichannel Womens E-Mail 0 0 0.0
63999 1 4) $350 - $500 472.82 0 1 Surburban 0 Web Mens E-Mail 0 0 0.0

데이터의 형태를 확인해 봅니다.

In [3]:
mailed_df.dtypes
Out[3]:
recency              int64
history_segment     object
history            float64
mens                 int64
womens               int64
zip_code            object
newbie               int64
channel             object
segment             object
visit                int64
conversion           int64
spend              float64
dtype: object

위의 결과에서 zip_code, channe, segment가 object인것을 볼 수 있습니다. segment는 label로 사용될 것입니다.

2.2. 원핫인코딩(one-hot encoding)

zip_code, channel 값은 범주형임으로 one-hot encoding을 수행합니다.

In [4]:
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()
Out[4]:
recency history mens womens newbie zip_code_Surburban zip_code_Urban channel_Phone channel_Web
0 10 142.44 1 0 0 1 0 1 0
1 6 329.08 1 1 1 0 0 0 1
2 7 180.65 0 1 1 1 0 0 1
3 9 675.83 1 0 1 0 0 0 1
4 2 45.34 1 0 0 0 1 0 1

남성 타겟 메일을 받은 고객들 중 사이트에 방문한 데이터를 분리해 줍니다.

In [5]:
is_treat_list = list(mailed_df["segment"] == "Mens E-Mail")
is_cv_list = list(mailed_df["visit"] == 1)

3. 학습하기

먼저, 데이터를 학습용과 테스트 데이터로 나누어 줍니다.

In [6]:
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을 이용해 학습을 하겠습니다.

In [7]:
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)
Out[7]:
LogisticRegression(C=0.01, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
          penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False)

4. 모델 평가하기

학습한 모델을 평가하기 위한 점수 시스템을 만들어보겠습니다. 업리프트 모델링에서는 treat_model, control_model로 부터 두개의 예측값을 얻게 됩니다 . 업리프트 모델링 점수는 실험군 예측값을 대조군 예측값으로 나눈 비율로 사용하겠습니다. sklearnpredict_proba 함수를 통해 아래와 같이 간단히 할 수 있습니다.

In [8]:
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. 백분위수 단위로 나타낸 전환율

먼저 점수가 큰 순서대로 정렬하고 백분위수마다 점수를 계산해봅니다.

In [9]:
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")
Out[9]:
<matplotlib.text.Text at 0x7f060b4c23c8>
No description has been provided for this image

백분위수 단위로 전환율을 시각화해보니 점수가 상위 60% 까지는 남성 타깃 메일을 보낸 쪽이 여성 타깃 메일을 보낸 쪽보다 반응이 좋음을 알 수 있습니다. 반대로 하위 40%까지는 여성 타깃 메일이 반응이 좋습니다. 그러므로 상위 60%는 남성 타깃 메일을 보내고 하위 40%는 여성 타깃 메일을 보내는 것이 좋음을 알 수 있습니다.

4.2. AUC로 평가하기

  1. 점수가 높은 순서대로 각 대상을 훑으면서 매 시점의 점수를 측정합니다.
  2. 점수의 값 차이로부터 개입이 일으킨 증가 건수(lift)를 계산합니다.
  3. lift의 원점과 끝점을 지나는 직선을 베이스라인으로 삼습니다.
  4. lift와 베이스라인 사이의 면적을 계산합니다.
In [10]:
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()
No description has been provided for this image

데이터를 정규화한 그림을 다시 그립니다.

In [11]:
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()
No description has been provided for this image

업리프트 모델링의 결과가 정확할 수록 방문하는 실험군의 고객과 방문하지 않은 대조군의 고객이 높은 점수대로 모입니다. 그리고 낮은 점수대에서는 반대현상이 일어납니다. 이 때문에 lift곡선은 실험군에서 방문하는 고객에 해당하는 초반 부분에서 양의 기울기를 가지며 정확도가 높을 수록 이 기울기가 가파릅니다. 같은 이유로 곡선의 후반 부분은 음의 기울기를 갖습니다. 또한 업리프트의 모델링의 정확도가 높을수록 이 기울기도 가팔라집니다. 결국 업리프트 모델링의 결과가 정확할수록 곡선의 형태가 볼록해지며 lift와 베이스라인 사이의 면적이 넓어지기 때문에 AUC값이 커지게 됩니다.

이 점수를 기초로 개입여부를 결정할 수 있습니다. lift점수가 최고점일때 개입하면 됩니다.

5. 마치며

업리프트 모델링을 소개하고 예를 살펴보았습니다. 업리프트 모델링은 무작위 대조시험과 고객 정보로 부터 추출한 특징을 조합하여, 효과가 좋은 고객을 예측해주는 모델을 구축하는 기법입니다. 이 기법을 사용하면 비용을 절감하고 효과를 최대화 할 수 있습니다.

회귀의 오류 지표 알아보기

회귀모델의 오류(Regression error metrics)

선형회귀 모델에서 오류를 측정하는 방법으로 아래 4가지를 예시를 통해 살펴보겠습니다.

  • Mean Absolute Error
  • Mean Absolute Percentage Error
  • Mean Square Error
  • Mean Percentage Error

데이터 설명

이 글에서 사용한 데이터는 캐글(Kaggle)의 비디오 게임 판매 입니다.

먼저 pandascsv를 불러오겠습니다.

In [1]:
import pandas as pd

df = (
    pd.read_csv("./data/Video_Games_Sales_as_at_22_Dec_2016.csv").dropna().reset_index()
)
df.tail()
Out[1]:
index Name Platform Year_of_Release Genre Publisher NA_Sales EU_Sales JP_Sales Other_Sales Global_Sales Critic_Score Critic_Count User_Score User_Count Developer Rating
6820 16667 E.T. The Extra-Terrestrial GBA 2001.0 Action NewKidCo 0.01 0.00 0.0 0.0 0.01 46.0 4.0 2.4 21.0 Fluid Studios E
6821 16677 Mortal Kombat: Deadly Alliance GBA 2002.0 Fighting Midway Games 0.01 0.00 0.0 0.0 0.01 81.0 12.0 8.8 9.0 Criterion Games M
6822 16696 Metal Gear Solid V: Ground Zeroes PC 2014.0 Action Konami Digital Entertainment 0.00 0.01 0.0 0.0 0.01 80.0 20.0 7.6 412.0 Kojima Productions M
6823 16700 Breach PC 2011.0 Shooter Destineer 0.01 0.00 0.0 0.0 0.01 61.0 12.0 5.8 43.0 Atomic Games T
6824 16706 STORM: Frontline Nation PC 2011.0 Strategy Unknown 0.00 0.01 0.0 0.0 0.01 60.0 12.0 7.2 13.0 SimBin E10+

전체의 데이터중에 Critic_Score, User_Score, Global_Sales열만 사용하도록 하겠습니다.

In [2]:
X = df[["Critic_Score", "User_Score"]].astype("float32")
# X = [tuple(x) for x in subset.values]
# X = list(subset.itertuples(index=False))
sales = df["Global_Sales"].astype("float32")
# y= list(sales.iteritems())
# y = [tuple(x) for x in sales.values]
# print(X.shape, sales.shape)

데이터 시각화

시각화를 통해 데이터의 모양을 확인해 보겠습니다.

In [3]:
%matplotlib inline

df[["Critic_Score", "User_Score", "Global_Sales"]].astype("float").plot.scatter(
    x="Critic_Score", y="Global_Sales", c="User_Score"
)
Out[3]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f0eba6d12b0>
No description has been provided for this image

x축은 평론가 점수이고 y축은 비디오 게임의 판매량입니다. 그리고 유저들의 평가는 색상으로 표현했습니다. 점수가 높을 수록 판매량이 높은 분명한 선형 관계가 보이네요. 그리고 몇개의 특이값(outliners) 보입니다.

선형회귀

이제 sklearn을 이용해 간단히 선형회귀 모델을 만들고 모델로 판매량을 예측해보겠습니다.

In [4]:
from sklearn.linear_model import LinearRegression

lm = LinearRegression(n_jobs=-1)
lm.fit(X, sales)
y_true = sales.values
y_pred = lm.predict(X)

생성한 선형회귀 모델을 평가하는 지표들을 차례로 살펴보죠. 각각의 지표는 특성을 이해하고 상황에 맞게 사용해야 합니다.

Mean Absolute Error (MAE)

MAE는 다음과 같이 정의됩니다. $$ MAE = \frac { \sum \vert y - \hat y \vert }{n} $$ 모델의 예측값과 실제값의 차이를 모두 더한다는 개념입니다.

  • 절대값을 취하기 때문에 가장 직관적으로 알 수 있는 지표입니다.
  • MSE 보다 특이치에 robust합니다.
  • 절대값을 취하기 때문에 모델이 underperformance 인지 overperformance 인지 알 수 없습니다.
    • underperformance: 모델이 실제보다 낮은 값으로 예측
    • overperformance: 모델이 실제보다 높은 값으로 예측
In [5]:
import numpy as np


def MAE(y_true, y_pred):
    return np.mean(np.abs((y_true - y_pred)))


MAE(y_true, y_pred)
Out[5]:
0.76997957836731656

MAE는 sklearn.metrics에서 지원합니다.

In [6]:
from sklearn.metrics import mean_absolute_error

mean_absolute_error(y_true, y_pred)
Out[6]:
0.76997957836731656

우리 모델의 MAE는 0.760이며, 우리의 데이터 판매 범위는 0.01에서 83이기 때문에 상당히 괜찮은 값입니다.

Mean Squared Error(MSE)

MSE는 다음과 같이 정의됩니다. $$ MSE = \frac { \sum (y - \hat y)^2 }{n} $$

제곱을 하기 때문에 MAE와는 다르게 모델의 예측값과 실제값 차이의 면적의 합입니다. 이런 차이로, 특이값이 존재하면 수치가 많이 늘어납니다.

  • 특이치에 민감하다
In [7]:
def MSE(y_true, y_pred):
    return np.mean(np.square((y_true - y_pred)))


MSE(y_true, y_pred)
Out[7]:
3.6227746702258123

MSE 또한 sklearn.metrics로 간단히 사용할 수 있습니다.

In [8]:
from sklearn.metrics import mean_squared_error

mean_squared_error(y_true, y_pred)
Out[8]:
3.6227746702258123

Root Mean Squared Error(RMSE)

RMSE는 MSE에 루트를 씌워 다음과 같이 정의합니다.

$$ RMSE = \sqrt { \frac { \sum (y - \hat y)^2 }{n} } $$

RMSE를 사용하면 오류 지표를 실제 값과 유사한 단위로 다시 변환하여 해석을 쉽게 합니다.

In [9]:
np.sqrt(MSE(y_true, y_pred))
Out[9]:
1.9033587865207684

Mean Absolute Percentage Error(MAPE)

MAPE는 MAE를 퍼센트로 변환한 것입니다. $$ MAPE = \frac { \sum \vert \frac { y - \hat y}{y} \vert }{n}*100\% $$

  • MAE와 마찬가지로 MSE보다 특이치에 robust합니다.
  • MAE와 같은 단점을 가집니다.
  • 추가적으로 모델에 대한 편향이 존재합니다.
    • 이 단점에 대응하기 위해 MPE도 추가로 확인하는게 좋습니다.
    • 0 근처의 값에서는 사용하기 어렵습니다.
In [11]:
def MAPE(y_true, y_pred):
    return np.mean(np.abs((y_true - y_pred) / y_true)) * 100


MAPE(y_true, y_pred)
Out[11]:
558.87888927059021

Mean Percentage Error(MPE)

MAPE에서 절대값을 제외한 지표입니다. 아래와 같이 정의합니다.

$$ MAE = \frac { \sum ( y - \hat y ) }{n}* 100\% $$

MPE의 가장 큰 장점은

  • 모델이 underperformance 인지 overperformance 인지 판단 할 수 있다는 것입니다.
In [12]:
def MPE(y_true, y_pred):
    return np.mean((y_true - y_pred) / y_true) * 100


MPE(y_true, y_pred)
Out[12]:
-468.23269419120248

음수의 값임으로, 모델이 overperformance임을 알 수있습니다.

마치며,

테이블로 간단하게 정리해보겠습니다.

Name Residual Operation Robust To Outliers
Mean Absolute Error Absolute Value Yes
Mean Squared Error Square No
Root Mean Squared Error Square No
Mean Absolute Percentage Error Absolute Value Yes
Mean Percentage Error N/A Yes

회귀문제에서 RMSE가 일반적으로 선호되는 방법이지만, 상황에 맞는 다른 방식을 사용해야 합니다. 특이값이 많은 경우에는 MAE를 사용하는게 좋죠.

출처