一、背景介绍
考拉电子公司(虚拟公司)是一家电子类产品经销商,成立于2020年1月,旗下产品有手机、电脑、影音设备等。公司从2020年1月至8月期间,整体发展良好,尤其是5至8月期间,公司GMV直线上升,但是从9月份开始,GMV出现下滑。考拉公司成立不足一年,GMV大幅下降是一个危险信号。
二、分析目的
1) 8月之后,考拉公司GMV下滑的原因?
2)如何提升考拉公司GMV?
三、分析思路
根据分析目的,我们以每月的GMV为核心指标。将GMV从产品和用户两个角度进行拆解,从用户角度分析,GMV=消费者数量客单价,消费者数量=老用户数量+新用户数量;从产品维度分析,GMV=产品销量平均单价,产品又有不同的品类,品类下面又分为不同的品牌。综上,本文根据下图的维度进行分析GMV下降原因,并提出改进建议,其中客单价和产品平均单价作为外部因素,不在分析范围内。
四、GMV异常原因分析
4.1 整体GMV分析
1到11月GMV:
从图中看出,考拉公司1~3月份处于缓慢发展阶段,4月份GMV出现大幅下滑后,公司做出调整,5月份GMV开始直线上升,并在8月份达到最大值,9月份开始下滑,10月虽有回升,但是仍没有回到8月的水平。
7至11月每日GMV
我们再把数据细分,以日为单位统计GMV,时间从7月到11月。从上图中可以看出,8月17日之后,GMV开始呈下降趋势,10月份,公司做出了调整,GMV有所回升,但是很不稳定,为了公司长期稳定发展,还需进一步分析。
4.2 用户维度分析
新用户:本月初次消费
老用户:本月之前,已经购买过考拉公司的产品
每月新老用户总量如下图:
从上图可以看出,8月之后,新老用户数都是直线下降,9月份开始,每月消费的老用户总量已经超越新用户。说明,8月份之后,公司老用户复购率直线下降,而新用户增长也尽显疲态。
我们再从性别、年龄和地区维度分析:
性别
年龄
地区
从上图看出,新老用户的变化与性别和年龄关系不大,而地区方面,广东、北京、上海三地每月消费者数量下降最多,当然这与这三地基数大有关。
4.3 产品维度分析
考拉公司各产品品类总GMV占比:
考拉公司销售的产品品类有124种,其中GMV前十的产品就占到了总量的75.13%,尤其是smartphone类产品,占到总体GMV的28.98%,所以我们单独来分析GMV前十的品类,如下图:
从图中可以看出,8月份中,smartphone、notebook、kitchen refrigerators、video tv、kitchen washer这六类产品都出现了大幅下滑,其中smartphone最为严重。
接着,我们以smartphone为例,进一步拆解到品牌:
从图中可以看出,8月份开始,apple销量直线下滑,samsung也出现大幅波动。
综上说明,考拉公司GMV中占比最大前六类产品,在8月份销量都明显下降,并对公司GMV产生了主要影响。其中,smartphone中的apple和samsung是“罪魁祸首”。
4.4 GMV出现异常的原因总结
五、解决方案
5.1 留住老用户
RFM用户价值分析
客户营销战略的倡导者Jay & Adam Curry从国外数百家公司进行的客户营销实施经验中提炼了如下经验:
1)公司收入的80%来自顶端20%的客户
2)20%的客户其利润率为100%
3)90%以上的收入来自现有客户
4)大部分的营销预算经常被用在非现有客户上
5)5%至30%的客户在客户金字塔中具有升级潜力
6)客户金字塔中客户升级2%,意味着销售收入增加10%,利润增加50%
通过数据库查询得知,顶端20%的客户消费占到了总体消费的73%,这也印证了Jay & Adam Curry总结的经验。所以,我们的目的不是提升所有用户的复购率,而是把预算用在最有价值的客户身上。这就需要我们对用户价值划分,下面根据RFM模型,量化用户价值,将用户划分成:
1)钻石用户(diamond)
2)黄金用户(gold)
3)白银用户(sliver)
4)青铜用户(general)
下图节选了20位用户的价值,我们先对RFM量化评分,再更根据总分划分用户价值。以公司目前状况,考拉公司的运营策略应该是在留住钻石用户的基础上,将黄金用户转化成钻石用户。
5.2 开发新客户
考拉公司用户分布:
从图中看出,北京、上海、广东三地是考拉公司的主营市场,三者总用户占到总体用户的57.50%。
我们再根据国家统计局统计的2020年各地区人口,计算出考拉公司用户占到当地总口的比例:
从图中可以看出北京和上海两地的用户占比最高,而广东作为主营地区,却与重庆持平。
综上,考拉公司的主战场是广东、北京和上海,其中广东人口基数大,潜在客户最多,也是开发新客户的最大突破口。其他地区仍处于起步阶段,重心要放在获客上面。
5.3 主营产品用户画像(以apple手机为例)
下图从性别、年龄和地区三个角度分析apple消费者:
从上图中看出,apple消费者与性别、年龄和地区并无太大区别,但这并不意味着不需要对apple消费者精细化运营,只是因为该数据集的数据不够。提升主营产品的销量是重中之重,所以还需要其他数据进一步分析。
5.4 用户消费习惯
下图统计了消费者消费的周次和时间:
从上图看出,周末的销量比工作日高,说明客户比较喜欢在周末购物,所以,考拉公司可以在周末做些活动,促销产品和获客。在消费时间上,用户喜欢在早上6点至中午12点之间进行购物,可以在这个时间段发送推荐内容给用户。
5.5 小结
为了使考拉公司GMV稳步上升,提出如下建议:
1)对于老用户,要精细化运营,将用户运营的预算,主要聚焦在钻石用户上;
2)开发新用户方面,以上海、北京、广东三地为基石,着力开发广东地区新用户;
3)少数主营产品,如:手机、笔记本电脑等,是公司盈利的根本,应先将精力主要放在这几类产品的推广和促销上,还需在根据其他数据进一步分析主营产品的用户画像,实现精细化推广;
4)周末和早上6点至中午12点之间是用户消费的主要时间,应在着力在这期间给用户推送产品最新消息等。
六、相关代码
python数据处理
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
import seaborn as sns
import datetime as dt
# matplotlib与pandas初始设置
plt.rcParams['font.sans-serif'] = ['SimHei'] #设置中文字体为黑体
plt.rcParams['axes.unicode_minus'] = False #正常显示负号
pd.set_option('display.max_columns', 30)
plt.rcParams.update({"font.family":"SimHei","font.size":14})
plt.style.use("tableau-colorblind10")
pd.set_option('display.float_format',lambda x : '%.2f' % x)#pandas禁用科学计数法
%matplotlib inline
#忽略警告
import warnings
warnings.filterwarnings('ignore')
1、数据预处理
查看数据初始状态
# 导入数据(指明格式,节省空间)
data = pd.read_csv('sales_report.csv')
data.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 564169 entries, 0 to 564168
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Unnamed: 0 564169 non-null int64
1 event_time 564169 non-null object
2 order_id 564169 non-null int64
3 product_id 564169 non-null int64
4 category_id 564169 non-null float64
5 category_code 434799 non-null object
6 brand 536945 non-null object
7 price 564169 non-null float64
8 user_id 564169 non-null float64
9 age 564169 non-null float64
10 sex 564169 non-null object
11 local 564169 non-null object
dtypes: float64(4), int64(3), object(5)
memory usage: 235.3 MB
data.tail()
Unnamed: 0 | event_time | order_id | product_id | category_id | category_code | brand | price | user_id | age | sex | local | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
564164 | 2633516 | 2020-11-21 10:10:01 UTC | 2388440981134693942 | 1515966223526602848 | 2268105428166508800.00 | electronics.smartphone | oppo | 138.87 | 1515915625514888704.00 | 21.00 | 男 | 上海 |
564165 | 2633517 | 2020-11-21 10:10:13 UTC | 2388440981134693943 | 1515966223509089282 | 2268105428166508800.00 | electronics.smartphone | apple | 418.96 | 1515915625514891264.00 | 21.00 | 女 | 北京 |
564166 | 2633518 | 2020-11-21 10:10:30 UTC | 2388440981134693944 | 1515966223509089917 | 2268105402447036928.00 | appliances.personal.scales | vitek | 12.48 | 1515915625514834176.00 | 19.00 | 女 | 上海 |
564167 | 2633519 | 2020-11-21 10:10:30 UTC | 2388440981134693944 | 2273948184839454837 | 2268105440371933952.00 | NaN | moulinex | 41.64 | 1515915625514834176.00 | 19.00 | 女 | 上海 |
564168 | 2633520 | 2020-11-21 10:10:30 UTC | 2388440981134693944 | 1515966223509127566 | 2268105441101742848.00 | appliances.kitchen.blender | redmond | 53.22 | 1515915625514834176.00 | 19.00 | 女 | 上海 |
data.describe(include='object')
event_time | category_code | brand | sex | local | |
---|---|---|---|---|---|
count | 564169 | 434799 | 536945 | 564169 | 564169 |
unique | 389835 | 123 | 868 | 2 | 11 |
top | 1970-01-01 00:33:40 UTC | electronics.smartphone | samsung | 男 | 广东 |
freq | 1307 | 102697 | 96239 | 284421 | 122909 |
压缩数据
指定数据类型,object类型少于50%的转换成:category
# 指定数据类型
d_type = {'category_code':'category','brand':'category','sex':'category','local':'category','price':'float32','age':'int8'}
df = pd.read_csv('sales_report.csv',dtype=d_type)
df.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 564169 entries, 0 to 564168
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Unnamed: 0 564169 non-null int64
1 event_time 564169 non-null object
2 order_id 564169 non-null int64
3 product_id 564169 non-null int64
4 category_id 564169 non-null float64
5 category_code 434799 non-null category
6 brand 536945 non-null category
7 price 564169 non-null float32
8 user_id 564169 non-null float64
9 age 564169 non-null int8
10 sex 564169 non-null category
11 local 564169 non-null category
dtypes: category(4), float32(1), float64(2), int64(3), int8(1), object(1)
memory usage: 70.0 MB
# 删除不需要的列
df.drop('Unnamed: 0',axis=1,inplace=True)
df.head()
event_time | order_id | product_id | category_id | category_code | brand | price | user_id | age | sex | local | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2020-04-24 11:50:39 UTC | 2294359932054536986 | 1515966223509089906 | 2268105426648171520.00 | electronics.tablet | samsung | 162.01 | 1515915625441993984.00 | 24 | 女 | 海南 |
1 | 2020-04-24 11:50:39 UTC | 2294359932054536986 | 1515966223509089906 | 2268105426648171520.00 | electronics.tablet | samsung | 162.01 | 1515915625441993984.00 | 24 | 女 | 海南 |
2 | 2020-04-24 14:37:43 UTC | 2294444024058086220 | 2273948319057183658 | 2268105430162997248.00 | electronics.audio.headphone | huawei | 77.52 | 1515915625447879424.00 | 38 | 女 | 北京 |
3 | 2020-04-24 14:37:43 UTC | 2294444024058086220 | 2273948319057183658 | 2268105430162997248.00 | electronics.audio.headphone | huawei | 77.52 | 1515915625447879424.00 | 38 | 女 | 北京 |
4 | 2020-04-24 19:16:21 UTC | 2294584263154074236 | 2273948316817424439 | 2268105471367840000.00 | NaN | karcher | 217.57 | 1515915625443148032.00 | 32 | 女 | 广东 |
创建年月、日、周、时间
# 创建日期列
df['date'] = df.event_time.apply(lambda x: x.split(' ')[0])
# 转换成日期格式
df['date'] = pd.to_datetime(df['date'])
# 创建月份列
df['month'] = df['date'].dt.strftime('%m')
# 创建小时列
df['hour'] = df.event_time.apply(lambda x: x.split(' ')[1].split(':')[0])
# 创建周列
df['weekday'] = df['date'].dt.strftime('%w')
# 删除多余列
df.drop('event_time',axis=1,inplace=True)
# 压缩新增数据列
hh = ['month','hour','weekday']
for i in hh:
df[i] = df[i].astype(dtype='category')
df.head()
order_id | product_id | category_id | category_code | brand | price | user_id | age | sex | local | date | month | hour | weekday | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2294359932054536986 | 1515966223509089906 | 2268105426648171520.00 | electronics.tablet | samsung | 162.01 | 1515915625441993984.00 | 24 | 女 | 海南 | 2020-04-24 | 04 | 11 | 5 |
1 | 2294359932054536986 | 1515966223509089906 | 2268105426648171520.00 | electronics.tablet | samsung | 162.01 | 1515915625441993984.00 | 24 | 女 | 海南 | 2020-04-24 | 04 | 11 | 5 |
2 | 2294444024058086220 | 2273948319057183658 | 2268105430162997248.00 | electronics.audio.headphone | huawei | 77.52 | 1515915625447879424.00 | 38 | 女 | 北京 | 2020-04-24 | 04 | 14 | 5 |
3 | 2294444024058086220 | 2273948319057183658 | 2268105430162997248.00 | electronics.audio.headphone | huawei | 77.52 | 1515915625447879424.00 | 38 | 女 | 北京 | 2020-04-24 | 04 | 14 | 5 |
4 | 2294584263154074236 | 2273948316817424439 | 2268105471367840000.00 | NaN | karcher | 217.57 | 1515915625443148032.00 | 32 | 女 | 广东 | 2020-04-24 | 04 | 19 | 5 |
缺失值处理
df.isnull().sum()
order_id 0
product_id 0
category_id 0
category_code 129370
brand 27224
price 0
user_id 0
age 0
sex 0
local 0
date 0
month 0
hour 0
weekday 0
dtype: int64
# category_code这一列缺失值过多,所以选择填充
# 先转化成object再填充
df['category_code'] = df['category_code'].astype('object')
df['category_code'] = df['category_code'].fillna('unkonwn')
df['category_code'] = df['category_code'].astype('category')
# brand这一列缺失值较少,选择删除
df = df[df.brand.notnull()]
df.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
Int64Index: 536945 entries, 0 to 564168
Data columns (total 14 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 order_id 536945 non-null int64
1 product_id 536945 non-null int64
2 category_id 536945 non-null float64
3 category_code 536945 non-null category
4 brand 536945 non-null category
5 price 536945 non-null float32
6 user_id 536945 non-null float64
7 age 536945 non-null int8
8 sex 536945 non-null category
9 local 536945 non-null category
10 date 536945 non-null datetime64[ns]
11 month 536945 non-null category
12 hour 536945 non-null category
13 weekday 536945 non-null category
dtypes: category(7), datetime64[ns](1), float32(1), float64(2), int64(2), int8(1)
memory usage: 31.3 MB
重复值处理
df.duplicated().sum()
634
# 重复值是单次订单购买了多件商品,所以增加购买数量和总金额列
df = df.value_counts().reset_index().rename(columns={0:'buy_cnt'})
df['buy_cnt'] = df['buy_cnt'].astype('int16')
df['expense'] = df['price'] * df['buy_cnt']
df.head()
order_id | product_id | category_id | category_code | brand | price | user_id | age | sex | local | date | month | hour | weekday | buy_cnt | expense | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2318945879811162983 | 2309018204833317816 | 2268105479144079872.00 | unkonwn | compliment | 0.56 | 1515915625465863936.00 | 28 | 女 | 浙江 | 2020-05-28 | 05 | 09 | 4 | 4 | 2.24 |
1 | 2295740594749702229 | 1515966223509104892 | 2268105428166508800.00 | electronics.smartphone | apple | 1387.01 | 1515915625448766464.00 | 21 | 男 | 北京 | 2020-04-26 | 04 | 09 | 0 | 4 | 5548.04 |
2 | 2388440981134674698 | 1515966223509106757 | 2360741867017995776.00 | appliances.environment.air_conditioner | samsung | 366.41 | 1515915625514599680.00 | 50 | 男 | 广东 | 2020-11-16 | 11 | 04 | 1 | 4 | 1465.64 |
3 | 2375043331555066740 | 2273948308370096764 | 2268105409048871168.00 | computers.network.router | altel | 57.85 | 1515915625504379136.00 | 19 | 女 | 上海 | 2020-08-13 | 08 | 19 | 4 | 4 | 231.40 |
4 | 2334999887038383089 | 1515966223509090031 | 2268105402673529600.00 | unkonwn | vitek | 18.50 | 1515915625447765248.00 | 18 | 男 | 广东 | 2020-06-19 | 06 | 13 | 5 | 3 | 55.50 |
查看异常值
df.describe(include='all').T
count | unique | top | freq | first | last | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
order_id | 536311.00 | NaN | NaN | NaN | NaT | NaT | 2370510904961807360.00 | 20245175202363216.00 | 2294359932054536960.00 | 2353674406846267392.00 | 2376454570843832320.00 | 2388440981134596608.00 | 2388440981134693888.00 |
product_id | 536311.00 | NaN | NaN | NaN | NaT | NaT | 1692699904749532672.00 | 327324678971262912.00 | 1515966223509088512.00 | 1515966223509104896.00 | 1515966223509261824.00 | 1515966223527326208.00 | 2388434452476881920.00 |
category_id | 536311.00 | NaN | NaN | NaN | NaT | NaT | 2273068434482616576.00 | 21891838583177408.00 | 2268105388421284352.00 | 2268105406549066752.00 | 2268105428166508800.00 | 2268105439323357952.00 | 2374498914001945600.00 |
category_code | 536311 | 124 | unkonwn | 116093 | NaT | NaT | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
brand | 536311 | 868 | samsung | 96123 | NaT | NaT | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
price | 536311.00 | NaN | NaN | NaN | NaT | NaT | 214.54 | 305.98 | 0.00 | 24.51 | 99.51 | 289.33 | 11574.05 |
user_id | 536311.00 | NaN | NaN | NaN | NaT | NaT | 1515915625486138112.00 | 23760191.50 | 1515915625439951872.00 | 1515915625467037184.00 | 1515915625486696704.00 | 1515915625511521280.00 | 1515915625514891264.00 |
age | 536311.00 | NaN | NaN | NaN | NaT | NaT | 33.18 | 10.12 | 16.00 | 24.00 | 33.00 | 42.00 | 50.00 |
sex | 536311 | 2 | 男 | 270454 | NaT | NaT | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
local | 536311 | 11 | 广东 | 117097 | NaT | NaT | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
date | 536311 | 323 | 2020-10-22 00:00:00 | 8310 | 1970-01-01 | 2020-11-21 | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
month | 536311 | 11 | 10 | 98047 | NaT | NaT | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
hour | 536311 | 24 | 09 | 50233 | NaT | NaT | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
weekday | 536311 | 7 | 6 | 86379 | NaT | NaT | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
buy_cnt | 536311.00 | NaN | NaN | NaN | NaT | NaT | 1.00 | 0.04 | 1.00 | 1.00 | 1.00 | 1.00 | 4.00 |
expense | 536311.00 | NaN | NaN | NaN | NaT | NaT | 214.73 | 306.48 | 0.00 | 24.98 | 99.51 | 289.33 | 11574.05 |
# date列有异常数据
df = df[df.date>'1970-01-01']
df.date.min()
Timestamp('2020-01-05 00:00:00')
备份并导出已处理数据
# 备份数据
df_backup = df.copy()
# 导入处理数据
df.to_csv('new_sales_report.csv')
2、建立RFM模型
查看数据分布情况
# 选择统计日为最后一日的后一天
cal_date = max(all_user.date) + dt.timedelta(days=1)
def cal_frequency(date):
return (cal_date-date.max()).days
#通过groupby语法对每一个用户进行分组聚合
rfm = all_user.groupby(['user_id']).agg({
'date': cal_frequency,
'order_id': 'count',
'expense': 'sum'
}).sort_index(ascending=True)
rfm.rename(columns = {'date': 'Recency',
'order_id': 'Frequency',
'expense': 'Monetary'}, inplace=True)
rfm.head()
Recency | Frequency | Monetary | |
---|---|---|---|
user_id | |||
1515915625439951872.00 | 136 | 1 | 416.64 |
1515915625440038400.00 | 25 | 2 | 56.43 |
1515915625440051712.00 | 6 | 13 | 7489.53 |
1515915625440099840.00 | 14 | 20 | 4929.86 |
1515915625440121600.00 | 131 | 2 | 182.83 |
#查看rfm数据分布
def data_distribution(keyvalue,data):
plt.figure(figsize = (18,4),dpi=600)
j=1
for i in keyvalue:
plt.subplot(1,3,j)
sns.distplot(data[i])
plt.title(i,fontsize = 15)
j+=1
keyvalue=['Recency', 'Frequency', 'Monetary']
data_distribution(keyvalue,rfm)
rfm.describe()
Recency | Frequency | Monetary | |
---|---|---|---|
count | 92755.00 | 92755.00 | 92755.00 |
mean | 99.59 | 5.77 | 1239.68 |
std | 54.53 | 27.14 | 4129.72 |
min | 1.00 | 1.00 | 0.00 |
25% | 62.00 | 1.00 | 148.11 |
50% | 102.00 | 2.00 | 456.89 |
75% | 128.00 | 4.00 | 1141.17 |
max | 322.00 | 1026.00 | 160604.07 |
对数据进行分箱处理
# 创建三个新的Column, 分别表示R,F,M的quntitle值
# 按照各个数值的1/4,1/2,3/4中位数进行数据分类
# 创建三个新的Column, 分别表示R,F,M的quntitle值
labels= list(range(1,5))
labels_reverse = list(range(4,0,-1))
Rquartiles = pd.cut(rfm['Recency'],bins=[0,62,102,128,322],labels=labels_reverse)
rfm = rfm.assign(R = Rquartiles.values)
Fquartiles = pd.cut(rfm['Frequency'],bins=[0,1,2,4,1026],labels=labels)
rfm = rfm.assign(F = Fquartiles.values)
Mquartiles = pd.cut(rfm['Monetary'],bins=[-1,148.11,456.89,1141.17,160605],labels=labels)
rfm = rfm.assign(M = Mquartiles.values)
rfm['RFM_Score'] = rfm[['R','F','M']].sum(axis=1)
labels=['general', 'sliver', 'gold', 'diamond']
RFM_Score=pd.cut(rfm['RFM_Score'],4,labels=labels)
rfm = rfm.assign(Category =RFM_Score.values)
rfm['Category'].value_counts().sort_index(ascending=True)
general 28335
sliver 22282
gold 18170
diamond 23968
Name: Category, dtype: int64
rfm.to_csv('RFM用户价值.csv')