Как вы понимаете, уровень отмены бронирований в индустрии онлайн-бронирования довольно высок. После того, как бронирование было отменено, почти ничего не поделаешь. Это создает дискомфорт для многих учреждений и вызывает желание принять меры предосторожности. Следовательно, прогнозирование бронирования, которое может быть отменено, и предотвращение такой отмены создаст дополнительную стоимость для учреждений.

В этой статье я попытаюсь объяснить, как с помощью методов машинного обучения можно заранее предсказать будущее отмененное бронирование. Начнем с предварительной обработки!

Предварительная обработка

Прежде всего, я должен сказать, что вы можете получить доступ к данным, используемым в моем репозитории, которыми я расскажу в конце своей статьи. Еще хочу поделиться, что это тема диссертации. [1]

У нас есть два отдельных набора данных, и, поскольку мы собираемся выполнить предварительную обработку для обоих, имеет смысл объединить их. Но на этапе моделирования мы захотим получить эти два набора данных по отдельности. Итак, чтобы различать их, я создал поле id.

import pandas as pd
h1 = pd.read_csv('data/H1.csv')
h2 = pd.read_csv('data/H2.csv')
h1.loc[:, 'id'] = range(1, len(h1) + 1)

start = h1['id'].max() + 1
stop = start + len(h2)
h2.loc[:, 'id'] = range(start, stop)
df = pd.concat([h1, h2], ignore_index=True, sort=False)

Вот шаги предварительной обработки для этого проекта:

  • Преобразование строки NULL или Undefined values ​​в np.nan
  • Удаление отсутствующих наблюдений из столбцов с небольшим количеством NULLvalues
  • Заполнение пропущенных значений по правилам
  • Удаление неверных значений
  • Обнаружение выбросов

Шаг 1. Строка NULL или неопределенных значений в np.nan

import numpy as np
for col in df.columns:
    if df[col].dtype == 'object' and col != 'country':
        df.loc[df[col].str.contains('NULL'), col] = np.nan
        df.loc[df[col].str.contains('Undefined', na=False), col] = np.nan
null_series = df.isnull().sum()
print(null_series[null_series > 0])

С помощью приведенного выше кода мы конвертируем строковые значения NULL и Undefined в значения np.nan. Затем мы печатаем количество NULL значений для каждого столбца. Вот как выглядит результат:

Шаг 2. Удалите некоторые пропущенные значения.

Мы можем удалить NULLvalues ​​в country, children, market_segment, distribution_channel, потому что в этих полях мало NULLvalues.

subset = [
    'country',      
    'children',      
    'market_segment',      
    'distribution_channel'
] 
df = df.dropna(subset=subset)

Шаг 3. Заполните отсутствующие значения набором правил

Для данных существует ряд правил. [2] Например, значения, которые равны Undefined/SC, означают, что они равны no meal type.. Поскольку мы ранее заменили Undefined значения на NULL, мы можем заполнить NULLvalues ​​в поле food значением SC.

Тот факт, что в поле gent указано NULL, означает, что бронирование не было получено от какого-либо агентства. Таким образом, эти бронирования могут рассматриваться как приобретенные напрямую клиентами, без каких-либо посреднических организаций, таких как агентства и т. Д. Поэтому мы не удаляем NULLvalues, а вместо этого выбрасываем случайное значение, например 999. То же самое и с полем c ompany.

Более подробную информацию можно найти в документе по второй ссылке в справочниках.

df.loc[df.agent.isnull(), 'agent'] = 999 
df.loc[df.company.isnull(), 'company'] = 999 df.loc[df.meal.isnull(), 'meal'] = 'SC'

Шаг 4. Удалите неправильные значения

В поле ADR указана средняя стоимость бронирования за ночь. Поэтому принимать значение меньше нуля - ненормально. Вы можете использовать df.describe().T, чтобы увидеть такие ситуации. Мы удаляем значения меньше нуля для поля ADR.

df = df[df.adr > 0]

Шаг 5 - Обнаружение отклонений

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

Нижняя и верхняя точки полей кажутся ниже,

