В этом посте я хочу поделиться интересным опытом работы с неразмеченными данными при помощи открытых ресурсов. К сожалению, из-за подписанного NDA, я не смогу полностью поделиться кодом, но, разумеется, всегда готов помочь в комментариях и личных сообщениях с разрешением какого-либо вопроса по теме.
Задача
-
По исходным данным определить является ли конкретное фото, сделанное пользователем туристическим или нет.
-
Масштабировать алгоритм решения на любой регион Российской Федерации.
-
Автоматизировать процесс, избегая ручную работу.
Данные
Представленные клиентом материалы относились к региону Приморскому Края, в координатах 131.304673 : 42.647718; 132.481172 : 43.490732.
Первый сет данных заключал в себе информацию о пользователе (8762 пользователя), а именно:
-
закодированный ID
-
год рождения
-
пол
-
населённый пункт проживания
-
населённый пункт рождения
-
интересы

Единственная нужная фича – город проживания – имела почти половину незаполненных данных.
Второй сет включал в себя информацию о самих фото (154297 фото)
-
ID пользователя (который сделал фото – те же пользователи что и в первой таблице)
-
дата фотографирования в формате Unixtime (данные собраны за один год)
-
координаты места фотографирования

Поиск решения
Я предположил, что турист выкладывает фото не чаще чем два месяца в году в определённом субъекте федерации. Почему два? Непродолжительная поездка или отпуск может попасть на стык календарных месяцев или и вовсе продлится более месяца. При помощи библиотеки datetime я преобразовал формат времени Unixtime в месяцы.
Визуализация данных количества фото в разные месяцы года дала неожиданный результат – доминируют весенние месяцы – не самые комфортные в Приморском Крае. В этом регионе считаются туристическими июль, август – летние, и декабрь, январь – зимние. Здесь, наверное, вопрос к тому, кто собирал данные, где и как.

При помощи QGIS, решил визуализировать локации фотографий на карте (координаты фото изи подгружаются на карту прям из csv файла – туториал на тему), предварительно разделив все фото на три группы руководствуясь следующим принципом:
-
«летние туристические» фото, фото пользователя, которые он выкладывал только два месяца в году и только в июле и августе.
-
«зимние туристические» фото, фото пользователя, которые он выкладывал только два месяца в году и только в декабре и январе.
-
«не туристические фото» — все остальные фото.
Так же в «летние туристические» и «зимние туристические» фото не попали фото пользователей, которые проживают в «местных» городах (воспользовался первой таблицей) — [‘Большой Камень’, ‘Кневичи’, ‘Славянка’, ‘Владивосток’, ‘Архангельск (хутор)’, ‘Вольно-Надеждинское’, ‘Барабаш’, ‘Шкотово’, ‘Суражевка’, ‘Артем’].

Проанализировав распределение маркеров на карте я заметил, что красные (летние) точки довольно редко располагаются в жилых массивах населённых пунктов в отличие от синих (зимние) и белых точек (остальные). Кроме прочего, красные точки локализованы именно в туристических местах (гугль).

Предварительно я пришёл к выводу что необходимо двигаться от локации. Для эксперимента, отобрал координаты двенадцати самых туристических мест Приморского Края. Далее по координатам фото отметил пользователей, которые делали фото в этих локациях – без учёта месяца фотографирования, условием остался только город проживания человека («местные» не попадали).

