Простота - ключ к успеху.

«Чтобы лучше понять рынок, организациям необходимо искать источники данных за пределами своих четырех стен».

Дуглас Лэйни (вице-президент, Gartner Research)

вступление

Было несколько применений популярного конкурса Прогноз продаж Walmart для прогнозирования продаж.

Однако все они, похоже, пытаются повысить точность (уменьшить количество ошибок) , сосредоточившись в основном на двух вещах:

1) Разработка функций (получение максимальной отдачи от ваших функций)

2) Оптимизация модели / параметров (выбор лучшей модели и лучших параметров)

И то, и другое действительно очень необходимо, но есть третья вещь, которая добавляет ценность дополнительным способом, и она используется не только в недостаточной степени. в этом случае использования (что, по понятным причинам, противоречило правилам конкурса), но в большинстве проектов по обработке и анализу данных:

  • Объединение внешней информации.

В этой статье мы сделаем простую модель прогноза, а затем будем смешивать внешние переменные (все сделано правильно).

В названии этой статьи говорится об улучшении всех моделей не за счет чего-то другого, а за счет выполнения того же действия с более полезными данными.

Таким образом, мы будем использовать ту же модель, и мы не будем заниматься обработкой данных или разработкой на любом этапе, так что мы можем выделить только преимущества добавления полезные функции.

Что мы будем делать

  • Шаг 1. Определите и поймите цель
  • Шаг 2. Создайте простую модель прогноза
  • Шаг 3. Добавьте финансовые показатели и новости
  • Шаг 4. Протестируйте модели
  • Шаг 5. Измерьте результаты

Шаг 1. Определите и поймите цель

Walmart опубликовал данные, содержащие еженедельные продажи для 99 отделов (одежда, электроника, продукты питания ...) в каждом физическом магазине, а также некоторые другие дополнительные функции.

Для этого мы создадим модель машинного обучения с «Weekly_Sales» в качестве цели и обучим первые 70% наблюдений и протестируем последующие 30%.

Цель состоит в том, чтобы минимизировать ошибку прогноза для будущих еженедельных продаж.

Мы добавим внешние переменные, которые влияют или имеют отношение к продажам, такие как индекс доллара, цена на нефть и новости по поводу Walmart.

Мы не будем использовать оптимизацию моделей / параметров или разработку функций, поэтому мы можем выделить преимущество от добавления внешнего Особенности.

Шаг 2. Создайте простую модель прогноза

Во-первых, вам необходимо установить Python 2 или 3 и следующие библиотеки:

$ pip install pandas OpenBlender scikit-learn

Затем откройте скрипт Python (желательно Jupyter notebook) и давайте импортируем необходимые библиотеки.

from sklearn.ensemble import RandomForestRegressor
import pandas as pd
import OpenBlender
import json

Теперь давайте определим методологию и модель, которые будут использоваться во всех экспериментах.

Во-первых, диапазон дат данных - с января 2010 г. по декабрь 2012 г. . Давайте определим первые 70% данных, используемых для обучения, и последующие 30% для тестирования (потому что мы не хотим утечки данных по нашим прогнозам).

Затем давайте определим в качестве нашей стандартной модели RandomForestRegressor с 50 оценщиками, что является достаточно хорошим вариантом.

Наконец, чтобы упростить задачу, давайте определим ошибку как абсолютную сумму ошибок.

Теперь давайте поместим его в класс Python.

class StandardModel:
    
    model = RandomForestRegressor(n_estimators=50, criterion='mse')
    

    def train(self, df, target):
        # Drop non numerics
        df = df.dropna(axis=1).select_dtypes(['number'])
        # Create train/test sets
        X = df.loc[:, df.columns != target].values
        y = df.loc[:,[target]].values
        # We take the first bit of the data as test and the 
        # last as train because the data is ordered desc.
        div = int(round(len(X) * 0.29))
        X_train = X[div:]
        y_train = y[div:]
        print('Train Shape:')
        print(X_train.shape)
        print(y_train.shape)
        #Random forest model specification
        self.model = RandomForestRegressor(n_estimators=50)
        # Train on data
        self.model.fit(X_train, y_train.ravel())

   def getMetrics(self, df, target):
        # Function to get the error sum from the trained model
        # Drop non numerics
        df = df.dropna(axis=1).select_dtypes(['number'])
        # Create train/test sets
        X = df.loc[:, df.columns != target].values
        y = df.loc[:,[target]].values
        div = int(round(len(X) * 0.29))
        X_test = X[:div]
        y_test = y[:div]
        print('Test Shape:')
        print(X_test.shape)
        print(y_test.shape)
        
        # Predict on test
        y_pred_random = self.model.predict(X_test)
        # Gather absolute error
        error_sum = sum(abs(y_test.ravel() - y_pred_random))
        return error_sum

