В этом посте я хочу поделиться интересным опытом работы с неразмеченными данными при помощи открытых ресурсов. К сожалению, из-за подписанного NDA, я не смогу полностью поделиться кодом, но, разумеется, всегда готов помочь в комментариях и личных сообщениях с разрешением какого-либо вопроса по теме.

Задача

  • По исходным данным определить является ли конкретное фото, сделанное пользователем туристическим или нет.

  • Масштабировать алгоритм решения на любой регион Российской Федерации.

  • Автоматизировать процесс, избегая ручную работу.

Данные

Представленные клиентом материалы относились к региону Приморскому Края, в координатах 131.304673 : 42.647718; 132.481172 : 43.490732.

Первый сет данных заключал в себе информацию о пользователе (8762 пользователя), а именно:

  1. закодированный ID

  2. год рождения

  3. пол

  4. населённый пункт проживания

  5. населённый пункт рождения

  6. интересы

Единственная нужная фича – город проживания – имела почти половину незаполненных данных.

Второй сет включал в себя информацию о самих фото (154297 фото)

  1. ID пользователя (который сделал фото – те же пользователи что и в первой таблице)

  2. дата фотографирования в формате Unixtime (данные собраны за один год)

  3. координаты места фотографирования

Поиск решения

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

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

При помощи QGIS, решил визуализировать локации фотографий на карте (координаты фото изи подгружаются на карту прям из csv файла – туториал на тему), предварительно разделив все фото на три группы руководствуясь следующим принципом:

  1. «летние туристические» фото, фото пользователя, которые он выкладывал только два месяца в году и только в июле и августе.

  2. «зимние туристические» фото, фото пользователя, которые он выкладывал только два месяца в году и только в декабре и январе.

  3. «не туристические фото» - все остальные фото.

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

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

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

Результат вышел хороший, но не настолько что бы считать работу законченной. Ведь по мере погружения в задачу, начинаешь задавать себе всё больше вопросов. Например, если пользователь проживает в городе Артём, а фото он сделал в воскресную поездку в центре Владивостока, можно ли считать его туристом? -  конечно. Или если пользователь заядлый турист и проживает в соседнем регионе, а в туристические походы в Приморский Край ходит пять раз в год в разные месяцы – тогда по существующему алгоритму его фото будут определяться не туристическими.

Решение

В ходе поиска решения я пришёл к тому, что следует провести многоступенчатую фильтрацию фото, по тем данным, которые у меня имелись, а также установить промежуточный класс между туристическими фото и не туристическими фото - «скорее туристическое фото».

Первый уровень фильтрации по месту жительства пользователя, второй - по дате, когда было сделано фото. Третий же по локации самого фото – производилось фотографирование в туристической локации или нет.

А как определить координаты туристических локаций региона, да ещё сделать процесс автоматизированным для всей России?

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. Фильтрация по месту прописки:)

Работа с данными из таблицы о пользователях.

Всех пользователей я разделяю на три группы:

  1. «Заезжие» - те чей город не попадает в зону, обусловленную задачей. Координаты границ этих городов получены от запроса к API openstreetmap. «Заезжие» не значит турист. Заезжий так же может быть кто угодно: студент, вахтёр, спортсмен и т.д.

  2. «Местные» - те чей город попадает в зону, обусловленную задачей. И да, местный — не значит НЕ турист. Он так же может считаться туристом если фотографирует в соседнем городе.

  3. «None» - многочисленная группа с неизвестным городом (примерно 35% пользователей). Эту группу на этом уровне фильтрации я решил рассеять между первыми двумя по принципу времени фотографирования – если пользователь делал фото более двух месяцев в году, он отправляется в группу «местные», если реже двух месяцев – в группу «заезжие».

В итоге мы получим две группы для дальнейшей работы (местные и заезжие)

Уровень 2. Фильтрация групп по дате фото и по координатам фото

На этом этапе группа «заезжие» разделяется на три группы по дате фотографирования.

