1. 关于项目
1.1 背景介绍
这是Kaggle竞赛上的一个项目。项目数据由俄罗斯最大的软件公司之一的 1C Company 提供。数据集包含了2013年1月1日到2015年10月31日该公司各商店的商品销售记录。
项目目标是预测该公司接下来2015年11月的商品销量。
项目得分使用RMSD(均方根误差,即得分越低代表预测结果的误差越小,预测效果越好。)进行评估。
项目提交的预测值范围需要在[0,20]。
项目链接:https://www.kaggle.com/c/competitive-data-science-predict-future-sales
1.2项目数据集说明
数据集 | 说明 |
---|---|
sales_train.csv | 训练集 (包含2013年1月至2015年10月间每天各商店各商品的销量) |
items.csv | 商品的补充数据集 (包含商品名称和所属分类字段) |
item_categories.csv | 商品类目的补充数据集 (包含商品所属类目的详细信息) |
shops.csv | 商店信息的补充数据集 (包含商店所在城市和商店规模的信息) |
shops.csv test.csv | 测试集 (需要预测的接下来的2015年11月份的各商店的商品销量) |
sample_submission.csv | 项目提交数据的模板 |
2. 目标
分析该公司的经营状况,找出影响商品销量的相关因素,预测未来一个月该公司各商店不同商品的销量。
3. 数据预处理
3.1 项目数据集预处理
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()
3.1.1 训练集和测试集
train = pd.read_csv('sales_train.csv')
test= pd.read_csv('test.csv')
train.head()
date | date_block_num | shop_id | item_id | item_price | item_cnt_day | |
---|---|---|---|---|---|---|
0 | 02.01.2013 | 0 | 59 | 22154 | 999.00 | 1.0 |
1 | 03.01.2013 | 0 | 25 | 2552 | 899.00 | 1.0 |
2 | 05.01.2013 | 0 | 25 | 2552 | 899.00 | -1.0 |
3 | 06.01.2013 | 0 | 25 | 2554 | 1709.05 | 1.0 |
4 | 15.01.2013 | 0 | 25 | 2555 | 1099.00 | 1.0 |
test.head()
ID | shop_id | item_id | |
---|---|---|---|
0 | 0 | 5 | 5037 |
1 | 1 | 5 | 5320 |
2 | 2 | 5 | 5233 |
3 | 3 | 5 | 5232 |
4 | 4 | 5 | 5268 |
print('训练集的商店数量: %d ,商品数量: %d;\n' % (train['shop_id'].unique().size, train['item_id'].unique().size),
'测试的商店数量: %d,商品数量: %d。' % (test['shop_id'].unique().size, test['item_id'].unique().size))
训练集的商店数量: 60 ,商品数量: 21807;
测试的商店数量: 42,商品数量: 5100。
test[~test['shop_id'].isin(train['shop_id'].unique())]
ID | shop_id | item_id |
---|
test[~test['item_id'].isin(train['item_id'].unique())]['item_id'].unique()[:10]
array([5320, 5268, 5826, 3538, 3571, 3604, 3407, 3408, 3405, 3984],
dtype=int64)
测试集里面出现了训练集没有的商品,但是可以根据商店的营销情况和商品类目进行预测。如果直接设置为0的话,反而会使提交的数据成绩减分。这个估计也是要考察模型的泛化能力。
3.1.2 商店数据集
shops = pd.read_csv('shops.csv')
shops.head()
shop_name | shop_id | |
---|---|---|
0 | !Якутск Орджоникидзе, 56 фран | 0 |
1 | !Якутск ТЦ "Центральный" фран | 1 |
2 | Адыгея ТЦ "Мега" | 2 |
3 | Балашиха ТРК "Октябрь-Киномир" | 3 |
4 | Волжский ТЦ "Волга Молл" | 4 |
经过谷歌翻译和百度翻译得知这个是俄罗斯的语言。其中有几个相同商店名称但是不同ID的店铺
- 39号: РостовНаДону ТРК "Мегацентр Горизонт"
- 40号: РостовНаДону ТРК "Мегацентр Горизонт" Островной
上面这两个商店名,差别在最后一个单词,翻译是“岛”,但是在谷歌地图中查找俄罗斯包含“Мегацентр Горизонт”这个名字的购物中心只有РостовНаДону ТРК这个地方上有且只有一个。推测这两个是同一个商店不同叫法。
- 10号: Жуковский ул. Чкалова 39м?
- 11号: Жуковский ул. Чкалова 39м2
这两个推测是书写不一致导致的。
- 0号: !Якутск Орджоникидзе, 56 фран
- 57号: !Якутск Орджоникидзе, 56
这两个也是书写的不一致的问题,类似某某街道56,和某某街道56号的区别。
- 58号:Якутск ТЦ "Центральный"
- 1号: !Якутск ТЦ "Центральный" фран
同上。
- 12 和 56 是线上商店
# 查看测试集是否包含了这几个商店
test[test['shop_id'].isin([39, 40, 10, 11, 0, 57, 58, 1, 12 ,56])]['shop_id'].unique()
array([10, 12, 57, 58, 56, 39], dtype=int64)
测试集中没有包含同一商店的不同ID, 需要对训练集重复商店的不同ID进行修改,修改的ID则以测试集为准。
shop_id_map = {
11: 10, 0: 57, 1: 58, 40: 39}
train.loc[train['shop_id'].isin(shop_id_map), 'shop_id'] = train.loc[train['shop_id'].isin(shop_id_map), 'shop_id'].map(shop_id_map)
train.loc[train['shop_id'].isin(shop_id_map), 'shop_id']
Series([], Name: shop_id, dtype: int64)
train.loc[train['shop_id'].isin([39, 40, 10, 11, 0, 57, 58, 1]), 'shop_id'].unique()
array([57, 58, 10, 39], dtype=int64)
对商店名称进行简单分析后,发现商店名称有一个命名规律
大部分商店的名称:
- 开头是一个地区的名称;
- 中间是商店的规模(比如购物中心:ТЦ、大型购物娱乐中心:ТРЦ等);
- 尾部带引号的是商店的名称,比如‘xxx’购物中心,大部分可以在谷歌地图上搜索到。
shops['shop_city'] = shops['shop_name'].map(lambda x:x.split(' ')[0].strip('!'))
shop_types = ['ТЦ', 'ТРК', 'ТРЦ', 'ТК', 'МТРЦ']
shops['shop_type'] = shops['shop_name'].map(lambda x:x.split(' ')[1] if x.split(' ')[1] in shop_types else 'Others')
shops.loc[shops['shop_id'].isin([12, 56]), ['shop_city', 'shop_type']] = 'Online' # 12和56号是网上商店
shops.head(13)
shop_name | shop_id | shop_city | shop_type | |
---|---|---|---|---|
0 | !Якутск Орджоникидзе, 56 фран | 0 | Якутск | Others |
1 | !Якутск ТЦ "Центральный" фран | 1 | Якутск | ТЦ |
2 | Адыгея ТЦ "Мега" | 2 | Адыгея | ТЦ |
3 | Балашиха ТРК "Октябрь-Киномир" | 3 | Балашиха | ТРК |
4 | Волжский ТЦ "Волга Молл" | 4 | Волжский | ТЦ |
5 | Вологда ТРЦ "Мармелад" | 5 | Вологда | ТРЦ |
6 | Воронеж (Плехановская, 13) | 6 | Воронеж | Others |
7 | Воронеж ТРЦ "Максимир" | 7 | Воронеж | ТРЦ |
8 | Воронеж ТРЦ Сити-Парк "Град" | 8 | Воронеж | ТРЦ |
9 | Выездная Торговля | 9 | Выездная | Others |
10 | Жуковский ул. Чкалова 39м? | 10 | Жуковский | Others |
11 | Жуковский ул. Чкалова 39м² | 11 | Жуковский | Others |
12 | Интернет-магазин ЧС | 12 | Online | Online |
# 对商店信息进行编码,降低模型训练的内存消耗
shop_city_map = dict([(v,k) for k, v in enumerate(shops['shop_city'].unique())])
shop_type_map = dict([(v,k) for k, v in enumerate(shops['shop_type'].unique())])
shops['shop_city_code'] = shops['shop_city'].map(shop_city_map)
shops['shop_type_code'] = shops['shop_type'].map(shop_type_map)
shops.head(7)
shop_name | shop_id | shop_city | shop_type | shop_city_code | shop_type_code | |
---|---|---|---|---|---|---|
0 | !Якутск Орджоникидзе, 56 фран | 0 | Якутск | Others | 0 | 0 |
1 | !Якутск ТЦ "Центральный" фран | 1 | Якутск | ТЦ | 0 | 1 |
2 | Адыгея ТЦ "Мега" | 2 | Адыгея | ТЦ | 1 | 1 |
3 | Балашиха ТРК "Октябрь-Киномир" | 3 | Балашиха | ТРК | 2 | 2 |
4 | Волжский ТЦ "Волга Молл" | 4 | Волжский | ТЦ | 3 | 1 |
5 | Вологда ТРЦ "Мармелад" | 5 | Вологда | ТРЦ | 4 | 3 |
6 | Воронеж (Плехановская, 13) | 6 | Воронеж | Others | 5 | 0 |
3.1.3 商品数据集
items = pd.read_csv('items.csv')
items
item_name | item_id | item_category_id | |
---|---|---|---|
0 | ! ВО ВЛАСТИ НАВАЖДЕНИЯ (ПЛАСТ.) D | 0 | 40 |
1 | !ABBYY FineReader 12 Professional Edition Full... | 1 | 76 |
2 | ***В ЛУЧАХ СЛАВЫ (UNV) D | 2 | 40 |
3 | ***ГОЛУБАЯ ВОЛНА (Univ) D | 3 | 40 |
4 | ***КОРОБКА (СТЕКЛО) D | 4 | 40 |
... | ... | ... | ... |
22165 | Ядерный титбит 2 [PC, Цифровая версия] | 22165 | 31 |
22166 | Язык запросов 1С:Предприятия [Цифровая версия] | 22166 | 54 |
22167 | Язык запросов 1С:Предприятия 8 (+CD). Хрустале... | 22167 | 49 |
22168 | Яйцо для Little Inu | 22168 | 62 |
22169 | Яйцо дракона (Игра престолов) | 22169 | 69 |
22170 rows × 3 columns
# 数据集比较大,只分析有没有重复名称不同ID的商品
items['item_name'] = items['item_name'].map(lambda x: ''.join(x.split(' '))) # 删除空格
duplicated_item_name = items[items['item_name'].duplicated()]
duplicated_item_name
item_name | item_id | item_category_id | |
---|---|---|---|
2558 | DEEPPURPLEComeHellOrHighWaterDVD | 2558 | 59 |
2970 | Divinity:DragonCommander[PC,Цифроваяверсия] | 2970 | 31 |
5063 | NIRVANAUnpluggedInNewYorkLP | 5063 | 58 |
14539 | МЕНЯЮЩИЕРЕАЛЬНОСТЬ(регион) | 14539 | 40 |
19475 | СтругацкиеА.иБ.Улитканасклоне(mp3-CD)(Jewel) | 19475 | 43 |
19581 | ТАРЗАН(BD) | 19581 | 37 |
duplicated_item_name_rec = items[items['item_name'].isin(duplicated_item_name['item_name'])] # 6个商品相同名字不同id的记录
duplicated_item_name_rec
item_name | item_id | item_category_id | |
---|---|---|---|
2514 | DEEPPURPLEComeHellOrHighWaterDVD | 2514 | 59 |
2558 | DEEPPURPLEComeHellOrHighWaterDVD | 2558 | 59 |
2968 | Divinity:DragonCommander[PC,Цифроваяверсия] | 2968 | 31 |
2970 | Divinity:DragonCommander[PC,Цифроваяверсия] | 2970 | 31 |
5061 | NIRVANAUnpluggedInNewYorkLP | 5061 | 58 |
5063 | NIRVANAUnpluggedInNewYorkLP | 5063 | 58 |
14537 | МЕНЯЮЩИЕРЕАЛЬНОСТЬ(регион) | 14537 | 40 |
14539 | МЕНЯЮЩИЕРЕАЛЬНОСТЬ(регион) | 14539 | 40 |
19465 | СтругацкиеА.иБ.Улитканасклоне(mp3-CD)(Jewel) | 19465 | 43 |
19475 | СтругацкиеА.иБ.Улитканасклоне(mp3-CD)(Jewel) | 19475 | 43 |
19579 | ТАРЗАН(BD) | 19579 | 37 |
19581 | ТАРЗАН(BD) | 19581 | 37 |
【依旧是查看测试里面包含了哪一些重复项】
test[test['item_id'].isin(duplicated_item_name_rec['item_id'])]['item_id'].unique()
array([19581, 5063], dtype=int64)
【测试集包含了2个同名不同id的商品。且都是较大的ID值。需要把训练集里小的ID值都映射为对应较大的ID值。】
old_id = duplicated_item_name_rec['item_id'].values[::2]
new_id = duplicated_item_name_rec['item_id'].values[1::2]
old_new_map = dict(zip(old_id, new_id))
old_new_map
{2514: 2558, 2968: 2970, 5061: 5063, 14537: 14539, 19465: 19475, 19579: 19581}
train.loc[train['item_id'].isin(old_id), 'item_id'] = train.loc[train['item_id'].isin(old_id), 'item_id'].map(old_new_map)
train[train['item_id'].isin(old_id)]
date | date_block_num | shop_id | item_id | item_price | item_cnt_day |
---|
train[train['item_id'].isin(duplicated_item_name_rec['item_id'].values)]['item_id'].unique() # 旧id成功替换成新id
array([ 2558, 14539, 19475, 19581, 5063, 2970], dtype=int64)
3.1.4 商品类目数据集
items.groupby('item_id').size()[items.groupby('item_id').size() > 1] # 检查同一个商品是否分了不同类目
Series([], dtype: int64)
cat = pd.read_csv('item_categories.csv')
cat
item_category_name | item_category_id | |
---|---|---|
0 | PC - Гарнитуры/Наушники | 0 |
1 | Аксессуары - PS2 | 1 |
2 | Аксессуары - PS3 | 2 |
3 | Аксессуары - PS4 | 3 |
4 | Аксессуары - PSP | 4 |
... | ... | ... |
79 | Служебные | 79 |
80 | Служебные - Билеты | 80 |
81 | Чистые носители (шпиль) | 81 |
82 | Чистые носители (штучные) | 82 |
83 | Элементы питания | 83 |
84 rows × 2 columns
cat[cat['item_category_name'].duplicated()]
item_category_name | item_category_id |
---|
对类别名称进行简单分析后,发现大部分都是‘大类-小类’的组合形式
Аксессуары :配件
Аксессуары - PS2 :PS2游戏机配件
Игровые консоли :游戏机
【先拆分大类】
cat['item_type'] = cat['item_category_name'].map(lambda x: 'Игры' if x.find('Игры ')>0 else x.split(' -')[0].strip('\"'))
cat.iloc[[32, 33, 34, -3, -2, -1]] # 有几个比较特殊,需要另外调整一下
item_category_name | item_category_id | item_type | |
---|---|---|---|
32 | Карты оплаты (Кино, Музыка, Игры) | 32 | Карты оплаты (Кино, Музыка, Игры) |
33 | Карты оплаты - Live! | 33 | Карты оплаты |
34 | Карты оплаты - Live! (Цифра) | 34 | Карты оплаты |
81 | Чистые носители (шпиль) | 81 | Чистые носители (шпиль) |
82 | Чистые носители (штучные) | 82 | Чистые носители (штучные) |
83 | Элементы питания | 83 | Элементы питания |
cat.iloc[[32,-3, -2], -1] = ['Карты оплаты', 'Чистые носители', 'Чистые носители' ]
cat.iloc[[32,-3, -2]]
item_category_name | item_category_id | item_type | |
---|---|---|---|
32 | Карты оплаты (Кино, Музыка, Игры) | 32 | Карты оплаты |
81 | Чистые носители (шпиль) | 81 | Чистые носители |
82 | Чистые носители (штучные) | 82 | Чистые носители |
item_type_map = dict([(v,k) for k, v in enumerate(cat['item_type'].unique())])
cat['item_type_code'] = cat['item_type'].map(item_type_map)
cat.head()
item_category_name | item_category_id | item_type | item_type_code | |
---|---|---|---|---|
0 | PC - Гарнитуры/Наушники | 0 | PC | 0 |
1 | Аксессуары - PS2 | 1 | Аксессуары | 1 |
2 | Аксессуары - PS3 | 2 | Аксессуары | 1 |
3 | Аксессуары - PS4 | 3 | Аксессуары | 1 |
4 | Аксессуары - PSP | 4 | Аксессуары | 1 |
【接着是拆分小类】
cat['sub_type'] = cat['item_category_name'].map(lambda x: x.split('-',1)[-1])
cat
item_category_name | item_category_id | item_type | item_type_code | sub_type | |
---|---|---|---|---|---|
0 | PC - Гарнитуры/Наушники | 0 | PC | 0 | Гарнитуры/Наушники |
1 | Аксессуары - PS2 | 1 | Аксессуары | 1 | PS2 |
2 | Аксессуары - PS3 | 2 | Аксессуары | 1 | PS3 |
3 | Аксессуары - PS4 | 3 | Аксессуары | 1 | PS4 |
4 | Аксессуары - PSP | 4 | Аксессуары | 1 | PSP |
... | ... | ... | ... | ... | ... |
79 | Служебные | 79 | Служебные | 15 | Служебные |
80 | Служебные - Билеты | 80 | Служебные | 15 | Билеты |
81 | Чистые носители (шпиль) | 81 | Чистые носители | 16 | Чистые носители (шпиль) |
82 | Чистые носители (штучные) | 82 | Чистые носители | 16 | Чистые носители (штучные) |
83 | Элементы питания | 83 | Элементы питания | 17 | Элементы питания |
84 rows × 5 columns
cat['sub_type'].unique()
array([' Гарнитуры/Наушники', ' PS2', ' PS3', ' PS4', ' PSP', ' PSVita',
' XBOX 360', ' XBOX ONE', 'Билеты (Цифра)', 'Доставка товара',
' Прочие', ' Аксессуары для игр', ' Цифра',
' Дополнительные издания', ' Коллекционные издания',
' Стандартные издания', 'Карты оплаты (Кино, Музыка, Игры)',
' Live!', ' Live! (Цифра)', ' PSN', ' Windows (Цифра)', ' Blu-Ray',
' Blu-Ray 3D', ' Blu-Ray 4K', ' DVD', ' Коллекционное',
' Артбуки, энциклопедии', ' Аудиокниги', ' Аудиокниги (Цифра)',
' Аудиокниги 1С', ' Бизнес литература', ' Комиксы, манга',
' Компьютерная литература', ' Методические материалы 1С',
' Открытки', ' Познавательная литература', ' Путеводители',
' Художественная литература', ' CD локального производства',
' CD фирменного производства', ' MP3', ' Винил',
' Музыкальное видео', ' Подарочные издания', ' Атрибутика',
' Гаджеты, роботы, спорт', ' Мягкие игрушки', ' Настольные игры',
' Настольные игры (компактные)', ' Открытки, наклейки',
' Развитие', ' Сертификаты, услуги', ' Сувениры',
' Сувениры (в навеску)', ' Сумки, Альбомы, Коврики д/мыши',
' Фигурки', ' 1С:Предприятие 8', ' MAC (Цифра)',
' Для дома и офиса', ' Для дома и офиса (Цифра)', ' Обучающие',
' Обучающие (Цифра)', 'Служебные', ' Билеты',
'Чистые носители (шпиль)', 'Чистые носители (штучные)',
'Элементы питания'], dtype=object)
sub_type_map = dict([(v,k) for k, v in enumerate(cat['sub_type'].unique())])
cat['sub_type_code'] = cat['sub_type'].map(sub_type_map)
cat.head()
item_category_name | item_category_id | item_type | item_type_code | sub_type | sub_type_code | |
---|---|---|---|---|---|---|
0 | PC - Гарнитуры/Наушники | 0 | PC | 0 | Гарнитуры/Наушники | 0 |
1 | Аксессуары - PS2 | 1 | Аксессуары | 1 | PS2 | 1 |
2 | Аксессуары - PS3 | 2 | Аксессуары | 1 | PS3 | 2 |
3 | Аксессуары - PS4 | 3 | Аксессуары | 1 | PS4 | 3 |
4 | Аксессуары - PSP | 4 | Аксессуары | 1 | PSP | 4 |
【合并商品和类目数据集】
items = items.merge(cat[['item_category_id', 'item_type_code', 'sub_type_code']], on='item_category_id', how='left')
items.head()
item_name | item_id | item_category_id | item_type_code | sub_type_code | |
---|---|---|---|---|---|
0 | !ВОВЛАСТИНАВАЖДЕНИЯ(ПЛАСТ.)D | 0 | 40 | 10 | 24 |
1 | !ABBYYFineReader12ProfessionalEditionFull[PC,Ц... | 1 | 76 | 14 | 59 |
2 | ***ВЛУЧАХСЛАВЫ(UNV)D | 2 | 40 | 10 | 24 |
3 | ***ГОЛУБАЯВОЛНА(Univ)D | 3 | 40 | 10 | 24 |
4 | ***КОРОБКА(СТЕКЛО)D | 4 | 40 | 10 | 24 |
import gc
del cat
gc.collect()
2917
3.2 训练集数据清洗
3.2.1 过滤离群值
利用散点图观察商品价格和单日销量的分布情况
sns.jointplot('item_cnt_day', 'item_price', train, kind='scatter')
<seaborn.axisgrid.JointGrid at 0xd6e1d68>
先过滤明显的离群值
train_filtered = train[(train['item_cnt_day'] < 800) & (train['item_price'] < 70000)].copy()
sns.jointplot('item_cnt_day', 'item_price', train_filtered, kind='scatter')
<seaborn.axisgrid.JointGrid at 0xd7bd7b8>
查看价格和销量的异常情况
outer = train[(train['item_cnt_day'] > 400) | (train['item_price'] > 40000)]
outer
date | date_block_num | shop_id | item_id | item_price | item_cnt_day | |
---|---|---|---|---|---|---|
885138 | 17.09.2013 | 8 | 12 | 11365 | 59200.000000 | 1.0 |
1006638 | 24.10.2013 | 9 | 12 | 7238 | 42000.000000 | 1.0 |
1163158 | 13.12.2013 | 11 | 12 | 6066 | 307980.000000 | 1.0 |
1488135 | 20.03.2014 | 14 | 25 | 13199 | 50999.000000 | 1.0 |
1501160 | 15.03.2014 | 14 | 24 | 20949 | 5.000000 | 405.0 |
1573252 | 23.04.2014 | 15 | 27 | 8057 | 1200.000000 | 401.0 |
1573253 | 22.04.2014 | 15 | 27 | 8057 | 1200.000000 | 502.0 |
1708207 | 28.06.2014 | 17 |