10.01 Chapter 05
회원 탈퇴를 예측하는 테크닉 10
의사결정 트리라는 지도학습의 분류 알고리즘을 통해 탈퇴를 예측
전제조건
No. | 파일 이름 | 개요 |
---|---|---|
1 | use_log.csv | 스포츠 센터의 이용 이력 데이터. 기간은 2018년 4월 ~ 2019년 3월 |
2 | customer_master.csv | 2019년 3월 말 시점의 회원 데이터 |
3 | class_master.csv | 회원 구분 데이터 (종일, 주간, 야간) |
4 | campaign_master.csv | 행사 구분 데이터 (입회비 유무 등) |
5 | customer_join.csv | 3장에서 작성한 이용 이력을 포함한 고객 데이터 |
6 | use_log_months.csv | 4장에서 작성한 이용 이력을 연월/고객별로 집계한 데이터 |
데이터를 읽어들이고 이용 데이터를 수정하자
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.csv
와customer_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 씨 중 하나만 선택한다는 뜻입니다.
예측할 달의 재적 기간을 작성하자
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()
- 재적 기간은 연월 칼럼과
start_date
칼럼의 차이로 구할 수 있습니다.
결측치를 제거하자
1
2
3
# dropna의 subset으로 특정 칼럼을 지정하면, 특정 칼럼의 결측 데이터를 제거합니다.
predict_data = predict_data.dropna(subset=['count_1'])
predict_data.isna().sum()
end_date
와exit_date
는 탈퇴 고객만 있으며, 유지 회원은 결측치가 됩니다.- 따라서 유의미한 결측치인
count_1
의 결손 데이터만 제거합니다. dropna
의subset
으로 특정 칼럼을 지정하면, 특정 칼럼의 결측 데이터를 제거합니다.
문자열 변수를 처리할 수 있게 가공하자
- 가입 캠페인 구분, 회원 구분, 성별과 같은 문자열 데이터를 카테고리 변수라고 합니다.
- 이런 데이터를 활용하기 위해서 앞에서
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_test
와y_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.