Наконец, мы собираемся поговорить о многомерном обнаружении выбросов. [3] Это особый вывод, над которым мы работаем немного больше, и он применим не к каждому бизнесу. Такие сборы, как 5 или 10 долларов за 1 ночь проживания, могут быть оплачены в обычном режиме, но это не нормально для 10 ночей. Следовательно, удаление этих значений, которые считаются противоположными, из набора данных поможет нашей модели учиться. Итак, я пробовал LocalOutlierFactor и EllipticEnvelope, я только EllipticEnvelope, потому что он дал лучшие результаты, но если вы хотите проверить оба, вы можете посмотреть мой репозиторий.

from sklearn.covariance import EllipticEnvelope
import matplotlib.pyplot as plt
import numpy as np
# create new features: total price and total nights
cleaned.loc[:, 'total_nights'] = \
cleaned['stays_in_week_nights'] + cleaned['stays_in_weekend_nights']
cleaned.loc[:, 'price'] = cleaned['adr'] * cleaned['total_nights']
# create numpy array
X = np.array(cleaned[['total_nights', 'price']])
# create model 
ee = EllipticEnvelope(contamination=.01, random_state=0)
# predictions 
y_pred_ee = ee.fit_predict(X)
# predictions (-1: outlier, 1: normal)
anomalies = X[y_pred_ee == -1]
# plot data and outliers
plt.figure(figsize=(15, 8))
plt.scatter(X[:, 0], X[:, 1], c='white', s=20, edgecolor='k')
plt.scatter(anomalies[:, 0], anomalies[:, 1], c='red');

График выглядит следующим образом. Красные точки показывают значения выбросов.

Как видите, имеет смысл оставить небольшие значения за пределами набора данных, особенно после 6 ночей. Применяя этот процесс, мы можем сохранить набор данных.

df_cleaned = cleaned[y_pred_ee != -1].copy()
h1_cleaned = df_cleaned[df_cleaned.id.isin(h1.id.tolist())]
h2_cleaned = df_cleaned[df_cleaned.id.isin(h2.id.tolist())]
h1_cleaned = h1_cleaned.drop('id', axis=1)
h2_cleaned = h2_cleaned.drop('id', axis=1)
h1_cleaned.to_csv('data/H1_cleaned.csv', index=False)
h2_cleaned.to_csv('data/H2_cleaned.csv', index=False)

Функциональная инженерия

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

Шаг 1 - корреляция

Сначала я собираюсь преобразовать категориальные данные в integer с помощью LabelEncoder, а затем посмотрю на корреляции. [4] Следующий код делает это,

from sklearn.preprocessing import LabelEncoder
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
train = pd.read_csv('./data/H1_cleaned.csv')
test = pd.read_csv('./data/H2_cleaned.csv')
df_le = train.copy()
le = LabelEncoder()

categoricals = [
    'arrival_date_month',
    'meal',
    'country',
    'market_segment',
    'distribution_channel',
    'reserved_room_type',
    'assigned_room_type',
    'deposit_type',
    'agent',
    'company',
    'customer_type',
    'reservation_status',
]

for col in categoricals:
    df_le[col] = le.fit_transform(df_le[col])
plt.figure(figsize=(20, 15))
sns.heatmap(df_le.corr(), annot=True, fmt='.2f');

Этот код дает нам корреляционную матрицу, подобную приведенной ниже,

В этой матрице, по-видимому, существует высокая отрицательная корреляция между функциями reserve_status и is_canceled. Также существует высокая корреляция между полями total_nights и stays_in_week_nights и stays_in_weekend_nights. Итак, мы удаляем функции reservation_status и total_nights из нашего набора данных. Поскольку существует связь между резервированием_дата и резервированием_статус, мы удалим эту функцию.

columns = [
    'reservation_status_date',
    'total_nights',
    'reservation_status',
]

train = train.drop(columns, axis=1)
test = test.drop(columns, axis=1)
df_le = df_le.drop(columns, axis=1)

Шаг 2 - фиктивные переменные и кодировщик меток

Для работы моделей машинного обучения требуются числовые данные. Поэтому, прежде чем мы сможем моделировать, нам нужно преобразовать категориальные переменные в числовые переменные. Для этого мы можем использовать два метода: Dummy variables и LabelEncoder. С помощью кода, который вы видите ниже, мы создаем функции как с использованием LabelEncoder, так и с использованием dummy variables.

