어떤 헬스장에서 회원에 대한 데이터를 차곡차곡 모아 두었는데요, 사장님이 이 데이터를 보고 경영에 도움이 되는 조언을 부탁했습니다.
이 데이터를 근거로 데이터 분석을 해 보고 어떤 것을 알아낼 것인가는 전체 데이터 모습을 한번 보고 생각해 보기로 하시죠. ㅠㅠ
데이터 위치
pyda100/3장 at master · wikibook/pyda100 · GitHub
https://github.com/wikibook/pyda100/tree/master/3%EC%9E%A5
데이터 종류
campaign_master.csv : 행사 데이터
class_master.csv : 회원구분 데이터
customer_master.csv : 관심 특정일의 회원 데이터
use_log.csv : 이용내역
datetime_is_numeric=True # 아무래도 datetime type을 다룰 일이 많으니까, 일단 설정.
데이터를 가져와 보자.
일단 github에서 각각의 csv를 다운로드 받아 봅시다! 데이터를 읽어들이는 것은 이제는 특별하게 이야기 하지 않아도 손가락이 자동으로 움직일 정도가 되어야 하겠군요. 이번엔 github에서 파일을 다운로드 받아야 해서 조금 다른 방식으로 해 보겠습니다.
lst_files = ["campaign_master", "class_master", "customer_master", "use_log" ]
import subprocess
# 여러개 파일 다운 받아야 해서 runcmd 함수로 wget을 돌려서 받습니다. 오, 뉴 팁!
def runcmd(cmd, verbose = False, *args, **kwargs):
process = subprocess.Popen( cmd, stdout = subprocess.PIPE, stderr = subprocess.PIPE, text = True, shell = True )
std_out, std_err = process.communicate()
if verbose:
print(std_out.strip(), std_err)
pass
print(cmd + " has been done")
#!wget https://github.com/wikibook/pyda100/blob/master/3%EC%9E%A5/campaign_master.csv
for file in lst_files :
addr = "https://github.com/wikibook/pyda100/blob/master/3%EC%9E%A5/" + file + ".csv"
runcmd('wget %s?raw=True -O %s.csv'%(addr, file), verbose=False)
가져온 파일들을 pandas의 Dataframe에 Loading을 하고요~
from datetime import datetime
import pandas as pd
lst_df = []
lst_files = ["campaign_master", "class_master", "customer_master", "use_log" ]
for i, file in enumerate(lst_files) :
file = file+".csv"
lst_df.append(pd.read_csv(file))
print("="*50)
print(file)
display(lst_df[i].head(2))
df_campaign_master = lst_df[0].copy()
df_class_master = lst_df[1].copy()
df_customer_master = lst_df[2].copy()
df_use_log = lst_df[3].copy()
lst_df = []
================================================== campaign_master.csv
campaign_id | campaign_name | |
---|---|---|
0 | CA1 | 2_일반 |
1 | CA2 | 0_입회비반액할인 |
================================================== class_master.csv
class | class_name | price | |
---|---|---|---|
0 | C01 | 0_종일 | 10500 |
1 | C02 | 1_주간 | 7500 |
================================================== customer_master.csv
customer_id | name | class | gender | start_date | end_date | campaign_id | is_deleted | |
---|---|---|---|---|---|---|---|---|
0 | OA832399 | XXXX | C01 | F | 2015-05-01 00:00:00 | NaN | CA1 | 0 |
1 | PL270116 | XXXXX | C01 | M | 2015-05-01 00:00:00 | NaN | CA1 | 0 |
================================================== use_log.csv
log_id | customer_id | usedate | |
---|---|---|---|
0 | L00000049012330 | AS009373 | 2018-04-01 |
1 | L00000049012331 | AS015315 | 2018-04-01 |
읽어들인 데이터를 나름 다시 이해!
가만히 보니까, 우리가 중심으로 생각해야 하는 데이터는 customer_master이겠군요. 기본적으로 회원들의 정보가 있고, 여기에 필요한 정보들을 덧붙여 가면 될 것 같은 느낌!입니다. 유훗. 그렇긴 한데, 뭐부터 알아봐야 할까요...
더 적극적으로 멍 때리고 싶긴 한데, 일단 customer_master를 잘 들여다 봅시다. 뭐뭐 있나.
customer_master가 일단 분석 목표!
df_customer_master.head()
customer_id | name | class | gender | start_date | end_date | campaign_id | is_deleted | |
---|---|---|---|---|---|---|---|---|
0 | OA832399 | XXXX | C01 | F | 2015-05-01 00:00:00 | NaN | CA1 | 0 |
1 | PL270116 | XXXXX | C01 | M | 2015-05-01 00:00:00 | NaN | CA1 | 0 |
2 | OA974876 | XXXXX | C01 | M | 2015-05-01 00:00:00 | NaN | CA1 | 0 |
3 | HD024127 | XXXXX | C01 | F | 2015-05-01 00:00:00 | NaN | CA1 | 0 |
4 | HD661448 | XXXXX | C03 | F | 2015-05-01 00:00:00 | NaN | CA1 | 0 |
일단, 이게 언제부터 언제까지의 데이터인지를 확인! start_date와 end_date를 pd.Datetime type으로!
df_customer_master['start_date'] = pd.to_datetime(df_customer_master['start_date'])
df_customer_master['end_date'] = pd.to_datetime(df_customer_master['end_date'])
df_customer_master['start_date'].describe(datetime_is_numeric=True)
count 4192 mean 2017-07-06 19:43:44.427480832 min 2015-05-01 00:00:00 25% 2016-09-01 00:00:00 50% 2017-09-01 00:00:00 75% 2018-05-12 00:00:00 max 2019-03-15 00:00:00 Name: start_date, dtype: object
df_customer_master['end_date'].describe(datetime_is_numeric=True)
count 1350 mean 2018-10-17 16:23:28 min 2018-04-30 00:00:00 25% 2018-06-30 00:00:00 50% 2018-10-31 00:00:00 75% 2019-01-31 00:00:00 max 2019-03-31 00:00:00 Name: end_date, dtype: object
보니까, 2015-05~2019-03-31까지의 데이터가 있군요. 물론 end_date가 null인 경우에는 지금도 회원이라는 뜻이니까, 현재 end_date보다 더 최신의 데이터가 (오늘까지) 있을수는 있겠습니다. 그런데 특이한 점은 시작일의 가장 오래된 데이터가 2015년 5월 1일인데 종료일의 가장 오래된 데이터가 2018년 4월 30일이라는 점인데요, 2018년 4월 30일 이전에는 탈퇴회원이 없다는 뜻이거든요. 아니, 이럴수도 있나요? 엇. 특이점 확인! 어쨌든 집계를 해 봅시다.
df_start = df_customer_master.groupby('start_date')['customer_id'].size().rename('start').to_frame()
df_start.head()
start | |
---|---|
start_date | |
2015-05-01 | 62 |
2015-06-01 | 69 |
2015-07-01 | 53 |
2015-08-01 | 53 |
2015-09-01 | 71 |
df_end = df_customer_master.groupby('end_date')['customer_id'].size().rename('end').to_frame()
df_end.head()
end | |
---|---|
end_date | |
2018-04-30 | 121 |
2018-05-31 | 125 |
2018-06-30 | 106 |
2018-07-31 | 102 |
2018-08-31 | 113 |
이 두 table을 비교하면 좋겠군요
시작회원과 탈퇴회원의 추이 비교
plot_start = df_start.plot()
df_end.plot(ax=plot_start)
이거 뭔가 2018년 4월 30일에 무슨 일이 있었던 것 같은데 그떄부터 가입회원수가 무지막지하게 떨어졌습니다. 마치 지구가 멸망한 것 처럼. 그리고 탈퇴회원수가 무지막지하게 올라가구요. 그러면 2019-4월이후를 좀더 자세히 볼까요?
df_dur = pd.concat([df_start, df_end], axis=1)
df_dur.columns = ['start', 'end']
print(df_dur.info())
print(type(df_dur.index))
df_dur[df_dur.index>="2018-04-30"].fillna(0).plot()
DatetimeIndex: 227 entries, 2015-05-01 to 2019-03-31 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 start 215 non-null float64 1 end 12 non-null float64 dtypes: float64(2) memory usage: 5.3 KB None
이거 뭔지 모르겠지만, 냄새가 이상합니다. 가입자가 뚝 떨어진 달 이후 부터는 조금 들어왔다가 확 나가고를 지그재그로 하고 있군요. 어허. 이게 머지.. 느낌적인 느낌으로도 2018년 4월 30일을 기준으로 이전과 이후를 조금 더 자세히 봐야 할 것 같네요.
customer master table을 보니, 캠페인이 있네요. 이게 좀 관련있지 않을까 생각합니다. 일단, 캠페인이 어떤 게 있었는지 볼까요?
그러면 캠페인이 문제인가? 하다가 멈췄나?
df_campaign_master
campaign_id | campaign_name | |
---|---|---|
0 | CA1 | 2_일반 |
1 | CA2 | 0_입회비반액할인 |
2 | CA3 | 1_입회비무료 |
3가지 정도의 캠페인이 있었군요? 이게 언제 있었던 캠페인인지 궁금해 지네요. 날짜 정보가 없으니까, customer_master의 날짜를 참고해야 하겠습니다. 일단, customer들에게 어떻게 영향을 끼쳤는지 양을 보면,
df_customer_master['campaign_id'].value_counts()
CA1 3050 CA2 650 CA3 492 Name: campaign_id, dtype: int64
일단 일반으로 가입한 회원이 압도적으로 많군요? 일반이라는게, 캠페인 없이 들어왔다는 이야기 같군요. 그렇다면 CA2와 CA3에 대한 비교를 좀 해봐야 하겠습니다.
df_customer_master[df_customer_master['campaign_id']=='CA2']['start_date'].describe(datetime_is_numeric=True)
count 650 mean 2017-11-24 16:34:42.461538560 min 2016-07-01 00:00:00 25% 2017-07-01 00:00:00 50% 2018-05-04 00:00:00 75% 2018-06-01 00:00:00 max 2018-06-15 00:00:00 Name: start_date, dtype: object
보니까, CA2 반액할인은 2018년 6월 15일이 마지막이었군요? 더 이상 하지 않았던 모양입니다.
df_customer_master[df_customer_master['campaign_id']=='CA3']['start_date'].describe(datetime_is_numeric=True)
count 492 mean 2018-05-14 09:04:23.414634240 min 2017-12-01 00:00:00 25% 2017-12-01 00:00:00 50% 2018-04-09 00:00:00 75% 2018-12-03 00:00:00 max 2018-12-15 00:00:00 Name: start_date, dtype: object
CA3 전액 할인도 2018년 12월 15일이 마지막이었습니다. 그 이후에는 전액할인이 없었네요. 관심시점인 2018년 4월 30일보다 뒤니까, 관련이 없을 수 있겠습니다.? 흐음~ 머져. 그러면 지구 대멸망 사건은 Promotion Campaign하고는 별로 관련이 없는 것 같은데 말이죠. 그럼, 특정 Class에서 사람들이 떠나갔을까요?
df_class_master
class | class_name | price | |
---|---|---|---|
0 | C01 | 0_종일 | 10500 |
1 | C02 | 1_주간 | 7500 |
2 | C03 | 2_야간 | 6000 |
print(df_customer_master[(df_customer_master['start_date']>='2018-04-30')].groupby('class').size())
print(df_customer_master[(df_customer_master['start_date']<'2018-04-30')].groupby('class').size())
class C01 574 C02 279 C03 348 dtype: int64 class C01 1471 C02 740 C03 780 dtype: int64
print(574/1471)
print(279/740)
print(348/780)
0.3902107409925221 0.377027027027027 0.4461538461538462
어디에서 많이 등록생이 줄었나 볼려고 했더니 모든 클래스에서 다 줄었네요. 예전에 비해 종일 약 39%, 주간은 37%, 야간은 44% 정도만 새로 등록을 하고 있습니다. 이거 총체적인 난관이로군요. 그러면 남녀 성별중 어느 성별이 더 많이 그만두고 있는지 볼까요?
df_customer_master[df_customer_master['end_date']>='2018-04-30'].groupby('gender').size().rename('gender').to_frame()#.plot(kind='bar', rot=0)
gender | |
---|---|
gender | |
F | 631 |
M | 719 |
1차 결론
2018년 04월 30일 이후로는 남자가 더 많이 그만두고 있습니다만, 그다지 차이는 없습니다. 엥. 도대체 뭐죠. 그냥 총체적으로 줄어들고 있네요.
여기에서 추리할 수 있는 것은 몇가지가 없군요. 아주 큰 변화가 있었을 거라 생각하는데,
1) 사장이 바뀌었다.
2) 서비스가 바뀌었다.
3) 사업장이 바뀌었다.
이정도 변화가 아니라면 이런 대폭적인 변화가 있기 어렵겠다는 생각이 듭니다.
이런 현실을 감안하여, 2018년 04월 30일 이전의 데이터는 "현재" 상황에 그다지 도움이 되지 않을 것 같습니다.
이제부터는 2018년 04월 30일 이후의 데이터를 기준으로 더 분석해 볼까 합니다.
2018년 4월 30일 이후의 데이터는 회원의 end_date가 null이거나, end_date가 2018년 4월 30일 이후인 경우가 되겠습니다. 그리고 당연하겠지만 start_date가 2018년 4월 30일 이후도 포함되어야 하겠습니다.
df_customer_recent_master = df_customer_master[(df_customer_master['start_date']>='2018-04-30') | \
(df_customer_master['end_date']>='2018-04-30') | \
(df_customer_master['end_date'].isnull()) ]
df_customer_recent_master.head()
customer_id | name | class | gender | start_date | end_date | campaign_id | is_deleted | |
---|---|---|---|---|---|---|---|---|
0 | OA832399 | XXXX | C01 | F | 2015-05-01 | NaT | CA1 | 0 |
1 | PL270116 | XXXXX | C01 | M | 2015-05-01 | NaT | CA1 | 0 |
2 | OA974876 | XXXXX | C01 | M | 2015-05-01 | NaT | CA1 | 0 |
3 | HD024127 | XXXXX | C01 | F | 2015-05-01 | NaT | CA1 | 0 |
4 | HD661448 | XXXXX | C03 | F | 2015-05-01 | NaT | CA1 | 0 |
이렇게 구해봤는데 와 이거 2015년부터 계속 회원인 고객들이 있군요? 대단합니다. 오늘 자 회원인 분들만 (end_date가 NaT가 되어야겠죠) 추려서 비율을 한번 볼까요?
df_curr_customer = df_customer_recent_master[df_customer_recent_master['end_date'].isnull()]
df_curr_customer.head()
customer_id | name | class | gender | start_date | end_date | campaign_id | is_deleted | |
---|---|---|---|---|---|---|---|---|
0 | OA832399 | XXXX | C01 | F | 2015-05-01 | NaT | CA1 | 0 |
1 | PL270116 | XXXXX | C01 | M | 2015-05-01 | NaT | CA1 | 0 |
2 | OA974876 | XXXXX | C01 | M | 2015-05-01 | NaT | CA1 | 0 |
3 | HD024127 | XXXXX | C01 | F | 2015-05-01 | NaT | CA1 | 0 |
4 | HD661448 | XXXXX | C03 | F | 2015-05-01 | NaT | CA1 | 0 |
장기 회원들 너무 멋집니다. 이런 분들이 정말 좋은 분들이잖아요? 이분들의 시작일을 한번 살펴보죠
df_curr_customer.groupby('start_date')['customer_id'].size().rename('counts').plot()
혹시 남녀 차이가 있는지도 궁금하군요.
print(df_curr_customer.groupby('gender')['customer_id'].size().rename('counts'))
df_curr_customer.groupby('gender')['customer_id'].size().rename('counts').plot(kind='bar')
gender F 1352 M 1490 Name: counts, dtype: int64
recent에서 2019-03-31이 마지막이므로 오늘을 2019-04-01로 설정 해서 보면 편리하겠습니다.
today = pd.to_datetime('2019-04-01')
df_customer_recent_master['duration'] = df_customer_recent_master.apply(lambda row : today - row['start_date'] if pd.isnull(row['end_date']) else row['end_date'] - row['start_date'], axis=1)
df_customer_recent_master.head()
customer_id | name | class | gender | start_date | end_date | campaign_id | is_deleted | duration | |
---|---|---|---|---|---|---|---|---|---|
0 | OA832399 | XXXX | C01 | F | 2015-05-01 | NaT | CA1 | 0 | 1431 days |
1 | PL270116 | XXXXX | C01 | M | 2015-05-01 | NaT | CA1 | 0 | 1431 days |
2 | OA974876 | XXXXX | C01 | M | 2015-05-01 | NaT | CA1 | 0 | 1431 days |
3 | HD024127 | XXXXX | C01 | F | 2015-05-01 | NaT | CA1 | 0 | 1431 days |
4 | HD661448 | XXXXX | C03 | F | 2015-05-01 | NaT | CA1 | 0 | 1431 days |
아주 긴 시간 동안 유지해온 회원이 꽤 많군요. 여러가지모로 좀 이상한 데이터인 것은 틀림 없습니다.
df_customer_recent_master.groupby('duration')['customer_id'].size().plot()
당연하겠지만, 최근 기간동안에 어느 캠페인으로 가입한 장기회원이 많은지 확인해 보시죠.
df_customer_recent_master.groupby('campaign_id')['customer_id'].size().rename('campaign').to_frame()
campaign | |
---|---|
campaign_id | |
CA1 | 3050 |
CA2 | 650 |
CA3 | 492 |
df_campaign_master
campaign_id | campaign_name | |
---|---|---|
0 | CA1 | 2_일반 |
1 | CA2 | 0_입회비반액할인 |
2 | CA3 | 1_입회비무료 |
최근 상황을 보니, 걍 입회한 회원의 수가 압도적으로 많군요? 사실 최근에 캠페인을 안했으니까 그렇다고도 볼 수 있겠습니다. 그러면 최근 회원중에 어떤 캠페인으로 입회한 회원들의 탈퇴가 많았을까요?
df_customer_recent_master[df_customer_recent_master['end_date'].notnull()].groupby('campaign_id').size()
campaign_id CA1 709 CA2 368 CA3 273 dtype: int64
일반 회원들의 탈퇴수가 많지만, 실제로 가입수와 비교해보면 오히려 탈퇴율이 적군요? 그러면 캠페인을 통해서 가입한 회원들이 체리피커처럼 탈퇴한다는 느낌도 듭니다. 여기까지 봤으면 그냥 이상하구만 증말로 껄껄껄. 정도로 끝낼 수도 있겠지만, 이상한 생각이 들었습니다. 과연 현재 등록되어 있는 회원들이 active한 회원인지 궁금해졌습니다. 그건 어떻게 알아내느냐? 당연히 회원로그로 알아보면 되겠습니다. ㅎㅎ
df_curr_customer 요게 아직 탈퇴하지 않은 회원들입니다.
df_curr_customer.head()
customer_id | name | class | gender | start_date | end_date | campaign_id | is_deleted | |
---|---|---|---|---|---|---|---|---|
0 | OA832399 | XXXX | C01 | F | 2015-05-01 | NaT | CA1 | 0 |
1 | PL270116 | XXXXX | C01 | M | 2015-05-01 | NaT | CA1 | 0 |
2 | OA974876 | XXXXX | C01 | M | 2015-05-01 | NaT | CA1 | 0 |
3 | HD024127 | XXXXX | C01 | F | 2015-05-01 | NaT | CA1 | 0 |
4 | HD661448 | XXXXX | C03 | F | 2015-05-01 | NaT | CA1 | 0 |
df_use_log.head()
log_id | customer_id | usedate | |
---|---|---|---|
0 | L00000049012330 | AS009373 | 2018-04-01 |
1 | L00000049012331 | AS015315 | 2018-04-01 |
2 | L00000049012332 | AS040841 | 2018-04-01 |
3 | L00000049012333 | AS046594 | 2018-04-01 |
4 | L00000049012334 | AS073285 | 2018-04-01 |
이렇게 보니 각 customer당 방문한 날짜들이 나오는군요? 그냥 궁금하니까 언제 많이 방문했는지 함 보죠 머. 어? 이거 보니까 우리 2018-04-30일부터 이상하다고 했잖아요? 근데 그 즈음이군요? 이게 죠? 이거 2018년 4월을 기준으로 뭔가 일이 있었나본데 이거참 상상도 안되네요. 어쨌든! 방문 날짜!
df_use_log.groupby('usedate')['customer_id'].size().plot()
에헴~ 이거 머.. 이렇게 봐서는 당연히 모르겠죠! 그러면 요일별로 좀 나눠볼까요? use_log에 요일을 추가해 보겠습니다. 후후.
df_use_log['usedate'] = pd.to_datetime(df_use_log['usedate'])
df_use_log['day'] = df_use_log['usedate'].dt.weekday
df_use_log.head() # Monday=0, Sunday=6.
log_id | customer_id | usedate | day | |
---|---|---|---|---|
0 | L00000049012330 | AS009373 | 2018-04-01 | 6 |
1 | L00000049012331 | AS015315 | 2018-04-01 | 6 |
2 | L00000049012332 | AS040841 | 2018-04-01 | 6 |
3 | L00000049012333 | AS046594 | 2018-04-01 | 6 |
4 | L00000049012334 | AS073285 | 2018-04-01 | 6 |
df_use_log.groupby('day')['customer_id'].size().plot()
아니, 요일별로 봐도 머 실제 이용 회원수가 들쑥 날쑥합니다. 자세하게 보면 토요일이 쑥 줄고, 일요일이 많군요. 금욜에 술먹고 못나오고, 월요일을 준비해서 그른가..? 이거 뭔가 특별한게 나올줄 알았는데 그렇지는 않군요. 그러면 여기에서 한가지 더 볼 수 있는 것들은 주기성이 있는 회원들을 찾아내면 좋겠군요?
일단, 도전!
# 일별 사용량 집계
df_daily_usage = df_use_log.groupby(['customer_id', 'usedate']).size().reset_index(name='count')
# 모든 사용자별로 이용한 평균 주기 계산
# `diff()` 함수를 적용하면 `[nan, 1, 2, 3]`과 같은 차이의 리스트를 반환
df_user_periods = df_daily_usage.groupby('customer_id')['usedate'].apply(lambda x: pd.Series(x).diff().mean())
# 주기성을 가진 사용자 추출
# `ge(6)`는 계산된 각 그룹 별 행(row)의 개수가 4 이상인지를 판단하는 함수. Log가 6개 이상 있는 사용자만 골라냄
df_periodic_users = df_user_periods[df_user_periods.notnull() & df_daily_usage.groupby('customer_id').size().ge(6)].to_frame()
df_periodic_users.head()
usedate | |
---|---|
customer_id | |
AS002855 | 6 days 17:39:37.358490566 |
AS008805 | 7 days 09:13:50.769230769 |
AS009373 | 6 days 01:36:00 |
AS015233 | 3 days 21:21:57.073170731 |
AS015315 | 6 days 09:15:47.368421052 |
df_periodic_users.describe()
usedate | |
---|---|
count | 4002 |
mean | 5 days 21:16:43.720672332 |
std | 1 days 23:30:38.980309177 |
min | 1 days 10:40:00 |
25% | 4 days 12:56:26.962025316 |
50% | 5 days 19:44:30.967741935 |
75% | 6 days 19:12:00 |
max | 17 days 18:00:00 |
df_periodic_users['usedate'].dt.days.hist(bins=17)
주기성이 없는 사용자는! (같은 요일이 5이상 되는 로그가 없는 사용자로 한정해서요)
df_non_periodic_users = df_user_periods[df_user_periods.isnull() | df_daily_usage.groupby('customer_id').size().le(5)].to_frame()
len(df_non_periodic_users.index)
190
df_non_periodic_users.describe()
usedate | |
---|---|
count | 159 |
mean | 12 days 00:01:30.566037735 |
std | 7 days 20:09:18.718305793 |
min | 1 days 00:00:00 |
25% | 7 days 00:00:00 |
50% | 10 days 12:00:00 |
75% | 15 days 08:00:00 |
max | 46 days 00:00:00 |
df_non_periodic_users['usedate'].dt.days.hist()
4002명이 어느 정도 주기성을 가지고 있고, 190명이 주기성이 없군요!
2차 결론
1) 2018년 4월을 기준으로 뭔가 큰일이 일어나긴 했는데, 알 수 없다.
2) campaign으로 들어온 회원들이 더 많이 그만두고 있다. 일반 회원의 충성심이 더 좋은 것 같다
하지만, 할인 campaign이 최근에는 아예 없었으므로 꼭 그렇다고 말하기는 어렵다고 할 수 있다고 생각한다.
3) 이제부터는 2018년 4월 이후 (아직까지 회원 또는 4월 이후에 가입회원)만을 중심으로 분석하는 편이 낫겠다.
주기성을 회원별 직접 산출해 봅시다.
일단 use_log를 이용할 건데, 이중에 날짜를 연월을 하나 추가하고, 이용날짜의 요일을 각 연월마다 카운트해서 회원마다 진짜로 주기적으로 사용하는지까지 데이터를 만들어 보도록 하겠습니다.
df_use_log['YM'] = df_use_log['usedate'].dt.strftime("%Y-%m")
df_use_log.head()
log_id | customer_id | usedate | day | YM | |
---|---|---|---|---|---|
0 | L00000049012330 | AS009373 | 2018-04-01 | 6 | 2018-04 |
1 | L00000049012331 | AS015315 | 2018-04-01 | 6 | 2018-04 |
2 | L00000049012332 | AS040841 | 2018-04-01 | 6 | 2018-04 |
3 | L00000049012333 | AS046594 | 2018-04-01 | 6 | 2018-04 |
4 | L00000049012334 | AS073285 | 2018-04-01 | 6 | 2018-04 |
자, 왔습니다. 느낌? 자, 가보시죠. 회원/연월/요일을 그룹핑!
df_use_log_periodic = df_use_log.groupby(['customer_id', 'YM', 'day'], as_index=False).size()
df_use_log_periodic.head(7)
customer_id | YM | day | size | |
---|---|---|---|---|
0 | AS002855 | 2018-04 | 5 | 4 |
1 | AS002855 | 2018-05 | 2 | 1 |
2 | AS002855 | 2018-05 | 5 | 4 |
3 | AS002855 | 2018-06 | 5 | 5 |
4 | AS002855 | 2018-07 | 1 | 1 |
5 | AS002855 | 2018-07 | 5 | 4 |
6 | AS002855 | 2018-08 | 0 | 1 |
헤헤 이걸 보니, 한달에 4번 이상을 같은 요일에 이용한 경우를 주기적으로 사용하는 회원이라고 한다면, 최대 요일만 뽑아내서 보면 말이죠.
df_use_log_periodic = df_use_log_periodic.loc[df_use_log_periodic.groupby('customer_id')['size'].idxmax()].reset_index(drop=True)#[['customer_id', 'YM', 'day', 'size']].reset_index(drop=True)
df_use_log_periodic.head()
customer_id | YM | day | size | |
---|---|---|---|---|
0 | AS002855 | 2018-06 | 5 | 5 |
1 | AS008805 | 2018-06 | 5 | 4 |
2 | AS009013 | 2018-04 | 0 | 2 |
3 | AS009373 | 2018-07 | 6 | 5 |
4 | AS015233 | 2018-09 | 6 | 5 |
각 사용자의 주기성을 periodic 이라는 Flag를 둬서 주기적인 사용자인지 볼 수 있으면 좋겠죠!
df_use_log_periodic['periodic'] = df_use_log_periodic.apply(lambda row: True if row['size']>=4 else False, axis=1)
df_use_log_periodic.head()
customer_id | YM | day | size | periodic | |
---|---|---|---|---|---|
0 | AS002855 | 2018-06 | 5 | 5 | True |
1 | AS008805 | 2018-06 | 5 | 4 | True |
2 | AS009013 | 2018-04 | 0 | 2 | False |
3 | AS009373 | 2018-07 | 6 | 5 | True |
4 | AS015233 | 2018-09 | 6 | 5 | True |
어느 요일에 주기성을 가진 회원들이 오고 있는지 확인해 보면,
df_use_log_periodic[df_use_log_periodic['periodic']==True].groupby('day')['customer_id'].size().plot()
주기성을 가진 회원들은 월요일, 금요일, 일요일이 많군요. 이 날에는 회원 retention을 위해서 조금 더 신경쓰는 편이 낫겠다는 생각이 듭니다. 주기적으로 오는 회원들을 대상으로 뭔가 benefit을 주는 이벤트를 한다던가 하는 것도 좋은 아이디어겠군요.
3차 결론
1) 2018년 4월을 기준으로 뭔가 지구종말에 버금가는 큰일이 일어나긴 했는데, 데이터만으로는 알 수 없다.
2) campaign으로 들어온 회원들이 더 많이 그만두고 있다. 일반 회원의 충성심이 더 좋은 것 같다
하지만, 할인 campaign이 최근에는 아예 없었으므로 꼭 그렇다고 말하기는 어렵다고 할 수 있다고 생각한다.
3) 2018년 4월 이후 (아직까지 회원 또는 4월 이후에 가입회원)만을 중심으로 분석하는 편이 낫겠다.
4) 주기성을 분석해 보니, 어느 정도 더 많은 회원이 사용하고 있는 요일이 있다. 그것은 월/금/일요일이고, 이 날을 주의깊게 보면 고객 retention을 더 잘 이끌어 낼 수 있을 것이다.
이제 마지막으로 개별 customer_master와 사용 로그를 하나의 테이블로 만들어보겠습니다. 이러면 개별 고객을 분석할 수도 있고, class/남녀/캠페인등을 따로 들여다 봄으로써 회원들의 특징을 알아낼 수 있을 거라 생각합니다.만. 이건 나중으로 미루고, 이제는 고객별 정보를 만들어서 예측 모형을 만들어 볼까 합니다.
display(df_customer_recent_master.head(),df_use_log_periodic.head())
df_master = pd.merge(df_customer_recent_master, df_use_log_periodic, on='customer_id', how='left')
df_master.head()
customer_id | name | class | gender | start_date | end_date | campaign_id | is_deleted | duration | |
---|---|---|---|---|---|---|---|---|---|
0 | OA832399 | XXXX | C01 | F | 2015-05-01 | NaT | CA1 | 0 | 1431 days |
1 | PL270116 | XXXXX | C01 | M | 2015-05-01 | NaT | CA1 | 0 | 1431 days |
2 | OA974876 | XXXXX | C01 | M | 2015-05-01 | NaT | CA1 | 0 | 1431 days |
3 | HD024127 | XXXXX | C01 | F | 2015-05-01 | NaT | CA1 | 0 | 1431 days |
4 | HD661448 | XXXXX | C03 | F | 2015-05-01 | NaT | CA1 | 0 | 1431 days |
customer_id | YM | day | size | periodic | |
---|---|---|---|---|---|
0 | AS002855 | 2018-06 | 5 | 5 | True |
1 | AS008805 | 2018-06 | 5 | 4 | True |
2 | AS009013 | 2018-04 | 0 | 2 | False |
3 | AS009373 | 2018-07 | 6 | 5 | True |
4 | AS015233 | 2018-09 | 6 | 5 | True |
customer_id | name | class | gender | start_date | end_date | campaign_id | is_deleted | duration | YM | day | size | periodic | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | OA832399 | XXXX | C01 | F | 2015-05-01 | NaT | CA1 | 0 | 1431 days | 2018-05 | 3 | 4 | True |
1 | PL270116 | XXXXX | C01 | M | 2015-05-01 | NaT | CA1 | 0 | 1431 days | 2018-05 | 2 | 5 | True |
2 | OA974876 | XXXXX | C01 | M | 2015-05-01 | NaT | CA1 | 0 | 1431 days | 2018-06 | 4 | 5 | True |
3 | HD024127 | XXXXX | C01 | F | 2015-05-01 | NaT | CA1 | 0 | 1431 days | 2018-05 | 3 | 5 | True |
4 | HD661448 | XXXXX | C03 | F | 2015-05-01 | NaT | CA1 | 0 | 1431 days | 2018-05 | 2 | 5 | True |
헤헤 최근 회원 데이터로만 분석용 master dataframe을 만들었습니다. 마지막으로 left join을 했기 때문에 뭔가 null값이 있는지 한번 확인해 보시죠.
df_master.info()
Int64Index: 4192 entries, 0 to 4191 Data columns (total 13 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 customer_id 4192 non-null object 1 name 4192 non-null object 2 class 4192 non-null object 3 gender 4192 non-null object 4 start_date 4192 non-null datetime64[ns] 5 end_date 1350 non-null datetime64[ns] 6 campaign_id 4192 non-null object 7 is_deleted 4192 non-null int64 8 duration 4192 non-null timedelta64[ns] 9 YM 4192 non-null object 10 day 4192 non-null int64 11 size 4192 non-null int64 12 periodic 4192 non-null bool dtypes: bool(1), datetime64[ns](2), int64(3), object(6), timedelta64[ns](1) memory usage: 429.8+ KB
헤헤 보니까, end_date빼고는 모두 null이 없는 것 보니, join이 잘 된 것 같습니다. 일단 데이터를 이해하는 분석은 어느정도 했다고 생각이 드니, 일단 마무리 지을까 합니다.
df_master.to_csv("customer_master_full.csv")
일단 df_master를 customer_master_full.csv로 저장했고요, 이걸 다음 이어지는 글에서 로딩하여 사용하려고 하니, 잘 챙겨주세요.
댓글