Напомним, что наши прогнозы выглядят вот так: \[ \hat{y_i} = \hat{w_0} + \hat{w_1} \cdot x_i^{(1)} + \ldots + \hat{w_k} \cdot x_i^{k} \] Функционалы, которые принято использовать, когда вы работаете с временными рядами, выглядят так:
\[ MAE = \frac{1}{n} \sum_{i=1}^{n} |y_i - \hat y_i| \]
\[ MAPE = \frac{1}{n} \sum_{i=1}^{n} |\frac{y_i - \hat y_i}{y_i}| \]
Нам предоставлены исторические данные о продажах 45 магазинов Walmart, расположенных в разных регионах. В каждом магазине есть несколько отделов, и нам поручено прогнозировать продажи в каждом магазине для каждого отдела.
Зачем нам прогнозировать продажи?
Нам доступны следующие переменные:
Weekly_Sales
– объём продаж в данную неделю в данном отделеStore
– номер магазина;Type
– тип магазина;Size
– размер магазина;Dept
– номер отдела;Date
– дата;IsHoliday
– является ли неделя праздничной;Temperature
– средняя температура в регионе в градусах по Фаренгейту;Fuel_Price
– стоимость топлива в регионе;MarkDown1-5
– данные, связанные с рекламными уценками, которые запускает Walmart. Данные уценки доступны только после ноября 2011 года и доступны не для всех магазинов. Данные анонимизированы. Непонятно на какие именно товары производилась уценка и в каких количествах. Компании часто анонимизируют данные, когда выкладывают их в открытый доступ.CPI
– индекс потребительских цен;Unemployment
– уровень безработицы.next_Weekly_Sales
– объём продаж в следующую неделю в данном отделе (target)## Rows: 418,239
## Columns: 17
## $ Store <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
## $ Date <date> 2010-02-05, 2010-02-12, 2010-02-19, 2010-02-26, 20…
## $ Dept <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
## $ Weekly_Sales <dbl> 24924.50, 46039.49, 41595.55, 19403.54, 21827.90, 2…
## $ IsHoliday <lgl> FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FAL…
## $ Type <chr> "A", "A", "A", "A", "A", "A", "A", "A", "A", "A", "…
## $ Size <dbl> 151315, 151315, 151315, 151315, 151315, 151315, 151…
## $ Temperature <dbl> 42.31, 38.51, 39.93, 46.63, 46.50, 57.79, 54.58, 51…
## $ Fuel_Price <dbl> 2.572, 2.548, 2.514, 2.561, 2.625, 2.667, 2.720, 2.…
## $ MarkDown1 <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ MarkDown2 <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ MarkDown3 <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ MarkDown4 <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ MarkDown5 <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ CPI <dbl> 211.0964, 211.2422, 211.2891, 211.3196, 211.3501, 2…
## $ Unemployment <dbl> 8.106, 8.106, 8.106, 8.106, 8.106, 8.106, 8.106, 8.…
## $ next_Weekly_Sales <dbl> 46039.49, 41595.55, 19403.54, 21827.90, 21043.39, 2…
Давайте сначала разберемся с пропущенными значениями. Набор данных достаточно большой, поэтому не будет его рисовать. Посмотрим на статистики.
## # A tibble: 17 x 3
## variable n_miss pct_miss
## <chr> <int> <dbl>
## 1 MarkDown2 310095 74.1
## 2 MarkDown4 285924 68.4
## 3 MarkDown3 283671 67.8
## 4 MarkDown1 270755 64.7
## 5 MarkDown5 270057 64.6
## 6 Store 0 0
## 7 Date 0 0
## 8 Dept 0 0
## 9 Weekly_Sales 0 0
## 10 IsHoliday 0 0
## 11 Type 0 0
## 12 Size 0 0
## 13 Temperature 0 0
## 14 Fuel_Price 0 0
## 15 CPI 0 0
## 16 Unemployment 0 0
## 17 next_Weekly_Sales 0 0
Видим, что пропущенные значения есть только в переменных Markdown1 - Markdown5. Их достаточно много, поэтому просто заменить средним нет смысла. Давайте выбросим их из нашего датасета.
Теперь в нашем наборе данных нет пропущенных значений.
## [1] 0
Давайте немного изучим наши данные перед тем как строить модель. Построим временной ряд продаж.
walmart %>%
group_by(Date) %>%
summarise(Mean_Weekly_Sales = mean(Weekly_Sales)) %>%
ggplot(aes(x = Date, y = Mean_Weekly_Sales)) +
geom_line(color = 'darkblue') +
labs(x = 'Дата',
y = 'Среднее количество проданных товаров')
## Store Date Dept Weekly_Sales
## Min. : 1.0 Min. :2010-02-05 Min. : 1.00 Min. : -4989
## 1st Qu.:11.0 1st Qu.:2010-10-08 1st Qu.:18.00 1st Qu.: 2090
## Median :22.0 Median :2011-06-17 Median :37.00 Median : 7626
## Mean :22.2 Mean :2011-06-14 Mean :44.25 Mean : 16000
## 3rd Qu.:33.0 3rd Qu.:2012-02-17 3rd Qu.:74.00 3rd Qu.: 20227
## Max. :45.0 Max. :2012-10-19 Max. :99.00 Max. :693099
## IsHoliday Type Size Temperature
## Mode :logical Length:418239 Min. : 34875 Min. : -2.06
## FALSE:388604 Class :character 1st Qu.: 93638 1st Qu.: 46.66
## TRUE :29635 Mode :character Median :140167 Median : 62.10
## Mean :136750 Mean : 60.08
## 3rd Qu.:202505 3rd Qu.: 74.36
## Max. :219622 Max. :100.14
## Fuel_Price CPI Unemployment next_Weekly_Sales
## Min. :2.472 Min. :126.1 Min. : 3.879 Min. : -4989
## 1st Qu.:2.925 1st Qu.:132.0 1st Qu.: 6.891 1st Qu.: 2090
## Median :3.445 Median :182.3 Median : 7.866 Median : 7624
## Mean :3.358 Mean :171.2 Mean : 7.968 Mean : 15990
## 3rd Qu.:3.735 3rd Qu.:212.4 3rd Qu.: 8.572 3rd Qu.: 20222
## Max. :4.468 Max. :227.2 Max. :14.313 Max. :693099
Видим, что переменная Weekly_Sales
и next_Weekly_Sales
принимают отрицательные значения. Такого быть не должно, так как это выручка магазина. Давайте выкинем такие значения.
Перед тем как строить модели давайте разделим наш датасет на train и test. Так как данные имеют временную структуру, не будем перемешивать наши данные. Посмотрим за сколько недель у нас есть данных.
## [1] 142
Видим, что у нас есть 142 недели в нашем распоряжении. Давайте возьмем первые 110 недель на обучение, а 32 оставшиеся на тестирование.
## [1] "2012-03-09"
В трейн у нас попадут недели до 2012-03-09, а тест после этой недели, включая эту самую неделю.
Давайте построим сначала некоторые простые модели. Их результат мы можем взять за базовый (baseline).
Возьмем константный алгоритм, который будет всегда предсказывать значение равное среднему значению по всем отделам всех магазинов.
train_1 <- train
test_1 <- test
MEAN <- mean(train_1$Weekly_Sales)
train_1$predict1 <- MEAN
test_1$predict1 <- MEAN
Подсчитаем чему равно MAPE и MAE.
## [1] 15228.08
## [1] 15134.17
## [1] 211.0388
## [1] 848.3803
Видим большое значение MAPE. Скорее всего MAE тоже большое. Давайте посмотрим на среднее значение нашего таргета.
## [1] 16118.9
Видим, что MAE и правда большое.
Магазины и отделы в них бывают разные. Попробуем находить среднее в каждом отделе для каждого магазина.
MEAN_by_Store_Dept <- train %>%
group_by(Store, Dept) %>%
summarise(predict2 = mean(Weekly_Sales))
train_1 <- train_1 %>%
left_join(MEAN_by_Store_Dept, by = c('Store', 'Dept'))
test_1 <- test_1 %>%
left_join(MEAN_by_Store_Dept, by = c('Store', 'Dept'))
Появляются NA, потому что появились новые отделы в магазинах. Пусть если у нас нет прогноза, то мы возьмем прогноз из прошлой модели.
test_1 <- test_1 %>%
mutate(predict2 = ifelse(is.na(predict2), predict1, predict2))
mae(train_1$next_Weekly_Sales, train_1$predict2)
## [1] 2781.925
## [1] 2606.157
## [1] 36.15821
## [1] 107.8383
Уже получается лучше.
Очень хорошим baseline’ом является следующая простая модель. Давайте привозить на следующую неделю столько товара, сколько мы продали на этой неделе.
train_1$predict3 <- train_1$Weekly_Sales
test_1$predict3 <- test_1$Weekly_Sales
mae(train_1$next_Weekly_Sales, train_1$predict3)
## [1] 2254.023
## [1] 1648.434
## [1] 1.176847
## [1] 2.129825
Мы получили достаточно хорошие результаты. Давайте ориентироваться на них.
Давайте попробуем построить следующую очень простую регрессию:
\[next\_Weekly\_Sales = w_0 + w_1 Weekly\_Sales\].
Она похожа на то, что мы делали ранее.
train_2 <- train
test_2 <- test
model4 <- lm(next_Weekly_Sales ~ Weekly_Sales , data = train_2)
train_2$predict4 <- predict(model4, train_2)
test_2$predict4 <- predict(model4, test_2)
mae(train_2$next_Weekly_Sales, train_2$predict4)
## [1] 2523.658
## [1] 1941.544
## [1] 13.88451
## [1] 53.67912
Результаты получились не такими хорошими как было раньше. Давайте попробуем нарисовать график рессеяния и понять почему так вышло.
train_2 %>%
ggplot(aes(Weekly_Sales, next_Weekly_Sales)) +
geom_point() +
geom_smooth(method = 'lm')
Видно, что облако вытянуто неоднозначно. Есть большие магазины, а есть маленькие. В такой модели мы совсем не учитываем это и мешаем все в одну кучу.
Попробуем добавить в регрессию номер магазина и номер отдела. Оставлять переменные такими какие они есть не очень корректно, так как мы говорили, что Label Encoding работает не очень хорошо. Давайте закодируем наши магазины и отделы с помощью One Hot Encoding.
# install.packages("fastDummies")
library(fastDummies)
walmart2 <- walmart %>%
select(Date, Store, Dept, Weekly_Sales, next_Weekly_Sales)
walmart2 <- dummy_cols(walmart2, select_columns = c('Store', 'Dept'),
remove_first_dummy = T)
train_3 <- walmart2 %>%
filter(Date < "2012-03-09")
test_3 <- walmart2 %>%
filter(Date >= "2012-03-09")
Обучим модель.
model5 <- lm(next_Weekly_Sales ~ . - 1, data = train_3)
train_3$predict <- predict(model5, train_3)
test_3$predict <- predict(model5, test_3)
mae(train_3$next_Weekly_Sales, train_3$predict)
## [1] 2726.129
## [1] 2186.507
## [1] 17.34136
## [1] 55.55517
Линейные модели любят, когда таргет да и вообще все переменнын имели нормальное распределение.
Один из легких способов получить нормальное распределение это взять логарифм.
Попробуем обучить модель с логарифмом таргета. При прогнозе не забываем возращать значения обратно с помощью экспоненты.
train_3 <- walmart2 %>%
filter(Date < "2012-03-09") %>%
mutate(next_Weekly_Sales = log(next_Weekly_Sales),
Weekly_Sales = log(Weekly_Sales)) %>%
select(-Date, Store, Dept)
test_3 <- walmart2 %>%
filter(Date >= "2012-03-09") %>%
mutate(next_Weekly_Sales = log(next_Weekly_Sales),
Weekly_Sales = log(Weekly_Sales)) %>%
select(-Date, Store, Dept)
model5 <- lm(next_Weekly_Sales ~ . - 1, data = train_3)
train_3$predict <- exp(predict(model5, train_3))
test_3$predict <- exp(predict(model5, test_3))
mae(exp(train_3$next_Weekly_Sales), train_3$predict)
## [1] 2304.386
## [1] 1741.404
## [1] 1.221557
## [1] 2.329645
walmart3 <- walmart
walmart3 <- dummy_cols(walmart3, select_columns = c('Store', 'Dept'),
remove_first_dummy = T)
train_4 <- walmart3 %>%
filter(Date < "2012-03-09") %>%
mutate(next_Weekly_Sales = log(next_Weekly_Sales),
Weekly_Sales = log(Weekly_Sales)) %>%
select(-Date, Store, Dept)
test_4 <- walmart3 %>%
filter(Date >= "2012-03-09") %>%
mutate(next_Weekly_Sales = log(next_Weekly_Sales),
Weekly_Sales = log(Weekly_Sales)) %>%
select(-Date, Store, Dept)
model6 <- lm(next_Weekly_Sales ~ . - 1, data = train_4)
train_4$predict <- exp(predict(model6, train_4))
test_4$predict <- exp(predict(model6, test_4))
mae(exp(train_4$next_Weekly_Sales), train_4$predict)
## [1] 2298.427
## [1] 1733.385
## [1] 1.22473
## [1] 2.342098