Этот пост посвящен моему подходу к проблеме прогнозирования продаж Bigmart с использованием Python.
У меня было собеседование, где мне прислали эту проблему в качестве задачи для решения. Прежде всего, посмотрите на BigMart описание проблемы:
Исследователи данных из BigMart собрали данные о продажах 1559 товаров в 10 магазинах в разных городах за 2013 год. Также были определены определенные атрибуты каждого продукта и магазина. Цель состоит в том, чтобы построить прогностическую модель и выяснить продажи каждого товара в конкретном магазине.
Используя эту модель, BigMart попытается понять свойства продуктов и магазинов, которые играют ключевую роль в увеличении продаж.
Вы можете найти набор данных здесь
Итак, мы хотим прогнозировать продажи, используя информацию BigMart. Что у нас есть в качестве записей?
train.info() # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Item_Identifier 8523 non-null object 1 Item_Weight 7060 non-null float64 2 Item_Fat_Content 8523 non-null object 3 Item_Visibility 8523 non-null float64 4 Item_Type 8523 non-null object 5 Item_MRP 8523 non-null float64 6 Outlet_Identifier 8523 non-null object 7 Outlet_Establishment_Year 8523 non-null int64 8 Outlet_Size 6113 non-null object 9 Outlet_Location_Type 8523 non-null object 10 Outlet_Type 8523 non-null object 11 Item_Outlet_Sales 8523 non-null float64
Мы можем видеть некоторые непрерывные атрибуты, такие как Item_Weight, Item_Visibility и Item_MRP. Item_Outlet_Sales — это то, что мы хотим предсказать. С другой стороны, хотя Outlet_Establishment_Year имеет тип int, он указывает год открытия магазина, поэтому мы будем считать его категоричным. Кроме того, мы видим, что Outlet_Size и Item_Weight имеют некоторые пропущенные значения. Мы поработаем над этим позже.
Кратко опишем каждый атрибут:
- Item_Identifier: код для идентификации продукта.
- Item_Weight: вес товара.
- Item_Fat_Content: классификация продукта по жирности.
- Item_Visibility: показатель, который относится к знанию товара
о продукте у потребителя. Насколько легко можно найти продукт? - Item_MRP: максимальная розничная цена. Цена, рассчитанная производителем, с указанием максимальной цены, которую можно взимать за продукт.
- Outlet_Identifier: идентификатор магазина.
- Outlet_Establishment_Year: год открытия магазина.
- Outlet_Size: размер магазина.
- Outlet_Location_Type: классификация магазинов по расположению.
- Outlet_Type: тип магазина.
- Item_Outlet_Sales: продажи товара в каждом наблюдении.
Теперь мы знаем, какие атрибуты непрерывны, а какие категоричны. Давайте посмотрим, какие разные категории у нас есть в каждом категориальном атрибуте:
train["Item_Fat_Content"].unique().tolist() train["Item_Type"].unique().tolist() train["Outlet_Identifier"].unique().tolist() train["Outlet_Size"].unique().tolist() train["Outlet_Location_Type"].unique().tolist() train["Outlet_Type"].unique().tolist() train["Outlet_Establishment_Year"].unique().tolist() ['Low Fat', 'Regular', 'low fat', 'LF', 'reg'] ['Dairy', 'Soft Drinks', 'Meat', 'Fruits and Vegetables', 'Household', 'Baking Goods', 'Snack Foods', 'Frozen Foods', 'Breakfast', 'Health and Hygiene', 'Hard Drinks', 'Canned', 'Breads', 'Starchy Foods', 'Others', 'Seafood'] ['OUT049', 'OUT018', 'OUT010', 'OUT013', 'OUT027', 'OUT045', 'OUT017', 'OUT046', 'OUT035', 'OUT019'] ['Medium', nan, 'High', 'Small'] ['Tier 1', 'Tier 3', 'Tier 2'] ['Supermarket Type1', 'Supermarket Type2', 'Grocery Store', 'Supermarket Type3'] [1999, 2009, 1998, 1987, 1985, 2002, 2007, 1997, 2004]
Мы видим, что Item_Fat_Content имеет несоответствие в том, как загружались обычные и обезжиренные продукты. Давайте заменим «с низким содержанием жира», «с низким содержанием жира» на «LF» и «обычный», «reg» на «R».
train['Item_Fat_Content'] = train['Item_Fat_Content'].replace('Low Fat', 'LF').replace('low fat', 'LF').replace('reg', 'R').replace('Regular', 'R')
Непрерывные атрибуты
Теперь давайте проанализируем непрерывные атрибуты. В первую очередь полезно посмотреть их гистограммы.
Хотя Item_Visibility выглядит нормально, если мы обратим внимание на низкие значения видимости, мы заметим большое скопление элементов с нулевой видимостью. Это не имеет смысла, поскольку нулевая видимость означает, что мы не можем увидеть товар в магазине! Итак, Item_Visibility имеет пропущенные значения.
len(train['Item_Visibility'][train['Item_Visibility']==0])) Zero visibility items 526
Теперь давайте разберемся с отсутствующими значениями в этих непрерывных атрибутах. У нас есть несколько стратегий для решения этой проблемы. Мы можем удалить записи с отсутствующими значениями, заменить их общим средним значением или заменить их более конкретным средним значением. Что я имею в виду под более конкретным значением? Посмотрим среднее значение Item_Visibility и Item_Weight по Item_Type:
Действительно, есть разница между средним содержанием жира и видимостью в зависимости от типа продукта. Чтобы заменить их полученным средним, мы могли бы использовать следующую функцию:
def MeanImputer_NA(data, data_t, atr, atr_filter): ''' Mean Imputer for NA values Parameters ---------- data : pandas.dataframe The training dataframe to process. data_t : pandas.dataframe The validaiton/test dataframe to process. atr: str Attribute that contains NA values. atr_filter : str Attribute used to filter data and compute mean value. Returns ------- data : pandas.dataframe Preprocessed data. data_t : pandas.dataframe Preprocessed data_t. ''' key_filter = data[atr_filter].unique() for key in key_filter: mean = np.mean(data[atr][data[atr_filter] == key]) data[atr][data[atr_filter] == key] = data[atr][data[atr_filter] == key].fillna(mean) data_t[atr][data_t[atr_filter] == key] = data_t[atr][data_t[atr_filter] == key].fillna(mean) return data, data_t
Хорошо, что мы можем сделать дальше? Возможно, мы могли бы попытаться проверить, будет ли какой-либо из непрерывных атрибутов важен для регрессии. Получим корреляционную матрицу.
Мы видим, что item_MRP имеет высокую корреляцию с Item_Outlet_Sales, поэтому, вероятно, это будет хорошая оценка. С другой стороны, Item_Visibility и Item_Weight не кажутся полезными, но так ли это? Давайте получим несколько диаграмм рассеяния.
Я не знаю, что вы, ребята, думаете, но я вижу, что товары с нормальным весом (я имею в виду не слишком тяжелые и не слишком легкие) покупаются больше. С другой стороны, предметы с низкой видимостью покупаются чаще, чем предметы с высокой видимостью, что довольно странно, если учесть описание этого атрибута.
Категориальные атрибуты
Теперь давайте разберемся с отсутствующими значениями в Outlet_Size. Прежде всего, я думаю, важно знать, что магазины с пропущенными значениями размера принадлежат трем разным торговым точкам:
train[‘Outlet_Identifier’][train[‘Outlet_Size’].isna()].value_counts() OUT045 929 OUT017 926 OUT010 555
Почему это важно? Посмотрим позже. А пока давайте сосредоточимся на том, как мы справляемся с этим. Опять же, мы можем удалить экземпляры с отсутствующими значениями, заменить их категорией общего среднего или попробовать более разумный подход. Мы обучим классификатор для определения размера этих магазинов. Обучающий набор будет состоять из всех записей без пропущенных значений, а набор обобщения будет состоять из записей с пропущенными значениями. Я выбрал KNN для этого и перекрестную проверку для поиска по гиперпараметрам.
from sklearn.neighbors import KNeighborsClassifier from sklearn.model_selection import GridSearchCV KNN=KNeighborsClassifier() idx=list(np.arange(5, 100, 5)) param_KNN = [{'n_neighbors': idx}] search_KNN = GridSearchCV(KNN, param_KNN, cv=5, scoring='balanced_accuracy', #se podría decir que hay un poco de data imbalance return_train_score=True, verbose=1) search_KNN.fit(x_train_dummies, y_train_aux)
X_train_dummies — это тренировочный набор, в котором я сделал один горячий кодировщик для категориальных атрибутов, а y_train_aux — это размер выхода. После обучения прогнозируемый размер розетки магазинов с отсутствующими значениями:
Outlet size 010: Medium 274 Small 252 High 29 Name: Outlet_Size, dtype: int64 Outlet size 045: Medium 561 Small 346 High 22 Name: Outlet_Size, dtype: int64 Outlet size 017: Medium 542 Small 348 High 36 Geneneral outlet sizes: Medium 2793 Small 2388 High 932
Обратите внимание, что я также напечатал общие размеры розеток. Это необходимо для того, чтобы учесть соотношение между различными размерами. Теперь мы уверены, что торговые точки 017 и 045 среднего размера. С другой стороны, магазин 010 может быть магазином малого или среднего размера.
Мы могли бы заполнить пропущенные значенияoutlet_size предсказаниями классификатора, но было бы бессмысленно, если бы один и тот же магазин имел три разных размера. Следовательно, нам нужно заполнить output_size магазинов 017 и 045 значением «Средний». Что касается магазина 010, нам нужно больше расследовать. Мы видим, что есть два магазина, которые являются продуктовыми магазинами: 010 и 019. По здравому смыслу продуктовые магазины — это небольшие магазины. Чтобы быть более уверенным, давайте посмотрим размер хранилища 019:
train['Outlet_Identifier'][train['Outlet_Type']=='Grocery Store'].value_counts() Stores that are grocerys: OUT010 555 OUT019 528 Name: Outlet_Identifier, dtype: int64 train['Outlet_Size'][train['Outlet_Identifier']=='OUT019'].value_counts() Size of 019: Small 528
Кроме того, мы можем проверить продажи торговых точек по магазинам и по типам магазинов.
Опять же, кажется правильным, что магазин 010 — это небольшой магазин. Поэтому давайте заменим Outlet_size магазинов 017 и 045 на «Средний», а Outlet_size магазина 010 на «Малый».
Обучение
Прежде всего, давайте определим некоторые общие функции, которые помогут нам реализовать всю необходимую предварительную обработку:
def preprocessing(dataframe, dataframe_t, droppable_columns, integer_columns, categorical_columns, objective, transform=None, one_hot=False): ''' Preprocessing function for the proposed dataframe. This functions modifies the dataframe from of the training set and the dataframe of the validation/test set to get categorical columns from strings, dropping non-informative attributes, and rescaling continuous attributes. Parameters ---------- dataframe : pandas.dataframe The training dataframe to process. dataframe_t : pandas.dataframe The validaiton/test dataframe to process. droppable_columns : list List of droppable column strings. continuous_columns : list List of continuous column strings. categorical_columns : list List of categorical column strings. Returns ------- dataframe : pandas.dataframe Preprocessed dataframe. dataframe_t : pandas.dataframe Preprocessed dataframe_t. ''' for c in droppable_columns: dataframe = dataframe.drop(c, axis="columns") dataframe_t = dataframe_t.drop(c, axis="columns") for c in categorical_columns: if c == 'Item_Fat_Content': dataframe[c] = dataframe[c].replace('Low Fat', 'LF').replace('low fat', 'LF').replace('reg', 'R').replace('Regular', 'R') dataframe_t[c] = dataframe_t[c].replace('Low Fat', 'LF').replace('low fat', 'LF').replace('reg', 'R').replace('Regular', 'R') if c == 'Outlet_Size': dataframe[c], dataframe_t[c] = MeanImputer_Size(dataframe, dataframe_t, 'Outlet_Size', 'Outlet_Identifier') dataframe[c] = dataframe[c].astype("category", copy=False) dataframe_t[c] = dataframe_t[c].astype("category", copy=False) for c in continuous_columns: if c == 'Item_Weight': dataframe[c], dataframe_t[c] = MeanImputer_NA(dataframe, dataframe_t, 'Item_Weight', 'Item_Type') if c == 'Item_Visibility': dataframe[c], dataframe_t[c] = MeanImputer_0(dataframe, dataframe_t, 'Item_Visibility', 'Item_Type') dataframe[c], dataframe_t[c] = StandarScaler(dataframe[c].astype('float64', copy=False), dataframe_t[c].astype('float64', copy=False)) if one_hot: dataframe = pd.get_dummies(dataframe, categorical_columns) dataframe_t = pd.get_dummies(dataframe_t, categorical_columns) if transform: return dataframe.drop(objective, axis="columns"), dataframe_t.drop(objective, axis="columns"), transform(dataframe[objective]), transform(dataframe_t[objective]) return dataframe.drop(objective, axis="columns"), dataframe_t.drop(objective, axis="columns"), dataframe[objective], dataframe_t[objective]
Теперь давайте используем LightGBM в качестве регрессора для этой задачи. Я выполнил поиск по гиперпараметрам путем перекрестной проверки. Кроме того, я разделил набор поездов, предоставленный BigMart, на поезд и тестовый набор. Я использовал RMSE в качестве функции потерь.
from sklearn.model_selection import GridSearchCV, train_test_split from sklearn.metrics import mean_squared_error as mse from sklearn.model_selection import KFold import lightgbm as lgb train = pd.read_csv('data/Train_BigMart.csv') droppable_columns = ['Item_Identifier'] continuous_columns = ['Item_Weight', 'Item_Visibility', 'Item_MRP'] categorical_columns = ['Item_Type', 'Outlet_Location_Type', 'Outlet_Type', 'Outlet_Size', 'Outlet_Identifier', 'Item_Fat_Content'] objective = 'Item_Outlet_Sales' train, test = train_test_split(train, test_size=1000) x_train, x_test, y_train, y_test = preprocessing(train, test, droppable_columns, continuous_columns, categorical_columns, objective) model = lgb.LGBMRegressor(random_state=42, objective="rmse", silent=True ) parameters = { 'task': ['train'], 'boosting_type': ['gbdt'], 'objective': ['regression'], 'metric': ['rmse'], 'learning_rate': [0.005], 'feature_fraction': [0.8, 0.9, 1], 'bagging_fraction': [0.7, 0.85, 1], 'bagging_freq': [10], 'verbose': [0], "num_leaves": [128], "max_bin": [512], "max_depth": [6, 8, 10], "silent": [True], "n_estimators": [1000], 'n_iterations:': [10000] } n_splits = 5 kfold_5 = KFold(shuffle = True, n_splits = n_splits, random_state=42) search = GridSearchCV(model, parameters, cv=kfold_5, scoring='neg_root_mean_squared_error', return_train_score=True, n_jobs=-1, verbose=True) search.fit(x_train, y_train)
Мы будем использовать все атрибуты, кроме Item_Identifier. В нем более 1600 категорий, что делает его бесполезным из-за проклятия размерности. Зачем использовать остальные атрибуты? Ну, мы можем думать примерно так:
- Item_Weight: мы видели, что люди склонны покупать товары среднего веса.
- Item_Fat_Content: гипотезой может быть то, что люди хотят заботиться о своем здоровье и покупать продукты с меньшим содержанием жира.
- Item_Visibility: мы заметили, что люди, как правило, покупают товары с низкой видимостью, что было странно.
- Item_Type: гипотеза может состоять в том, что люди, как правило, покупают предметы первой необходимости, такие как еда, и менее роскошные предметы, такие как крепкие напитки.
- Item_MRP: мы увидели корреляцию между этим атрибутом и продажами.
- Outlet_Identifier: мы пока оставляем это, потому что у меня нет веских причин отказываться от этого.
- Outlet_Establishment_Year: можно предположить, что люди склонны покупать больше в известных магазинах.
- Outlet_Size: можно предположить, что люди склонны покупать больше в крупных магазинах, где есть разнообразие товаров.
- Outlet_Location_Type: гипотеза может состоять в том, что люди, как правило, покупают больше в магазинах в центре города, чем на окраинах города.
- Outlet_Type: это может сжать информацию о Outlet_location_type, Outlet_size и Outlet_stitution_year.
После тренировки мы получили такие результаты:
Total predicted test: 2178685; Total real test: 2192608 Total predicted training: 16401934, Total real training: 16398516 RMSE test: 1046.73
Мы видим, что, хотя сумма прогнозируемого количества продаж похожа на реальную сумму продаж, мы недооцениваем товары, которые покупаются много.
Давайте посмотрим, какие функции важны для LightGBM.
Мы видим, чтоoutlet_type,outlet_location_type и item_fat_contentменее важны, чем остальные атрибуты, так что давайте опустим их. Спойлер: результаты не лучше.
Total predicted test: 2217288, Total real test: 2210842 Total predicted training: 16368628, Total real training: 16380282 RMSE test: 1055.84
И у нас все еще есть проблема недооценки вещей, которые покупают много. Мы могли бы попробовать трюк с логарифмом (x+1), чтобы решить асимметричное распределениеoutlet_item_sales и проверить, решает ли это проблему недооценки. Опять же, спойлер: это не так.
Total predicted test: 1907749, Total real test: 2186165 Total predicted training: 14413580, Total real training: 16404959 RMSE test: 2202.43
На самом деле результаты хуже, общая сумма отличается больше, чем без преобразования log(x+1), а RMSE больше.
Вывод
- Item_Fat_Content
- Item_Weight, Item_Visibility и Outlet_Size: представлены отсутствующие значения. Item_Weight и Item_Visibility зависели от Item_Type. С другой стороны, отсутствующие значения Outlet_Size относятся к 3 конкретным магазинам. Разумно было бы считать, что магазины 017 и 045 средние, а магазин 010 — маленький.
- LightGBM недооценивает товары, которые покупают много
- Лучший показатель RMSE для тестового набора составил 1045.
- Наиболее важными атрибутами были: Item_MRP, Item_Weight, Outlet_Establishment_Year, Outlet_Identifier, Item_Visibility, Item_Type, Outlet_Size.
Проверить код можно здесь