Post

10.01 Chapter 05

회원 탈퇴를 예측하는 테크닉 10

  • 의사결정 트리라는 지도학습의 분류 알고리즘을 통해 탈퇴를 예측

  • 전제조건

No.파일 이름개요
1use_log.csv스포츠 센터의 이용 이력 데이터. 기간은 2018년 4월 ~ 2019년 3월
2customer_master.csv2019년 3월 말 시점의 회원 데이터
3class_master.csv회원 구분 데이터 (종일, 주간, 야간)
4campaign_master.csv행사 구분 데이터 (입회비 유무 등)
5customer_join.csv3장에서 작성한 이용 이력을 포함한 고객 데이터
6use_log_months.csv4장에서 작성한 이용 이력을 연월/고객별로 집계한 데이터

데이터를 읽어들이고 이용 데이터를 수정하자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pandas as pd
customer = pd.read_csv('data/chap05/customer_join.csv')
uselog_months = pd.read_csv('data/chap05/uselog_months.csv')

# 이번 달과 1개월 전의 이용 횟수를 집계
year_months = list(uselog_months['연월'].unique())
uselog = pd.DataFrame()
for i in range(1, len(year_months)):
    tmp = uselog_months.loc[uselog_months['연월']==year_months[i]]
    tmp.rename(columns={'count': 'count_0'}, inplace=True)
    tmp_before = uselog_months.loc[uselog_months['연월']==year_months[i-1]]
    del tmp_before['연월']
    tmp_before.rename(columns={'count': 'count_1'}, inplace=True)
    tmp = pd.merge(tmp, tmp_before, on='customer_id', how='left')
    uselog = pd.concat([uselog, tmp], ignore_index=True)
uselog.head()
  • 우선, use_log_months.csvcustomer_join.csv를 읽어옵니다.
  • 과거 6개월분의 데이터로부터 이용 횟수를 예측하는 경우, 가입 5개월 이내인 회원의 탈퇴는 예측할 수 없습니다. 또한 불과 몇 개월만에 그만둔 회원도 많기에 과거 6개월분의 데이터를 이용하여 예측하는 것은 의미가 없습니다.
  • 이로 인해 여기에서는 그 달과 1개월 전의 이용 이력만으로 데이터를 작성합니다.

탈퇴 전월의 고객 데이터를 작성하자

  • 탈퇴를 예측하는 목적은 탈퇴를 미연에 방지하는 것입니다. 즉, 탈퇴 월이 2018년 8월인 경우 그 1개월 전인 7월의 데이터로부터 8월에 탈퇴 신청을 할 확률을 예측해야 합니다.
1
2
3
4
5
6
7
8
9
10
11
from dateutil.relativedelta import relativedelta
exit_customer = customer.loc[customer['is_deleted']==1]
exit_customer['exit_date'] = None
exit_customer['end_date'] = pd.to_datetime(exit_customer['end_date'])
for i in range(len(exit_customer)): # end_date에서 한 달을 뺀 값을 계산합니다.
    exit_customer['exit_date'].iloc[i] = exit_customer['end_date'].iloc[i] - relativedelta(months=1)
exit_customer['연월'] = pd.to_datetime(exit_customer['exit_date']).dt.strftime('%Y%m')
uselog['연월'] = uselog['연월'].astype(str)
exit_uselog = pd.merge(uselog, exit_customer, on=['customer_id', '연월'], how='left')
print(len(uselog))
exit_uselog.head()
  • Q: name, class, gender, start_date 같은 컬럼에 대해서는 왜 결측치가 생기는 거지?
  • exit_date라는 칼럼을 작성하고 end_date의 1개월 전을 계산합니다.

지속 회원의 데이터를 작성하자.

1
2
3
4
5
6
7
conti_customer = customer.loc[customer['is_deleted']==0]
conti_uselog = pd.merge(uselog, conti_customer, on=['customer_id'], how='left')
print(len(conti_uselog))
conti_uselog = conti_uselog.dropna(subset=['name']) # name 칼럼의 결손 데이터 제거
print(len(conti_uselog))

# is_deleted==0으로 처음 탈퇴회원을 필터링했는데, 왜 탈퇴회원에 대한 결손 데이터가 또 생기는 거지?
  • name 칼럼의 결손 데이터를 제거하고 탈퇴 회원을 제거합니다.
  • 간단히 지속 회원 데이터도 회원당 1개가 되게 언더샘플링합니다. 즉, 2018년 5월 A씨와 2018년 12월 A 씨 중 하나만 선택한다는 뜻입니다.

predict_data.csv

예측할 달의 재적 기간을 작성하자

1
2
3
4
5
6
7
predict_data['period'] = 0 # 모든 회원들의 uselog 정보가 결합된 데이터
predict_data['now_date'] = pd.to_datetime(predict_data['연월'], format='%Y%m')
predict_data['start_date'] = pd.to_datetime(predict_data['start_date'])
for i in range(len(predict_data)):
    delta = relativedelta(predict_data['now_date'][i], predict_data['start_date'][i])
    predict_data['period'][i] = int(delta.years*12 + delta.months)
