Всем привет!
Это третья статья о там, как я делаю небольшой и уютный сервис, которыйв теории должен помочь с планированием путешествий. В этой статье я расскажу про то, как предсказывать цены на авиабилеты, имея под рукой Clickhouse, Catboost и 1TB* данных.
Одна из основных фичей cheapster.travel — это гибкое комбинирование сложных маршрутов (подробнее в предыдущей статье). Для того, чтобы комбинировать «все-со-всем» используется кэш агрегаторов, в котором не всегда есть билеты, которые редко ищут, а их катастрофически не хватает, чтобы строить сложные маршруты. Т.е. горячие билеты (дешевые) на котором будет основываться сложный маршрут есть, но при этом не хватает 1-2 сегментов из «обычных» билетов (по обычной цене, на не самом популярном направлении). Именно эта проблема привела меня к необходимости построить модель, которая смогла бы предсказывать цены на авиабилеты.
Для начала обучим модель: готовим датасет, выделяя максимальное кол-во фичей в колонки, выгружаем его в tsv, загружаем его в DataFrame/Pool,проводим анализ, подбираем параметры... Стоп, у нас слишком много данных и они не помещаются в память, — ловим такие ошибки:
Чтобы обойти это ограничение пришлось итеративно обучаться на маленьких кусочках, выглядит это так:
В итоге получилась модель с RMSE~100 — в целом меня бы устроил и такой результат, но после небольшого анализа и «нормализации» предсказаний (отрицательные и значения, которые сильно отличаются от min/max значений в истории, приведены к соответствующим границам исторических цен). После этого целевая метрика~80, с учетом того, что по моему опыту, логики и здравого смысла при формировании цен на авиабилеты почти нет.
Фичи, которые больше всего влияют на цену:
Статистика для фичи «Расстояние между городами»:
Отлично, модель у нас есть — теперь пора ее использовать. Первым делом, добавляем модель КХ, это делается простым конфигом:
Делаем регулярный батчевый процесс предсказания — это достаточно просто сделать с помощью Apache Airflow.
Для предсказания «на лету» используется обычный sql:
Хочу заменить, что такой подход выбран, не только потому, что его проще реализовать, — есть еще плюсы:
Немного правим API и фронтенд и получаем долгожданные предсказания.
Эти предсказания также хорошо вписались в раздел История цен на авиабилеты:
Функционал доступен по ссылке cheapster.travel/history (на мобильном откроется криво, только большие экраны).
На этом всё, всем продуктивного дня!
Попытка решить проблему выбора авиабилетов перед отпуском
Попытка решить проблему выбора авиабилетов перед отпуском #2
Комбинатор сложных маршрутов
Сложные билеты (треугольники)
P.S.
Важно! Не воспринимайте эти предсказания, как что-то что помогает выбрать дату покупки — модель может предсказать неправильно, более того ее адекватность не проверена мной или кем-либо другим (все на свой страх и риск, без гарантий).
1TB* — это если выгрузить в tsv, в КХ это занимает на порядок меньше.
Это третья статья о там, как я делаю небольшой и уютный сервис, который
Для чего это нужно?
Одна из основных фичей 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, с учетом того, что по моему опыту, логики и здравого смысла при формировании цен на авиабилеты почти нет.
Фичи, которые больше всего влияют на цену:
Статистика для фичи «Расстояние между городами»:
Отлично, модель у нас есть — теперь пора ее использовать. Первым делом, добавляем модель КХ, это делается простым конфигом:
Конфиг
<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 выглядит так
Один элемент DAGa выглядит так(для тех кто не знаком с Airflow):
Один элемент 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 и фронтенд и получаем долгожданные предсказания.
Эти предсказания также хорошо вписались в раздел История цен на авиабилеты:
Функционал доступен по ссылке cheapster.travel/history (на мобильном откроется криво, только большие экраны).
На этом всё, всем продуктивного дня!
Предыдущие статьи
Попытка решить проблему выбора авиабилетов перед отпуском
Попытка решить проблему выбора авиабилетов перед отпуском #2
Другой интересный функционал
Комбинатор сложных маршрутов
Сложные билеты (треугольники)
P.S.
Важно! Не воспринимайте эти предсказания, как что-то что помогает выбрать дату покупки — модель может предсказать неправильно, более того ее адекватность не проверена мной или кем-либо другим (все на свой страх и риск, без гарантий).
1TB* — это если выгрузить в tsv, в КХ это занимает на порядок меньше.
prokuratdm
как предсказание в реальности идет? метрики же мерились на исторических данных?
в первую очередь интересны метрики уже обученной модели на новых\актуальных данных.