Как прогнозировать цены на авиабилеты?

от автора

Всем привет!
Это третья статья о там, как я делаю небольшой и уютный сервис, который в теории должен помочь с планированием путешествий. В этой статье я расскажу про то, как предсказывать цены на авиабилеты, имея под рукой Clickhouse, Catboost и 1TB* данных.
image

Для чего это нужно?

Одна из основных фичей cheapster.travel — это гибкое комбинирование сложных маршрутов (подробнее в предыдущей статье). Для того, чтобы комбинировать «все-со-всем» используется кэш агрегаторов, в котором не всегда есть билеты, которые редко ищут, а их катастрофически не хватает, чтобы строить сложные маршруты. Т.е. горячие билеты (дешевые) на котором будет основываться сложный маршрут есть, но при этом не хватает 1-2 сегментов из «обычных» билетов (по обычной цене, на не самом популярном направлении). Именно эта проблема привела меня к необходимости построить модель, которая смогла бы предсказывать цены на авиабилеты.

Формализация задачи

  • Нужно уметь предсказывать билеты на прямые рейсы (только туда или туда-обратно)
  • Нужно уметь регулярно предсказывать и сохранять это в базу (простой сценарий)
  • Нужно уметь предсказывать «на лету» (сложный сценарий)
  • Это все происходит на сильно ограниченном железе — поэтому минимум манипуляций с большими объемами данных

Как это сделать?

Для начала обучим модель: готовим датасет, выделяя максимальное кол-во фичей в колонки, выгружаем его в tsv, загружаем его в DataFrame/Pool, проводим анализ, подбираем параметры… Стоп, у нас слишком много данных и они не помещаются в память, — ловим такие ошибки:
MemoryError: Unable to allocate array with shape (38, 288224989) and data type float64
OSError: [Errno 12] Cannot allocate memory

Чтобы обойти это ограничение пришлось итеративно обучаться на маленьких кусочках, выглядит это так:

model = CatBoostRegressor(cat_features=cat_features,           iterations=100,           learning_rate=.5,           depth=10,           l2_leaf_reg=9,           one_hot_max_size=5000)  for df in tqdm(pd.read_csv('history.tsv', sep='\t',                             na_values=['\\N'],                             chunksize=2_000_000)):     ...      model.fit(X=df[df.columns[:-1]][:train_size].values,                   y=df['price'][:train_size].values,                   eval_set=eval_pool,                   verbose=False,                   plot=False,                   init_model=model) # <-- В каждой итерации на вход подается предыдущая модель 

В итоге получилась модель с RMSE~100 — в целом меня бы устроил и такой результат, но после небольшого анализа и «нормализации» предсказаний (отрицательные и значения, которые сильно отличаются от min/max значений в истории, приведены к соответствующим границам исторических цен). После этого целевая метрика~80, с учетом того, что по моему опыту, логики и здравого смысла при формировании цен на авиабилеты почти нет.

Фичи, которые больше всего влияют на цену:
image
Статистика для фичи «Расстояние между городами»:
image

Отлично, модель у нас есть — теперь пора ее использовать. Первым делом, добавляем модель КХ, это делается простым конфигом:

Конфиг

<models>     <model>         <!-- Model type. Now catboost only. -->         <type>catboost</type>         <!-- Model name. -->         <name>price</name>         <!-- Path to trained model. -->         <path>/opt/models/price_iter_model_2.bin</path>         <!-- Update interval. -->         <lifetime>0</lifetime>     </model> </models>

Делаем регулярный батчевый процесс предсказания — это достаточно просто сделать с помощью Apache Airflow.

Получившийся DAG выглядит так

image
Один элемент DAGa выглядит так(для тех кто не знаком с Airflow):

SimpleHttpOperator

insert_ow_in_tmp = SimpleHttpOperator(     task_id='insert_ow_in_tmp',     http_conn_id='clickhouse_http',     endpoint=dll_endpoint,     method='POST',     data=sql_templates.INSERT_OW_PREDICTIONS_IN_TMP,     pool='clickhouse_select',     dag=dag )

Для предсказания «на лету» используется обычный sql:

select origin, destination, date,          modelEvaluate('price', *)  predicted_price -- да, именно так просто from log.history  +--------+-------------+------------+-----------------+ | origin | destination | date       | predicted_price | +--------+-------------+------------+-----------------+ | VKO    | DEB         | 2020-03-20 | 3234.43244      | +--------+-------------+------------+-----------------+ --*Пример сокращен, чтобы проще воспринимался 

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

  • Нет необходимости выгружать данные во вне КХ (это значит быстрее и менее затратно по нагрузке на железо)
  • Не нужно делать etl-процессы (проще=надежнее)

Немного правим API и фронтенд и получаем долгожданные предсказания.
Эти предсказания также хорошо вписались в раздел История цен на авиабилеты:
image
Функционал доступен по ссылке cheapster.travel/history (на мобильном откроется криво, только большие экраны)

На этом всё, всем продуктивного дня!

Предыдущие статьи

Попытка решить проблему выбора авиабилетов перед отпуском
Попытка решить проблему выбора авиабилетов перед отпуском #2

Другой интересный функционал

Комбинатор сложных маршрутов
Сложные билеты (треугольники)

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

1TB* — это если выгрузить в tsv, в КХ это занимает на порядок меньше.

ссылка на оригинал статьи https://habr.com/ru/post/490762/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *