目录
介绍
以往数据分析中,都是根据提供的数据特征来构建模型,也就是说,数据集中会含有许多的特征列。本次数据分析将会介绍如何去处理另一种常见的数据,即时间序列数据。具体来说就是如何根据以往的销售额来预测未来短期内的销售额。
知识点
- 时间序列数据
- 数据预处理
未来销售额预测介绍
对于一个产品来说,其未来销售额的预测是一个重要的指标,也是一项重要的任务。例如,对于一部苹果手机来说。在上市之前,得先对销售额进行预测,才能确定出货量的大小。而本次数据分析的任务就是根据提供的数据,包含商品类别、商品名称、商店等信息和商品的历史销售数据来预测接下来一个月的销售额。
导入数据并预览
首先下载本次数据分析所用数据集。主页有该数据集
在sales数据集中,公共含有5份数据集,分别如下:
- sales_train_v2.csv:训练数据集,包含 2013 年到 2015 年的历史销售数据;
- item_categories.csv:销售商品的类别信息;
- items.csv:销售商品名称;
- shops.csv:销售商店的名称;
- test.csv:测试数据集,包含商品 ID 和商店 ID。
现在依次读取这5份数据:
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()
商品信息数据集
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$\times$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('date')
# 对列名重新命名
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.iloc[:, 0].astype(str), y=dates_temp.Total_Bills,
opacity=0.8, name='Total tickets')
trace1 = go.Scatter(x=dates_temp_sum.iloc[:, 0].astype(str), name="Total Amount",
y=dates_temp_sum['item_price'], opacity=0.8)
trace2 = go.Scatter(x=dates_temp_count.iloc[:, 0].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")
训练集和测试集有两列是相同的,即商店(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()
分析总结
本次数据分析主要讲述如何根据历史数据来预测未来,具体来说就是根据历史的销售数据来预测未来的销售额,在本次实验中,我们主要将其看做是一个回归任务,将其当做回归任务来实现,除了这种实现方式之外,还可以使用时间序列处理的方法来进行预测。