predict_data.head()

predict_data_period.csv

  • 재적 기간은 연월 칼럼과 start_date 칼럼의 차이로 구할 수 있습니다.

결측치를 제거하자

1
2
3
# dropna의 subset으로 특정 칼럼을 지정하면, 특정 칼럼의 결측 데이터를 제거합니다.
predict_data = predict_data.dropna(subset=['count_1'])
predict_data.isna().sum()
  • end_dateexit_date는 탈퇴 고객만 있으며, 유지 회원은 결측치가 됩니다.
  • 따라서 유의미한 결측치인 count_1 의 결손 데이터만 제거합니다.
  • dropnasubset으로 특정 칼럼을 지정하면, 특정 칼럼의 결측 데이터를 제거합니다.

문자열 변수를 처리할 수 있게 가공하자

  • 가입 캠페인 구분, 회원 구분, 성별과 같은 문자열 데이터를 카테고리 변수라고 합니다.
  • 이런 데이터를 활용하기 위해서 앞에서 routine_flg를 작성한 것처럼 플래그를 만듭니다. 이를 더미 변수라고 합니다.
No.변수 종류변수 이름
1설명 변수count_1, campaign, name, class_name, gender, routine_flg, period
2목적 변수is_deleted
1
2
3
# 카테고리 변수를 더미 변수로 만듭니다.
predict_data = pd.get_dummies(predict_data)
predict_data.head()
1
2
3
4
del predict_data['campaign_name_2_일반']
del predict_data['class_name_2_야간']
del predict_data['gender_M']
predict_data.head()
  • 각 더미 변수에서 하나씩 지워 필요하지 않은 칼럼을 제거합니다.

의사결정 트리를 사용해서 탈퇴 예측 모델을 구축하자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from sklearn.tree import DecisionTreeClassifier
import sklearn.model_selection

exit = predict_data.loc[predict_data['is_deleted']==1]
# exit 데이터의 개수만큼 랜덤샘플링을 진행해서 비율을 1:1로 동일하게 맞춤
conti = predict_data.loc[predict_data['is_deleted']==0].sample(len(exit))

X = pd.concat([exit, conti], ignore_index=True)
y = X['is_deleted']
del X['is_deleted']

X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split(X, y)

model = DecisionTreeClassifier(random_state=0)
model.fit(X_train, y_train)
y_test_pred = model.predict(X_test)
print(y_test_pred)
  • 탈퇴 데이터와 유지 데이터의 개수를 1:1 의 비율에 맞게 정리합니다.

예측 모델을 평가하고 모델을 튜닝해 보자

  • result_test 데이터의 y_testy_pred가 일치하는 개수를 전체 데이터의 개수로 나누어 정답률을 계산합니다.
  • 학습용 데이터로 예측한 정확도와 평가용 데이터로 예측한 정확도의 차이가 작은 것이 이상적입니다.
1
2
3
4
correct = len(results_test.loc[results_test['y_test']==results_test['y_pred']])
data_count = len(results_test)
score_test = correct / data_count
print(score_test)
1
2
print(model.score(X_test, y_test))
print(model.score(X_train, y_train)) # 학습용 데이터에 약간 과적합
  • 위와 같이 학습용 데이터에 너무 맞춘 과적합 경향이 있는 경우에는 데이터 늘리기, 변수 재검토, 모델의 파라미터 변경과 같은 방법을 적용해 가면서 이상적인 모델로 만들어 갑니다.
1
2
3
4
5
6
7
8
9
X = pd.concat([exit, conti], ignore_index=True)
y = X['is_deleted']
del X['is_deleted']
X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split(X, y)

model = DecisionTreeClassifier(random_state=0, max_depth=5)
model.fit(X_train, y_train)
print(model.score(X_test, y_test))
print(model.score(X_train, y_train))
  • max_depth를 지정하여 의사결정 트리가 깊이 5단계에서 멈추게 합니다. 이를 통해 훈련 데이터의 과적합을 방지합니다.

모델에 기여하는 변수를 확인하자

1
2
importance = pd.DataFrame({'feature_names': X.columns, 'coefficient': model.feature_importances_ })
importance

회원 탈퇴를 예측하자

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
count_1 = 5
routine_flg = 1
period = 7
campaign_name = '입회비무료'
class_name = '종일'
gender = 'M'

if campaign_name == '입회비반값할인':
    campaign_name_list = [1, 0]
elif campaign_name == '입회비무료':
    campaign_name_list = [0, 1]
elif campaign_name == '일반':
    campaign_name_list = [0, 0]

if class_name == '종일':
    class_name_list = [1, 0]
elif class_name == '주간':
    class_name_list = [0, 1]
elif class_name == '야간':
    class_name_list = [0, 0]

if gender == 'F':
    gender_list = [1]
elif gender == 'M':
    gender_list = [0]

input_data = [count_1, routine_flg, period]
input_data.extend(campaign_name_list)
input_data.extend(class_name_list)
input_data.extend(gender_list)

print(model.predict([input_data]))
print(model.predict_proba([input_data]))
This post is licensed under CC BY 4.0 by the author.