Вот как можно использовать модель LSTM для прогнозирования ADR (средней дневной ставки) для отелей - краеугольного показателя в отрасли.

Средняя дневная ставка (ADR) признана одним из важнейших показателей для отелей.

Он рассчитывается следующим образом:

ADR = Revenue ÷ sold rooms

По сути, ADR измеряет среднюю цену гостиничного номера за определенный период.

Фон

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

Используя анализ временных рядов, предположим, что отель желает: 1) рассчитать среднее значение ADR для всех бронирований за данную неделю и 2) использовать эти данные для прогнозирования будущих тенденций недельного ADR - под недельным ADR мы подразумеваем средний ADR по всем бронирования в любую неделю - далее именуемые «еженедельные ADR».

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

Для этого используется сеть долговременной памяти (LSTM). LSTM - это последовательные нейронные сети, которые предполагают зависимость между наблюдениями в определенной серии. Таким образом, они все чаще используются для прогнозирования временных рядов.

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

Манипуляция данными

Используя pandas, полная дата (год и номер недели) объединяется с соответствующим значением ADR для каждого бронирования.

Затем эти точки данных были сгруппированы вместе для получения среднего показателя ADR за неделю по всем бронированиям следующим образом:

df4 = df3.groupby('FullDate').agg("mean")
df4
df4.sort_values(['FullDate'], ascending=True)

Вот как выглядит новый фрейм данных:

В качестве примечания: полный блокнот и наборы данных доступны по ссылке на репозиторий GitHub, представленной ниже, где процедуры обработки данных проиллюстрированы более подробно.

Построен график временного ряда:

import matplotlib.pyplot as plt
plt.plot(tseries)
plt.tick_params(
    axis='x',          # changes apply to the x-axis
    which='both',      # both major and minor ticks are affected
    bottom=False,      # ticks along the bottom edge are off
    top=False,         # ticks along the top edge are off
    labelbottom=False) # labels along the bottom edge are off
plt.ylabel('ADR')
plt.title("Weekly ADR")
plt.show()

Конфигурация модели LSTM

Начнем анализ набора данных H1. Вызываются первые 100 наблюдений из созданного временного ряда. Затем создается матрица набора данных, и данные масштабируются.

df = df[:100]
# Form dataset matrix
def create_dataset(df, previous=1):
    dataX, dataY = [], []
    for i in range(len(df)-previous-1):
        a = df[i:(i+previous), 0]
        dataX.append(a)
        dataY.append(df[i + previous, 0])
    return np.array(dataX), np.array(dataY)

Затем данные нормализуются с помощью MinMaxScaler, чтобы нейронная сеть могла правильно их интерпретировать:

# normalize dataset with MinMaxScaler
scaler = MinMaxScaler(feature_range=(0, 1))
df = scaler.fit_transform(df)
df

Вот образец вывода:

array([[0.35915778],
       [0.42256282],
       [0.53159902],
...
       [0.27125524],
       [0.26293747],
       [0.25547682]])

Данные разделены на обучающие и тестовые наборы с параметром previous, установленным на 5:

import tensorflow as tf
from tensorflow.keras import layers
from tensorflow.keras.layers import Dense
from tensorflow.keras.layers import LSTM
# Training and Validation data partition
train_size = int(len(df) * 0.8)
val_size = len(df) - train_size
train, val = df[0:train_size,:], df[train_size:len(df),:]
# Number of previous
previous = 5
X_train, Y_train = create_dataset(train, previous)
X_val, Y_val = create_dataset(val, previous)

Когда для параметра previous установлено значение this, это по существу означает, что значение в момент времени t (Y_train для данных обучения) прогнозируется с использованием значений t -1, t-2, t-3, t-4 и t-5 (все под X_train).

Вот образец массива Y_train:

array([0.70858066, 0.75574219, 0.7348692 , 0.63555916, 0.34629856,
       0.32723163, 0.18514608, 0.21056117, 0.13243974, 0.1321469 ,
       0.06636683, 0.09516089, 0.02223529, 0.02497857, 0.06036494,
...
       0.12222412, 0.07324677, 0.05206859, 0.05937164, 0.04205497,
       0.0867528 , 0.10976084, 0.0236608 , 0.11987636])

Вот образец массива X_train:

array([[0.35915778, 0.42256282, 0.53159902, 0.6084246 , 0.63902841],
       [0.42256282, 0.53159902, 0.6084246 , 0.63902841, 0.70858066],
       [0.53159902, 0.6084246 , 0.63902841, 0.70858066, 0.75574219],
...
       [0.07324677, 0.05206859, 0.05937164, 0.04205497, 0.0867528 ],
       [0.05206859, 0.05937164, 0.04205497, 0.0867528 , 0.10976084],
       [0.05937164, 0.04205497, 0.0867528 , 0.10976084, 0.0236608 ]])

Проходит 100 эпох:

# reshape input to be [samples, time steps, features]
X_train = np.reshape(X_train, (X_train.shape[0], 1, X_train.shape[1]))
X_val = np.reshape(X_val, (X_val.shape[0], 1, X_val.shape[1]))
# Generate LSTM network
model = tf.keras.Sequential()
model.add(LSTM(4, input_shape=(1, previous)))
model.add(Dense(1))
model.compile(loss='mean_squared_error', optimizer='adam')
history=model.fit(X_train, Y_train, validation_split=0.2, epochs=100, batch_size=1, verbose=2)

Вот несколько примеров результатов:

Train on 59 samples, validate on 15 samples
Epoch 1/100
59/59 - 1s - loss: 0.0689 - val_loss: 0.0027
Epoch 2/100
59/59 - 0s - loss: 0.0431 - val_loss: 0.0118
...
Epoch 99/100
59/59 - 0s - loss: 0.0070 - val_loss: 0.0031
Epoch 100/100
59/59 - 0s - loss: 0.0071 - val_loss: 0.0034
dict_keys(['loss', 'val_loss'])

Это визуальное представление потери при обучении и проверке:

Прогнозы обучения и проверки

А теперь давайте сделаем несколько прогнозов.

# Generate predictions
trainpred = model.predict(X_train)
valpred = model.predict(X_val)

Вот пример прогнозов для обучения и тестирования:

Прогнозы тренировок

>>> trainpred
array([[0.6923234 ],
       [0.73979336],
       [0.75128263],
...
       [0.09547461],
       [0.11602292],
       [0.050261  ]], dtype=float32)

Тестовые прогнозы

>>> valpred
array([[0.06604623],
       [0.0982968 ],
       [0.10709635],
...
       [0.3344252 ],
       [0.2922875 ]], dtype=float32)

Прогнозы конвертируются обратно в нормальные значения с помощью scaler.inverse_transform, и вычисляются оценки обучения и проверки.

import math
from sklearn.metrics import mean_squared_error
# calculate RMSE
trainScore = math.sqrt(mean_squared_error(Y_train[0], trainpred[:,0]))
print('Train Score: %.2f RMSE' % (trainScore))
valScore = math.sqrt(mean_squared_error(Y_val[0], valpred[:,0]))
print('Validation Score: %.2f RMSE' % (valScore))

Результаты обучения и проверки

Train Score: 12.71 RMSE
Validation Score: 8.83 RMSE

Вот сюжет с предсказаниями:

Формы массивов тестов и прогнозов изменяются соответствующим образом, и определяется функция для средней точности по направлению:

import numpy as np
def mda(actual: np.ndarray, predicted: np.ndarray):
    """ Mean Directional Accuracy """
    return np.mean((np.sign(actual[1:] - actual[:-1]) == np.sign(predicted[1:] - predicted[:-1])).astype(int))

Результаты модели

Теперь рассчитывается средняя точность по направлению:

>>> mda(Y_val, predictions)
0.8571428571428571

Получено значение MDA, равное 86%, что означает, что модель правильно предсказывает направление фактических еженедельных тенденций ADR в 86% случаев.

Как видно выше, также была получена оценка 8,83 RMSE. RMSE - это мера отклонения еженедельного ADR от фактических значений, принимающая тот же числовой формат, что и тот же. Среднее недельное значение ADR по данным валидации составило 69,99.

Средняя ошибка прогноза для данных проверки составила -1,419:

>>> forecast_error = (predictions-Y_val)
>>> forecast_error
>>> mean_forecast_error = np.mean(forecast_error)
>>> mean_forecast_error
-1.419167548625413

Тестирование на невидимых (тестовых) данных

Теперь, когда модель обучена, следующим шагом будет проверка прогнозов модели на невидимых (или тестовых данных).