Выше у нас есть объект с 3 элементами:

  • модель (RandomForestRegressor)
  • обучение: функция для обучения этой модели с фреймом данных и целью.
  • getMetrics: функция для тестирования обученной модели с тестовыми данными и получения ошибки.

Мы будем использовать эту конфигурацию для всех экспериментов, хотя вы можете изменить ее, если хотите протестировать различные модели, параметры, конфигурацию или что-то еще. Добавленная стоимость останется и потенциально может улучшиться.

Теперь давайте возьмем данные Walmart. Вы можете получить этот CSV здесь.

df_walmart = pd.read_csv('walmartData.csv')
print(df_walmart.shape)
df_walmart.head()

Всего 421 570 наблюдений. Как мы уже говорили ранее, наблюдения представляют собой регистры еженедельных продаж по магазинам и отделам.

Давайте подключим данные к модели, не вмешиваясь в нее вообще.

our_model = StandardModel()
our_model.train(df_walmart, 'Weekly_Sales')
total_error_sum = our_model.getMetrics(df_walmart, 'Weekly_Sales')
print("Error sum: " + str(total_error_sum))
> Error sum: 967705992.5034052

Сумма всех ошибок для полной модели составляет 967 705 992,5 доллара США из всех прогнозов по сравнению с реальными продажами.

Само по себе это ничего не значит, единственное, что указывает на то, что сумма всех продаж за этот период составляет 6 737 218 987,11 долларов США.

Поскольку данных очень много, в этом руководстве мы сосредоточимся только на Магазине №1, но методика абсолютно воспроизводима для всех магазинов.

Давайте посмотрим на ошибку, генерируемую только Store 1.

# Select store 1
df_walmart_st1 = df_walmart[df_walmart['Store'] == 1]
error_sum_st1 = our_model.getMetrics(df_walmart_st1, 'Weekly_Sales')
print("Error sum error_sum_st1: " + str(error_sum_st1))
# > Error sum error_sum_st1: 24009404.060399983

Таким образом, Магазин 1 несет ответственность за ошибку 24 009 404,06 доллара США, и это будет нашим пороговым значением для сравнения.

А теперь давайте разберем ошибку по отделам, чтобы позже было легче ее увидеть.

error_summary = []
for i in range(1,100):
    try:
        df_dept = df_walmart_st1[df_walmart_st1['Dept'] == i]
        error_sum = our_model.getMetrics(df_dept, 'Weekly_Sales')
        print("Error dept : " + str(i) + ' is: ' + str(error_sum))
        error_summary.append({'dept' : i, 'error_sum_normal_model' : error_sum})
    except: 
        error_sum = 0
        print('No obs for Dept: ' + str(i))
error_summary_df = pd.DataFrame(error_summary)
error_summary_df.head()

Теперь у нас есть фрейм данных с ошибками, соответствующими каждому отделу в Магазине 1 с нашей пороговой моделью.

Давайте улучшим эти цифры.

Шаг 3. Добавьте финансовые показатели и новости

Для простого примера выберем отдел 1.

df_st1dept1 = df_walmart_st1[df_walmart_st1['Dept'] == 1]

Теперь давайте подготовим переменную отметки времени.

# First we need to add the UNIX timestamp which is the number 
# of seconds since 1970 on UTC, it is a very convenient 
# format because it is the same in every time zone in the world!
df_st1dept1['timestamp'] = OpenBlender.dateToUnix(df_st1dept1['Date'], 
                       date_format = '%Y-%m-%d', 
                       timezone = 'GMT')
df_st1dept1 = df_st1dept1.sort_values('timestamp').reset_index(drop = True)

Теперь давайте поищем пересекающиеся (перекрывающиеся по времени) наборы данных о "бизнесе" или "Walmart" в OpenBlender.

Примечание. Чтобы получить токен, который вам необходим, необходимо создать учетную запись на openblender.io (бесплатно), вы найдете ее на вкладке Учетная запись на своем значок профиля.

token = 'YOUR_TOKEN_HERE'
print('From : ' + OpenBlender.unixToDate(min(df_st1dept1.timestamp)))
print('Until: ' + OpenBlender.unixToDate(max(df_st1dept1.timestamp)))
# Now, let's search on OpenBlender
search_keyword = 'business walmart'
# We need to pass our timestamp column and 
# search keywords as parameters.
OpenBlender.searchTimeBlends(token,
                             df_st1dept1.timestamp,
                             search_keyword)

Поиск обнаружил несколько наборов данных. Мы можем видеть имя, описание, URL-адрес, функции и, что наиболее важно, время взаимодействия с нашими, чтобы мы могли смешать их с нашим набором данных.

Давайте начнем с объединения этого набора данных твиты Walmart и поищем промо.

  • Примечание. Я выбрал этот, потому что он имеет смысл, но вы можете найти сотни других.

Мы можем добавить новые столбцы в наш набор данных, выполнив поиск по словам в текстах или новостях, агрегированных по времени. Например, мы могли бы создать «промо» с количеством упоминаний, которое будет соответствовать нашим самодельным нограммам:

text_filter = {'name' : 'promo', 
               'match_ngrams': ['promo', 'dicount', 'cut', 'markdown','deduction']}
# blend_source needs the id_dataset and the name of the feature.blend_source = {
                'id_dataset':'5e1deeda9516290a00c5f8f6',
                'feature' : 'text',
                'filter_text' : text_filter
            }
df_blend = OpenBlender.timeBlend( token = token,
                                  anchor_ts = df_st1dept1.timestamp,
                                  blend_source = blend_source,
                                  blend_type = 'agg_in_intervals',
                                  interval_size = 60 * 60 * 24 * 7,
                                  direction = 'time_prior',
                                  interval_output = 'list')
df_anchor = pd.concat([df_st1dept1, df_blend.loc[:, df_blend.columns != 'timestamp']], axis = 1)

Параметры функции timeBlend (документацию можно найти здесь):

  • anchor_ts: нам нужно только отправить столбец с отметкой времени, чтобы его можно было использовать в качестве привязки для смешивания внешних данных.
  • blend_source: информация о нужной нам функции.
  • blend_type: «agg_in_intervals», потому что мы хотим агрегировать интервалы в 1 неделю для каждого из наших наблюдений.
  • Inverval_size: размер интервала в секундах (в данном случае 24 * 7 часов).
  • direction: «time_prior», потому что мы хотим, чтобы за интервал собирались наблюдения за предыдущие 7 дней, а не вперед, чтобы избежать утечки данных.

Теперь у нас есть исходный набор данных, но с 2 новыми столбцами, «COUNT» нашей «рекламной» функции и списком фактических текстов на тот случай, если кто-то захочет повторить каждый из них.

df_anchor.tail()

Итак, теперь у нас есть числовая функция о том, сколько раз были упомянуты наши нграммы. Вероятно, мы могли бы делать программы лучше, если бы знали, какой магазин или отдел соответствует «1» (Walmart не поделился этим).

Давайте применим стандартную модель и сравним ошибку с исходной.

our_model.train(df_anchor, 'Weekly_Sales')
error_sum = our_model.getMetrics(df_anchor, 'Weekly_Sales')
error_sum
#> 253875.30

Текущая модель имела ошибку 253 975 долларов, а предыдущая - 290 037 долларов. Это улучшение на 12%.

Но это мало что доказывает, возможно, RandomForest повезло. В конце концов, исходная модель тренировалась с более чем 299 тысячами наблюдений. Текущая только тренировка со 102 !!

Мы также можем смешивать числовые характеристики. Давайте попробуем совместить Индекс доллара, Цена на нефть и Месячные настроения потребителей.

# OIL
blend_source = {
                'id_dataset':'5e91045a9516297827b8f5b1',
                'feature' : 'price'
            }
df_blend = OpenBlender.timeBlend( token = token,
                                  anchor_ts = df_anchor.timestamp,
                                  blend_source = blend_source,
                                  blend_type = 'agg_in_intervals',
                                  interval_size = 60 * 60 * 24 * 7,
                                  direction = 'time_prior',
                                  interval_output = 'avg',
                                  missing_values = 'impute')
df_anchor = pd.concat([df_anchor, df_blend.loc[:, df_blend.columns != 'timestamp']], axis = 1)
# DOLLAR INDEX
blend_source = {
                'id_dataset':'5e91029d9516297827b8f08c',
                'feature' : 'price'
            }
df_blend = OpenBlender.timeBlend( token = token,
                                  anchor_ts = df_anchor.timestamp,
                                  blend_source = blend_source,
                                  blend_type = 'agg_in_intervals',
                                  interval_size = 60 * 60 * 24 * 7,
                                  direction = 'time_prior',
                                  interval_output = 'avg',
                                  missing_values = 'impute')