Результат вышел хороший, но не настолько что бы считать работу законченной. Ведь по мере погружения в задачу, начинаешь задавать себе всё больше вопросов. Например, если пользователь проживает в городе Артём, а фото он сделал в воскресную поездку в центре Владивостока, можно ли считать его туристом? — конечно. Или если пользователь заядлый турист и проживает в соседнем регионе, а в туристические походы в Приморский Край ходит пять раз в год в разные месяцы – тогда по существующему алгоритму его фото будут определяться не туристическими.
Решение
В ходе поиска решения я пришёл к тому, что следует провести многоступенчатую фильтрацию фото, по тем данным, которые у меня имелись, а также установить промежуточный класс между туристическими фото и не туристическими фото — «скорее туристическое фото».
Первый уровень фильтрации по месту жительства пользователя, второй — по дате, когда было сделано фото. Третий же по локации самого фото – производилось фотографирование в туристической локации или нет.
А как определить координаты туристических локаций региона, да ещё сделать процесс автоматизированным для всей России?
opentripmap
Есть такой замечательный ресурс opentripmap (https://opentripmap.io).
«Этот API позволяет получать данные объектов из базы данных OpenTripMap с помощью HTTP-запросов. Вы можете легко интегрировать API в свое приложение или веб-сайт
OpenTripMap основан на совместной обработке различных открытых источников данных (OpenStreetMap, Викиданные, Википедия, Министерство культуры и Министерство природных ресурсов и экологии Российской Федерации) и охватывает более 10 миллионов туристических достопримечательностей и объектов по всему миру.»
Типы объектов иерархически структурированы, легко выбрать категорию и подкатегорию.

Приведу пример кода запроса к API. Для Приморского Края я смог получить чуть больше тысячи границ (боксов) туристических мест по заданным категориям, что позволило в дальнейшем намного точнее определять принадлежность фотографий к классам.
# Список категорий для поиска kinds = ['category1','category2','category3','category4','category5','category6','category7','category8'] class OTMApi: url = 'https://api.opentripmap.com/0.1/ru' def __init__(self, api_key): self._api_key = api_key def list_places(self, bbox, kinds): params = { "lon_min": bbox[0][0], "lon_max": bbox[1][0], "lat_min": bbox[0][1], "lat_max": bbox[1][1], "apikey": self._api_key, "format": "json", "kinds": kinds } return json.loads(call_api(f"{self.url}/places/bbox", params)) # bbox - для получения границ координат def get_details(self, xid): params = {"apikey": self._api_key} return json.loads(call_api(f"{self.url}/places/xid/{xid}", params)) # Ключ для доступа к api можно получить свой при регистрации на сайте - пришлют на почту. api = OTMApi("5ae2e3f221c38a28845f05b6ca363051e35b05054ad36c9e5ded14d2") # Парсим отдельно каждую категорию, так как на сайте стоит ограничение 500 объектов для одного запроса # Число объектов для категории в одном регионе как правило не привышает 500 places = [] for category in kinds: # Берём максимальные и минимальные значения координат из своего фрейма place = api.list_places([[df['long'].min(), df['lat'].min()], [df['long'].max(), df['lat'].max()]], category) places.append(place) # Парсим сами координаты границ объектов list_bbox = [] for i_place in range(len(places)): for place in places[i_place]: details = api.get_details(place["xid"]) # У некоторых объектов нет координат границ(.. но есть другая полезная информация, которая в данной задаче не рассматривается if 'bbox' in details: list_bbox.append(details['bbox']) list_bbox_tour_place = [] # Получаем результирующий список координат for coor in list_bbox: temp = [] for c in coor.values(): temp.append(c) list_bbox_tour_place.append(temp)
Хорошо! Мы получили все необходимые данные для трёхуровневой фильтрации фото и можем безотлагательно приступать к решению!
Уровень 1. Фильтрация по месту прописки:)

Работа с данными из таблицы о пользователях.
Всех пользователей я разделяю на три группы:
-
«Заезжие» — те чей город не попадает в зону, обусловленную задачей. Координаты границ этих городов получены от запроса к API openstreetmap. «Заезжие» не значит турист. Заезжий так же может быть кто угодно: студент, вахтёр, спортсмен и т.д.
-
«Местные» — те чей город попадает в зону, обусловленную задачей. И да, местный — не значит НЕ турист. Он так же может считаться туристом если фотографирует в соседнем городе.
-
«None» — многочисленная группа с неизвестным городом (примерно 35% пользователей). Эту группу на этом уровне фильтрации я решил рассеять между первыми двумя по принципу времени фотографирования – если пользователь делал фото более двух месяцев в году, он отправляется в группу «местные», если реже двух месяцев – в группу «заезжие».
В итоге мы получим две группы для дальнейшей работы (местные и заезжие)
Уровень 2. Фильтрация групп по дате фото и по координатам фото
На этом этапе группа «заезжие» разделяется на три группы по дате фотографирования.

