数据来源:Baby Goods Info Data
读取数据
import numpy as np
import pandas as pd
from pyecharts.charts import Pie,Bar,Page
from pyecharts import options as opts
import matplotlib.pyplot as plt
filename = r'train.csv' #联结年龄和性别后的表
filename1 = r'trade_history.csv'
data = pd.read_csv(filename)
data1 = pd.read_csv(filename1)
test = data.copy()
test1 = data1.copy()
销量曲线
# 汇总销量
month_summary = test['buy_mount'].groupby([test['year'],test['month']]).sum()
# x 轴
func = lambda x:str(x[0])+"-"+str(x[1])
x_index = list(map(func,month_summary.index))
month_summary_line = (Line()
.add_xaxis(x_index)
.add_yaxis('销量',month_summary.values.tolist(),
is_smooth=True,label_opts=opts.LabelOpts(is_show=False),
markpoint_opts=opts.MarkPointOpts(data=[opts.MarkPointItem(type_="max")]))
.set_global_opts(xaxis_opts=opts.AxisOpts(type_="category", boundary_gap=False,
axislabel_opts={'rotate':90}))
.set_series_opts(markarea_opts=opts.MarkAreaOpts(
data=[
opts.MarkAreaItem(name="2012", x=('2012-7', '2013-1'),
itemstyle_opts=opts.ItemStyleOpts(color='red',opacity=0.4)),
opts.MarkAreaItem(name="2013", x=('2013-1', '2014-1'),
itemstyle_opts=opts.ItemStyleOpts(color='blue',opacity=0.4)),
opts.MarkAreaItem(name="2014", x=('2014-1', '2015-1'),
itemstyle_opts=opts.ItemStyleOpts(color='yellow',opacity=0.4)),
opts.MarkAreaItem(name="2015", x=('2015-1', '2015-2'),
itemstyle_opts=opts.ItemStyleOpts(color='green',opacity=0.4)),
]
)
)
.render_notebook())
# 在设置 MarkAreaItem 的时候,标记区域的颜色可以手动,即 ItemStyleOpts 中的 color
# 自定义颜色时需要连着设置透明度参数 opacity,否则标记区域的色块会挡住曲线
计算可知,从2012-7到2015-1(2月数据只有5天)销量提升接近5倍。
尝试用 Holt-Winters 法拟合这条时间序列曲线
index = pd.date_range(start='2012-7',periods=32,freq='BM') # 构建 x 轴
data = pd.DataFrame(month_summary.values,index=index,columns=['销量'])
data.drop(data.index[-1],axis=0,inplace=True) # 去除2月份的数据
from statsmodels.tsa.holtwinters import ExponentialSmoothing
fit1 = ExponentialSmoothing(data, seasonal_periods=4, trend='add', seasonal='add').fit(use_boxcox=True)
fit2 = ExponentialSmoothing(data, seasonal_periods=4, trend='add', seasonal='mul').fit(use_boxcox=True)
fit3 = ExponentialSmoothing(data, seasonal_periods=4, trend='add', seasonal='add', damped=True).fit(use_boxcox=True)
fit4 = ExponentialSmoothing(data, seasonal_periods=4, trend='add', seasonal='mul', damped=True).fit(use_boxcox=True)
data['fit1'] = fit1.fittedvalues
data['fit2'] = fit2.fittedvalues
data['fit3'] = fit3.fittedvalues
data['fit4'] = fit4.fittedvalues
# 分块展示,共享 x,y 轴
fig,axes = plt.subplots(2,2,sharex=True,sharey=True)
x_index = np.arange(31)
axes[0, 0].plot(x_index,data.iloc[:,[0,1]])
axes[0, 1].plot(x_index,data.iloc[:,[0,2]])
axes[1, 0].plot(x_index,data.iloc[:,[0,3]])
axes[1, 1].plot(x_index,data.iloc[:,[0,4]])
#去掉 subplot 周围的间距
plt.subplots_adjust(wspace=0,hspace=0)
plt.show()
# 计算 MAE
data['mse1'] = (abs(data["销量"]-data["fit1"]))
data['mse2'] = (abs(data["销量"]-data["fit2"]))
data['mse3'] = (abs(data["销量"]-data["fit3"]))
data['mse4'] = (abs(data["销量"]-data["fit4"]))
print(data.mean()[-4:])
# mse1 555.499377
# mse2 557.494903
# mse3 481.261358
# mse4 455.901166
- 从 MAE 来看,拟合效果最好的是第四条曲线,在尝试去掉几个异常值后拟合效果会更好一点。
将年份拆分出来:
year_line = Line().add_xaxis(['一月','二月','三月','四月','五月',
'六月','七月','八月','九月','十月','十一月','十二月'])
for i in range(4):
year_line.add_yaxis(str(i+2012),
stack='销量',
y_axis=month_summary1.iloc[i].values.tolist(),
is_smooth=True
)
year_line.set_global_opts(
tooltip_opts=opts.TooltipOpts(trigger="axis"),
yaxis_opts=opts.AxisOpts(
type_="value",
axistick_opts=opts.AxisTickOpts(is_show=True),
splitline_opts=opts.SplitLineOpts(is_show=True),
),
xaxis_opts=opts.AxisOpts(type_="category", boundary_gap=False),
)
year_line.render_notebook()
- 从时间线完整的2013年,2014年来看,月份-销量的波动趋势基本一致,六月份之后七月份开始整体的销量会有明显的上升,双11节点会迎来一次较高的涨幅。数据真实的情况下,2014-11的销量环比增长近3倍,同比增长4倍。
- 双11大促对于销量的提升效果还是很恐怖的,同时观察双11前后两个月的销量,10月和12月的销量基本处于正常波动范围内,也就是说双11销量提升的主要原因并不是来源于前期销量的延迟和后期销量的透支。
用户
# 性别
gender_pie = Pie().add('gender',
[(str(i),j) for i,j in enumerate(test['gender'].value_counts())])
# str(i) 索引中含有数字0,需要转成字符串,否则pyecharts识别不出来
- 从图中可以看出男女所占比例基本一致,0代表女孩,1代表男孩,2未知
# 年龄
age_count = test['age'].value_counts()
age_bar = (Bar().add_xaxis(age_count.index.to_list())
.add_yaxis('age',age_count.values.tolist())
.render_notebook())
# .index DataFrame 类型,.values ndarray 类型,两者转列表的方式不一样
年龄跨度从-3岁到28岁,分布过于分散。将数据重新离散化,不考虑数据质量的可靠性,直接划分为1岁以前(包含未出生),1-3岁,4-6岁,>6岁。
def func(x):
if x <= 0:
return 0
elif x <= 3:
return 1
elif x <= 6:
return 2
elif x > 6:
return 3
test['age'] = test['age'].apply(func)
- 划分后的年龄分布如下图,年龄主要分布在4岁以前,占比86.5%。在数据真实可靠的前提下,合理怀疑三岁以后的分布可能是家里的二胎。
商品类目和年龄,性别交叉分析
# 类目-性别
cat_gender = pd.pivot_table(test,index=['cat1'],columns=['gender'],
values=['user_id'],aggfunc=lambda x:len(x.unique()))
# 这里以去重用户作为统计值
类目/性别 | 0 | 1 | 2 |
---|---|---|---|
28 | 92.0 | 92.0 | 10.0 |
38 | 24.0 | 21.0 | 1.0 |
50008168 | 216.0 | 173.0 | 4.0 |
50014815 | 97.0 | 87.0 | 10.0 |
50022520 | 37.0 | 40.0 | 0.0 |
122650008 | 26.0 | 25.0 | 1.0 |
做一个独立性检验
from scipy import stats
stats.chi2_contingency(cat_gender.iloc[:,:2])
- 计算
p
值为 0.775 > 0.05,说明性别在这6个类目的选择上是相互独立的,推测是因为这6个类目的划分上不存在性别上的区别。
# 类目-性别-销量
cat_gender_qsum = pd.pivot_table(test,index=['cat1'],columns=['gender'],
values=['buy_mount'],aggfunc='sum')
c_bar = (Bar().add_xaxis(cat_gender_qsum.index.to_list())
.add_yaxis('女',cat_gender_qsum.iloc[:,0].values.tolist())
.add_yaxis('男',cat_gender_qsum.iloc[:,1].values.tolist())
.render_notebook())
- 就图表中的数据显示,男女在各类目下的购买量存在差异,但是考虑到数据集的大小,差异并不明显,需要更多的数据来做结论支撑。
# 类目-年龄
cat_age = pd.pivot_table(test,index=['cat1'],columns=['age'],
values=['buy_mount'],aggfunc='sum',fill_value=0)
cat_age_bar = Bar().add_xaxis(cat_age.index.to_list())
for i in range(4):
cat_age_bar.add_yaxis(str(i),cat_age.iloc[:,i].values.tolist())
类目/年龄 | 0 | 1 | 2 | 3 |
---|---|---|---|---|
28 | 145 | 115 | 32 | 5 |
38 | 38 | 97 | 13 | 0 |
50008168 | 151 | 208 | 73 | 19 |
50014815 | 359 | 125 | 11 | 1 |
50022520 | 81 | 9 | 2 | 0 |
122650008 | 9 | 32 | 11 | 2 |
- 销量排名:50014815 > 50008168 > 28 > 38 > 50022520 > 122650008
- 各类目的销量主要集中在4岁之前,但是销量在不同年龄段之间存在明显的变动,尤其是在前两个年龄段之间。推测这可能跟商品属性有关,母婴对某类商品的需求是在随婴儿年龄的增长而快速变化的。
商品
类目销量
page = Page(layout=Page.DraggablePageLayout)
for i in cat_age.index:
df = pd.pivot_table(test1[test1['cat1']==i],index=['cat_id'],
values=['buy_mount'],aggfunc='sum')
df = df.sort_values(['buy_mount'],ascending=False)
# df.values矩阵的维度(17,1),需要转换维度
bar = (Bar().add_xaxis(df.index.to_list())
.add_yaxis(str(i),df.values.reshape(17).tolist())
.set_series_opts(label_opts=opts.LabelOpts(is_show=False))
.set_global_opts(toolbox_opts=opts.ToolboxOpts())
)
page.add(bar)
商品销量
# 商品连带率
upt = pd.pivot_table(test1,index=['cat_id'],values=['buy_mount','user_id'],
aggfunc={'buy_mount':'sum','user_id':lambda x:len(x.unique())})
upt['upt'] = (upt['buy_mount']/upt['user_id']).round(2)
upt.sort_values(by=['upt'],ascending=False,inplace=True)
# 按平均购买人数和连带率将商品分级
user_avg = upt.mean()[1] # 平均购买人数,值为 45
def rank(r):
x = r['user_id']
y = r['buy_mount']
if x>=45 and y>=45:
return "A"
elif x<45 and y>=45:
return "B"
else:
return "C"
upt['rank'] = upt.apply(rank,axis=1)
# 可视化
# 两列数据基数相差太大,用 log 函数处理方便画图
ndata = upt.iloc[:,:2].applymap(np.log).round(2)
ndata['rank'] = upt['rank']
# 矩阵图
threshold = ndata.groupby(['rank'])['user_id'].max()[1]
plt.xlim(0,9)
plt.ylim(0,10)
plt.scatter(ndata['user_id'][ndata['rank']=="A"],ndata['buy_mount'][ndata['rank']=="A"],c='b',s=8)
plt.scatter(ndata['user_id'][ndata['rank']=="B"],ndata['buy_mount'][ndata['rank']=="B"],c='y',s=8)
plt.scatter(ndata['user_id'][ndata['rank']=="C"],ndata['buy_mount'][ndata['rank']=="C"],c='c',s=8)
plt.vlines(threshold,ymin=0,ymax=10,color='r',linewidth=1)
plt.hlines(threshold,xmin=0,xmax=9,color='r',linewidth=1)
- 各类目细分下的商品销量分布都是很明显的长尾分布,细分产品越多则越明显。
- 将商品按平均购买人数和连带率为阈值划分商品。
- A类商品位为右上角,购买人数多且连带率大于1,可打造成爆款做引流。
- B类商品位于左上角,购买人数低但连带率大于1,且普遍大于A类商品,可尝试提高曝光率或分析购买用户画像做精准推荐。
- C类商品位于左下角,购买人数低于平均水平,需要进一步提炼买点。若存在库存成本压力,可考虑精简下架。