df_anchor = pd.concat([df_anchor, df_blend.loc[:, df_blend.columns != 'timestamp']], axis = 1)
# CONSUMER SENTIMENT
blend_source = {
                'id_dataset':'5e979cf195162963e9c9853f',
                'feature' : 'umcsent'
            }
df_blend = OpenBlender.timeBlend( token = token,
                                  anchor_ts = df_anchor.timestamp,
                                  blend_source = blend_source,
                                  blend_type = 'agg_in_intervals',
                                  interval_size = 60 * 60 * 24 * 7,
                                  direction = 'time_prior',
                                  interval_output = 'avg',
                                  missing_values = 'impute')
df_anchor = pd.concat([df_anchor, df_blend.loc[:, df_blend.columns != 'timestamp']], axis = 1)
df_anchor

Теперь у нас есть еще 6 функций: среднее значение нефтяного индекса, индекса доллара и настроения потребителей за 7-дневные интервалы, а также их количество (что в данном случае не имеет значения).

Давайте снова запустим эту модель.

our_model.train(df_anchor, 'Weekly_Sales')
error_sum = our_model.getMetrics(df_anchor, 'Weekly_Sales')
error_sum
>223831.9414

Теперь мы получили ошибку в размере 223 831 долларов США. Это улучшение на 24,1% по сравнению с первоначальными 290 037 долларов !!

Итак, давайте теперь попробуем это с каждым отделом в отдельности, чтобы измерить, насколько последовательна выгода.

Шаг 4. Тестирование во всех отделах

Чтобы получить представление, мы сначала поэкспериментируем с первыми 10 отделами и сравним преимущества добавления каждого дополнительного источника.

# Function to filter features from other sources
def excludeColsWithPrefix(df, prefixes):
    cols = df.columns
    for prefix in prefixes:
        cols = [col for col in cols if prefix not in col]
    return df[cols]
error_sum_enhanced = []
action = 'API_getObservationsFromDataset'
# Loop through the first 10 Departments and test them.
for dept in range(1, 10):
    print('---')
    print('Starting department ' + str(dept))
    
    # Get it into a dataframe
    df_dept = df_walmart_st1[df_walmart_st1['Dept'] == dept]
    
    
    # Unix Timestamp
    df_dept['timestamp'] = OpenBlender.dateToUnix(df_dept['Date'], 
                                           date_format = '%Y-%m-%d', 
                                           timezone = 'GMT')
# Function to filter features from other sources
def excludeColsWithPrefix(df, prefixes):
    cols = df.columns
    for prefix in prefixes:
        cols = [col for col in cols if prefix not in col]
    return df[cols]
error_sum_enhanced = []
# Loop through the first 10 Departments and test them.
for dept in range(1, 10):
    print('---')
    print('Starting department ' + str(dept))
    
    # Get it into a dataframe
    df_dept = df_walmart_st1[df_walmart_st1['Dept'] == dept]
    
    
    # Unix Timestamp
    df_dept['timestamp'] = OpenBlender.dateToUnix(df_dept['Date'], 
                                           date_format = '%Y-%m-%d', 
                                           timezone = 'GMT')
df_dept = df_dept.sort_values('timestamp').reset_index(drop = True)
    
    
    # "PROMO" FEATURE OF MENTIONS ON WALMART
    
    text_filter = {'name' : 'promo', 
               'match_ngrams': ['promo', 'dicount', 'cut', 'markdown','deduction']}
    
    blend_source = {
                    'id_dataset':'5e1deeda9516290a00c5f8f6',
                    'feature' : 'text',
                    'filter_text' : text_filter
                }
df_blend = OpenBlender.timeBlend( token = token,
                                      anchor_ts = df_st1dept1.timestamp,
                                      blend_source = blend_source,
                                      blend_type = 'agg_in_intervals',
                                      interval_size = 60 * 60 * 24 * 7,
                                      direction = 'time_prior',
                                      interval_output = 'list')
df_anchor = pd.concat([df_st1dept1, df_blend.loc[:, df_blend.columns != 'timestamp']], axis = 1)
    
    # OIL 
    blend_source = {
                    'id_dataset':'5e91045a9516297827b8f5b1',
                    'feature' : 'price'
                }
df_blend = OpenBlender.timeBlend( token = token,
                                      anchor_ts = df_anchor.timestamp,
                                      blend_source = blend_source,
                                      blend_type = 'agg_in_intervals',
                                      interval_size = 60 * 60 * 24 * 7,
                                      direction = 'time_prior',
                                      interval_output = 'avg',
                                      missing_values = 'impute')
