Это третья статья о там, как я делаю небольшой и уютный сервис, который

Для чего это нужно?
Одна из основных фичей 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.

Один элемент DAGa выглядит так(для тех кто не знаком с Airflow):
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, в КХ это занимает на порядок меньше.
ссылка на оригинал статьи https://habr.com/ru/post/490762/
Добавить комментарий