Это история о том, что подсказка умная, а мы — нет :))
Рассказываем, как мы планировали сделать всё быстро и просто, а получилось как всегда не так. Но мы справились, хоть и не сразу. Речь пойдёт о навигаторе 2ГИС: пользователи строят в нём маршруты и приезжают, куда планировали. Это навело нас на мысль, что хорошо бы предугадывать желание пользователя — предлагать точку назначения и строить маршрут сразу, как только он открыл приложение. Например, пользователь-отец только подумал, что пора забирать ребёнка из детского сада, открыл 2ГИС, а мы ему — готовый маршрут с учётом пробок.
Все счастливы — мы понимаем пользователя, пользователь благодарен (ему не пришлось вбивать адрес, а возможно, и вспоминать его), ребёнка забрали домой.
Понятно, что не у всех есть дети или работа, на которую надо ездить, поэтому решили сосредоточится на качестве, а не на охвате.
Что мы стали делать
«Начните с себя» — говорят нам мотивационные плакаты, и мы задумались: ездим ли мы куда-то постоянно? «Да», − сказали мы себе. Проверили гипотезу и выяснили, что другие люди тоже ездят в одни и те же места.
Результат превзошел все ожидания:
во-первых, 40% маршрутов в приложении приходятся на объекты, куда человек уже ездил;
во вторых, число таких мест не так велико: человек в среднем строит повторные маршруты всего в 10 мест.
Таким образом, задача довольно проста: выбрать одно место из 10 и построить по нему маршрут. Нужно только научиться определять, почему и при каких условиях люди возвращаются в эти условные 10 мест.
Что могло пойти так
Итак, мы прикинули сценарии, по которым люди строят повторные маршруты: забрать ребёнка из детсада и школы, съездить на работу и вернуться с работы домой, увезти рассаду на дачу, вечером заехать поужинать в кафе. Логично, что люди ездят по этим сценариям примерно в одно и то же время: на работу с утра, после 18.00 — домой; на дачу или в кафе — пятничным вечером.
Таким образом, повторюсь, казалось абсолютно логичным сделать алгоритм на основе периодичности обращений пользователя к одному и тому же месту. Казалось, что найти такие повторяющиеся паттерны будет просто, и что их будет много — все мы и правда ездим домой каждый вечер.
Но всё пошло не так.
Что конкретно пошло не так
Напомню, что мы решили сделать алгоритм на основе периодичности обращений, но поведение пользователей на переодичность мы не проверили. Периодичность нам казалась чем-то само собой разумеющимся — мы сами работаем по постоянному графику (и живём тоже :)). Да, бывают большие праздники, но обычных «одинаковых» дней сильно больше.
Мы ожидали увидеть, что большинство маршрутов в определенное место будут выполнятся в один и тот же день недели и схожее время суток. Например, пользователь-отец ездит в детский сад утром каждого вторника.
Во-первых, это оказалось ложным ожиданием: люди не ездят в одно и то же время в одно и то же место. Даже домой. Даже в детский сад.
Во-вторых (и, думаю, вы уже давно догадались), периодически ездить в одно и то же место и периодически использовать для этого навигатор — хоть и связанные процессы, но явно не одинаковые. Фактически мы предполагали, что повторные поездки — как долгосрочные интересы, нечто устоявшееся. Конечно, люди действительно каждый день ездят с работы домой. Но они могут делать это без навигатора.
А ещё они могут ездить в спортзал три месяца каждый день, потом бросить на полгода и снова начать, но теперь уже — два раза в неделю.
Оказалось гораздо эффективнее прогнозировать интерес к месту в определенное время вне зависимости от того, бывал человек там в это время или нет. Мы смещаем фокус, и вместо устоявшихся привычек пытаемся восстановить полную картину возможных действий пользователя.
В этой картине пользователь — хаотический нейтрал, он может сделать что угодно в любой момент! Поехать в университет в три часа ночи или утром построить маршрут домой находясь… дома? Запросто! *нервный смех*
Прогнозировать все возможные маршруты пользователя — более сложная задача, так нужно учитывать гораздо больше факторов, чем просто «ездил на прошлой неделе», однако и охват пользователей будет в разы выше.
Что пошло так
Мы начали решать задачу как классическую классификацию, где мы прогнозируем вероятность, что пользователь построит маршрут в тот или иной объект в определенную дату и время суток.
Мы уже поняли, что человек может построить неожиданный маршрут (вау!) и что маршруты в приложении не всегда отражают поездки в реальности. Если человек в прошлом не строил маршрутов вечером в четверг, это вовсе не значит, что он не сделает это в будущем.
Поэтому при классификации мы отталкивались от того, что вероятность построения маршрута — пусть и минимальная — но есть всегда, для любой комбинации даты и времени. И наша задача — научить модель эту вероятность считать.
Какие данные рассматриваем
Для начала нам нужно было:
выбрать период, который будем рассматривать при формировании признаков для модели;
выделить маршруты, которые мы будем пытаться классифицировать.
Если смотреть на данные за слишком большой период, в признаки попадут интересы пользователя, которые уже изменились. А если за слишком короткий — не получится понять пользователя достаточно хорошо. Здесь мы просто увеличивали период рассмотрения до тех пор, пока это увеличение не перестало давать положительного эффекта, и остановились на данных за последние два месяца.
Ниже — график с качеством модели в зависимости от периода, за который мы смотрим на маршруты. Можно заметить, что после ~45 дней качество перестаёт расти, и, в целом, можно было остановиться здесь. Но мы хотели захватывать чуть больше долгосрочных интересов пользователей, немного жертвуя точностью, поэтому пошли дальше.
При выборе маршрутов для классификации важно определить, что́ мы вообще можем адекватно предсказывать.
С одной стороны, мы не можем хорошо понять интерес человека к месту, если ранее он не строил туда маршрутов или делал это очень редко.
С другой стороны, большинство маршрутов в приложении как раз приходятся на подобные места. 82% маршрутов построены туда, где за последние два месяца пользователь был однажды или не был вообще.
В данном случае мы отсекли эти 82% маршрутов, потому что качество подсказок на них было бы слишком низким. Например, человек вряд ли вернётся туда, где был всего однажды за последние два месяца (вероятность в среднем 0.8%). И ещё менее вероятно, что он поедет в конкретное новое место.
В итоге мы смотрим только на места, куда человек ездил больше двух раз за последние два месяца.
Можно поспорить, что два маршрута — тоже слишком мало для понимания поведения пользователя. Но, игнорируя такие маршруты, мы бы игнорировали и малоактивных пользователей, что не здорово.
Как считаем вероятность повторяющегося маршрута
Допустим, мы хотим понять, поедет ли Человек Человекович в школу утром вторника, чтобы отвезти туда детей. Для этого мы посмотрим на маршруты Человека, построенные в эту школу (и во все остальные объекты) за последние два месяца, и дадим ответы на всевозможные вопросы про прошлые маршруты, например:
Ездил ли Человек в школу утром вторника до этого?
Ездил ли Человек в школу утром любого другого дня?
Ездил ли Человек в школу во вторник вне зависимости от времени?
Ездил ли Человек в школу в похожее время с тем, что мы рассматриваем (например, утро вторника похоже на утро среды и утро понедельника, но не похоже на вечер субботы)?
Как давно был первый и последний маршрут в эту школу?
Как часто и с какими промежутками времени человек ездил в эту школу?
И так далее.
В зависимости от комбинации значений этих характеристик, мы будем увеличивать или уменьшать ожидаемую вероятность поездки. В качестве алгоритма для выявления подобных зависимостей используем градиентный бустинг, в частности, lightGBM. Результат работы этого алгоритма — набор деревьев, каждое из которых отвечает на последовательность вопросов «Да / Нет», и на основе ответов выдаёт маршруту определенный скор. Потом все деревья голосуют вместе, чтобы получить финальную вероятность маршрута.
Представим: мы прогнозируем маршрут вечером вторника. Тогда одно из деревьев в урезанном варианте может выглядеть так (урезанное, потому что целиком оно даже не влезет на страницу):
Понятно, что мы знаем только о поездках через 2ГИС, и что маршруты могут быть неожиданными. Поэтому в частных случаях вероятность, которую мы считаем, не будет отражать реальную картину. Но в среднем — будет.
Вероятности построения маршрутов мы считаем каждый день, чтобы учесть в том числе активность человека за вчерашний день. Затем передаём данные в приложение, где подсказка «Построить маршрут» отображается на главном экране, если вероятность построить маршрут выше определенной границы, например, 15%. Исключение составляют дом и работа пользователя, которые отображаются всегда. Например, вечером пятницы подсказки могут выглядеть так, где зеленым выделены подсказки которые пойдут в приложение:
С границей для отображения подсказки сейчас экспериментируем. Выставление слишком высокой границы ведёт к тому, что мы не спрогнозируем важные для пользователя маршруты и не сэкономим ему время. А слишком низкая граница заспамит пользователя бесполезными подсказками.
Работаем и над второй итерацией алгоритма. Он станет более качественным за счёт прогнозирования отдельных часов (а не утро/день/вечер) и благодаря приоритезации объектов, которые имеют большую значимость для пользователя в целом. Спрогнозировать, что человек поедет в магазин, в котором был вчера — неплохо, но гораздо важнее прогнозировать постоянные маршруты.
Итоговые итоги
Мы хотели сделать простенький алгоритм, который прогнозировал бы интересные пользователю места чуть ли не SQL-запросом. Аспекты использования приложения и случайные решения людей заставили нас задуматься — в итоге мы воротим признаки, описывающие очень разряженный временной ряд. Зато повеселились!
Комментарии (11)
Skynet2034
17.08.2022 12:45+2Как вариант - может быть стоит учитывать ситуации, когда текущее местоположение пользователя находится на траектории одного из предыдущих маршрутов?
Т.к. когда едут по более-менее знакомому маршруту - при старте навигатор зачастую не включают. А вот если по дороге пробка, и юзер хочет посмотреть маршруты её объезда (или хотя бы - на сколько она тянется) - он как раз навигатор и включит.
VadimAr Автор
17.08.2022 14:55+1Тут в идеале понять что человек поедет в какое-то место ещё до того как он на маршрут вышел, но учитывая что часто это сложно идея имеет право на жизнь, главное чтобы с ней меня разработчики на дыбы не подняли =D
FrolVII
17.08.2022 13:50+2мы не можем хорошо понять интерес человека к месту, если ранее он не строил туда маршрутов
Да ладно, это же скучно. Надо так: садишься утром в понедельник за руль, а тебе в подсказках маршрут до какого-нить нового бара неподалеку.
VadimAr Автор
17.08.2022 14:55+1Реально! А вечером погнали в кругосветное путешествие, долой однообразие в маршрутах! ????
azizoid
17.08.2022 17:14С "во-вторых" у вас случилась стандартная ошибка выжившего. Это как с дельфинами, мы знаем много историй о том как дельфины спасали людей потому что люди спаслись и рассказали нам про это. но мы не знаем ничего о людях которых дельфины утопили потому что некому было нам про это рассказать
avdosev
При формировании подсказки учитывается текущее положение пользователя?
Например, если человек находится на работе, то некоторые подсказки могут стать более логичными или наоборот менее.
VadimAr Автор
Сейчас нет, нам хотелось в рамках первой итерации всё считать на бэкэнде, без логики по корректировке весов на стороне приложения. Но мы не отображаем подсказку, если человек находится ближе чем в 150 метрах от неё.
А в целом конечно признак основанный на расстоянии до объекта в текущий момент или что-нибудь вроде доли маршрутов которые были построены пользователем в подсказку из текущей области должны сработать неплохо, подумаем.
censor2005
Мне кажется, если пользователь N-раз построил маршрут из точки А в точку Б, то чем больше N, тем меньше вероятность, что он построит маршрут повторно. Логично строить маршрут, когда едешь в садик за ребёнком в первый раз, но на десятый раз уже без навигатора знаешь, где меньше пробок и как быстрее доехать.
VadimAr Автор
Это так, люди часто перестают ездить по навигатору просто потому что маршрут уже знают наизусть, но есть и пользователи которые ездят с навигатором всё время (их меньше). Плюс ещё мы выдаём подсказки для маршрутов на общественном транспорте и там уже своя атмосфера.
А получиться с полезностью признака может по-разному, например инфа о маршрутах из определенного места может быть важной когда оттуда построено меньше чем N маршрутов и начинать игнорироваться сверх этого значения, а местом может быть область площадью 500 квадратных метров или целый район города, нужно смотреть.