df_anchor = pd.concat([df_anchor, df_blend.loc[:, df_blend.columns != 'timestamp']], axis = 1)
# DOLLAR INDEX
blend_source = {
                    'id_dataset':'5e91029d9516297827b8f08c',
                    'feature' : 'price'
                }
df_blend = OpenBlender.timeBlend( token = token,
                                      anchor_ts = df_anchor.timestamp,
                                      blend_source = blend_source,
                                      blend_type = 'agg_in_intervals',
                                      interval_size = 60 * 60 * 24 * 7,
                                      direction = 'time_prior',
                                      interval_output = 'avg',
                                      missing_values = 'impute')
df_anchor = pd.concat([df_anchor, df_blend.loc[:, df_blend.columns != 'timestamp']], axis = 1)
# CONSUMER SENTIMENT
blend_source = {
                    'id_dataset':'5e979cf195162963e9c9853f',
                    'feature' : 'umcsent'
                }
df_blend = OpenBlender.timeBlend( token = token,
                                      anchor_ts = df_anchor.timestamp,
                                      blend_source = blend_source,
                                      blend_type = 'agg_in_intervals',
                                      interval_size = 60 * 60 * 24 * 7,
                                      direction = 'time_prior',
                                      interval_output = 'avg',
                                      missing_values = 'impute')
df_anchor = pd.concat([df_anchor, df_blend.loc[:, df_blend.columns != 'timestamp']], axis = 1)
    
    
    try:
        
        error_sum = {}
        
        # Gather errors from every source by itself# Dollar Index
        df_selection = excludeColsWithPrefix(df_anchor, ['WALMART_TW', 'US_MONTHLY_CONSUMER', 'OIL_INDEX'])
        our_model.train(df_selection, 'weekly_sales')
        error_sum['1_features'] = our_model.getMetrics(df_selection, 'weekly_sales')
        
        # Walmart News
        df_selection = excludeColsWithPrefix(df_anchor, [ 'US_MONTHLY_CONSUMER', 'OIL_INDEX'])
        our_model.train(df_selection, 'weekly_sales')
        error_sum['2_feature'] = our_model.getMetrics(df_selection, 'weekly_sales')
        
        # Oil Index
        df_selection = excludeColsWithPrefix(df_anchor,['US_MONTHLY_CONSUMER'])
        our_model.train(df_selection, 'weekly_sales')
        error_sum['3_features'] = our_model.getMetrics(df_selection, 'weekly_sales')
        
        # Consumer Sentiment (All features)
        df_selection = df
        our_model.train(df_selection, 'weekly_sales')
        error_sum['4_features'] = our_model.getMetrics(df_selection, 'weekly_sales')
        
    except:
        
        print(traceback.format_exc())
        print("No observations found for department: " + str(dept))
        error_sum = 0
        
    error_sum_enhanced.append(error_sum)

Давайте перенесем результаты в DataFrame и визуализируем.

separated_results = pd.DataFrame(error_sum_enhanced)
separated_results['original_error'] = error_summary_df[0:10]['error_sum_normal_model']
separated_results = separated_results[['original_error', '1_feature', '2_features', '3_features', '4_features']]
separated_results
separated_results.transpose().plot(kind='line')

Отделы 4 и 6 находятся на более высоком уровне, чем остальные. Давайте удалим их, чтобы поближе познакомиться с остальными.

separated_results.drop([6, 4]).transpose().plot(kind=’line’)

Мы видим, что почти во всех отделах ошибка уменьшилась по мере того, как мы добавляли новые функции. Мы также видим, что Oil Index (третья характеристика) не только не помог, но даже вреден для некоторых ведомств.

Я исключил Oil Index и запустил алгоритм с тремя функциями для всех отделов (что можно сделать, повторяя все error_summary_df, а не только первые 10).

Посмотрим на результаты.

Шаг 5. Измерьте результаты

Это результаты сочетания "трех функций" и процент улучшения по всем отделам:

Мало того, что добавленные функции уменьшили количество ошибок в более чем 88% отделов, но и некоторые улучшения были значительными.

Это гистограмма для процента улучшения.

Первоначальная ошибка (рассчитанная в начале статьи) составляла 24 009 404,06 доллара США, а окончательная ошибка - 9 596 215,21 доллара США, что означает, что она была уменьшена более чем на 60%

И это всего лишь один магазин.

Спасибо за чтение.