前言
前一个数据集,我们分析了一元时间序列的预测,主要是基于lightgbm,季节性信号分解,以及采用prophet 的算法。 juejin.cn/post/684490…
这次我们来挑战一下多元时间序列的预测。相比于一元时间序列,多元时间序列无非是多了一些特征,这样我们就有更多的选择,因为我们可以采用回归的方法,而不是受限于经典的时间序列分析算法。
这次我们要分析的数据集同样来自kaggle,数据集名称叫做“predict future sales”。点击链接可以下载数据集。
简单介绍一下这个项目,这是kaggle目前还在进行的挑战赛,还有7个月就截止。 数据集来自于俄罗斯一家数码电商(1C Company)。很遗憾,里面的描述是俄语,我也不懂。
该项目的目标是:预测测试数据集中下一个月(2015年11月)的销售量
加载数据
我们将下载好的数据放在data 文件夹中,然后使用pandas 进行文件读取。
第一步,自然是导入需要的模块,这些都是经典的模块。
import pandas as pd
import lightgbm as lgb
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
import numpy as np
import matplotlib.pyplot as plt
列出文件清单,读取文件内容。这里有5个文件,我们一一解读它的内容,以及用法。
# read data
sales_train_file = 'data/sales_train.csv'
final_test_file = 'data/test.csv'
category_file = 'data/item_categories.csv'
item_file = 'data/items.csv'
shop_file = 'data/shops.csv'
raw_df = pd.read_csv(sales_train_file)
final_test_df = pd.read_csv(final_test_file)
category_df = pd.read_csv(category_file)
item_df = pd.read_csv(item_file)
shop_df = pd.read_csv(shop_file)
首先我们print raw_df 的内容,可以看到数据有6列。这个raw_df 就是我们用来训练模型的基本数据集。
date: 时间列,精度为天,也就是每天的情况
date_block_num: 按月进行递增的一个量,可以理解为从开始进行计数,每月加一
shop_id: 顾名思义,店铺的代码
item_id: 顾名思义,商品代码
item_price :商品价格
item_cnt_day:商品的日销售量
如果细心一点,可以看到item_cnt_day(日销售量)竟然还有负值,比如-1。 按照我的理解,有可能是两种情况,一种是损耗,一种是退货。
因为这两种都是有意义的信息,比如退货,它极有可能意味着商品有一些质量问题,销售量可能会有影响。
所以对于这种销售量的负值,我们这里认为是合理的,对模型有益的,不会进行数据清理。
print(raw_df.info())
print(raw_df.head(5))
结果如下:
我们接着看一下 测试数据final_test_file 。它竟然只有三列:
ID: 就是一个index,用于提交最终答案,没有意义
shop_id: 店铺代码
item_id:商品代码
我们再来快速浏览其他几个文件的内容。
print(category_df.head(5))
print(item_df.head(5))
print(shop_df.head(5))
预览如下:
全是俄语,庆幸的是header是英文。
category_df 包含两列:item_category_id 和item_category_name , 也就是对每个类别的ID 提供了描述。
item_df 包含了三列,item_name , item_id 和item_category_id。 从这个数据集中,我们可以了解每个item对应的描述,以及他们对应的商品类别。
shop_df 包含了两列,shop_name 和shop_id。 同样的,该文件只是对商铺ID添加了描述而已。
整理数据
我们需要理清当前的数据集情况与我们的预测目标:
- 我们需要预测的final_test_df里面的2015年11月的销售额
- final_test_df 里面feature只有两列(除去没用的ID列),shop_id 和item_id
- 但是训练数据集里面除了shop_id 和item_id 还有别的信息,比如价格
- 训练集中的销售额是每日数据,但是我们需要预测下一个月的数据,两者时间维度不同
- 另外还有一些信息在其他文件中,比如商铺描述,商品描述,商品类别
可以看到这里面有两处明显的不匹配:
- 训练集和测试集的特征不同,训练集明显有更多的特征
- 训练集中包含每天的销售数据,而我们需要预测的下一个月的销售数据
这里我们可能有多种选择:
- 特征选择shop_id 和item_id,预测每天的销售量,然后求和,得到每月的销售量
- 扩展测试集的特征,预测每天的销售量,然后求和,得到每月的销售量
- 特征选择shop_id 和item_id,先对数据进行月度求和,然后训练模型,预测每月的销量
- 扩展测试集的特征,使其和训练集的一致,先对数据进行月度求和,然后训练模型,预测每月的销量
最终我们选择第四个方案,因为我们希望能用到的特征越多越好,这些对我们的模型是有帮助的。另外,不建议预测每天的,然后累加成每月的销量,因为这样会累计误差。当然了,我们会做一些特征工程,因此最后的特征会更多。
由于商品的描述,商铺的描述,以及类别的描述都是俄语,我们这里先不用NLP进行处理。
合并训练文件
这里我们采用pandas merge 函数,将item_df 和 raw_df 进行合并。合并后的raw_df 我们再移除item_name列,因为我们已经有item_ID列。
我们将date 列的字符串转成timestamp格式,方便我们对时间进行处理。
# handle time data
dt_format = '%d.%m.%Y'
raw_df['date'] = pd.to_datetime(raw_df['date'], format=dt_format)
all_info_df = pd.merge(
left=raw_df,
right=item_df,
on=['item_id'],
how='left')
print(all_info_df.info())
all_info_df.drop(['item_name'], axis=1, inplace=True)
print(all_info_df.info())
print(all_info_df.columns)
结果如下:
创建时间特征
对于时间格式,我们可以提取更多的时间特征。比如对于本案例中的日期格式,我们可以提取月份,季度,年的信息。
我们不会提取周,日等更精确的信息,是因为我们需要预测是下一月的销售额,后面我们的数据中就不会有日期级别的信息。
将每日销售额合并成每月销售
每月销售可以按照date_block_num 来合并,因为这个数字就是代表某一个月。我们在时间序列特征中已经提取了年和月份信息,我们也可以通过这两个数据来进行合并: 将某年某月的所有销售数据求和,但是对于价格数据,我们需要做的是求均值。
代码如下:
sales_per_month = pd.pivot_table(
data=all_info_df, values=[ 'item_cnt_day', 'item_price'], index=[ 'shop_id', 'item_id', 'item_category_id'], columns=[ 'year', 'month'], aggfunc={
'item_cnt_day': 'sum', 'item_price': 'mean'})
# sales_per_month.fillna(value=0,inplace=True)
print(sales_per_month.info())
主要采用pandas pivot_table 函数,这里注意的是aggfunc 需要对价格和销售额做不同的算法。
这是一个multi_index 数据结构,可以看到列有三层:
- 第一层是item_cnt_day 和item_price
- 第一层的每一个项下面有第二层 year
- 每个year 下面有第三层month。
至于index ,也是三层:
- shop_id,
- item_id
- item_category_id。
另外还有一些NaN数据,表示没有销售额(或者是退市了,或者是没有上市,或者就是销售为0)。
所以我们需要数据清理,并且重置index,将index变成列。
sales_per_month.fillna(0, inplace=True)
sales_per_month.reset_index(inplace=True)
print(sales_per_month.info())
统一测试集和训练集的特征
到目前为止,我们已经解决了第一个问题,就是将每日销售额转成每月销售额。
训练数据已经变成了sales_per_month,测试数据依然为final_test_df。
我们来比较两者的数据shape。
print(sales_per_month.shape)
print(final_test_df.shape)
结果如下: `
(424124, 71)
(214200, 3)
我们发现我们的行数也不同,目前的测试集的每一行代表一个shop_id 和item_id的组合,训练集也是每一行代表一个shop_id 和item_id的组合。但是数目还是不同,这是因为训练集中有些商品已经退市了,在测试集中没有出现。测试集中也有一些新品,在训练集中自然不会出现。
于是我们需要合并两个数据集,注意这里并不是数据泄漏,因为该信息其实包含在item_df 和shop_df中。理论上每个shop 都可以售卖某个item。
所以你可以仅仅把测试集中的新品添加到训练集中,将销售额设为0..
这里我为了从实际项目的角度来出发,我建立了全矩阵,也就是包含所有的shop和item组合。这样即便有新的测试集,一样可以预测。
full_shop_item_matrix = pd.DataFrame([])
all_items = item_df[['item_id']]
for shop_id in shop_df['shop_id'].values:
all_items_per_shop = all_items.copy()
all_items_per_shop['shop_id'] = shop_id
if full_shop_item_matrix.shape[0] ==0:
full_shop_item_matrix = all_items_per_shop
else:
full_shop_item_matrix = pd.concat([full_shop_item_matrix, all_items_per_shop], axis=0)
print(full_shop_item_matrix.shape)
这里有一个trick,因为全矩阵的数据很庞大,内存小的电脑(小于12GB)很容易就崩溃。所以我们需要对数据类型机型downcast。将float64 变成float32,减少内存使用。
for col in list(sales_per_month.columns)[3:]:
sales_per_month[col] = sales_per_month[col].astype(np.float32)
print(sales_per_month.info())
我们将sales_per_month 扩展到所有组合。
sales_per_month = pd.merge(left=sales_per_month,right= full_shop_item_matrix,on=['shop_id','item_id'],how='right')
print(sales_per_month.info())
sales_per_month.drop([('item_category_id', '', '')], axis=1, inplace=True)
sales_per_month = pd.merge(left=sales_per_month,
right=item_df[['item_id',
'item_category_id']],
left_on=[('item_id',
'',
'')],
right_on=['item_id'],
how='right')
sales_per_month[('item_category_id', '', '')
] = sales_per_month['item_category_id']
sales_per_month.drop(['item_category_id', 'item_id'], inplace=True, axis=1)
print(sales_per_month.info())
sales_per_month.fillna(value=0, inplace=True)
print(sales_per_month.info())
结果如下,可以看到有1330200 种组合。
这样我们的训练集已经包含了一些特征,对于item和shop的组合,我们有以下特征:
- 2013年1月到2015年10月的价格信息
- 2013年1月到2015年10月的月度销售信息
- 产品类别
我们这里又有一些选择:
- 我们可以直接用2013年1月到2015年9月的信息作为feature,target为2015年10月的销售量。 这样我们可以直接训练模型。 然后用2013年2月到2015年10月的信息作为feature,预测2015年11月的销售,完成我们的预测任务。
- 我们还可以选择用更少的月份信息作为feature,比如2013年1月到2013年7月,来预测2013年7月的销售量。这样我们每6个月就有一组训练数据,滚动迭代,这样我们就可以有更多的训练数据
后记
由于文章篇幅过长,所以我们切分成两篇。第二篇,我们会进一步做特征工程,以及滚动melt新的测试特征,之后我们会用lightgbm建模预测销售额。