本案例将通过一个电商用户交易数据集,以用户的实际购买行为数据作为基础,进行用户群体的划分,再基于不同分类信息,分解成不同群体针对运营,从而使企业能更有效的获取客户、使客户更加满意、留住客户成为高价值客户、避免客户流失。
一、数据介绍
数据集详细描述:数据形状为:542k 行x 8列,8个字段分别为订单号,订单日期,商品ID,商品描述,数量,单价,客户ID,城市。InvoiceNo: 订单号,每笔交易分配唯一的6位整数,而退货订单的代码以字母'c'开头。 StockCode: 商品ID,每个不同的产品分配唯一的5位整数。 Description: 商品描述,对每件产品的简略描述。 Quantity: 商品数量,每笔交易的每件产品的数量。 InvoiceDate: 订单日期和时间,每笔交易发生的日期和时间。 UnitPrice: 单价,单位产品价格。 CustomerID:客户ID,每个客户分配唯一的5位整数。 Country: 城市,每个客户所在国家/地区的名称。 接下来将通过下面步骤来分析该数据集:
提出问题->清洗数据->构建模型->提出建议
二、提出问题
首先介绍一下什么是RFM模型:RFM模型是以用户的实际购买行为数据,将用户群体进行分类,其中
R(Recency): 表示客户最近一次购买的时间距离现在有多远
F(Frequency): 表示客户在定义时间段内够奶产品或服务的次数
M(Monetary): 表示客户在定义时间段内购买产品或服务的金额
然后再更具R、F、M指标进行客户的细致分类: 包括重要价值客户、重要发展客户、重要保持客户、重要挽留客户、一般价值客户、一般发展客户、一般保持客户、一般挽留客户等八类用户。 再根据模型提出以下问题: 1、谁是你最好的客户 2、有哪些客户在流逝的边缘 3、有哪些客户能转化能为公司创造更多的价值 4、你必须保留哪些客户 5、谁是你的忠实客户 6、哪些客户有最大的转化率和可能性
三、清洗数据
1、数据导入
import pandas as pd
import datetime
import math
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
sns.set(style="ticks",color_codes=True,font_scale=1.5)
color=sns.color_palette()
sns.set_style('darkgrid')
from sklearn import preprocessing
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_samples,silhouette_score
df_initial=pd.read_csv('data.csv')
df_eda = df_initial
df_eda.head()
2、数据类型转化
#检查data type 和空值
df_eda.info()
# 进行InvoiceDate 日期类型转化
print('Dataframe dimensions:', df_eda.shape)
df_eda['InvoiceDate'] = pd.to_datetime(df_eda['InvoiceDate'])
print('Dataframe dimensions:', df_eda.shape)
df_eda['InvoiceDate'] = pd.to_datetime(df_eda['InvoiceDate'])
# 提供有关列类型和空值数目的信息
tab_info=pd.DataFrame(df_eda.dtypes).T.rename(index={0:'column type'})
tab_info=tab_info.append(pd.DataFrame(df_eda.isnull().sum()).T.rename(index={0:'null values (nb)'}))
tab_info=tab_info.append(pd.DataFrame(df_eda.isnull().sum()/df_eda.shape[0]*100).T.
rename(index={0:'null values (%)'}))
display(tab_info)
display(df_eda[:5])
df_eda['amount'] = df_eda['UnitPrice']*df_eda['Quantity']
# 检查数据集是否有重复行
print(df_initial.shape)
print(df_eda.duplicated().count())
def dp(df):
for i in df.columns:
duplicated_count = df.duplicated().count()
print(i,":",duplicated_count)
dp(df_eda)
def rstr(df, pred=None):
obs = df.shape[0]
types = df.dtypes
counts = df.apply(lambda x: x.count())
uniques = df.apply(lambda x: [x.unique()])
nulls = df.apply(lambda x: x.isnull().sum())
distincts = df.apply(lambda x: x.unique().shape[0])
missing_ratio = (df.isnull().sum()/ obs) * 100
skewness = df.skew()
kurtosis = df.kurt()
print('Data shape:', df.shape)
cols = ['types', 'counts', 'distincts', 'nulls', 'missing ratio', 'uniques', 'skewness', 'kurtosis']
str = pd.concat([types, counts, distincts, nulls, missing_ratio, uniques, skewness, kurtosis], axis = 1, sort=True)
str.columns =cols
return str
details = rstr(df_eda)
display(details.sort_values(by='missing ratio', ascending=False))
4、异常值处理
#检查df_eda.describe()发现销售数量和单价有负值
# /*异常值处理*/
print('购买数量和单价小于都小于等于0的有多少:',df_eda[(df_eda.Quantity<=0) & (df_eda.UnitPrice<=0)].shape[0],
',这些用户是谁? ',df_eda.loc[(df_eda.Quantity<=0) & (df_eda.UnitPrice<=0), ['CustomerID']].CustomerID.unique())
print('购买数量小于等于0的:',df_eda[(df_eda.Quantity<=0)].shape[0],
',这些用户是谁? ',df_eda.loc[(df_eda.Quantity<=0), ['CustomerID']].CustomerID.unique(),
',比例是多少: {:3.2%}'.format(df_eda[(df_eda.Quantity<=0)].shape[0]/df_eda.shape[0]))
print('有用户ID且购买数量小于0的,发票抬头:',
df_eda.loc[(df_eda.Quantity<=0) & ~(df_eda.CustomerID.isnull()), 'InvoiceNo'].apply(lambda x: x[0]).unique())
购买数量和单价小于都小于等于0的有多少: 1336 ,这些用户是谁? [nan] 购买数量小于等于0的: 10624 ,这些用户是谁? [14527. 15311. 17548. ... 12985. 15951. 16446.] ,比例是多少: 1.96% 有用户ID且购买数量小于0的,发票抬头: ['C']
# 放弃数量和单价为负的
df_eda = df_eda[~(df_eda.Quantity<0)]
df_eda = df_eda[df_eda.UnitPrice>0]
# 考虑到数量为负的其实是取消的订单,应该将相应的下单记录也同时删除。否则计算总购买金额时会有误差。
5、缺失值处理
# 删除没有用户ID的
df_eda = df_eda[~(df_eda.CustomerID.isnull())]
# 再次检查是否有异常数据
details = rstr(df_eda)
display(details.sort_values(by='distincts', ascending=False))
四、数据探索(EDA)
数据清洗完以后,通过Tableau对在线零售的数据进行进一步的探索分析。
# 将清洗后的数据导出,并导入Tableau
df_eda.to_csv('电商用户.csv')
五、通过Pyhton构建复杂RFM模型
df_eda.duplicated().sum()
df1 = df_eda
df1.drop_duplicates(inplace=True)
df1.head(1)
Recency
refrence_date = df1.InvoiceDate.max() + datetime.timedelta(days = 1)
print('Reference Date:', refrence_date)
df1['days_since_last_purchase'] = (refrence_date - df1.InvoiceDate).astype('timedelta64[D]')
customer_history_df = df1[['CustomerID', 'days_since_last_purchase']].groupby("CustomerID").min().reset_index()
customer_history_df.rename(columns={'days_since_last_purchase':'recency'}, inplace=True)
customer_history_df.describe().transpose()
Frequency
customer_freq = (df1[['CustomerID', 'InvoiceNo']].groupby(["CustomerID", 'InvoiceNo']).count().reset_index()).\
groupby(["CustomerID"]).count().reset_index()
customer_freq.rename(columns={'InvoiceNo':'frequency'},inplace=True)
customer_history_df = customer_history_df.merge(customer_freq)
customer_history_df.head(1)
Monetary
customer_monetary_val = df1[['CustomerID', 'amount']].groupby("CustomerID").sum().reset_index()
customer_history_df = customer_history_df.merge(customer_monetary_val)
customer_history_df.rename(columns={'amount':'monetary'}, inplace=True)
rfmTable = customer_history_df
rfmTable.head()
注释:CustomerID 12346的粘性(最近一次交易距离)为326天,忠诚度(累计单数)为1单,收入(累计交易金额)为77183.6美元; CustomerID 12347的粘性(最近一次交易距离)为2天,忠诚度(累计单数)为182单,收入(累计交易金额)为4310.0美元。
# 12346客户的详细信息
first_customer = df1[df1['CustomerID']==12346]
first_customer
可以看出,第一位客户只够买了一次,但是购买这件产品的数量很高,单价很低,有可能是清仓大甩买
RFM打分
# 手动利用四分位数将指标划分,便于理解和解释
quantiles = rfmTable.quantile(q=[0.25,0.5,0.75])
quantiles = quantiles.to_dict()
# 创建一个新DF,叫'segmented_rfm'
segmented_rfm = rfmTable
quantiles
# 我们最想要的客户是:粘性高,忠诚度和收入高的用户
# 一般分成 3~5 段,这里我们分为4段
def RScore(x,p,d):
if x <= d[p][0.25]:
return 1
elif x <= d[p][0.50]:
return 2
elif x <= d[p][0.75]:
return 3
else:
return 4
def FMScore(x,p,d):
if x <= d[p][0.25]:
return 4
elif x <= d[p][0.50]:
return 3
elif x <= d[p][0.75]:
return 2
else:
return 1
segmented_rfm['r_quartile'] = segmented_rfm['recency'].apply(RScore, args=('recency',quantiles,))
segmented_rfm['f_quartile'] = segmented_rfm['frequency'].apply(FMScore, args=('frequency',quantiles,))
segmented_rfm['m_quartile'] = segmented_rfm['monetary'].apply(FMScore, args=('monetary',quantiles,))
segmented_rfm.head()
# R F M
#RFM score = r*100+f*10+m, 111 为rfm score的最高分
segmented_rfm['RFMScore'] = segmented_rfm.r_quartile*100 + segmented_rfm.f_quartile*10 +segmented_rfm.m_quartile
segmented_rfm.head(5)
# 很明显第一个顾客不是我们最想要的顾客,他是通过特定时间以便宜的单价买入很高的量
# 我们现在来选择出我们最好的10个用户
segmented_rfm[segmented_rfm['RFMScore']==111].sort_values('monetary',ascending=False).head(10)
print(segmented_rfm['RFMScore'].value_counts().count())
print(segmented_rfm['RFMScore'].max())
print(segmented_rfm['RFMScore'].min())
基于此做打分分组,制定不同的策略,比如
0 - 122, 最有价值客户,价格不会是很敏感,所以主要推广忠实项目和新品 122 - 223,快要逐渐失去的客户,email或者渠道推广一下 223 - 333, 最近没怎么购买的有价值客户,需要进一步激活,给一下打折,做一波email推广 或者可以分得更细致,主要看运营能力
segmented_rfm['RFMScore'] = segmented_rfm['RFMScore'].astype(int)
def rfm_level(RFMScore):
if (RFMScore >= 0 and RFMScore < 122):
return '1'
elif (RFMScore >= 122 and RFMScore < 223):
return '2'
elif (RFMScore >= 223 and RFMScore < 333):
return '3'
return '4'
segmented_rfm['RFMScore_level'] = segmented_rfm['RFMScore'].apply(rfm_level).astype(str)
segmented_rfm.head()
# 不同rfm 分数的分布
import seaborn as sns
color = sns.color_palette()
from pylab import mpl
mpl.rcParams['font.sans-serif'] = ['SimHei']
plt.figure(figsize=(6,8))
sns.countplot(x='RFMScore_level',data=segmented_rfm, color = color[9])
plt.ylabel('计数',fontsize=12)
plt.xlabel('RFM得分水平', fontsize=12)
plt.xticks(rotation='vertical')
plt.title('RFM得分水平分布',fontsize=15)
plt.show()
基于机器模型来分类RFM指标
K-means Clustering
#数据一览
customer_history_df.head()
customer_history_df.describe()
#归一化处理
from sklearn import preprocessing
segmented_rfm['recency_log'] = customer_history_df['recency'].apply(math.log)
customer_history_df['frequency_log'] = customer_history_df['frequency'].apply(math.log)
customer_history_df['monetary_log'] = customer_history_df['monetary'].apply(math.log)
feature_vector = ['monetary_log', 'recency_log','frequency_log']
X_subset = customer_history_df[feature_vector] #.as_matrix()
print(X_subset.head(5))
scaler = preprocessing.StandardScaler().fit(X_subset)
X_scaled = scaler.transform(X_subset)
pd.DataFrame(X_scaled, columns=X_subset.columns).describe().T
cl = 50
corte = 0.1
anterior = 100000000000000
cost = []
K_best = cl
for k in range (1, cl+1):
#使用k集群在我们的数据上创建一个kmeans模型。random_state有助于确保算法每次返回相同的结果。
model = KMeans(
n_clusters=k,
init='k-means++', #'random',
n_init=10,
max_iter=300,
tol=1e-04,
random_state=101)
model = model.fit(X_scaled)
# 聚类标签
labels = model.labels_
# interia 为簇中某一点到簇中距离的和,手肘法评估指标
interia = model.inertia_
print('K值及误差:',k,':',interia)
print('聚类标签:',labels)
#print(('anterior - interia)/anterior:',(anterior - interia)/anterior))
if (K_best == cl) and (((anterior - interia)/anterior) < corte): K_best = k - 1
cost.append(interia)
anterior = interia
print('簇中心:',model.cluster_centers_)
print(K_best)
plt.figure(figsize=(8, 6))
plt.scatter(range (1, cl+1), cost, c='red')
plt.show()
# 用最好的k值创建kmeans模型
print('The best K sugest: ',K_best)
model = KMeans(n_clusters=K_best, init='k-means++', n_init=10,max_iter=300, tol=1e-04, random_state=101)
# 使用缩放使数据进行缩放能达到比较好的聚类效果
model = model.fit(X_scaled)
# 这些是我们聚类后的标签
labels = model.labels_
# 聚类效果可视化
#plt.scatter(X_scaled[:,0], X_scaled[:,1], c=model.labels_.astype(float))
fig = plt.figure(figsize=(20,5))
ax = fig.add_subplot(121)
plt.scatter(x = X_scaled[:,1], y = X_scaled[:,0],c=model.labels_.astype(float))
ax.set_xlabel(feature_vector[1])
ax.set_ylabel(feature_vector[0])
ax = fig.add_subplot(122)
plt.scatter(x = X_scaled[:,2], y = X_scaled[:,0], c=model.labels_.astype(float))
ax.set_xlabel(feature_vector[2])
ax.set_ylabel(feature_vector[0])
plt.show()
from mpl_toolkits.mplot3d import Axes3D
# 3D plot
fig = plt.figure(figsize=(15, 10))
ax = fig.add_subplot(111, projection='3d')
xs = X_scaled[:,1]
ys = X_scaled[:,2]
zs = X_scaled[:,0]
ax.scatter(xs, ys, zs, s=5,c=model.labels_.astype(float) )
ax.set_xlabel('Recency_log')
ax.set_ylabel('Frequency_log')
ax.set_zlabel('Monetary_log')
plt.show()
# 保存聚类结果
customer_history_df['labels'] =labels
customer_history_df.to_csv('聚类结果.csv')
customer_history_df.head()