Как объяснялось ранее, значение в момент времени t прогнозируется LSTM с использованием значений t-1, t-2, t -3, t-4 и t-5.

В этом случае прогнозируются последние 15 недельных значений ADR в серии.

actual = tseries.iloc[100:115]
actual = np.array(actual)
actual

Созданная ранее модель теперь используется для прогнозирования каждого значения с использованием предыдущих пяти значений временного ряда:

# Test (unseen) predictions
# (t) and (t-5)
>>> XNew
array([[ 82.1267268 ,  90.48381679,  85.81940503,  84.46819121,
         83.25621451],
       [ 90.48381679,  85.81940503,  84.46819121,  83.25621451,
         84.12304147],
...
       [189.16831978, 198.22268542, 208.71251185, 211.52835052,
        211.16204036],
       [198.22268542, 208.71251185, 211.52835052, 211.16204036,
        210.28488251]])

Переменные соответствующим образом масштабируются, и вызывается model.predict:

Xnew = scaler.transform(Xnew)
Xnew
Xnewformat = np.reshape(Xnew, (Xnew.shape[0], 1, Xnew.shape[1]))
ynew=model.predict(Xnewformat)

Вот массив сгенерированных прогнозов:

array([0.02153895, 0.0157201 , 0.12966183, 0.22085814, 0.26296526,
       0.33762595, 0.35830092, 0.54184073, 0.73585206, 0.8718423 ,
       0.92918825, 0.9334069 , 0.8861607 , 0.81483454, 0.76510745],
      dtype=float32)

Массив преобразуется обратно в исходный формат значения:

>>> ynew = ynew * np.abs(maxt-mint) + np.min(tseries)
>>> ynewpd=pd.Series(ynew)
>>> ynewpd
0      45.410988
1      44.423096
2      63.767456
3      79.250229
4      86.398926
5      99.074379
6     102.584457
7     133.744766
8     166.682877
9     189.770493
10    199.506348
11    200.222565
12    192.201385
13    180.092041
14    171.649673
dtype: float32

Вот рассчитанные MDA, RMSE и MFE (средняя ошибка прогноза).

MDA = 0,86

>>> mda(actualpd, ynewpd)
0.8666666666666667

RMSE = 33,77

>>> mse = mean_squared_error(actualpd, ynewpd)
>>> rmse = sqrt(mse)
>>> print('RMSE: %f' % rmse)
RMSE: 33.775573

MFE = -30,17

>>> forecast_error = (ynewpd-actualpd)
>>> mean_forecast_error = np.mean(forecast_error)
>>> mean_forecast_error
-30.173496939933216

При среднем недельном ADR для набора тестов, равном 160,49, показатели RMSE и MFE действительно выглядят достаточно высокими (чем меньше ошибка, тем лучше).

H2 результаты

Та же процедура была проведена с набором данных H2 (данные ADR для отдельного отеля в Португалии). Вот результаты при сравнении прогнозов с тестовым набором:

MDA = 0,86

>>> mda(actualpd, ynewpd)
0.8666666666666667

RMSE = 38,15

>>> mse = mean_squared_error(actualpd, ynewpd)
>>> rmse = sqrt(mse)
>>> print('RMSE: %f' % rmse)
RMSE: 38.155347

MFE = -34,43

>>> forecast_error = (ynewpd-actualpd)
>>> mean_forecast_error = np.mean(forecast_error)
>>> mean_forecast_error
-34.437111023457376

Для набора данных H2 среднее недельное значение ADR по тестируемому набору составило 131,42, при этом для сравнения ошибки RMSE и MFE были низкими.

Заключение

В этом примере вы увидели, как можно прогнозировать ADR с помощью модели LSTM. В частности, приведенные выше примеры иллюстрируют:

  • Как построить модель LSTM
  • Методы измерения погрешности и точности прогнозов модели LSTM

Наборы данных и записные книжки для этого примера доступны в репозитории MGCodesandStats GitHub вместе с дополнительными исследованиями по этой теме.

Вы также можете найти больше моих материалов по науке о данных на michael-grogan.com.

Заявление об ограничении ответственности: эта статья написана на условиях «как есть» без каких-либо гарантий. Он был написан с целью предоставить обзор концепций науки о данных и никоим образом не должен интерпретироваться как профессиональный совет.