반응형
Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- python error plot
- data analysis
- 빈도주의론
- 시각화
- python
- marketing
- 고객가치분석
- error bar plot
- E-Commerce
- FWER
- 다중검정
- ecommerce data
- 데이터 시각화
- marketing insight
- lifetimevalue
- python significance level
- 베이지안론
- 데이터 분석
- cltv
- spearman
- p value plot
- Pearson
- matplotlib
- plot
- 백분위 변환
- ttest
- 통계
- box-cox변환
- Yeo-Johnsom 변환
- 분위수 변환
Archives
- Today
- Total
Data 공부
[LTV] Marketing insights for E-commerce company 본문
LTV
이전 Cohort Analysis에서 고객이탈에 대한 주기에 대한 지표에 대한 중요성 등의 필요성을 확인했으므로 LTV 분석을 통해 고객의 예상 구매 횟수, 예상 구매 금액을 예측한다.
신규 고객 유치에 드는 비용(Acquisition Cost)가 통상적으로 기존 고객을 유지하는데 드는 비용(Retention Cost)보다 크다는 점을 이용하여 기존 고객들의 특성 파악을 통해 고객 중심의 마케팅 전략을 설정한다.
*참고:https://pl ayinpap.github.io/ltv-practice/
0. Import Package & Data Load¶
In [1]:
import pandas as pd
import seaborn as sns
import numpy as np
import os
import scipy.stats as st
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
from datetime import datetime
from datetime import timedelta
# 고객들의 RFMT 계산할 경우, 이전 RFM분석에서 사용한 지표가 아닌 lifetimes의 모듈을 이용한다.
from lifetimes.plotting import *
from lifetimes.utils import *
from lifetimes import BetaGeoFitter
from lifetimes.fitters.gamma_gamma_fitter import GammaGammaFitter
from hyperopt import hp, fmin, tpe, rand, SparkTrials, STATUS_OK, space_eval, Trials
from sklearn.metrics import mean_squared_error
%matplotlib inline
data_cus = pd.read_pickle("data_cus.pkl")
data_cou = pd.read_pickle("data_cou.pkl")
data_ma = pd.read_pickle("data_ma.pkl")
data_on = pd.read_pickle("data_on.pkl")
data_tax = pd.read_pickle("data_tax.pkl")
data_merge = pd.read_pickle("data_merge.pkl") # simple analysis에서 작업한 data
1. R, F, M, T 계산¶
In [2]:
# LTV 계산을 위해 구매 횟수가 1인 고객을 거래내역 테이블에서 제거
# 한 번만 구매한 고객의 경우 일반적으로 LTV계산이 어려우며, 추가 구매의 가능성이 낮다고 가정한다.
transaction_dates_count = data_merge.groupby('CustomerID')['Transaction_Date'].nunique()
single_transaction_customers = transaction_dates_count[transaction_dates_count == 1].index
filtered_df = data_merge[~data_merge['CustomerID'].isin(single_transaction_customers)]
filtered_df.head()
Out[2]:
CustomerID | Transaction_ID | Transaction_Date | Product_SKU | Product_Description | Product_Category | Quantity | Avg_Price | Delivery_Charges | Coupon_Status | GST | Month | CouponID | Discount_pct | invoice_value | Week | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 17850 | 16679 | 2019-01-01 | GGOENEBJ079499 | Nest Learning Thermostat 3rd Gen-USA - Stainle... | Nest-USA | 1 | 153.71 | 6.5 | Used | 0.10 | 1 | 4.0 | 0.1 | 158.6729 | 0 |
1 | 17850 | 16680 | 2019-01-01 | GGOENEBJ079499 | Nest Learning Thermostat 3rd Gen-USA - Stainle... | Nest-USA | 1 | 153.71 | 6.5 | Used | 0.10 | 1 | 4.0 | 0.1 | 158.6729 | 0 |
2 | 17850 | 16681 | 2019-01-01 | GGOEGFKQ020399 | Google Laptop and Cell Phone Stickers | Office | 1 | 2.05 | 6.5 | Used | 0.10 | 1 | 7.0 | 0.1 | 8.5295 | 0 |
3 | 17850 | 16682 | 2019-01-01 | GGOEGAAB010516 | Google Men's 100% Cotton Short Sleeve Hero Tee... | Apparel | 5 | 17.53 | 6.5 | Not Used | 0.18 | 1 | 1.0 | 0.0 | 109.9270 | 0 |
4 | 17850 | 16682 | 2019-01-01 | GGOEGBJL013999 | Google Canvas Tote Natural/Navy | Bags | 1 | 16.50 | 6.5 | Used | 0.18 | 1 | 16.0 | 0.1 | 24.0230 | 0 |
In [3]:
# lifetime 모듈을 통한 R, F, M, T 계산
current_date = filtered_df['Transaction_Date'].max()
metrics_df = summary_data_from_transaction_data(filtered_df,
customer_id_col = 'CustomerID',
datetime_col = 'Transaction_Date',
monetary_value_col = 'invoice_value',
observation_period_end = current_date)
metrics_df.head()
Out[3]:
frequency | recency | T | monetary_value | |
---|---|---|---|---|
CustomerID | ||||
12347 | 2.0 | 223.0 | 282.0 | 1392.78090 |
12348 | 1.0 | 119.0 | 192.0 | 811.33870 |
12370 | 1.0 | 30.0 | 219.0 | 683.49810 |
12377 | 1.0 | 139.0 | 179.0 | 7025.03568 |
12383 | 3.0 | 72.0 | 171.0 | 936.55806 |
2. BG/NBD, Gamma-Gamma Model 최적화(l2-penalty)¶
- BG/NBD(Beta-Geometric/Negative Binominal Distrubution)
- 미래의 예상 구매횟수를 대변(고객 생존 확률, 구매 주기)
- R, F, T를 이용
- 베타, 기하 분포/음이항분포(포아송분포 + 감마분포)
- Beta-Geometric: 고객이 재구매를 할 때 까지 걸리는 시간 간격(회사의 제품을 계속해서 구매할 확률)
- NBD: 고객이 한 번의 구매시 특정 제품을 구매하는 횟수(각 구매시 고객이 구매하는 제품의 수)
- Gamma-Gamma
- 미래의 예상 평균 수익을 대변(고객의 구매 금액)
- F, M을 이용
- 고객별 구매 금액이 평균 구매 금액을 중심으로 랜덤하게 분포하므로 Gamma 분포이용
- l2 penalty
- 데이터 크기가 작으면 parameter가 과도하게 추정될 수 있으므로 l2 penalty를 부여
- l2 penalty는 파라미터 벡터의 각 요소를 제곱한 값들의 합에 규제를 함으로써 일반화 성능을 높임
- overfitting 방지
- lifetimes package 설명에선 0.001에서 0.1 정도의 penalty가 효과적이라고 나옴
- 데이터 크기가 작으면 parameter가 과도하게 추정될 수 있으므로 l2 penalty를 부여
2-1. Calibration/holdout 분리¶
In [4]:
# l2 penalty 최적화 학습을 위한 calibration/holdout 분리
# 시계열 데이터의 특수성(랜덤이 아님)의 이유로 사용
holdout_days = 90
calibration_end_date = current_date - timedelta(days = holdout_days)
metrics_cal_df = calibration_and_holdout_data(filtered_df,
customer_id_col = 'CustomerID',
datetime_col = 'Transaction_Date',
calibration_period_end = calibration_end_date,
monetary_value_col = 'invoice_value',
observation_period_end = current_date)
metrics_cal_df.head()
Out[4]:
frequency_cal | recency_cal | T_cal | monetary_value_cal | frequency_holdout | monetary_value_holdout | duration_holdout | |
---|---|---|---|---|---|---|---|
CustomerID | |||||||
12347 | 0.0 | 0.0 | 192.0 | 0.00000 | 2.0 | 96.053855 | 90.0 |
12348 | 0.0 | 0.0 | 102.0 | 0.00000 | 1.0 | 135.223117 | 90.0 |
12370 | 1.0 | 30.0 | 129.0 | 683.49810 | 0.0 | 0.000000 | 90.0 |
12377 | 0.0 | 0.0 | 89.0 | 0.00000 | 1.0 | 206.618696 | 90.0 |
12383 | 3.0 | 72.0 | 81.0 | 936.55806 | 0.0 | 0.000000 | 90.0 |
In [5]:
# frequency가 0인(한번만 구매한 데이터 제외)
whole_data = metrics_df[metrics_df.frequency > 0] # LTV 계산 DATA
cal_data = metrics_cal_df[metrics_cal_df.frequency_cal > 0] # L2 penalty 및 모델 최적화 DATA, holdout 데이터는 frequency가 0이여도됨
2-2 BG/NBD, GAMMA-GAMMA 최적 L2 penalty 찾기¶
In [6]:
# BG/NBD 모델 평가
def evaluate_bgnbd_model(param):
data = inputs
l2_reg = param
model = BetaGeoFitter(penalizer_coef=l2_reg)
model.fit(data['frequency_cal'], data['recency_cal'], data['T_cal'])
frequency_actual = data['frequency_holdout']
frequency_predicted = model.predict(data['duration_holdout']
, data['frequency_cal']
, data['recency_cal']
, data['T_cal']
)
rmse = np.sqrt(mean_squared_error(frequency_actual, frequency_predicted))
return {'loss': rmse, 'status': STATUS_OK}
# Gamma/Gamma 모델 평가
def evaluate_gg_model(param):
data = inputs
l2_reg = param
model = GammaGammaFitter(penalizer_coef=l2_reg)
model.fit(data['frequency_cal'], data['monetary_value_cal'])
monetary_actual = data['monetary_value_holdout']
monetary_predicted = model.conditional_expected_average_profit(data['frequency_holdout'], data['monetary_value_holdout'])
rmse = np.sqrt(mean_squared_error(monetary_actual, monetary_predicted))
# return score and status
return {'loss': rmse, 'status': STATUS_OK}
In [7]:
# BG/NBD 모델 l2 penalty
search_space = hp.uniform('l2', 0.0, 1.0)
algo = tpe.suggest
trials = Trials()
inputs = cal_data
argmin = fmin(
fn = evaluate_bgnbd_model, # 목적함수
space = search_space, # 파라미터 공간
algo = algo, # 최적화 알고리즘: Tree of Parzen Estimators (TPE)
max_evals=100, # 반복수
trials=trials
)
l2_bgnbd = space_eval(search_space,argmin)
print("BG/NBD l2 penalty:", l2_bgnbd)
100%|██████████████████████████████████████████████| 100/100 [00:09<00:00, 11.01trial/s, best loss: 0.9048589509939571]
BG/NBD l2 penalty: 0.0012581325688182004
In [8]:
# Gamma-Gamma 모델 L2 penalty
trials = Trials()
# GammaGamma
argmin = fmin(
fn = evaluate_gg_model,
space = search_space,
algo = algo,
max_evals=100,
trials=trials
)
l2_gg = space_eval(search_space,argmin)
print("Gamma-Gamma l2 penalty:", l2_gg)
100%|██████████████████████████████████████████████| 100/100 [00:04<00:00, 20.28trial/s, best loss: 22.092073287841632]
Gamma-Gamma l2 penalty: 0.008743147673232421
2-3. BG/NBD, GAMMA-GAMMA 모델 최적화¶
In [9]:
"""
BG/NBD
"""
lifetimes_model = BetaGeoFitter(penalizer_coef=l2_bgnbd)
# calibration 데이터의 R,F,T로 델 적합
lifetimes_model.fit(cal_data['frequency_cal'], cal_data['recency_cal'], cal_data['T_cal'])
# holdout 데이터로 모델 평가: F의 실제값과 예측값의 MSE
frequency_actual = cal_data['frequency_holdout']
frequency_predicted = lifetimes_model.predict(cal_data['duration_holdout']
,cal_data['frequency_cal']
,cal_data['recency_cal']
,cal_data['T_cal'])
mse = mean_squared_error(frequency_actual, frequency_predicted)
rmse = np.sqrt(mse)
print('RMSE: {0}'.format(rmse))
print('MSE: {0}'.format(mse))
RMSE: 0.9048589509939571
MSE: 0.8187697211938844
In [10]:
#Visualization
from lifetimes.plotting import plot_probability_alive_matrix
plot_probability_alive_matrix(lifetimes_model)
plt.show()
- BG/NBD 모델의 최적화 결과 rmse < 1로 구매일수에 대한 평균 제곱오차 오차가 1일 이하로 나타남
- F-R 시각화 결과, 노란색이 짙을 수록 충성심이 큰 고객이다.
In [11]:
"""
GAMMA-GAMMA
"""
spend_model = GammaGammaFitter(penalizer_coef=l2_gg)
spend_model.fit(cal_data['frequency_cal'], cal_data['monetary_value_cal'])
# conditional_expected_average_profit: 고객별 평균 구매 금액 예측
monetary_actual = cal_data['monetary_value_holdout']
monetary_predicted = spend_model.conditional_expected_average_profit(cal_data['frequency_holdout']
,cal_data['monetary_value_holdout'])
mse = mean_squared_error(monetary_actual, monetary_predicted)
print('MSE: {0}'.format(mse))
MSE: 488.0597021553658
In [12]:
bins = 100
plt.figure(figsize=(15, 5))
# 실제 구매가 0인 회원은 제외(holdout 기간 내 구매 X)
plt.hist(monetary_actual[monetary_actual!=0], bins, label='actual', histtype='bar', color='STEELBLUE', rwidth=0.95, alpha=.5)
plt.hist(monetary_predicted[monetary_actual!=0], bins, label='predict', histtype='bar', color='ORANGE', rwidth=0.95, alpha=.5)
plt.legend(loc='upper right')
plt.title("Monetary, Actual vs Predict", fontsize=15)
plt.show()
- Gamma-Gamma 모델의 최적화 결과 mse=487로 구매금액에 대한 평균 제곱오차 오차가 $487로 나타남.
- 실제와 예측값으 비교결과 대략적인 분포는 비슷해보인다. 이는 l2 penalty의 영향이며 과적합인지는 추후에 분석이 필요
2-4. Lifetime Value¶
In [13]:
# LTV 구하기
final_df = whole_data.copy()
final_df['ltv'] = spend_model.customer_lifetime_value(lifetimes_model,
final_df['frequency'],
final_df['recency'],
final_df['T'],
final_df['monetary_value'],
time=12,
discount_rate=0.01 # monthly discount rate ~12.7% 연간
)
t=365 #향후 365일에 대한 예측
final_df['predicted_purchases'] = lifetimes_model.conditional_expected_number_of_purchases_up_to_time(t
, final_df['frequency']
, final_df['recency']
, final_df['T'])
final_df['predicted_monetary_value'] = spend_model.conditional_expected_average_profit(final_df['frequency']
,final_df['monetary_value'])
In [14]:
plt.figure(figsize=(10,4))
final_df['predicted_purchases'].hist()
plt.grid(False); plt.gca().spines[['top', 'right']].set_visible(False)
plt.title("Predicted_Purchase", fontsize=15)
plt.show()
plt.figure(figsize=(10,4))
final_df['predicted_monetary_value'].hist()
plt.grid(False); plt.gca().spines[['top', 'right']].set_visible(False)
plt.title("Predicted_Monetary_Value", fontsize=15)
plt.show()
plt.figure(figsize=(10,4))
final_df['ltv'].hist()
plt.grid(False); plt.gca().spines[['top', 'right']].set_visible(False)
plt.title("LTV", fontsize=15)
plt.show()
print("예측 구매 횟수 median {}회".format(round(final_df['predicted_purchases'].median(), 2)))
print("예측 구매 금액 median ${}".format(round(final_df['predicted_monetary_value'].median(), 2)))
final_df.sort_values(by="ltv", ascending=False).head()
예측 구매 횟수 median 1.23회
예측 구매 금액 median $1173.69
Out[14]:
frequency | recency | T | monetary_value | ltv | predicted_purchases | predicted_monetary_value | |
---|---|---|---|---|---|---|---|
CustomerID | |||||||
17337 | 1.0 | 1.0 | 17.0 | 26834.681690 | 80490.799526 | 2.519483 | 34030.307915 |
15311 | 23.0 | 351.0 | 363.0 | 3583.682006 | 49249.891434 | 14.640771 | 3617.064716 |
14606 | 26.0 | 349.0 | 349.0 | 2296.325963 | 36876.476218 | 17.125181 | 2315.274796 |
17841 | 19.0 | 337.0 | 354.0 | 2912.158906 | 33808.841260 | 12.341961 | 2945.094791 |
14911 | 25.0 | 344.0 | 354.0 | 2210.081895 | 33382.429067 | 16.102566 | 2229.059822 |
- 고객 1명 당 예측된 구매횟수는 1년 간 1.22회로 주관적인 생각으로 매우 적은 값을 나타낸다.
- 이는 retention 전략이 강화되야한다. 충성고객들에 대한 특별 혜택, 개인화된 마케팅전략 등이 필요하다.
- 고객 1명 당 예측된 구매 금액은 1년간 $1157 이다.
- 1인당 마케팅 비용과 비교하여 결과에 따라 고객별 LTV에 따른 마케팅 비용측정이 필요.
- 고객의 예측된 LTV를 통해 회사의 내년 목표와 맞는지 판단하고, 부족하다면 리텐션 전략 강화, 마케팅 비용관리 등의 방법을 통해 고객 중심의 전략을 설정해야할 것이다.
3. RFM 고객세분화, LTV 비교¶
In [15]:
final_df.reset_index(inplace=True)
df_rfm = pd.read_pickle("df_rfm.pkl")
temp_df = pd.merge(final_df, df_rfm, how='left', on='CustomerID').sort_values(by='ltv')[['CustomerID', 'ltv', 'RFM_grade(score_mean)', 'RFM_grade(marketing)']]
print(temp_df.groupby('RFM_grade(score_mean)')['ltv'].agg(['median', 'mean']).reset_index().sort_values(by='median', ascending=False))
print()
print(temp_df.groupby('RFM_grade(marketing)')['ltv'].agg(['median', 'mean']).reset_index().sort_values(by='median', ascending=False))
RFM_grade(score_mean) median mean
3 VIP 4561.425985 6638.980976
1 Platinum 638.996247 1151.776095
0 Gold 95.869765 220.061656
2 Silver 29.469771 43.164964
RFM_grade(marketing) median mean
0 VIP 고객 6604.086910 8562.399257
4 충성 고객 2099.066116 3346.384936
2 우수 고객 508.609959 906.733239
3 이탈 우려 고객 102.950214 243.325124
1 놓치면 안될 고객 36.726627 53.510517
- 기존에 분석했던 RFM 등급별 LTV의 비교결과
- 단순 RFM 평균 score 등급에 대한 비교결과는 등급별 LTV의 분포가 기존 분석과 일치한다.
- RFM 각 항목을 기준으로 나눈 등급에 대한 비교결과 역시 LTV의 분포가 대부분 일치하나, 이탈 우려고객의 LTV가 최저로 나타나지 않은 것으로 보아 해당 고객들에 대한 retention을 유지하기 위하여 심화된 마케팅 전략을 취해야 한다.
반응형
'Data 분석 > E-Commerce data' 카테고리의 다른 글
[Cohort Analysis] Marketing insights for E-commerce company (0) | 2024.06.15 |
---|---|
[Customer Segment] Marketing insights for E-commerce company (0) | 2024.06.06 |
[Simple Analysis] Marketing insights for E-commerce company (1) | 2024.06.03 |
[EDA] Marketing insights for E-commerce company (0) | 2024.06.03 |
Comments