Работа с данными из таблицы о фото

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

  2. Фото, которые выкладывали только два месяца в году и не только в туристические месяцы. Их могли делать, как туристы (путешествующие по каким-то причинам не в тур. месяцы), так и, например дальнобойщик, который приехал в ноябре и сделал сэлфи на заправке. Эта подгруппа требует дальнейшей фильтрации.

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

Группа «местные» фильтруется с учётом координат места жительства и координат фотографий, произведённых им.

Работа с данными из таблицы о фото

  1. Фотографии, которые пользователь выложил из своего населённого пункта автоматически определяется «не туристические».

  2. Подгруппа с фото вне координат населённого пункта пользователя дополнительно фильтруется на третьем этапе.

Уровень 3. Фильтрация подгрупп по координатам туристических локаций

Фото, которые выкладывали только два месяца в году и не только в туристические месяцы (подгруппа «заезжих»)

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

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

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

И наконец, подгруппа с фото вне координат населённого пункта «местного» пользователя.

Если фото сделано в туристической локации (а фильтрация по месту прописки уже пройдена на втором уровне) – уверенно присваиваю «туристическое фото». Фотография вне туристической локации – я решил присвоить метку «скорее туристическое фото», в чём у меня есть сомнения...

В эту выборку попали самые неопределённые данные. Нужно помнить, что пользователи со значением None в месте жительства, которые просеялись до этого этапа, а также то, что границы туристической локации на карте не всегда будут 100% процентов определять мотив сделать фотографию – стоишь в 15 метрах от границы бокса и фотографируешь водопад в границах этого самого бокса. Буду рад Вашим соображениям в комментариях по этому поводу.

Так же неоднозначный для меня момент на первом этапе фильтрации с выборкой пользователей с отсутствием информации о месте проживания. Правильно ли их распределять между местными и заезжими? Возможно, логичней предложить для них собственный алгоритм фильтрации...

Оценка решения задачи

Оценка работы с не размеченными данными задача не проще самой разметки данных. Как оценить результат, когда не знаешь правильный ответ?! Вопрос скорее риторический. Опять же будет интересно услышать в комментариях мнение читателя. Уверен, мои метрики не идеальны.

Для начала посмотрим на карту. Расположение маркеров стало более смешанным, детальным. В городских массивах появились «туристические фото» и напротив за населёнными пунктами встречаются фото, сделанные не туристами. В сельской местности, в небольших посёлках, превалируют фото с маркером «скорее туристические» - то есть фото в которых есть сомнения о их принадлежности к классу.

Исходя из идеи распределения при фильтрации фотографий пользователя относительно месяцев, я заключил что и метрика оценки должна быть связана с этим предположением.

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

На графики количественно выделяются именно туристические месяцы: июль, август, декабрь и январь. То есть те месяцы, в которые и должно делаться максимальное количество туристических фото. А собственно, что стоило ожидать если фильтрация происходила по этим месяцам? – Нет. Количество фотографий в тех подгруппах куда попадали фотографии в туристические месяцы довольно малочисленна и не могла весомо повлиять данный график, бОльшую роль играла фильтрация по туристической локации.

Так же стоит отметить то, что количество наблюдений в итоговой выборке распределились примерно пропорционально, что скорей свидетельствует правильном подходе к классификации.

  • «не туристические» - 54961

  • «скорее туристические» - 60537

  • «туристические» - 38799

Спасибо за внимание! Буду рад обсуждению темы в комментариях.

Комментарии (5)


  1. MLukash
    17.07.2022 09:14

    Здравствуйте!

    Почему выбран для примера Приморский край?

    Вы из Приморья?


    1. databorodata Автор
      17.07.2022 09:16

      Здравствуйте

      В Приморье не был никогда. Честно говоря, уже не вспомню почему такой выбор)


  1. MLukash
    18.07.2022 06:39

    Понятно.

    Просто подумалось, что может какой-то патриот Приморья))

    Есть геопортал Приморья 188.170.233.213 развиваемый практически на голом энтузиазме((


    1. databorodata Автор
      18.07.2022 10:04

      Надо бы там побывать) в Приморье


  1. MLukash
    18.07.2022 10:19
    +1

    Приезжайте)))