Работа с данными из таблицы о фото
-
Те фото, которые пользователи выкладывали только два месяца (соседние) в году и только в туристические месяцы (июль, август, декабрь, январь – для Приморского Края) сразу помечаем как «туристические фото» без дальнейшей фильтрации.
-
Фото, которые выкладывали только два месяца в году и не только в туристические месяцы. Их могли делать, как туристы (путешествующие по каким-то причинам не в тур. месяцы), так и, например дальнобойщик, который приехал в ноябре и сделал сэлфи на заправке. Эта подгруппа требует дальнейшей фильтрации.
-
Подгруппа фото, выложенные в период более двух месяцев в году и не только в туристические месяцы. В эту выборку так же могут попасть абсолютно разные люди. Забегая вперёд, по факту она у меня получилась самой малочисленной (что-то около 3к из 156к всех фото).
Группа «местные» фильтруется с учётом координат места жительства и координат фотографий, произведённых им.

Работа с данными из таблицы о фото
-
Фотографии, которые пользователь выложил из своего населённого пункта автоматически определяется «не туристические».
-
Подгруппа с фото вне координат населённого пункта пользователя дополнительно фильтруется на третьем этапе.
Уровень 3. Фильтрация подгрупп по координатам туристических локаций
Фото, которые выкладывали только два месяца в году и не только в туристические месяцы (подгруппа «заезжих»)

Эта подгруппа с большей долей вероятности принадлежит туристам. При совпадении координат с туристической локацией смело присваиваю метку «туристическое фото», при отрицательным результате отмечаю «скорее туристическое»
Подгруппа фото («заезжих»), выложенные в период более двух месяцев в году и не только в туристические месяцы

Как говорилось выше – подгруппа малочисленная, с неоднозначным статусом. Но если фото производилось в туристическом месте, можно быть уверенным в метке «туристическое фото», и напротив в обратном случае – отнести фото к классу «не туристическое фото»
И наконец, подгруппа с фото вне координат населённого пункта «местного» пользователя.

Если фото сделано в туристической локации (а фильтрация по месту прописки уже пройдена на втором уровне) – уверенно присваиваю «туристическое фото». Фотография вне туристической локации – я решил присвоить метку «скорее туристическое фото», в чём у меня есть сомнения…
В эту выборку попали самые неопределённые данные. Нужно помнить, что пользователи со значением None в месте жительства, которые просеялись до этого этапа, а также то, что границы туристической локации на карте не всегда будут 100% процентов определять мотив сделать фотографию – стоишь в 15 метрах от границы бокса и фотографируешь водопад в границах этого самого бокса. Буду рад Вашим соображениям в комментариях по этому поводу.
Так же неоднозначный для меня момент на первом этапе фильтрации с выборкой пользователей с отсутствием информации о месте проживания. Правильно ли их распределять между местными и заезжими? Возможно, логичней предложить для них собственный алгоритм фильтрации…
Оценка решения задачи
Оценка работы с не размеченными данными задача не проще самой разметки данных. Как оценить результат, когда не знаешь правильный ответ?! Вопрос скорее риторический. Опять же будет интересно услышать в комментариях мнение читателя. Уверен, мои метрики не идеальны.
Для начала посмотрим на карту. Расположение маркеров стало более смешанным, детальным. В городских массивах появились «туристические фото» и напротив за населёнными пунктами встречаются фото, сделанные не туристами. В сельской местности, в небольших посёлках, превалируют фото с маркером «скорее туристические» — то есть фото в которых есть сомнения о их принадлежности к классу.

Исходя из идеи распределения при фильтрации фотографий пользователя относительно месяцев, я заключил что и метрика оценки должна быть связана с этим предположением.
График ниже визуализирует распределение фотографий по месяцам, как и график в начале статьи, но с указанием меток (2 – туристические, 1 – скорее туристические, 0 – не туристические)

На графики количественно выделяются именно туристические месяцы: июль, август, декабрь и январь. То есть те месяцы, в которые и должно делаться максимальное количество туристических фото. А собственно, что стоило ожидать если фильтрация происходила по этим месяцам? – Нет. Количество фотографий в тех подгруппах куда попадали фотографии в туристические месяцы довольно малочисленна и не могла весомо повлиять данный график, бОльшую роль играла фильтрация по туристической локации.
Так же стоит отметить то, что количество наблюдений в итоговой выборке распределились примерно пропорционально, что скорей свидетельствует правильном подходе к классификации.
-
«не туристические» — 54961
-
«скорее туристические» — 60537
-
«туристические» — 38799
Спасибо за внимание! Буду рад обсуждению темы в комментариях
ссылка на оригинал статьи https://habr.com/ru/post/677298/
Добавить комментарий