1 业务背景
数据集来源于:kaggle数据集(链接),该数据集记录了某全球超市四年的销售数据,通过分析该超市四年内的销售数据,从不同角度出发,分析经营现状,发掘提高销量的销售策略,利用数据找到新的增长点,并提出建议。
1.1 分析思路及指标
1. 数据总览 (Data Describe)
2. 数据预处理 (Data Preprocessing)
- 数据类型转换
- 空值、缺失值、异常值处理
3. 数据分析 (Data Analysis)
4. 模型构建 (Bulid Model)
- RFM 用户价值模型
- Kmeans 机器学习模型
5. 模型评估 (Evaluate)
- TSNE降维可视化
6. 结论建议 (Conclusion)
2 数据加载与清洗
2.1 载入数据分析库及数据
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warningssns.set(style='white')
sns.set_context('notebook', font_scale=1.5, rc={'lines.linewidth':2.5,'figure.figsize':(17, 10)})
large = 22; med = 16; small = 12
params = {'axes.titlesize': large,'legend.fontsize': med,'figure.figsize': (16, 10),'axes.labelsize': med,'xtick.labelsize': med,'ytick.labelsize': med,'figure.titlesize': large}
plt.rcParams.update(params)
plt.rcParams['figure.figsize'] = (17, 10)
plt.rcParams["font.family"] = 'SimHei'
plt.rcParams["axes.unicode_minus"] = False
warnings.filterwarnings("ignore")
df = pd.read_csv('./superstore_dataset2011-2015.csv',encoding='ISO-8859-1')
df.head().append(df.tail())
Row ID | Order ID | Order Date | Ship Date | Ship Mode | Customer ID | Customer Name | Segment | City | State | ... | Product ID | Category | Sub-Category | Product Name | Sales | Quantity | Discount | Profit | Shipping Cost | Order Priority | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 42433 | AG-2011-2040 | 1/1/2011 | 6/1/2011 | Standard Class | TB-11280 | Toby Braunhardt | Consumer | Constantine | Constantine | ... | OFF-TEN-10000025 | Office Supplies | Storage | Tenex Lockers, Blue | 408.300 | 2 | 0.0 | 106.1400 | 35.46 | Medium |
1 | 22253 | IN-2011-47883 | 1/1/2011 | 8/1/2011 | Standard Class | JH-15985 | Joseph Holt | Consumer | Wagga Wagga | New South Wales | ... | OFF-SU-10000618 | Office Supplies | Supplies | Acme Trimmer, High Speed | 120.366 | 3 | 0.1 | 36.0360 | 9.72 | Medium |
2 | 48883 | HU-2011-1220 | 1/1/2011 | 5/1/2011 | Second Class | AT-735 | Annie Thurman | Consumer | Budapest | Budapest | ... | OFF-TEN-10001585 | Office Supplies | Storage | Tenex Box, Single Width | 66.120 | 4 | 0.0 | 29.6400 | 8.17 | High |
3 | 11731 | IT-2011-3647632 | 1/1/2011 | 5/1/2011 | Second Class | EM-14140 | Eugene Moren | Home Office | Stockholm | Stockholm | ... | OFF-PA-10001492 | Office Supplies | Paper | Enermax Note Cards, Premium | 44.865 | 3 | 0.5 | -26.0550 | 4.82 | High |
4 | 22255 | IN-2011-47883 | 1/1/2011 | 8/1/2011 | Standard Class | JH-15985 | Joseph Holt | Consumer | Wagga Wagga | New South Wales | ... | FUR-FU-10003447 | Furniture | Furnishings | Eldon Light Bulb, Duo Pack | 113.670 | 5 | 0.1 | 37.7700 | 4.70 | Medium |
51285 | 32593 | CA-2014-115427 | 31-12-2014 | 4/1/2015 | Standard Class | EB-13975 | Erica Bern | Corporate | Fairfield | California | ... | OFF-BI-10002103 | Office Supplies | Binders | Cardinal Slant-D Ring Binder, Heavy Gauge Vinyl | 13.904 | 2 | 0.2 | 4.5188 | 0.89 | Medium |
51286 | 47594 | MO-2014-2560 | 31-12-2014 | 5/1/2015 | Standard Class | LP-7095 | Liz Preis | Consumer | Agadir | Souss-Massa-Draâ | ... | OFF-WIL-10001069 | Office Supplies | Binders | Wilson Jones Hole Reinforcements, Clear | 3.990 | 1 | 0.0 | 0.4200 | 0.49 | Medium |
51287 | 8857 | MX-2014-110527 | 31-12-2014 | 2/1/2015 | Second Class | CM-12190 | Charlotte Melton | Consumer | Managua | Managua | ... | OFF-LA-10004182 | Office Supplies | Labels | Hon Color Coded Labels, 5000 Label Set | 26.400 | 3 | 0.0 | 12.3600 | 0.35 | Medium |
51288 | 6852 | MX-2014-114783 | 31-12-2014 | 6/1/2015 | Standard Class | TD-20995 | Tamara Dahlen | Consumer | Juárez | Chihuahua | ... | OFF-LA-10000413 | Office Supplies | Labels | Hon Legal Exhibit Labels, Alphabetical | 7.120 | 1 | 0.0 | 0.5600 | 0.20 | Medium |
51289 | 36388 | CA-2014-156720 | 31-12-2014 | 4/1/2015 | Standard Class | JM-15580 | Jill Matthias | Consumer | Loveland | Colorado | ... | OFF-FA-10003472 | Office Supplies | Fasteners | Bagged Rubber Bands | 3.024 | 3 | 0.2 | -0.6048 | 0.17 | Medium |
10 rows × 24 columns
2.2 总览数据
数据维度有点多,显示不全,先转置看看各个维度的数据大概长什么样
df[-5:].T
51285 | 51286 | 51287 | 51288 | 51289 | |
---|---|---|---|---|---|
Row ID | 32593 | 47594 | 8857 | 6852 | 36388 |
Order ID | CA-2014-115427 | MO-2014-2560 | MX-2014-110527 | MX-2014-114783 | CA-2014-156720 |
Order Date | 31-12-2014 | 31-12-2014 | 31-12-2014 | 31-12-2014 | 31-12-2014 |
Ship Date | 4/1/2015 | 5/1/2015 | 2/1/2015 | 6/1/2015 | 4/1/2015 |
Ship Mode | Standard Class | Standard Class | Second Class | Standard Class | Standard Class |
Customer ID | EB-13975 | LP-7095 | CM-12190 | TD-20995 | JM-15580 |
Customer Name | Erica Bern | Liz Preis | Charlotte Melton | Tamara Dahlen | Jill Matthias |
Segment | Corporate | Consumer | Consumer | Consumer | Consumer |
City | Fairfield | Agadir | Managua | Juárez | Loveland |
State | California | Souss-Massa-Draâ | Managua | Chihuahua | Colorado |
Country | United States | Morocco | Nicaragua | Mexico | United States |
Postal Code | 94533 | NaN | NaN | NaN | 80538 |
Market | US | Africa | LATAM | LATAM | US |
Region | West | Africa | Central | North | West |
Product ID | OFF-BI-10002103 | OFF-WIL-10001069 | OFF-LA-10004182 | OFF-LA-10000413 | OFF-FA-10003472 |
Category | Office Supplies | Office Supplies | Office Supplies | Office Supplies | Office Supplies |
Sub-Category | Binders | Binders | Labels | Labels | Fasteners |
Product Name | Cardinal Slant-D Ring Binder, Heavy Gauge Vinyl | Wilson Jones Hole Reinforcements, Clear | Hon Color Coded Labels, 5000 Label Set | Hon Legal Exhibit Labels, Alphabetical | Bagged Rubber Bands |
Sales | 13.904 | 3.99 | 26.4 | 7.12 | 3.024 |
Quantity | 2 | 1 | 3 | 1 | 3 |
Discount | 0.2 | 0 | 0 | 0 | 0.2 |
Profit | 4.5188 | 0.42 | 12.36 | 0.56 | -0.6048 |
Shipping Cost | 0.89 | 0.49 | 0.35 | 0.2 | 0.17 |
Order Priority | Medium | Medium | Medium | Medium | Medium |
df.columns
Index(['Row ID', 'Order ID', 'Order Date', 'Ship Date', 'Ship Mode','Customer ID', 'Customer Name', 'Segment', 'City', 'State', 'Country','Postal Code', 'Market', 'Region', 'Product ID', 'Category','Sub-Category', 'Product Name', 'Sales', 'Quantity', 'Discount','Profit', 'Shipping Cost', 'Order Priority'],dtype='object')
- 各维度分别为:行编号、订单编号、订购日期、发货日期、运送方式、客户ID、客户姓名、客户类型、客户城市、客户所在州、客户国家、邮编、店铺所在区域、店铺所属州、产品ID、类别、子类别、产品名称、销售额、销售量、折扣、利润、运输费、订单优先级 共24个维度
- 行编号无用,直接删除
del df['Row ID']
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 51290 entries, 0 to 51289
Data columns (total 23 columns):# Column Non-Null Count Dtype
--- ------ -------------- ----- 0 Order ID 51290 non-null object 1 Order Date 51290 non-null object 2 Ship Date 51290 non-null object 3 Ship Mode 51290 non-null object 4 Customer ID 51290 non-null object 5 Customer Name 51290 non-null object 6 Segment 51290 non-null object 7 City 51290 non-null object 8 State 51290 non-null object 9 Country 51290 non-null object 10 Postal Code 9994 non-null float6411 Market 51290 non-null object 12 Region 51290 non-null object 13 Product ID 51290 non-null object 14 Category 51290 non-null object 15 Sub-Category 51290 non-null object 16 Product Name 51290 non-null object 17 Sales 51290 non-null float6418 Quantity 51290 non-null int64 19 Discount 51290 non-null float6420 Profit 51290 non-null float6421 Shipping Cost 51290 non-null float6422 Order Priority 51290 non-null object
dtypes: float64(5), int64(1), object(17)
memory usage: 9.0+ MB
2.3 查询、处理空值
print('邮编缺失数据占比: {:.2%}'.format(df['Postal Code'].isnull().sum() / len(df)))
邮编缺失数据占比: 80.51%
邮编一栏缺失数据超过80%,又因为邮编对数据几乎无影响,采取措施为直接删除此列
del df['Postal Code']
2.4 查询重复记录
df.duplicated().sum()
0
无重复记录
2.5 更改数据类型,并创建时间维度字段
# 将订单日期更改为日期格式
df['Order Date']=pd.to_datetime(df['Order Date'])# 添加年和月的列,便于后续进行时间维度的分析
df['year'] = df['Order Date'].dt.year
df['month'] = df['Order Date'].dt.month
3 经营分析
3.1 销售额分析
# 创建销售额透视表
sales = pd.pivot_table(df, values='Sales', index='month', columns='year',aggfunc=[np.sum])
sales.columns = ['2011年','2012年','2013年','2014年']
sales.index=['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']
sales
2011年 | 2012年 | 2013年 | 2014年 | |
---|---|---|---|---|
1月 | 138241.30042 | 162800.89338 | 206459.19582 | 268265.52240 |
2月 | 134969.94086 | 152661.15144 | 191062.77216 | 244159.30486 |
3月 | 171455.59372 | 201608.72784 | 230547.79468 | 347720.96868 |
4月 | 128833.47034 | 187469.96192 | 233181.34844 | 302132.54000 |
5月 | 148146.72092 | 218960.16042 | 304509.96336 | 304798.83604 |
6月 | 189338.43966 | 249289.77172 | 341162.34370 | 372577.23298 |
7月 | 162034.69756 | 174394.02808 | 223642.65664 | 278672.17326 |
8月 | 219223.49524 | 271669.66086 | 323876.60716 | 432731.04194 |
9月 | 255237.89698 | 256567.85308 | 326897.27044 | 405436.70584 |
10月 | 204675.07846 | 239321.09904 | 270121.87570 | 406659.41500 |
11月 | 214934.29386 | 270723.05356 | 383039.21248 | 508954.73156 |
12月 | 292359.96752 | 291972.33306 | 371245.40880 | 427757.39800 |
3.1.1 时间维度销售额
3.1.1.1 年度销售额及增长率
rise_12 = (sales.sum()[1]-sales.sum()[0])/sales.sum()[0]
rise_13 = (sales.sum()[2]-sales.sum()[1])/sales.sum()[1]
rise_14 = (sales.sum()[3]-sales.sum()[2])/sales.sum()[2]
rise_rate = [0, rise_12, rise_13, rise_14]sales_sum=pd.DataFrame({'Sales_sum':sales.sum()})
sales_sum['rise_rate'] = rise_rate
sales_sum.index=pd.Series(['2011年','2012年','2013年','2014年'])
sales_sum
Sales_sum | rise_rate | |
---|---|---|
2011年 | 2.259451e+06 | 0.000000 |
2012年 | 2.677439e+06 | 0.184995 |
2013年 | 3.405746e+06 | 0.272017 |
2014年 | 4.299866e+06 | 0.262533 |
x1 = x2 = sales_sum.index
y1 = sales_sum.Sales_sum
y2 = sales_sum.rise_rate# 建立左侧纵坐标画板
fig, ax1 = plt.subplots()
# 画柱状图
bar = ax1.bar(x1, y1, alpha=0.8, label = '销售额')
# 显示左侧纵坐标
ax1.set_ylabel('销售额', fontsize=20)
plt.yticks(range(0,5500000,500000),('0','50W','100W','150W','200W','250W','300W','350W','400W','450W'))
ax1.legend(bar, ("销售额",), loc = [0.05, 0.9])
plt.tick_params(labelsize=14)# 建立右侧坐标画板
ax2 = ax1.twinx()
# 画折线图
line = ax2.plot(x2, y2, marker='o' ,c='y')
# 折线图显示标识
for a, b in zip(x2, y2): ax2.text(a, b, "%.2f" % (100 * b) + '%', ha='center', fontsize=20)
ax2.set_ylabel('增长率', fontsize=20, rotation=270)
ax2.legend(line, ('增长率',), loc = [0.05, 0.85])
plt.tick_params(labelsize=14)
ax2.set_title("年度销售额及其增长率", fontsize=22);
从年度销售额及其增长率来看:
- 该超市2011年销售额只有226万,2014年销售额达到了430万,GMV提升非常快,从行业发展阶段来看,该超市应该处于导入期或成长期
- 该超市主营办公用品,据了解,全球领先的办公用品供应商包括:Costco、Office Depot、Staples、Tesco和沃尔玛,查询沃尔玛2014年销售额得知,沃尔玛2014年销售额为4731亿美元,可见与全球大型超市对比,差距大,可提升空间较大
- 从增长率来看,增长率非常高,12-13年、13-14年GMV增长率接近均超过25%,而沃尔玛2013-2014的增长率大概为3%,可见该超市潜力非常大
- 了解了超市整体销售额后,再对每年每月的销售额进行分析,了解不同月份的销售情况,找出是否有淡旺季之分,找出重点销售月份,以便制定经营策略与业绩的月度及季度指标拆分
3.1.1.2 月度销售额
sales.plot.area(colormap = 'tab20c', stacked=False)
plt.title('月度销售额')
plt.ylabel('销售额')
plt.xlabel('月份');
通过不同年份月度销售额我们可以看出:
- 该超市2011年-2014年每一年的销售额同比上一年都是上升趋势,销售季节性明显,总体上半年是淡季,下半年是旺季
- 上半年中6月份销售额比较高,下半年中7月份的销售额偏低。对于旺季的月份,运营推广等策略要继续维持,还可以加大投入,提高整体销售额;对于淡季的月份,可以结合产品特点进行新产品拓展,举办一些促销活动等吸引客户
3.2 利润分析
# 创建利润透视表
profit = pd.pivot_table(df, values='Profit', index='month', columns='year',aggfunc=[np.sum])
profit.columns = ['2011年','2012年','2013年','2014年']
profit.index=['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']
profit
2011年 | 2012年 | 2013年 | 2014年 | |
---|---|---|---|---|
1月 | 13457.23302 | 19627.42058 | 26052.40252 | 31447.74660 |
2月 | 17588.83726 | 17828.18244 | 31553.99756 | 29454.01876 |
3月 | 16169.36062 | 22752.87684 | 34873.71938 | 50097.04458 |
4月 | 13405.46924 | 20804.00532 | 26008.05144 | 35709.53320 |
5月 | 14777.45792 | 22865.39572 | 39053.30946 | 34335.60404 |
6月 | 25932.87796 | 34358.03962 | 43298.85000 | 40869.02108 |
7月 | 10631.84406 | 21725.18808 | 27019.91294 | 26450.70856 |
8月 | 19650.67124 | 36511.44996 | 32977.75576 | 46443.47934 |
9月 | 32313.25458 | 25039.93498 | 18850.09084 | 52533.83284 |
10月 | 30745.54166 | 27773.87454 | 27871.97470 | 52342.49740 |
11月 | 21261.40536 | 26160.60686 | 51720.88568 | 55561.72406 |
12月 | 33006.85862 | 31968.30416 | 47654.27990 | 48920.76000 |
3.2.1 时间维度利润情况
3.2.1.1 年度利润、增长率、利润率
# 计算利润增长率
rise_12 = (profit.sum()[1] - profit.sum()[0]) / profit.sum()[0]
rise_13 = (profit.sum()[2] - profit.sum()[1]) / profit.sum()[1]
rise_14 = (profit.sum()[3] - profit.sum()[2]) / profit.sum()[2]
rise_rate = [0, rise_12, rise_13, rise_14]# 计算每年的利润率
rate_11 = profit.sum()[0] / sales.sum()[0]
rate_12 = profit.sum()[1] / sales.sum()[1]
rate_13 = profit.sum()[2] / sales.sum()[2]
rate_14 = profit.sum()[3] / sales.sum()[3]
profit_rate = [rate_11, rate_12, rate_13, rate_14]profit_sum = pd.DataFrame({'Profit_sum': profit.sum()})
profit_sum['rise_rate'] = rise_rate
profit_sum['profit_rate'] = profit_rate
profit_sum.index = pd.Series(['2011年', '2012年', '2013年', '2014年'])
profit_sum
Profit_sum | rise_rate | profit_rate | |
---|---|---|---|
2011年 | 248940.81154 | 0.000000 | 0.110178 |
2012年 | 307415.27910 | 0.234893 | 0.114817 |
2013年 | 406935.23018 | 0.323731 | 0.119485 |
2014年 | 504165.97046 | 0.238934 | 0.117252 |
# 建立左侧纵坐标画板
fig, ax1 = plt.subplots()
# 画堆叠图
s = plt.bar(sales_sum.index,sales_sum.Sales_sum, alpha=0.5, label='销售额')
p = plt.bar(profit_sum.index, profit_sum.Profit_sum,color='b' ,alpha=0.8, label='利润')
# 显示左侧纵坐标
ax1.set_ylabel('销售额', fontsize=20)
plt.yticks(range(0,5500000,500000),('0','50W','100W','150W','200W','250W','300W','350W','400W','450W'))
# 显示文字
for x1,y1 in zip(profit_sum.index, profit_sum.profit_rate):plt.text(x1, y1+20000, '%.2f%%'% (y1*100), ha='center', color='w',fontsize=20)
plt.legend()
# 建立右侧坐标画板
ax2 = ax1.twinx()
# # 画折线图
line = ax2.plot(profit_sum.index, profit_sum.rise_rate, marker='o' ,c='y',ms=10)
# # 折线图显示标识
for a, b in zip(profit_sum.index, profit_sum.rise_rate): ax2.text(a, b, "%.2f" % (100 * b) + '%', ha='center', fontsize=20)
ax2.set_ylabel('利润增长率', fontsize=20, rotation=270)
ax2.set_title("年度利润占比及其增长率", fontsize=22)
ax2.legend(line, ('利润增长率',), loc = [0.007, 0.89]);
从各年度利润及利润增长率可以看出:
- 利润是在逐年递增,利润率平稳位置在11%-12%之间,说明超市的经营策略比较完善,价格波动不大
- 除了13-14年利润增长率小于销售额增长率,其他时间利润增长率均超过销售额增长率,可见13-14季度存在一些让利策略来增加销售额
3.2.1.2 每月利润的同比增长率
rise_2=pd.DataFrame()
rise_2['rise_2012']=(profit['2012年']-profit['2011年']) / profit['2011年']
rise_2['rise_2013']=(profit['2013年']-profit['2012年']) / profit['2012年']
rise_2['rise_2014']=(profit['2014年']-profit['2013年']) / profit['2013年']
rise_2
rise_2012 | rise_2013 | rise_2014 | |
---|---|---|---|
1月 | 0.458503 | 0.327347 | 0.207096 |
2月 | 0.013608 | 0.769894 | -0.066552 |
3月 | 0.407160 | 0.532717 | 0.436527 |
4月 | 0.551904 | 0.250146 | 0.373018 |
5月 | 0.547316 | 0.707966 | -0.120802 |
6月 | 0.324883 | 0.260225 | -0.056118 |
7月 | 1.043407 | 0.243714 | -0.021066 |
8月 | 0.858026 | -0.096783 | 0.408327 |
9月 | -0.225088 | -0.247199 | 1.786927 |
10月 | -0.096654 | 0.003532 | 0.877962 |
11月 | 0.230427 | 0.977052 | 0.074261 |
12月 | -0.031465 | 0.490673 | 0.026576 |
plt.plot(rise_2.index, rise_2.rise_2012,linewidth=5, marker='o',ms=15,label='2012年同比增长')
plt.plot(rise_2.index, rise_2.rise_2013,linewidth=5, marker='o',ms=15,label='2013年同比增长')
plt.plot(rise_2.index, rise_2.rise_2014,linewidth=5, marker='o',ms=15,label='2014年同比增长')
plt.ylabel('增长率')
plt.title('每年不同月份利润同比增长率')
plt.legend(loc='upper left');
从每一年不同月份的利润同比增长率来看:
- 整体来看,利润同比波动较大,存在不少负增长的情况,说明该超市处于导入期和成长期的概率较大,注重用户体量,整体规模的扩大,并不是非常重视盈利情况,这与公司的发展阶段和战略布局有关,比如我国的京东,毛利率就不高,去掉运营费用,管理费用,履约费用等,利润率常年负数,但依然能够发展壮大
3.2.2 每单利润
def boxplot(x, y, **kwargs):sns.boxplot(x=x, y=y)m = pd.melt(df, id_vars=['Profit'], value_vars=['Market','Category'])
g = sns.FacetGrid(m, col='variable', col_wrap=2, sharex=False, sharey=False, size=5)
g = g.map(boxplot, 'value', 'Profit')
print('亏损单占比: {:.2%}'.format(df[df['Profit'] < 0].shape[0] / len(df)))
亏损单占比: 24.46%
从每单利润情况来看:
- 每单利润非常离散,盈利单与亏损单相差不大,可见该超市经常会进行促销活动
- Canada销售额不高,但几乎没有负利润的订单
- 亏损订单占了接近1/4,亏损严重,后续对商品维度和折扣情况进行分析,从而改善负利润订单情况
3.2.3 不同商品销售额与利润
# 创建不同商品销售额与利润情况
cg_s_p = df.groupby(['Category','Sub-Category'])['Sales','Profit'].sum()sortIndex = np.argsort(cg_s_p.Sales) # 倒序,返回排序后各数据的原始下标
x_sort = cg_s_p.index[sortIndex] # 重新进行排序,与y保持初始顺序一致
y_sort = cg_s_p.Sales[sortIndex] # 重新进行排序,倒序
y_sort2 = cg_s_p.Profit[sortIndex]rects1=plt.barh(np.arange(len(x_sort)), width=y_sort,height=0.35,alpha=0.8,label='销售额')
rects2=plt.barh(np.arange(len(x_sort)) - 0.35, width=y_sort2, height=0.35, label='利润',alpha=0.8)
plt.yticks(np.arange(len(x_sort)), x_sort, fontsize=14)
plt.legend()
plt.title('不同商品的销售额与利润',size=30)def add_labels(rects):for rect in rects:width = rect.get_width()plt.text(width+30000, rect.get_y(), '%d' % (width/1000) + 'K', ha='center',size=14)add_labels(rects1)
add_labels(rects2)
df[df['Sub-Category'].isin(['Phones','Copiers','Chairs','Bookcases','Storage','Appliances','Machines','Accessories','Tables'])].Sales.sum() / df.Sales.sum()
0.8390747077198337
从不同商品销售额与利润可以看出:
- furniture(家具类)中Bookcases(书柜)、Chairs(椅子)、Tables(桌子);Office supplies(办公用品)中Appliances(电器)、Storage(储藏箱);Technology(技术类)中Accessories(附件)、Copiers(复印机)、Machines(机器)、Phones(电话),是销售额最好的产品,这几类产品占了销售额总额的84%。这是该超市的主营产品类别,可以制定一些战略来提高我们超市在顾客心智中对这几类产品的认知,提高这几类商品就能想到我们超市,从而提高我们的品牌影响力
- 销售额较少的几类产品,inders(粘合剂)、Furnishings(家具)、Art(艺术品)、Paper(纸)、Supplies(供应品)、Envelopes(信封)、Fasteners(紧固件)、Labels(标签)均是办公用品中的小物件,可以探索顾客购买商品的连带关系,进行组合销售优化
- Tales(桌子)是唯一负利润的产品,出现此状况的原因可能是清理库存或者是市场竞争大,需要结合实际业务来进行分析,改善此商品结构及经营策略
3.2.4 地域维度销售额与利润对比
从最开始的数据集信息可以知道,这是一所全球超市,在全球7个地方有分店,那么再来看一下不同分店的销售额和利润占比,以便对不同分店采取对应的经营策略(这里为了方便对比展示,使用tableau仪表盘)
从以上图表可以看出
- 每家market总体每年销售额均处于上升趋势,其中APAC(亚太地区)、EU(欧盟)、US(美国)、LATAM(拉丁美洲)的market占到了总销售额的85%,总体也与地区的经济发展相匹配
- 在APAC地区和EU地区的增长速度比较快速,可以看出市场占有能力在不断增加,企业市场前景比较好,下一年可以适当加大运营成本,其他地区可以根据自身地区消费特点,借鉴上面APAC和EU地区的运营模式
- 通过销售额与利润占比来看,US、APAC、EU、Canada的利润情况较好,其余地区利润率较低,利润低的地区可能折扣让利幅度较大,后续分析地域维度对折扣的敏感度
3.3 效率指标
3.3.1 客单价
客单价是指商场(超市)每一个顾客平均购买商品的金额,客单价也即是平均交易金额。从某种程度上反映了企业的消费群体的许多特点以及企业的销售类目的盈利状态是否健康。
总消费次数:同一天内,同一个人发生的所有消费算作一次消费。
客单价=总消费金额 / 总消费次数
# 2011-2014年客单价
for i in range(2011,2015):data=df[df['year']==i]price=data[['Order Date','Customer ID','Sales']]# 计算总消费次数price_dr=price.drop_duplicates(subset=['Order Date', 'Customer ID'])# 总消费次数:有多少行total_num=price_dr.shape[0]print(f'{i}年总消费次数=',total_num)unit_price = price['Sales'].sum()/total_numprint(f'{i}年客单价=', unit_price,'\n')
2011年总消费次数= 4453
2011年客单价= 507.3997070604087 2012年总消费次数= 5392
2012年客单价= 496.55762136498515 2013年总消费次数= 6753
2013年客单价= 504.3308824788983 2014年总消费次数= 8696
2014年客单价= 494.4647965225392
- 每年的消费次数呈不断上升趋势,但是客单价总体浮动范围不是很大 ,稳定在500左右
- 如果想要提高客单件的,可以在促销时,赠送顾客一些满399才能使用的一些折扣券,让顾客多购买一些商品是常用的策略
- 提高客单价的另一个方式是增加单个订单内商品的数量,但考虑到该超市主要商品类型是办公用品类型的,商品消耗周期较长,可操作性不大,唯一可操的就是设置购买商品数量越多,折扣越大,从而降低商品的单价,增加顾客购买更多件商品的欲望
3.3.2 不同市场连带率分布
# 每条记录是一种类别的商品,但是探索发现,存在同一订单,购买了多个种类的商品,先通过订单号聚合,再计算订单中商品的总数
order_together = df.groupby(['Order ID']).Quantity.sum().reset_index().merge(df[['Order Date','Order ID','Market']]).drop_duplicates()
order_together.to_csv('order_together.csv') # 这里使用tableau展示
- 总体来看,EU地区的连带率最高,2014年连带率超过9件,APAC、LATAM、US地区表现较好,Afica、EMEA、Canada地区连带率差,均在5件左右
- 4年来各地区连带率波动较为平稳,表现突出的是EU和Canada,这两地连带率有增长趋势,其他地区可以借鉴这两地的运营策略,如使用组合推荐等方式来提高连带率,从而提高我们的整体销售额
3.4 促销活动分析
# 定义折扣等级,便于后续分析不同折扣带来的销售额和利润
bins=[-0.01,0.01,0.2,0.5,1]
df['discount_level'] = pd.cut(df.Discount, bins=bins, labels=['无折扣','低折扣','中折扣','高折扣'])# 创建不同折扣等级的销售额,利润DataFrame
dis_s_p = df.groupby('discount_level')['Sales','Profit'].sum()
dis_s_p
Sales | Profit | |
---|---|---|
discount_level | ||
无折扣 | 7.253807e+06 | 1.828672e+06 |
低折扣 | 3.458485e+06 | 4.534675e+05 |
中折扣 | 1.558586e+06 | -4.021004e+05 |
高折扣 | 3.716247e+05 | -4.125817e+05 |
3.4.1 不同折扣等级订单的销售额与利润
index = np.arange(dis_s_p.shape[0])
rects1=plt.bar(x=index, height=dis_s_p.Sales, width=0.35, label='销售额',alpha=0.8)
rects2=plt.bar(x=index + 0.35, height=dis_s_p.Profit, width=0.35, label='利润',alpha=0.8)
plt.xticks(index +0.175, dis_s_p.index)
plt.legend()
plt.title('不同折扣等级订单的销售额与利润')def add_labels(rects):for rect in rects:height = rect.get_height()plt.text(rect.get_x() + rect.get_width() / 2, height, '%d' % (height/10000) + 'W', ha='center', va='bottom',size=16)add_labels(rects1)
add_labels(rects2)
3.4.2 不同地区对折扣的敏感度
sns.barplot(x='Market', y='Sales', data=df, hue='discount_level',estimator=np.sum,alpha=0.8)
plt.legend(loc='upper right')
plt.title('不同市场不同折扣等级订单销售额');
从上面两张图可以看出:
- 经过计算,对于无折扣订单,利润大约是销售额的25%,低折扣订单利润大约是销售额的13%,而折扣力度在0.2-0.5之间的订单,已经是负利润了,超过0.5折扣比例的订单,几乎是销售额多少,就亏损多少
- 高折扣的订单可能是因为商品积压导致贬值,被迫亏本甩卖,应及时对商品结构进行诊断,对于难销售,易积压的商品,及时调整进货策略
- 看不同地区对于折扣的敏感度可以看出,优质地区为APAC、EU、US、LATAM,而在Canada我们没有促销活动,Africa和EMEA占比最大的是无折扣,可能因为商品的刚需所导致,而低折扣和中折扣几乎的订单几乎没有,只有高折扣的订单,才能吸引到他们下单,由于高折扣订单亏损严重,应及时进行战略调整
4 用户分析
4.1 用户质量
4.1.1 用户生命周期
用户生命周期(Life Time),也称作留存天数,从用户第一次购买,到最后一次购买的天数
# 计算用户第一次和最后一次消费时间
order_min = df.groupby('Customer ID')['Order Date'].min()
order_max = df.groupby('Customer ID')['Order Date'].max()
(order_max - order_min).mean()
Timedelta('1158 days 15:55:28.301886')
用户的平均生命周期是1158天,即平均每个用户最后一次购买时间与第一次购买时间的差值是1158天
# 用户生命周期直方图分布
((order_max - order_min)/np.timedelta64(1,'D')).hist(bins=50,alpha=0.8);
- 总体来看,该超市用户的粘性较大,4年的统计周期内,大部分用户的生命周期是1200天至1500天,根据此情况,我们可以在客户消费后3年左右对其进行引导,促使其再次消费并形成消费习惯
- 另外针对不同阶段的用户,采取针对性的运营策略,从而延长用户的生命周期,并且尽可能让用户产生商业价值
4.1.2 时间维度新增顾客数量
根据Customer ID列数据进行重复值的删除,保证数据集中所有的客户ID都是唯一的,然后使用透视表对数据进行整理。
data = df.drop_duplicates(subset=['Customer ID'])
new_consumer = data.groupby(by=['year','month']).size().reset_index().rename(columns={0:"count"})
sns.factorplot(x='month',y='count',data=new_consumer,hue='year',size=10,aspect=1.3)
plt.title('每年新顾客数量');
从2011年到2014年总体来看
- 每一年的新增客户数是逐年减少的趋势,可以看出该超市对保持老用户是有效的,超市的运营状况较为稳定,但新客户获取率比较低,可以不定期的进行主动推广营销,从而增加新客户数。
4.1.3 复购率
# 每一条记录是一个种类的商品,但是一个订单可能包含多条记录,由于计算复购不涉及商品信息,将一个订单包含多件商品的记录值保留一条
data = df.drop_duplicates(subset='Order ID')
data['year_month'] = data['Order Date'].values.astype('datetime64[M]')
pivoted=data.pivot_table(index='Customer ID',columns='year_month',values='Order Date',aggfunc='count').fillna(0)
pivoted
year_month | 2011-01-01 | 2011-02-01 | 2011-03-01 | 2011-04-01 | 2011-05-01 | 2011-06-01 | 2011-07-01 | 2011-08-01 | 2011-09-01 | 2011-10-01 | ... | 2014-03-01 | 2014-04-01 | 2014-05-01 | 2014-06-01 | 2014-07-01 | 2014-08-01 | 2014-09-01 | 2014-10-01 | 2014-11-01 | 2014-12-01 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Customer ID | |||||||||||||||||||||
AA-10315 | 0.0 | 0.0 | 1.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | ... | 0.0 | 1.0 | 0.0 | 2.0 | 1.0 | 1.0 | 0.0 | 0.0 | 0.0 | 2.0 |
AA-10375 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 1.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 2.0 | 0.0 | 1.0 | 1.0 | 2.0 |
AA-10480 | 1.0 | 0.0 | 1.0 | 1.0 | 0.0 | 1.0 | 0.0 | 1.0 | 0.0 | 0.0 | ... | 1.0 | 2.0 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 | 0.0 | 0.0 | 0.0 |
AA-10645 | 2.0 | 0.0 | 0.0 | 1.0 | 0.0 | 1.0 | 1.0 | 0.0 | 0.0 | 0.0 | ... | 1.0 | 1.0 | 0.0 | 3.0 | 0.0 | 0.0 | 0.0 | 3.0 | 1.0 | 1.0 |
AA-315 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 1.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
YS-21880 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | ... | 0.0 | 2.0 | 0.0 | 1.0 | 0.0 | 2.0 | 0.0 | 0.0 | 0.0 | 2.0 |
ZC-11910 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
ZC-21910 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 2.0 | ... | 0.0 | 0.0 | 1.0 | 2.0 | 2.0 | 1.0 | 0.0 | 1.0 | 2.0 | 5.0 |
ZD-11925 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | ... | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
ZD-21925 | 0.0 | 1.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 1.0 | 1.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 2.0 |
1590 rows × 48 columns
使用数据透视表对数据进行整理,将年月作为数据的列,客户id作为数据的行索引。计算求复购率,复购率的定义是某月消费两次及以上的用户在总消费用户中的占比,并且如果一个用户在同一天下了两笔订单,也算作复购用户
# 将消费两次以上记为1,消费1次记为0,没有消费记为nan
pivoted = pivoted.applymap(lambda x:1 if x > 1 else np.nan if x == 0 else 0)
pivoted
year_month | 2011-01-01 | 2011-02-01 | 2011-03-01 | 2011-04-01 | 2011-05-01 | 2011-06-01 | 2011-07-01 | 2011-08-01 | 2011-09-01 | 2011-10-01 | ... | 2014-03-01 | 2014-04-01 | 2014-05-01 | 2014-06-01 | 2014-07-01 | 2014-08-01 | 2014-09-01 | 2014-10-01 | 2014-11-01 | 2014-12-01 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Customer ID | |||||||||||||||||||||
AA-10315 | NaN | NaN | 0.0 | 0.0 | NaN | NaN | NaN | NaN | 0.0 | NaN | ... | NaN | 0.0 | NaN | 1.0 | 0.0 | 0.0 | NaN | NaN | NaN | 1.0 |
AA-10375 | NaN | NaN | NaN | 0.0 | NaN | NaN | 0.0 | NaN | NaN | 0.0 | ... | NaN | NaN | NaN | NaN | NaN | 1.0 | NaN | 0.0 | 0.0 | 1.0 |
AA-10480 | 0.0 | NaN | 0.0 | 0.0 | NaN | 0.0 | NaN | 0.0 | NaN | NaN | ... | 0.0 | 1.0 | 0.0 | NaN | NaN | 0.0 | 0.0 | NaN | NaN | NaN |
AA-10645 | 1.0 | NaN | NaN | 0.0 | NaN | 0.0 | 0.0 | NaN | NaN | NaN | ... | 0.0 | 0.0 | NaN | 1.0 | NaN | NaN | NaN | 1.0 | 0.0 | 0.0 |
AA-315 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 0.0 | NaN | NaN | ... | NaN | NaN | 0.0 | NaN | 0.0 | NaN | NaN | NaN | NaN | NaN |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
YS-21880 | 0.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 0.0 | ... | NaN | 1.0 | NaN | 0.0 | NaN | 1.0 | NaN | NaN | NaN | 1.0 |
ZC-11910 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | 0.0 | NaN | NaN | NaN | NaN | NaN | NaN |
ZC-21910 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 0.0 | 1.0 | ... | NaN | NaN | 0.0 | 1.0 | 1.0 | 0.0 | NaN | 0.0 | 1.0 | 1.0 |
ZD-11925 | NaN | NaN | NaN | NaN | NaN | NaN | 0.0 | NaN | NaN | NaN | ... | 0.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 0.0 |
ZD-21925 | NaN | 0.0 | NaN | NaN | 0.0 | NaN | NaN | 0.0 | 0.0 | NaN | ... | NaN | NaN | NaN | 0.0 | NaN | NaN | NaN | NaN | NaN | 1.0 |
1590 rows × 48 columns
使用sum和count计算复购率,这两个函数都会忽略NaN值,sum计算的是在一个月内消费两次及以上的用户数,count计算的是在一个月内有过消费的客户数
repeat_rate=pivoted.sum()/pivoted.count()
repeat_rate.plot()
plt.title('每年不同月份复购率波动')
plt.ylabel('复购率',rotation=0);
从上图可以看出:
- 该超市用户的整体复购率随着年份的增长,逐渐增加,从最初2011年的0.1出头,到2014年末,一月内复购率最高值达到了将近0.35,提升巨大
- 早期由于大量新用户的加入,新客的复购率相对较低,而在后期复购率基本可以稳定在20%-30%,可以看出用户的粘性还是比较高的
4.2 用户分类
4.2.1 客户RFM模型
在面向客户制定运营策略、营销策略时,我们希望能够针对不同的客户推行不同的策略,实现精准化运营,以期获取最大的转化率。精准化运营的前提是客户关系管理,而客户关系管理的核心是客户分类。
通过客户分类,对客户群体进行细分,区别出低价值客户、高价值客户,对不同的客户群体开展不同的个性化服务,将有限的资源合理地分配给不同价值的客户,实现效益最大化。
#定义用户类别
def transform_label(x):if x == 111:label = '重要价值客户'elif x == 110:label = '潜力客户'elif x == 101:label = '重要发展客户'elif x == 100:label = '新客户'elif x == 11:label = '重要唤回客户'elif x == 10:label = '一般客户'elif x == 1:label = '重要挽留客户'elif x == 0:label = '流失客户'return labeldf = df[['Order ID','Order Date','Customer ID','Sales']]r = df.groupby('Customer ID')['Order Date'].max().reset_index()
r['R'] = (pd.to_datetime('2015-1-1') - r['Order Date']).dt.days
r = r[['Customer ID','R']]#每一条记录代表一种商品,有些订单有多种商品,原始数据会把订单展开成多行,将其算成一次购买记录,即频次算1次
dup_f = df.groupby(['Customer ID','Order ID'])['Order Date'].count().reset_index()
f = dup_f.groupby('Customer ID')['Order Date'].count().reset_index()
f.columns = ['Customer ID','F']sum_m = df.groupby('Customer ID')['Sales'].sum().reset_index()
com_m = pd.merge(sum_m,f,left_on = 'Customer ID',right_on = 'Customer ID',how = 'inner')#计算用户平均支付金额
com_m['M'] = com_m['Sales'] / com_m['F']rfm = pd.merge(r,com_m,left_on = 'Customer ID',right_on = 'Customer ID',how = 'inner')
rfm = rfm[['Customer ID','R','F','M']]rfm['R-SCORE'] = pd.cut(rfm['R'],bins = rfm['R'].quantile(q=np.linspace(0,1,num=6),interpolation='nearest'),labels = [5,4,3,2,1],right = False).astype(float)
rfm['F-SCORE'] = pd.cut(rfm['F'],bins = rfm['F'].quantile(q=np.linspace(0,1,num=6),interpolation='nearest'),labels = [1,2,3,4,5],right = False).astype(float)
rfm['M-SCORE'] = pd.cut(rfm['M'],bins = rfm['M'].quantile(q=np.linspace(0,1,num=6),interpolation='nearest'),labels = [1,2,3,4,5],right = False).astype(float)rfm['R>mean'] = (rfm['R-SCORE'] > rfm['R-SCORE'].mean()) * 1
rfm['F>mean'] = (rfm['F-SCORE'] > rfm['F-SCORE'].mean()) * 1
rfm['M>mean'] = (rfm['M-SCORE'] > rfm['M-SCORE'].mean()) * 1rfm['Score'] = (rfm['R>mean'] * 100) + (rfm['F>mean'] * 10) + (rfm['M>mean'] * 1)rfm['客户类型'] = rfm['Score'].apply(transform_label)count = rfm['客户类型'].value_counts().reset_index()
count.columns = ['客户类型','人数']
count['人数占比'] = count['人数'] / count['人数'].sum()rfm['购买总金额'] = rfm['F'] * rfm['M']
mon = rfm.groupby('客户类型')['购买总金额'].sum().reset_index()
mon.columns = ['客户类型','消费金额']
mon['金额占比'] = mon['消费金额'] / mon['消费金额'].sum()
result = pd.merge(count,mon,left_on = '客户类型',right_on = '客户类型')
result
客户类型 | 人数 | 人数占比 | 消费金额 | 金额占比 | |
---|---|---|---|---|---|
0 | 流失客户 | 402 | 0.252830 | 6.342346e+05 | 0.050167 |
1 | 重要价值客户 | 320 | 0.201258 | 5.534281e+06 | 0.437752 |
2 | 新客户 | 294 | 0.184906 | 7.515087e+05 | 0.059443 |
3 | 潜力客户 | 210 | 0.132075 | 2.302542e+06 | 0.182127 |
4 | 重要挽留客户 | 132 | 0.083019 | 7.162249e+05 | 0.056652 |
5 | 重要发展客户 | 118 | 0.074214 | 1.072944e+06 | 0.084868 |
6 | 重要唤回客户 | 66 | 0.041509 | 1.123060e+06 | 0.088832 |
7 | 一般客户 | 48 | 0.030189 | 5.077063e+05 | 0.040159 |
顾客分层结构分析
- 各类顾客对销售额的贡献
from pyecharts.charts import Grid, Pie, Bar
from pyecharts import options as opts
from pyecharts.globals import ThemeType# 这里为了美观和交互性,使用pyecharts做南丁格尔玫瑰图,需要将numpy.int转化成python原生态int
customer_category_sum = []
for i in result['人数'].values:customer_category_sum.append(int(i))
customer_list = [list(z) for z in zip(result['客户类型'], customer_category_sum)]# 绘制饼图
pie = (Pie(init_opts=opts.InitOpts(theme=ThemeType.MACARONS,bg_color='white')).add('',customer_list,radius=['30%','75%'],rosetype='radius',label_opts=opts.LabelOpts(is_show=True)).set_global_opts(title_opts=opts.TitleOpts(title='顾客分层结构',pos_left='center'),toolbox_opts=opts.ToolboxOpts(is_show=True),legend_opts=opts.LegendOpts(orient='vertical',pos_right='0%',pos_top='30%')).set_series_opts(label_opts=opts.LabelOpts(formatter='{b}:{d}%')))
pie.render_notebook()
<div id="fcb635cef5044684b6483e669497d314" style="width:900px; height:500px;"></div>
从顾客分层结构来看:
- 可以看到无价值的流失客户占据了最大的一部分,这里统计了三年内的用户进行RFM模型构建,可见我们三年时间流失了超过1/4的用户(或者说是沉睡客户),虽然我们的营业额一直在提升,但是也不能掩饰我们流失客户过多的问题
- 做的好的方面是我们的重要价值客户占比非常大,超过了20%,贡献了主要的销售额流水,可以针对他们进行针对性的定制性、个性化服务
- 单纯看占比是没有意义的,因为我们的目的是扩大GMV,因此下面我们再结合消费金额分布情况,来看下我们实际应该针对哪些层级的用户进行精准投放
result.sort_values(by=['消费金额'],ascending=False,inplace=True)
sales_rate=[]; a=0
for i in result.金额占比:a +=isales_rate.append(a)
result['金额占比累加'] = sales_rate
result
客户类型 | 人数 | 人数占比 | 消费金额 | 金额占比 | 金额占比累加 | |
---|---|---|---|---|---|---|
1 | 重要价值客户 | 320 | 0.201258 | 5.534281e+06 | 0.437752 | 0.437752 |
3 | 潜力客户 | 210 | 0.132075 | 2.302542e+06 | 0.182127 | 0.619879 |
6 | 重要唤回客户 | 66 | 0.041509 | 1.123060e+06 | 0.088832 | 0.708711 |
5 | 重要发展客户 | 118 | 0.074214 | 1.072944e+06 | 0.084868 | 0.793579 |
2 | 新客户 | 294 | 0.184906 | 7.515087e+05 | 0.059443 | 0.853022 |
4 | 重要挽留客户 | 132 | 0.083019 | 7.162249e+05 | 0.056652 | 0.909674 |
0 | 流失客户 | 402 | 0.252830 | 6.342346e+05 | 0.050167 | 0.959841 |
7 | 一般客户 | 48 | 0.030189 | 5.077063e+05 | 0.040159 | 1.000000 |
# 建立左侧纵坐标画板
fig, ax1 = plt.subplots()
# 画柱状图图
s = plt.bar(result.客户类型, result.消费金额, alpha=0.5, label='消费金额')
# 显示左侧纵坐标
ax1.set_ylabel('消费金额', fontsize=20)
plt.yticks(range(0,6000000,500000),['0','50W','100W','150W','200W','250W','300W','350W','400W','450W','500W','550W'])
plt.tick_params(labelsize=14)
# 显示文字
for x1,y1 in zip(result.客户类型, result.消费金额):plt.text(x1, y1+20000, str(int(y1/10000)) + 'W' , ha='center',fontsize=20)ax2 = ax1.twinx()
# # 画折线图
line = ax2.plot(result.客户类型, result.金额占比累加, linewidth=3,marker='o' ,c='y',ms=10)
# # 折线图显示标识
for a, b in zip(result.客户类型, result.金额占比累加): ax2.text(a, b+0.02, "%.0f" % (100 * b) + '%', ha='center', fontsize=20)
ax2.set_ylabel('金额占比累加', fontsize=20, rotation=270)
plt.ylim(0,1.05)
plt.tick_params(labelsize=14)
ax2.set_title("各类型顾客消费额及其累加占比", fontsize=22)
plt.show()
从各类型客户和销售额占比可以看出:
- 重要价值客户贡献金额最高,44%的比例,可见该超市是有一群稳定的老顾客,经常来超市消费,是销售额的主要来源,潜力客户和重要价值客户,占了销售额的62%,而其他6类用户的贡献金额较少,每个类群的客户贡献金额都不超过销售额的10%
- 人数占比最多的流失客户,贡献金额只有5%左右,可见是有一群低质量的用户,后续可以进行渠道跟踪,摒弃用户质量较差的渠道
- 应该着重将运营的重心放在重要价值客户,如何留住核心收入来源的重要价值用户以及通过推荐高质量产品等方法来扩大潜力顾客的购买金额,是现阶段的任务。
- 通过整体的探索发现,此数据集收集的数据显示,该超市4年销售额累计1200余万元,4年共有客户接近1600人,可见规模不大,运营能力不会很强,以上图的用户分法并不能很好的去进行分组运营的落地,可以将用户分成三类,重要价值客户、潜力客户、其他客户,针对三类用户群展开不同的运营策略会更容易实行,对这三类不同的客户群体开展不同的个性化服务,将有限的资源合理地分配给不同价值的客户,从而实现效益最大化。
4.2.2 K-means挖掘价值客户
如上图所示,将用户分为8类,并不能很好的分群实行不同的运营策略,这里再尝试用K-means机器学习算法进行分群
RFM=rfm[['Customer ID','R','F','购买总金额']]
RFM.columns=['CustomerID','Recency','Frequency','Monetary']
RFM.describe()
Recency | Frequency | Monetary | |
---|---|---|---|
count | 1590.000000 | 1590.000000 | 1590.000000 |
mean | 88.616981 | 16.196855 | 7951.259063 |
std | 127.879879 | 10.619199 | 6936.570172 |
min | 1.000000 | 1.000000 | 7.173000 |
25% | 15.000000 | 6.000000 | 1674.812250 |
50% | 41.000000 | 14.000000 | 6248.135590 |
75% | 104.000000 | 26.000000 | 13133.098420 |
max | 1119.000000 | 41.000000 | 35668.120800 |
# 观测一下数据分布
f,ax = plt.subplots(3,1,figsize=(10, 12))
plt.subplot(3,1,1); sns.distplot(RFM['Recency'],label='Recency')
plt.subplot(3,1,2); sns.distplot(RFM['Frequency'],label='Frequency')
plt.subplot(3,1,3); sns.distplot(RFM['Monetary'],label='Monetary');
数据预处理
# 对数转化
RFM_log = RFM[['Recency', 'Frequency', 'Monetary']].apply(np.log, axis=1).round(2)
f,ax=plt.subplots(3,1,figsize=(10,12))
plt.subplot(3,1,1); sns.distplot(RFM_log['Recency'], label='Recency')
plt.subplot(3,1,2); sns.distplot(RFM_log['Frequency'], label='Frequency')
plt.subplot(3,1,3); sns.distplot(RFM_log['Monetary'],label='MemoryError');
4.2.2.2 标准化处理
from sklearn.preprocessing import StandardScaler,NormalizerRFM_normalization = StandardScaler().fit_transform(RFM_log)
4.2.2.3 选择聚类数目
通常有两种方法,一是肘部法则(Elbow Criterion method),选择代价函数下降的显著转折点;二是业务经验
这里使用肘部法则进行K值选择,并且使用Calinski-Harabasz Index和TSNE降维可视化分析进行评估
from sklearn.cluster import KMeans
# K值的选择,1-8
ks = range(1,9)
inertias=[]
for k in ks:kc=KMeans(n_clusters=k, init='k-means++', random_state=1)kc.fit(RFM_normalization)inertias.append(kc.inertia_) # 样本距离其聚类中心的距离平方和print('k=',k,'迭代次数',kc.n_iter_)
k= 1 迭代次数 2
k= 2 迭代次数 4
k= 3 迭代次数 20
k= 4 迭代次数 8
k= 5 迭代次数 14
k= 6 迭代次数 11
k= 7 迭代次数 22
k= 8 迭代次数 12
# 绘制每个K值对应的inertia_f,ax=plt.subplots(figsize=(10,6))
plt.plot(ks,inertias,'-o')
plt.xlabel('number of clusters')
plt.ylabel('sum of squared distances')
plt.title('Elbow criter method to find best k');
from sklearn import metrics
kk = range(2,9)
for k in kk:y_pred = KMeans(n_clusters=k, random_state=1).fit_predict(RFM_normalization) #k必须大于1calinski = metrics.calinski_harabaz_score(RFM_normalization, y_pred)print('k:',k,' calinski=',calinski)
k: 2 calinski= 2290.9185040560255
k: 3 calinski= 1746.1656166404057
k: 4 calinski= 1721.8535796545677
k: 5 calinski= 1741.4614805385613
k: 6 calinski= 1590.1114912259004
k: 7 calinski= 1527.4148500701804
k: 8 calinski= 1506.3665579664887
- 根据肘部法则定理,可以看到当k=2时,代价函数下降会有一个显著转折点,但分成两类太少,对精益化运营气不到效果,从calinski_harabaz_scores来看,除了分成2类,其次是k=3最大。结合业务而言,将用户分成3类应该会是比较好的选择
4.2.2.4 模型计算
kc = KMeans(n_clusters=3, random_state=1)
kc.fit(RFM_normalization)
# 每个样本对应的类簇标签,顺序与样本原始顺序一致
RFM['K-means_label'] = kc.labels_
RFM.head()
CustomerID | Recency | Frequency | Monetary | K-means_label | |
---|---|---|---|---|---|
0 | AA-10315 | 9 | 19 | 13747.41300 | 0 |
1 | AA-10375 | 7 | 23 | 5884.19500 | 0 |
2 | AA-10480 | 118 | 20 | 17695.58978 | 0 |
3 | AA-10645 | 27 | 36 | 15343.89070 | 0 |
4 | AA-315 | 3 | 7 | 2243.25600 | 1 |
组内特征
RFM_normalization1=pd.DataFrame(RFM_normalization, index=RFM.index, columns=['Recency', 'Frequency', 'Monetary'])
RFM_normalization1['K-means_label']=kc.labels_
RFM_melt=pd.melt(RFM_normalization1, id_vars=['K-means_label'],value_vars=['Recency', 'Frequency', 'Monetary'],var_name='Metric',value_name='Value')sns.lineplot(x = 'Metric', y = 'Value', hue = 'K-means_label',data = RFM_melt, palette=sns.color_palette("hls", 3))
plt.title("Plot of RFM");
k_cluster =RFM.groupby(['K-means_label']).agg({'Recency':'mean','Frequency':'mean','Monetary':['mean','count']}).round(2); k_cluster
Recency | Frequency | Monetary | ||
---|---|---|---|---|
mean | mean | mean | count | |
K-means_label | ||||
0 | 32.53 | 26.08 | 13849.78 | 793 |
1 | 29.26 | 7.66 | 2536.67 | 316 |
2 | 220.07 | 5.51 | 1783.87 | 481 |
对于RFM模型而言,R越小越好,而F和M则越大越好。从图中3类用户可以看出:
- 0:平均消费次数为26次,平均消费金额1.4W元左右,平均最近一次购买在一个月左右
- 1:平均消费次数为7次,平均消费金额2500元左右,平均最近一次购买在一个月左右
- 2: 平均消费次数为5次,平均消费金额1800元左右,平均最近一次购买在220天左右
针对类别0的高价值用户,对其购物行为进行针对性分析,可以从以下几个方面进行,从而提供个性化得消费方案,形成智能商业模式。
- 高价值用户的消费习惯(购物时段)
- 购买的商品偏好
- 购物种类的关联性(同时购买得产品)
# TSNE降维
from sklearn.manifold import TSNEtsne = TSNE(n_components=2, random_state=1)
X_tsne = tsne.fit_transform(RFM_normalization)
k_range = KMeans(n_clusters=3, random_state=1)
k_range.fit(RFM_normalization)
cluster_labels = kc.labels_plt.scatter(X_tsne[:, 0],X_tsne[:, 1], c=cluster_labels);
4.2.2.6 顾客分层结构分析
# 构建各类别顾客数量的python原生态list
labels=['类别0','类别1','类别2']
labels_count = []
for i in k_cluster['Monetary', 'count'].values:labels_count.append(int(i))
customer_list = [list(z) for z in zip(labels, labels_count)]# 构建各类别顾客销售额的python原生态list
customer_sales=RFM.groupby('K-means_label').agg({'Monetary': 'sum'})
labels_sales = []
for i in customer_sales.values:labels_sales.append(int(i))
sales_list = [list(z) for z in zip(labels, labels_sales)]p =(Pie(init_opts=opts.InitOpts(width="950px", height="600px",theme=ThemeType.MACARONS)).add(series_name="顾客数量及占比",data_pair=customer_list,radius=[0, "30%"],label_opts=opts.LabelOpts(position="inner"),).add(series_name="销售额及占比",radius=["40%", "55%"],data_pair=sales_list,label_opts=opts.LabelOpts(position="outside",formatter="{a|{a}}{abg|}\n{hr|}\n {b|{b}: }{c} {per|{d}%} ",background_color="#eee",border_color="#aaa",border_width=1,border_radius=4,rich={"a": {"color": "#999", "lineHeight": 15, "align": "center"},"abg": {"backgroundColor": "#e3e3e3","width": "100%","align": "right","height": 15,"borderRadius": [4, 4, 0, 0],},"hr": {"borderColor": "#aaa","width": "100%","borderWidth": 0.5,"height": 0,},"b": {"fontSize": 16, "lineHeight": 20},"per": {"color": "#eee","backgroundColor": "#334455","padding": [2, 4],"borderRadius": 2,},},),).set_global_opts(title_opts=opts.TitleOpts(title='各类别顾客数量及销售额占比',pos_left='center'),legend_opts=opts.LegendOpts(pos_left="left", orient="vertical")).set_series_opts(tooltip_opts=opts.TooltipOpts(trigger="item", formatter="{a} <br/>{b}: {c} ({d}%)"))
)
p.render_notebook()
<div id="8380e5561d83423ca753622d0798ca56" style="width:950px; height:600px;"></div>
从上图可以看出:
- 销售额的主要来源是类别0的人群,他们人数占比50%,消费金额占比87%左右
- 可见此超市是有一群固定的高价值客户来稳定消费,粉丝群体稳定,而且占比大,需要针对他们提供个性化方案,预防流失。
- 类别1和类别2的顾客区分并不明显,如果运营能力有限,可以将其合并为一类用户