电商销售预测(1)--数据清理与融合

740 阅读8分钟

前言

前一个数据集,我们分析了一元时间序列的预测,主要是基于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))

结果如下:

image.png 我们接着看一下 测试数据final_test_file 。它竟然只有三列:

ID: 就是一个index,用于提交最终答案,没有意义

shop_id: 店铺代码

item_id:商品代码

image.png

我们再来快速浏览其他几个文件的内容。

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添加了描述而已。

image.png

整理数据

我们需要理清当前的数据集情况与我们的预测目标:

  • 我们需要预测的final_test_df里面的2015年11月的销售额
  • final_test_df 里面feature只有两列(除去没用的ID列),shop_id 和item_id
  • 但是训练数据集里面除了shop_id 和item_id 还有别的信息,比如价格
  • 训练集中的销售额是每日数据,但是我们需要预测下一个月的数据,两者时间维度不同
  • 另外还有一些信息在其他文件中,比如商铺描述,商品描述,商品类别

可以看到这里面有两处明显的不匹配:

  1. 训练集和测试集的特征不同,训练集明显有更多的特征
  2. 训练集中包含每天的销售数据,而我们需要预测的下一个月的销售数据

这里我们可能有多种选择:

  1. 特征选择shop_id 和item_id,预测每天的销售量,然后求和,得到每月的销售量
  2. 扩展测试集的特征,预测每天的销售量,然后求和,得到每月的销售量
  3. 特征选择shop_id 和item_id,先对数据进行月度求和,然后训练模型,预测每月的销量
  4. 扩展测试集的特征,使其和训练集的一致,先对数据进行月度求和,然后训练模型,预测每月的销量

最终我们选择第四个方案,因为我们希望能用到的特征越多越好,这些对我们的模型是有帮助的。另外,不建议预测每天的,然后累加成每月的销量,因为这样会累计误差。当然了,我们会做一些特征工程,因此最后的特征会更多。

由于商品的描述,商铺的描述,以及类别的描述都是俄语,我们这里先不用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)

结果如下:

image.png

创建时间特征

对于时间格式,我们可以提取更多的时间特征。比如对于本案例中的日期格式,我们可以提取月份,季度,年的信息。

我们不会提取周,日等更精确的信息,是因为我们需要预测是下一月的销售额,后面我们的数据中就不会有日期级别的信息。

image.png

将每日销售额合并成每月销售

每月销售可以按照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 需要对价格和销售额做不同的算法。

image.png

这是一个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 种组合

image.png

这样我们的训练集已经包含了一些特征,对于item和shop的组合,我们有以下特征:

  • 2013年1月到2015年10月的价格信息
  • 2013年1月到2015年10月的月度销售信息
  • 产品类别

我们这里又有一些选择:

  1. 我们可以直接用2013年1月到2015年9月的信息作为feature,target为2015年10月的销售量。 这样我们可以直接训练模型。 然后用2013年2月到2015年10月的信息作为feature,预测2015年11月的销售,完成我们的预测任务。
  2. 我们还可以选择用更少的月份信息作为feature,比如2013年1月到2013年7月,来预测2013年7月的销售量。这样我们每6个月就有一组训练数据,滚动迭代,这样我们就可以有更多的训练数据

后记

由于文章篇幅过长,所以我们切分成两篇。第二篇,我们会进一步做特征工程,以及滚动melt新的测试特征,之后我们会用lightgbm建模预测销售额。