[코드쉐도잉]/산업
[CRM] e-commerce
에디터 윤슬
2025. 1. 10. 14:18
라이브러리 호출
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings("ignore")
import datetime as dt
pd.set_option('display.max_columns', None)
sns.set_style('whitegrid')
palette = 'Set2'
import squarify
from operator import attrgetter
import matplotlib.colors as mcolors
from sklearn.metrics import (silhouette_score,
calinski_harabasz_score,
davies_bouldin_score)
from lifetimes import BetaGeoFitter, GammaGammaFitter
from lifetimes.plotting import plot_period_transactions
%matplotlib inline
데이터 체크
- 기본 정보 체크 함수 생성
# 데이터 체크
def check_data(dataframe, head=5):
print(" SHAPE ".center(70,'-'))
print('Rows: {}'.format(dataframe.shape[0]))
print('Columns: {}'.format(dataframe.shape[1]))
print(" TYPES ".center(70,'-'))
print(dataframe.dtypes)
print(" HEAD ".center(70,'-'))
print(dataframe.head(head))
print(" TAIL ".center(70,'-'))
print(dataframe.tail(head))
print(" MISSING VALUES ".center(70,'-'))
print(dataframe.isnull().sum())
print(" DUPLICATED VALUES ".center(70,'-'))
print(dataframe.duplicated().sum())
print(" QUANTILES ".center(70,'-'))
print(dataframe.quantile([0, 0.05, 0.50, 0.95, 0.99, 1]).T)
check_data(merged_df)
- describe 히트맵 생성하는 함수
# describe 함수
def desc_stats(dataframe):
desc_df = pd.DataFrame(index= dataframe.columns,
columns= dataframe.describe().T.columns,
data= dataframe.describe().T)
f,ax = plt.subplots(figsize=(10,
desc_df.shape[0] * 0.81))
sns.heatmap(desc_df,
annot = True,
cmap = "Greens",
fmt = '.2f',
ax = ax,
linecolor = 'white',
linewidths = 1.1,
cbar = False,
annot_kws = {"size": 12})
plt.xticks(size = 18)
plt.yticks(size = 14,
rotation = 0)
plt.title("Descriptive Statistics", size = 14)
plt.show()
desc_stats(merged_df.select_dtypes(include = [float, int]))
RFM
- RFM Dataframe 생성
# rfm dataframe 생성
def rfm(df):
present_day = df['transaction_date'].max() + dt.timedelta(days = 2) # Timestamp('2022-08-02 00:00:00')
rfm = df.groupby('customer_id').agg({'transaction_date': lambda x: (present_day - x.max()).days,
'session_id': lambda x: x.nunique(),
'amount': lambda x: x.sum()})
rfm.columns = ['recency', 'frequency', 'monetary']
rfm = rfm.reset_index()
return rfm
rfm_df = rfm(merged_df)
rfm_df
- RFM Score
# rfm score
def get_rfm_scores(dataframe) -> pd.core.frame.DataFrame:
df_ = dataframe.copy()
df_["recency_score"] = pd.qcut(df_["recency"], 5, labels=[5, 4, 3, 2, 1])
df_["frequency_score"] = pd.qcut(
df_["frequency"].rank(method="first"), 5, labels=[1, 2, 3, 4, 5]
)
df_["monetary_score"] = pd.qcut(df_["monetary"], 5, labels=[1, 2, 3, 4, 5])
df_["RFM_SCORE"] = df_["recency_score"].astype(str) + df_["frequency_score"].astype(str) + \
df_["monetary_score"].astype(str)
return df_
rfm_df = get_rfm_scores(rfm_df)
rfm_df
- RFM 세그먼트
# rfm segmentation
def get_segment(df):
seg_map= {
r'111|112|121|131|141|151': 'Lost customers',
r'332|322|233|232|223|222|132|123|122|212|211': 'Hibernating customers',
r'155|154|144|214|215|115|114|113': 'Cannot Lose Them',
r'255|254|245|244|253|252|243|242|235|234|225|224|153|152|145|143|142|135|134|133|125|124': 'At Risk',
r'331|321|312|221|213|231|241|251': 'About To Sleep',
r'535|534|443|434|343|334|325|324': 'Need Attention',
r'525|524|523|522|521|515|514|513|425|424|413|414|415|315|314|313': 'Promising',
r'512|511|422|421|412|411|311': 'New Customers',
r'553|551|552|541|542|533|532|531|452|451|442|441|431|453|433|432|423|353|352|351|342|341|333|323': 'Potential Loyalist',
r'543|444|435|355|354|345|344|335': 'Loyal',
r'555|554|544|545|454|455|445': 'Champions'
}
df['segment'] = df['RFM_SCORE'].replace(seg_map, regex = True)
return df
rfm_df = get_segment(rfm_df)
rfm_df
- 세그먼트 맵 시각화
# segmentation Map
seg_map= {
r'111|112|121|131|141|151': 'Lost customers',
r'332|322|233|232|223|222|132|123|122|212|211': 'Hibernating customers',
r'155|154|144|214|215|115|114|113': 'Cannot Lose Them',
r'255|254|245|244|253|252|243|242|235|234|225|224|153|152|145|143|142|135|134|133|125|124': 'At Risk',
r'331|321|312|221|213|231|241|251': 'About To Sleep',
r'535|534|443|434|343|334|325|324': 'Need Attention',
r'525|524|523|522|521|515|514|513|425|424|413|414|415|315|314|313': 'Promising',
r'512|511|422|421|412|411|311': 'New Customers',
r'553|551|552|541|542|533|532|531|452|451|442|441|431|453|433|432|423|353|352|351|342|341|333|323': 'Potential Loyalist',
r'543|444|435|355|354|345|344|335': 'Loyal',
r'555|554|544|545|454|455|445': 'Champions'
}
segments = rfm_df["segment"].value_counts().sort_values(ascending=False)
fig = plt.gcf()
ax = fig.add_subplot()
fig.set_size_inches(16, 10)
squarify.plot(
sizes=segments,
label=[label for label in seg_map.values()],
color=[
"#AFB6B5",
"#F0819A",
"#926717",
"#F0F081",
"#81D5F0",
"#C78BE5",
"#748E80",
"#FAAF3A",
"#7B8FE4",
"#86E8C0",
],
pad=False,
bar_kwargs={"alpha": 1},
text_kwargs={"fontsize": 15},
)
plt.title("Customer Segmentation Map", fontsize=20)
plt.xlabel("Frequency", fontsize=18)
plt.ylabel("Recency", fontsize=18)
plt.show()
- 모델 평가
# model evaluation
print(' RFM Model Evaluation '.center(70, '='))
X = rfm_df[['recency_score', 'frequency_score']]
labels = rfm_df['segment']
print(f'Number of Observations: {X.shape[0]}')
print(f'Number of Segments: {labels.nunique()}')
print(f'Silhouette Score: {round(silhouette_score(X, labels), 3)}')
print(f'Calinski Harabasz Score: {round(calinski_harabasz_score(X, labels), 3)}')
print(f'Davies Bouldin Score: {round(davies_bouldin_score(X, labels), 3)} \n{70*"="}')
- 세그먼트 확인
# segment analysis
rfm_df[['recency','monetary','frequency','segment']]\
.groupby('segment')\
.agg({'mean','std','max','min'})
RFM 시각화
plt.figure(figsize = (18, 8))
ax = sns.countplot(data = rfm_df,
x = 'segment',
palette = palette)
total = len(rfm_df.segment)
for patch in ax.patches:
percentage = '{:.1f}%'.format(100 * patch.get_height()/total)
x = patch.get_x() + patch.get_width() / 2 - 0.17
y = patch.get_y() + patch.get_height() * 1.005
ax.annotate(percentage, (x, y), size = 14)
plt.title('Number of Customers by Segments', size = 16)
plt.xlabel('Segment', size = 14)
plt.ylabel('Count', size = 14)
plt.xticks(size = 10)
plt.yticks(size = 10)
plt.show()
plt.figure(figsize=(18, 8))
sns.scatterplot(
data=rfm_df, x="recency", y="frequency", hue="segment", palette=palette, s=60
)
plt.title("Recency & Frequency by Segments", size=16)
plt.xlabel("Recency", size=12)
plt.ylabel("Frequency", size=12)
plt.xticks(size=10)
plt.yticks(size=10)
plt.legend(loc="best", fontsize=12, title="Segments", title_fontsize=14)
plt.show()
fig, axes = plt.subplots(1, 3, figsize=(18, 8))
fig.suptitle("RFM Segment Analysis", size=14)
feature_list = ["recency", "monetary", "frequency"]
for idx, col in enumerate(feature_list):
sns.boxplot(
ax=axes[idx], data=rfm_df, x="segment", y=feature_list[idx], palette=palette
)
axes[idx].set_xticklabels(axes[idx].get_xticklabels(), rotation=60)
if idx == 1:
axes[idx].set_ylim([0, 400])
if idx == 2:
axes[idx].set_ylim([0, 30])
plt.tight_layout()
plt.show()
fig, axes = plt.subplots(3, 1, figsize=(16, 12))
fig.suptitle('RFM Segment Analysis', size = 14)
feature_list = ['recency', 'monetary', 'frequency']
for idx, col in enumerate(feature_list):
sns.histplot(ax = axes[idx], data = rfm_df,
hue = 'segment', x = feature_list[idx],
palette= palette)
if idx == 1:
axes[idx].set_xlim([0, 400])
if idx == 2:
axes[idx].set_xlim([0, 30])
plt.tight_layout()
plt.show()
코호트 분석
def CohortAnalysis(dataframe):
yearago = dataframe['transaction_date'].max() - pd.DateOffset(years = 1)
mask = dataframe['transaction_date'] >= yearago
dataframe = dataframe[mask]
data = dataframe.copy()
data = data[["customer_id", "session_id", "transaction_date"]].drop_duplicates()
data["order_month"] = data["transaction_date"].dt.to_period("M")
data["cohort"] = (
data.groupby("customer_id")["transaction_date"].transform("min").dt.to_period("M")
)
cohort_data = (
data.groupby(["cohort", "order_month"])
.agg(n_customers=("customer_id", "nunique"))
.reset_index(drop=False)
)
cohort_data["period_number"] = (cohort_data.order_month - cohort_data.cohort).apply(
attrgetter("n")
)
cohort_pivot = cohort_data.pivot_table(
index="cohort", columns="period_number", values="n_customers"
)
cohort_size = cohort_pivot.iloc[:, 0]
retention_matrix = cohort_pivot.divide(cohort_size, axis=0)
with sns.axes_style("white"):
fig, ax = plt.subplots(
1, 2, figsize=(12, 8), sharey=True, gridspec_kw={"width_ratios": [1, 11]}
)
sns.heatmap(
retention_matrix,
mask=retention_matrix.isnull(),
annot=True,
cbar=False,
fmt=".0%",
cmap="coolwarm",
ax=ax[1],
)
ax[1].set_title("Monthly Cohorts: User Retention", fontsize=14)
ax[1].set(xlabel="# of periods", ylabel="")
white_cmap = mcolors.ListedColormap(["white"])
sns.heatmap(
pd.DataFrame(cohort_size).rename(columns={0: "cohort_size"}),
annot=True,
cbar=False,
fmt="g",
cmap=white_cmap,
ax=ax[0],
)
fig.tight_layout()
CohortAnalysis(merged_df)
- 코호트와 주문 월 생성
data["order_month"] = data["transaction_date"].dt.to_period("M")
data["cohort"] = (
data.groupby("customer_id")["transaction_date"].transform("min").dt.to_period("M")
)
• `order_month`: 각 거래가 발생한 월.
• `cohort`: 각 고객이 처음 거래한 월(가입 또는 첫 구매 시점).
- 코호트 데이터 집계
cohort_data = (
data.groupby(["cohort", "order_month"])
.agg(n_customers=("customer_id", "nunique"))
.reset_index(drop=False)
)
• 각 코호트(`cohort`)와 주문 월(`order_month`)에 대해 고유 고객 수(`n_customers`)를 집계합니다.
- 유지율 계산
cohort_pivot = cohort_data.pivot_table(
index="cohort", columns="period_number", values="n_customers"
)
cohort_size = cohort_pivot.iloc[:, 0]
retention_matrix = cohort_pivot.divide(cohort_size, axis=0)
• `period_number`: 각 코호트가 시작된 이후 몇 번째 기간인지 나타냅니다.
• `retention_matrix`: 각 코호트의 유지율을 계산합니다.
- 히트맵 시각화
sns.heatmap(
retention_matrix,
mask=retention_matrix.isnull(),
annot=True,
cbar=False,
fmt=".0%",
cmap="coolwarm",
ax=ax[1],
)
• 유지율 데이터를 히트맵으로 시각화하여 각 코호트의 유지율 변화를 보여줍니다.
CLV(Customer Lifetime Value)
- CLV 변수 생성
present_day = merged_df['transaction_date'].max() + dt.timedelta(days = 2)
cltv_df = merged_df.groupby("customer_id").agg(
{
"transaction_date": [
lambda x: (x.max() - x.min()).days,
lambda x: (present_day - x.min()).days,
],
"session_id": "nunique",
"amount": "sum",
}
)
cltv_df.columns = cltv_df.columns.droplevel(0)
cltv_df.columns = ["recency", "T", "frequency", "monetary"]
cltv_df.head()
>>> lambda x: (x.max() - x.min()).days
• 각 고객의 거래 날짜 중 가장 최근 날짜(`x.max()`)와 가장 오래된 날짜(`x.min()`)의 차이를 계산합니다.
• 이는 고객의 활동 기간(Recency)을 나타냅니다.
>>> lambda x: (present_day - x.min()).days
• 현재 날짜(`present_day`)와 고객의 첫 거래 날짜(`x.min()`) 간의 차이를 계산합니다.
• 이는 고객이 처음 거래한 이후 경과한 시간(T)을 나타냅니다.
>>> "session_id": "nunique"
• 고유 세션 수를 계산하여 고객이 몇 번의 세션에서 활동했는지 나타냅니다.
• 이는 고객의 **활동 빈도(Frequency)**를 나타냅니다.
>>> "amount": "sum"
• 각 고객이 지출한 총 금액을 계산합니다.
• 이는 고객의 **지출 금액(Monetary)**을 나타냅니다.
>>> cltv_df.columns = cltv_df.columns.droplevel(0)
cltv_df.columns = ["recency", "T", "frequency", "monetary"]
• `groupby`와 `agg`를 사용하면 다중 인덱스가 생성되므로, 이를 단순화하기 위해 첫 번째 레벨을 제거합니다.
• 각 컬럼에 의미 있는 이름을 부여합니다:
• `recency`: 고객의 최근 활동 기간 (최신 거래와 최초 거래 간 차이).
• `T`: 고객이 처음 거래한 이후 경과한 시간.
• `frequency`: 고유 세션 수.
• `monetary`: 총 지출 금액.
- AOV 계산 // recency & tenure(활동 기간) 단위 변경 // frequency 필터링
#Average Order Value
cltv_df["monetary"] = cltv_df["monetary"] / cltv_df["frequency"]
#Recency & Tenure
cltv_df["recency"] = cltv_df["recency"] / 7
cltv_df["T"] = cltv_df["T"] / 7
#Frequency
cltv_df = cltv_df[(cltv_df['frequency'] > 1)]
>>> cltv_df["monetary"] = cltv_df["monetary"] / cltv_df["frequency"]
• 의미:
• `monetary`(총 지출 금액)를 `frequency`(구매 빈도)로 나누어 **평균 주문 금액(Average Order Value, AOV)**를 계산합니다.
• AOV는 고객이 한 번 구매할 때 평균적으로 얼마나 지출했는지를 나타냅니다.
• 예시:
• 고객이 총 5번 구매했고, 총 지출 금액이 5000이라면: 5000/5 = 1000
• 따라서 `monetary` 컬럼은 이제 AOV를 나타냅니다.
>>> cltv_df["recency"] = cltv_df["recency"] / 7
cltv_df["T"] = cltv_df["T"] / 7
• 의미:
• `recency`와 `T` 값을 주 단위로 변환합니다.
• `recency`: 고객의 마지막 거래가 얼마나 최근에 이루어졌는지(일 단위 → 주 단위).
• `T`: 고객이 처음 거래한 이후 경과한 시간(일 단위 → 주 단위).
• 예시:
• 만약 `recency`가 14일이었다면: 14/7 = 2
• 이 변환은 데이터를 더 직관적으로 해석할 수 있도록 도와줍니다.
>>> cltv_df = cltv_df[(cltv_df['frequency'] > 1)]
• 의미:
• 구매 빈도(`frequency`)가 1보다 큰 고객만 필터링합니다.
• 즉, 한 번 이상 구매한 고객만 분석에 포함됩니다.
• 목적:
• CLTV 분석은 일반적으로 반복 구매를 기반으로 하기 때문에, 한 번만 구매한 고객은 제외하여 분석의 정확성을 높입니다.
• 결과:
• 구매 빈도가 1인 고객은 제외되며, 반복 구매를 한 고객만 남게 됩니다.
BG/NBD 모델
- 모델 설정
BGF = BetaGeoFitter(penalizer_coef=0.001) # avoid overfitting
BGF.fit(cltv_df["frequency"], cltv_df["recency"], cltv_df["T"])
Beta-Geometric/NBD (BG/NBD) 모델을 사용하여 고객 생애 가치(Customer Lifetime Value, CLTV)를 예측하기 위한 모델링 작업을 수행합니다.
이 모델은 `lifetimes` 라이브러리의 `BetaGeoFitter` 클래스를 활용하여 고객의 구매 행동 데이터를 학습합니다.
(a) `BetaGeoFitter` 초기화
• `BetaGeoFitter(penalizer_coef=0.001)`:
• `BetaGeoFitter`는 BG/NBD 모델을 구현한 클래스입니다.
• `penalizer_coef=0.001`: 과적합(overfitting)을 방지하기 위해 정규화(regularization) 항을 추가합니다.
• 값이 작을수록 과적합 방지 효과가 적고, 값이 클수록 더 강하게 정규화를 적용합니다.
(b) 데이터 학습
• `BGF.fit(cltv_df"frequency", cltv_df"recency", cltv_df"T")`:
• BG/NBD 모델에 고객 데이터를 학습시킵니다.
• 입력 변수:
1. `frequency`: 고객의 반복 구매 횟수.
2. `recency`: 고객의 마지막 거래가 첫 거래 이후 얼마나 지났는지 (주 단위).
3. `T`: 고객의 첫 거래 이후 현재까지 경과한 시간 (주 단위).
모델의 역할
• BG/NBD 모델은 고객의 구매 행동을 기반으로 다음을 예측합니다:
1. 특정 기간 동안 고객이 추가로 구매할 확률.
2. 고객의 생애 동안 예상 구매 횟수.
- Top 10 향후 1주 동안 고객 예상 구매 거래 수
# Top 10 Expected Number of Transaction (1 Week)
BGF.conditional_expected_number_of_purchases_up_to_time(
1, cltv_df["frequency"], cltv_df["recency"], cltv_df["T"]
).sort_values(ascending=False).head(10).to_frame(
"Expected Number of Transactions"
).reset_index()
1. 함수 설명
• `BGF.conditional_expected_number_of_purchases_up_to_time()`:
• BG/NBD 모델에서 제공하는 함수로, 특정 기간 동안 고객이 예상 구매할 거래 수를 계산합니다.
• 입력값:
• `1`: 예측 기간 (향후 1주).
• `cltv_df"frequency"`: 각 고객의 구매 빈도.
• `cltv_df"recency"`: 고객의 마지막 거래가 첫 거래 이후 얼마나 지났는지 (주 단위).
• `cltv_df"T"`: 고객의 첫 거래 이후 현재까지 경과한 시간 (주 단위).
• 출력값:
• 각 고객의 예상 거래 횟수로 구성된 Pandas Series.
2. 정렬 및 상위 10명 추출
>>> .sort_values(ascending=False).head(10)
3. 데이터프레임 변환
>>> .to_frame("Expected Number of Transactions").reset_index()
- Top 10 향후 1달 동안 고객 예상 구매 거래 수
# Top 10 Expected Number of Transaction (1 month)
BGF.conditional_expected_number_of_purchases_up_to_time(
4, cltv_df["frequency"], cltv_df["recency"], cltv_df["T"]
).sort_values(ascending=False).head(10).to_frame(
"Expected Number of Transactions"
).reset_index()
- 반복 거래 빈도 분석 시각화
from lifetimes.plotting import plot_period_transactions
import matplotlib.pyplot as plt
# Plot the actual and predicted transactions for the specified period
plot_period_transactions(BGF, max_frequency=7)
plt.show()
1. `plot_period_transactions` 함수
• `plot_period_transactions(BGF, max_frequency=7)`:
• 이 함수는 BG/NBD 모델이 예측한 거래 빈도와 실제 데이터에서 관측된 거래 빈도를 비교하는 히스토그램을 생성합니다.
• 입력값:
• `BGF`: 학습된 Beta-Geometric/NBD 모델 객체.
• `max_frequency=7`: 플롯에 표시할 최대 거래 빈도. 여기서는 최대 7번의 반복 거래를 대상으로 분석합니다.
• 출력:
• 히스토그램:
• x축: 고객의 반복 거래 횟수(빈도).
• y축: 실제 데이터와 모델이 예측한 고객 수.
2. `plt.show()`
• Matplotlib의 플롯을 화면에 표시합니다.
결과 해석
• 이 플롯은 모델의 성능을 평가하는 데 사용됩니다. 주요 요소는 다음과 같습니다:
1. 파란 막대: 실제 데이터에서 관측된 각 거래 빈도의 고객 수.
2. 검은 선: BG/NBD 모델이 예측한 각 거래 빈도의 고객 수.
해석 포인트
• 파란 막대와 검은 선이 잘 일치하면, 모델이 실제 데이터를 잘 설명하고 있다는 것을 의미합니다.
• 불일치가 크다면, 모델의 성능이 부족하거나 데이터의 특성을 제대로 반영하지 못했음을 나타냅니다.