分销商产品未来销售情况预测
介绍
前面的几个实验中,都是根据提供的数据特征来构建模型,也就是说,数据集中会含有许多的特征列。本次将会介绍如何去处理另一种常见的数据,即时间序列数据。具体来说就是如何根据以往的销售额来预测未来短期内的销售额。
知识点
- 时间序列数据
- 数据预处理
未来销售额预测介绍
对于一个产品来说,其未来销售额的预测是一个重要的指标,也是一项重要的任务。例如,对于一部苹果手机来说。在上市之前,得先对销售额进行预测,才能确定出货量的大小。
本次实验来源于 Kaggle 上的一个挑战,即: 未来销售额预测,由俄罗斯的 1C-Company 软件分销公司发起,并提供数据。
而本次实验的任务就是根据提供的数据,包含商品类别、商品名称、商店等信息和商品的历史销售数据来预测接下来一个月的销售额。
导入数据并预览
先下载数据,网盘链接:https://pan.baidu.com/s/1yU2zjF0H7BerSnYbmGGfhQ 提取码:o62g
在 Kaggle 提供的数据集 sales 中,公共含有 5 份数据集,分别如下:
- sales_train_v2.csv:训练数据集,包含 2013 年到 2015 年的历史销售数据;
- item_categories.csv:销售商品的类别信息;
- data/sales/items.csv:销售商品名称;
- data/sales/shops.csv:销售商店的名称;
- data/sales/test.csv:测试数据集,包含商品 ID 和商店 ID。
现在依次读取这五份数据集。
import pandas as pd
import warnings
warnings.filterwarnings("ignore")
df_train = pd.read_csv('./sales/sales_train_v2.csv')
df_categories = pd.read_csv("./sales/item_categories.csv")
df_items = pd.read_csv("./sales/items.csv")
df_shops = pd.read_csv("./sales/shops.csv")
df_test = pd.read_csv("./sales/test.csv")
查看一下,训练集。
df_train.head()
在训练集中,数据总的列名表示含义如下:
- data:时间信息;
- date_block_num:月数,例如:将 2013.2 看做是 1 月,2014.2 看作是 13 月;
- shop_id:商店的 ID;
- item_id:商品的 ID;
- item_price:商品的价格;
- item_cnt_day:上面每天的销售量。
现在来看一下,销售商品的类别信息。
df_categories.head()
item_category_name 表示类别名称,因为该数据集是俄罗斯的一家科技公司提供的,所以。数据集中的信息都是俄文。现在看一下商品的信息数据集。
df_items.head()
同样的方法,来看商店的信息。
df_shops.head()
为了方便处理,我们将这几份数据集合并为一个数据集。
df_train = pd.merge(df_train, df_items, on='item_id', how='inner')
df_train = pd.merge(df_train, df_categories,
on='item_category_id', how='inner')
df_train = pd.merge(df_train, df_shops, on='shop_id', how='inner')
df_test = pd.merge(df_test, df_items, on='item_id', how='inner')
df_test = pd.merge(df_test, df_categories, on='item_category_id', how='inner')
df_test = pd.merge(df_test, df_shops, on='shop_id', how='inner')
合并完成之后预览一下数据。
df_train.head()
数据可视化
上面我们完成了数据的导入和简单的处理,现在对数据进行可视化分析。
画出每天的销售量分布图。
import seaborn as sns
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
plt.figure(figsize=(14, 4))
# 这里使用了对数函数平滑,所以只选大于 0 的数据据
g = sns.distplot(
np.log(df_train[df_train['item_cnt_day'] > 0]['item_cnt_day']))
g.set_title("Item Sold Count Distribuition", fontsize=18)
g.set_ylabel("Frequency", fontsize=12)
画出销售价格分布图。这里为了更加直观,在绘图时,使用对数函数对商品价格进行平滑。
plt.figure(figsize=(14, 4))
# 这里使用了对数函数平滑,所以只选大于 0 的数据据
g2 = sns.distplot(np.log(df_train[df_train['item_price'] > 0]['item_price']))
g2.set_title("Items Price Log Distribuition", fontsize=18)
g2.set_xlabel("")
g2.set_ylabel("Frequency", fontsize=15)
我们可以使用商品的销售量乘以商品的价格得到商品一天总的营业额。
df_train['total_amount'] = df_train['item_price'] * df_train['item_cnt_day']
查看一下这商品价格、商品销售额、商品销售数据的分布情况,这里使用 四分位数 来描述。
def quantiles(df, columns):
for name in columns:
print(name + " quantiles")
# 打印出四分位数
print(df[name].quantile([.01, .25, .5, .75, .99]))
print("")
quantiles(df_train, ['item_cnt_day', 'item_price', 'total_amount'])
现在画出商店的分布情况,这里为了更加直观。我们使用 squarify 库提供的树地图来进行绘制。先安装该库。
import squarify
import random
# 指定颜色
color = ["#"+''.join([random.choice('0123456789ABCDEF')
for j in range(6)]) for i in range(30)]
# 获得数量最多的前 20 个商店。
shop = round((df_train["shop_name"].value_counts()[:20]
/ len(df_train["shop_name"]) * 100), 2)
plt.figure(figsize=(20, 10))
# 画出图形
g = squarify.plot(sizes=shop.values, label=shop.index,
value=shop.values,
alpha=.8, color=color)
g.set_title("'TOP 20 Stores/Shop - % size of total", fontsize=20)
g.set_axis_off()
plt.show()
在上图中,占的空格越大,表示该商店的数量越多,里面的数字表示所占的百分比。由于都是俄文,我们可以翻译一下左边三个商店的名字,加深我们对数据的理解。
- Химки ТЦ "Мега":希米“兆赫”中心
- Москва ТРК "Атриум":莫斯科电视台
- Москва ТЦ "Семеновский":莫斯科“谢苗诺夫斯基”贸易中心
现在我们可以查看一下,商店所有商品的价格情况。这里我们使用著名的绘图工具 plotly 来绘制。同样要先下载该工具库。。
import plotly.offline as offline
import plotly.graph_objs as go
import plotly.offline as py
from plotly.offline import init_notebook_mode, iplot
init_notebook_mode(connected=True)
offline.init_notebook_mode()
绘制出商店所有商品的价格情况。
temp = df_train.groupby('shop_name')['item_price'].sum()
# 画出柱状图
trace = [go.Bar(x=temp.index, y=temp.values,)]
# 设置图的字体颜色等
layout = go.Layout(
title="TOP 25 Shop Name by Total Amount Sold ",
yaxis=dict(title='Total Sold')
)
# 画出图形
fig = go.Figure(data=trace, layout=layout)
iplot(fig, filename='schoolStateNames')
从上图可看出,每个商店所销售的商品价格总和都是不一样的,差异非常明显。
同样的方法,我来看商店每天的销售情况。
temp = df_train.groupby('shop_name')['item_cnt_day'].sum()
# 画出柱状图
trace = [go.Bar(x=temp.index, y=temp.values,)]
# 设置图的字体颜色等
layout = go.Layout(
title="TOP 25 Shop Name by Total Amount Sold ",
yaxis=dict(title='Total Sold')
)
# 画出图形
fig = go.Figure(data=trace, layout=layout)
iplot(fig, filename='schoolStateNames')
商店每天的销售量与上面所绘制的价格分布类似。可能的原因是,前面所绘制的价格分布图是商店所有商品的价格总和,其值越高,意味着商品的数量越多,销量也就越高。
现在来看一下商品的种类信息。这里为了方便,只取数量最多的前十五个商品类别来进行观察。
# 获得商品类别计数
top_cats = df_train.item_category_name.value_counts()[:15]
plt.figure(figsize=(14, 4))
# 画出统计图
g1 = sns.countplot(x='item_category_name',
data=df_train[df_train.item_category_name.isin(top_cats.index)])
g1.set_xticklabels(g1.get_xticklabels(), rotation=70)
g1.set_title("TOP 15 Principal Products Sold", fontsize=22)
g1.set_xlabel("")
g1.set_ylabel("Count", fontsize=18)
同样我们来最高的三个类别,如下:
- Кино - DVD:DVD 电影
- Игры PC - Стандартные издания:PC 游戏标准版
- Музыка - CD локального производства:音乐 CD 本地生产
上面主要是查看哪种类别的商品最多,现在看一下哪种类别的商品卖得最好。
plt.figure(figsize=(14, 4))
# 画出箱线图
g2 = sns.boxplot(x='item_category_name', y='item_cnt_day',
data=df_train[df_train.item_category_name.isin(top_cats.index)])
g2.set_xticklabels(g2.get_xticklabels(), rotation=70)
g2.set_title("Principal Categories by Item Solds Log", fontsize=22)
g2.set_xlabel("")
g2.set_ylabel("Items Sold Log Distribution", fontsize=18)
上图为箱线图,可以看到,各种类型的商品的销售额基本上是差不多的。
现在来分析,数量最多的前二十五个商品的一些信息。绘制出其柱状图。
# 获得商品名的计数
count_item = df_train.item_name.value_counts()[:25]
plt.figure(figsize=(14, 4))
# 画出统计图
g = sns.countplot(
x='item_name', data=df_train[df_train.item_name.isin(count_item.index)])
g.set_xticklabels(g.get_xticklabels(), rotation=90)
g.set_title("Count of Most sold Items", fontsize=22)
g.set_xlabel('', fontsize=18)
g.set_ylabel("Total Count of ", fontsize=18)
从上图可知,大多数商品的数量是一致的,但有一个比较例外,即图中的倒数第二个,翻译如下:
- Фирменный пакет майка 1С Интерес белый (34××42) 45 мкм :迈克1c品牌套装白色(34*42)45微米
画出这些商品与总销售额的关系。
plt.figure(figsize=(14, 4))
# 画出箱线图
g1 = sns.boxplot(x='item_name', y='total_amount',
data=df_train[df_train.item_name.isin(count_item.index)])
g1.set_xticklabels(g1.get_xticklabels(), rotation=90)
g1.set_title("Count of Category Name in Top Bills", fontsize=22)
g1.set_xlabel('', fontsize=18)
g1.set_ylabel("Total Count in expensive bills", fontsize=18)
从上图可以看出,大多数商品的销售额是一致的。但在图中的最后一列,销售额明显要比其它产品高一点点,翻译如下:
- Sony PlayStation 4 (500 Gb) Black (CUH-1008A/1108A/B01):索尼PS 4(500 GB)黑色(CUH-1008A/1108A/B01)
前面我们主要分析了商品和商店等信息情况,现在查看一下销售量等信息与时间的关系。我们先将数据集中的 date 列转化为时间戳。
def date_process(df):
# 转化为时间戳
df["date"] = pd.to_datetime(df["date"], format="%d.%m.%Y")
df["_weekday"] = df['date'].dt.weekday # 获取周
df["_day"] = df['date'].dt.day # 获取天
df["_month"] = df['date'].dt.month # 获取月
return df
df_train = date_process(df_train)
df_train[["date", "_weekday", "_day", "_month"]].head()
从上面的结果可以看到,时间序列并不是按照顺序来的,先对其进行处理。
dates_temp = df_train['date'].value_counts().reset_index().sort_values('index')
# 对列名重新命名
dates_temp = dates_temp.rename(
columns={"date": "Total_Bills"}).rename(columns={"index": "date"})
dates_temp.head()
现在统计一下,每天的商品价格总和。
dates_temp_sum = df_train.groupby('date')['item_price'].sum().reset_index()
dates_temp_sum.head()
统计一下每天卖出去的商品的数量。
dates_temp_count = df_train[df_train['item_cnt_day'] > 0].groupby(
'date')['item_cnt_day'].sum().reset_index()
dates_temp_count.head()
为了更加直观,对上面的统计结果用图形绘制出来。
# 定义图形
trace0 = go.Scatter(x=dates_temp.date.astype(str), y=dates_temp.Total_Bills,
opacity=0.8, name='Total tickets')
trace1 = go.Scatter(x=dates_temp_sum.date.astype(str), name="Total Amount",
y=dates_temp_sum['item_price'], opacity=0.8)
trace2 = go.Scatter(x=dates_temp_count.date.astype(str), name="Total Items Sold",
y=dates_temp_count['item_cnt_day'], opacity=0.8)
# 设置标题等参数
layout = dict(
title="Informations by Date",
xaxis=dict(rangeselector=dict(buttons=list([
dict(count=1, label='1m', step='month', stepmode='backward'),
dict(count=3, label='3m', step='month', stepmode='backward'),
dict(count=6, label='6m', step='month', stepmode='backward'),
dict(step='all')])),
rangeslider=dict(visible=True), type='date'))
# 画出图形
fig = dict(data=[trace0, trace1, trace2], layout=layout)
iplot(fig)
我们可以看到,在 2014 年 1 月的某天和 2015 年 1 月的某天。销售额都达到了顶峰。可能的原因是在这一段时间俄罗斯可能有某个特殊的节日,例如像我们国家的双 11。因此销售额提升了。
下面再来看一下,每个月的总销售量。
temp = df_train.groupby(['_month'])['item_cnt_day'].sum()
# 画出柱状图
trace = [go.Bar(x=temp.index, y=temp.values,)]
# 设置图的字体颜色等
layout = go.Layout(
title="Total orders by Month",
xaxis=dict(title='Months'),
yaxis=dict(title='Total Orders')
)
# 画出图形
fig = go.Figure(data=trace, layout=layout)
iplot(fig, filename='schoolStateNames')
从上图可以看到,1 月和 12 月的销售量都相对较高。
特征工程
上面我们主要完成了对数据的可视化分析,先来对数据进行特征提取。让我们重新读取数据。
# 重新导入数据
df_train = pd.read_csv("./sales/sales_train_v2.csv")
df_test = pd.read_csv("./sales/test.csv")
现在查看训练集和测试集的形状。
df_train.shape, df_test.shape
可能你已经忘了,现在再来重新查看训练集。
df_train.head()
同样,现在来查看测试集。
df_test.head()
训练集和测试集有两列是相同的,即商店(shop_id)和商品(item_id)。
现在对时间信息进行重新提取,只提取年和月。
df_train['date'] = pd.to_datetime(df_train['date'], format='%d.%m.%Y')
df_train['month'] = df_train['date'].dt.month
df_train['year'] = df_train['date'].dt.year
删除掉原来的 data 列,为了方便处理,同时也删除掉 item_price 列。
df_train1 = df_train.drop(['date', 'item_price'], axis=1)
我们提取每个月的销售总量。
# 求出商品每个月的销售总量
df_train1 = df_train1.groupby([c for c in df_train1.columns if c not in [
'item_cnt_day']], as_index=False)[['item_cnt_day']].sum()
df_train2 = df_train1.rename(columns={'item_cnt_day': 'item_cnt_month'})
df_train2.head()
我们对某个商店的某个商品在每个月的销量取平均值。
shop_item_monthly_mean = df_train2[['shop_id', 'item_id', 'item_cnt_month']].groupby(
['shop_id', 'item_id'], as_index=False)[['item_cnt_month']].mean()
shop_item_monthly_mean1 = shop_item_monthly_mean.rename(
columns={'item_cnt_month': 'item_cnt_month_mean'})
shop_item_monthly_mean1.head()
将上面所提取到的平均值合并到训练数据集中,作为一个新的特征列。
df_train3 = pd.merge(df_train2, shop_item_monthly_mean1,
how='left', on=['shop_id', 'item_id'])
df_train3.head()
在我们的训练数据集中,为 2013 年 1 月到 2015 年 10 月的历史数据,而现在我们要预测的是 2015 年 11 月的数据。因此在测试集中创建相关的特征列。
df_test['month'] = 11
df_test['year'] = 2015
df_test['date_block_num'] = 34
df_test.head()
同样把上面所提取的某个商店的某个商品在每个月的销量取平均值合并到测试集中。
df_test1 = pd.merge(df_test, shop_item_monthly_mean1,
how='left', on=['shop_id', 'item_id'])
df_test1.head()
从上面的结果可以看到,数据中存在缺失值,可能是原因是,测试集中的商品在训练集中不存在,所以为缺失值。现在对这些缺失值用 0 来填充。
df_test1 = df_test1.fillna(0.)
df_test1.head()
构建模型
在构建模型之前,我们先对训练集进行划分,取一部分来测试一下我们的模型,这样方便我们对模型的调试。当我们把模型调试好时,再用全部的数据进行训练,然后对测试集进行预测。
feature_list = [c for c in df_train3.columns if c not in 'item_cnt_month']
x1 = df_train3[df_train3['date_block_num'] < 33]
y1 = np.log1p(x1['item_cnt_month'].clip(0., 20.))
x1 = x1[feature_list]
x2 = df_train3[df_train3['date_block_num'] == 33]
y2 = np.log1p(x2['item_cnt_month'].clip(0., 20.))
x2 = x2[feature_list]
构建模型,这里同样我们使用前面几个实验所用到的随机森林来构建。
from sklearn.ensemble import RandomForestRegressor
# 定义模型
model = RandomForestRegressor(n_estimators=20,
random_state=42,
max_depth=5,
n_jobs=-1)
model.fit(x1, y1) # 训练模型
模型训练完成之后,使用前面对训练集划分出来的一部分验证集进行测试。看一下我们构建的模型的情况。
plt.figure(figsize=(14, 4))
y_pred = model.predict(x2)
plt.plot(y_pred[:100])
plt.plot(y2.values[:100])
从上面的结果可以看出,所构建的随机森林模型预测效果并不是很好,可能的原因是,我们没有对其进行调参。
接下来,我们对所有的训练数据集进行训练,然后对测试集进行预测。
model.fit(df_train3[feature_list], df_train3['item_cnt_month'].clip(0., 20.))
df_test1['item_cnt_month'] = model.predict(
df_test1[feature_list]).clip(0., 20.)
df_test1.head()
总结
本次主要讲述如何根据历史数据来预测未来,具体来说就是根据历史的销售数据来预测未来的销售额,在本次实验中,我们主要将其看做是一个回归任务,将其当做回归任务来实现,除了这种实现方式之外,还可以使用时间序列处理的方法来进行预测。如果你感兴趣可以参考 Kaggle 上的内容。