В этом посте я хочу поделиться интересным опытом работы с неразмеченными данными при помощи открытых ресурсов. К сожалению, из-за подписанного 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
Спасибо за внимание! Буду рад обсуждению темы в комментариях.
Комментарии (5)
MLukash
18.07.2022 06:39Понятно.
Просто подумалось, что может какой-то патриот Приморья))
Есть геопортал Приморья 188.170.233.213 развиваемый практически на голом энтузиазме((
MLukash
Здравствуйте!
Почему выбран для примера Приморский край?
Вы из Приморья?
databorodata Автор
Здравствуйте
В Приморье не был никогда. Честно говоря, уже не вспомню почему такой выбор)