import pandas as pd
new_categoricals = [col for col in categoricals if col in train.columns]
df_hot = pd.get_dummies(data=train, columns=new_categoricals)
test_hot = pd.get_dummies(data=test, columns=new_categoricals)
X_hot = df_hot.drop('is_canceled', axis=1)
X_le = df_le.drop('is_canceled', axis=1)
y = train['is_canceled']

Затем мы строим logistic regression модель с dummy variables и изучаем отчет о классификации в качестве первого взгляда на данные.

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X_hot, y, test_size=.2, random_state=42)

log = LogisticRegression().fit(X_train, y_train)
y_pred = log.predict(X_test)
print(accuracy_score(y_test, y_pred))
print(classification_report(y_test, y_pred))

Оценка точности составляет 0,8584, но точность для отмененных бронирований очень низкая при просмотре отчета о классификации. Потому что в наших данных 23720 успешных дел и 8697 отмененных. В таких случаях предпочтительно разбавлять взвешенный класс или увеличивать количество образцов для меньшего количества отобранных классов. Сначала мы выберем объекты с помощью алгоритма выбора признаков, а затем сравним фиктивные переменные и кодировщик меток, используя разбавленные данные.

Шаг 3 - выбор функции

Выбор функций - один из наиболее важных вопросов при проектировании функций. Здесь мы будем использовать SelectKBest, популярный алгоритм выбора признаков для задач классификации. Наша функция оценки будет chi². [5]

С помощью вышеупомянутой функции мы выбираем лучшие функции как для LabelEncoder, так и для dummy variables.

selects_hot = select(X_hot)
selects_le = select(X_le)

Затем мы просто сравниваем эти особенности.

Результаты сравнения следующие:

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

from sklearn.model_selection import train_test_split
from sklearn.utils import resample
import pandas as pd
last = test_hot[selects_hot + ['is_canceled']]

X_last = last.drop('is_canceled', axis=1)
y_last = last['is_canceled']
# separate majority and minority classes
major = selected[selected['is_canceled'] == 0]
minor = selected[selected['is_canceled'] == 1]

# downsample majority class
downsampled = resample(major, replace=False, n_samples=len(minor), random_state=123) 

# combine minority class with downsampled majority class
df_new = pd.concat([downsampled, minor])

# display new class counts
print(df_new['is_canceled'].value_counts())
X = df_new.drop('is_canceled', axis=1)
y = df_new['is_canceled']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.2, random_state=42)

С помощью приведенного выше кода мы уравняли как количество успешных резервирований, так и количество отмененных резервирований на 8697 и разделили наш набор данных на обучающий и тестовый. Затем мы измерим производительность наших моделей, создав следующий класс.

Перейдем к последнему шагу и сравним наши модели!

Моделирование

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

from sklearn.model_selection import GridSearchCV
from xgboost import XGBClassifier
report = Report(X_test, y_test)
xgb = XGBClassifier().fit(X_train, y_train)
xgb_params = {
    'n_estimators': [100, 500, 1000],     
    'max_depth': [3, 5, 10],     
    'min_samples_split': [2, 5, 10]
}
params = {
    'estimator': xgb,
    'param_grid': xgb_params,
    'cv': 5,
    'refit': False,
    'n_jobs': -1,
    'verbose': 2,
    'scoring': 'recall',
}
xgb_cv = GridSearchCV(**params)
_ = xgb_cv.fit(X_train, y_train)
print(xgb_cv.best_params_)
xgb = XGBClassifier(**xgb_cv.best_params_).fit(X_train, y_train)
report.metrics(xgb)
report.plot_roc_curve(xgb, save=True)

Результаты XGBoost следующие:

Если мы заменим XGBoost на GBM, используя приведенные выше коды, результаты будут следующими:

Заключение

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

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

Надеюсь, это была полезная статья!

Спасибо за чтение! Если вам интересно узнать больше и вы хотите увидеть результаты для файла H2, посетите мой репозиторий!



использованная литература

[1] Нуно Антонио, Ана де Алмейда и Луис Нуньес, Прогнозирование отмены бронирования отелей для уменьшения неопределенности и увеличения доходов (2017)

[2] Нуно Антонио, Ана де Алмейда и Луис Нуньес, Наборы данных о спросе на бронирование отелей (2019)

[3] Кристофер Хосе, Методы обнаружения аномалий в Python (2019)

[4] Вишал Р., Выбор характеристик - корреляция и P-значение (2018)

[5] Выбор функции с помощью SelectKBest (2018)