II. Геокодирование#

Простыми словами, геокодирование – это процесс, когда мы берём адрес или название места и превращаем его в точку на карте. (Если делать наоборот – получать адрес по координатам, это называется обратное геокодирование.)

Чаще всего геокодирование делается с помощью специального сервиса, к которому мы обращаемся через API.

🔗 API (application programming interface) – это набор инструкций и стандартов, которые позволяют разным программам взаимодействовать друг с другом. Проще говоря, API – это посредник, который даёт возможность использовать функционал другой программы.

Процесс геокодирования можно свести к нескольким шагам:

  1. Пишем запрос к сервису через API (указываем адрес и другие важные параметры)

  2. Получаем ответ

  3. Обрабатываем ответ

На Python мы отправляем такие запросы с помощью библиотеки requests. Если запрос выполнен успешно, мы получаем ответ и дальше работаем с ним: например, вытаскиваем координаты.

Куда именно отправлять запрос и какие параметры указывать – всегда написано в документации к API (их много разных; часть можно посмотреть здесь).

Здесь мы рассмотрим процесс геокодирования на примере Yandex API.

0. Импортируем библиотеки#

# Для работ с табличными и пространственными данными

import pandas as pd
import geopandas as gpd

# Для запросов к API
import requests
import time  # для задержки между запросами, чтобы не заблокировали

1. Основы геокодирования#

Мы рассмотрим основные шаги геокодирования на примере Yandex API для одного адреса: определим параметры запроса, отправим его, разберём, из каких частей состоит ответ, и научимся извлекать из него координаты.

Всю информацию о работе с Yandex API можно (важно) посмотреть в документации

1.1 Определение параметров для запроса#

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

Когда мы отправляем запрос, нам нужно объяснить сервису, что именно мы хотим. Для этого мы передаём параметры запроса.

В нашем случае параметры будут следующими:

  • сам адрес, который нужно найти

  • API-ключ, чтобы наш запрос был рассмаотрен

  • в каком формате мы хотим получить ответ (JSON или XML)

  • сколько результатов нам нужно (1 или несколько)

  • на каком языке хотим получить ответ

API_KEY = ''  # Ваш API-ключ Яндекс Геокодера

params = {
    'apikey': API_KEY, # Ключ доступа к API
    'geocode': 'Москва, Волхонка, д.12', # Адрес для геокодирования
    'format': 'json', # Формат ответа
    'results': 1, # Ограничить количество результатов до 1
    'lang': 'ru_RU', # Язык ответа
}

# URL для запроса к Яндекс Геокодеру
url = 'https://geocode-maps.yandex.ru/1.x/'

1.2 Отправка запроса#

GET-запрос чаще всего используется, когда мы хотим получить данные (какой именно конкретный тип запроса нужен важно смотреть в документации)

С помощью библиотеки requests мы отправляем запрос, в котором указываем url и параметры.

В ответ мы получаем код статуса - он показывает, как обработан наш запрос.

Основные статусы

  • 200 OK – всё прошло успешно, данные получены.

  • 400 Bad Request – неправильный запрос (например, ошибка в параметрах).

  • 401 Unauthorized – нет доступа, нужен правильный API-ключ или токен.

  • 403 Forbidden – доступ запрещён, даже если ключ есть.

  • 404 Not Found – адрес или ресурс не найден.

  • 500 Internal Server Error – ошибка на стороне сервера.

# Отправляем
response = requests.get(url, params=params)

# Проверяем ответ
if response.status_code == 200:
    data = response.json()
    print("Успешный запрос")
else:
    print("Ошибка:", response.status_code)
Успешный запрос

1.3 Извлечение ответа#

Если запрос прошел успешно, то мы смотрим, что у нас получилось в ответе, и извлекаем из него нужную информацию

data
{'response': {'GeoObjectCollection': {'metaDataProperty': {'GeocoderResponseMetaData': {'request': 'Москва, Волхонка, д.12',
     'results': '1',
     'found': '1'}},
   'featureMember': [{'GeoObject': {'metaDataProperty': {'GeocoderMetaData': {'precision': 'exact',
        'text': 'Россия, Москва, улица Волхонка, 12',
        'kind': 'house',
        'Address': {'country_code': 'RU',
         'formatted': 'Россия, Москва, улица Волхонка, 12',
         'postal_code': '119019',
         'Components': [{'kind': 'country', 'name': 'Россия'},
          {'kind': 'province', 'name': 'Центральный федеральный округ'},
          {'kind': 'province', 'name': 'Москва'},
          {'kind': 'locality', 'name': 'Москва'},
          {'kind': 'street', 'name': 'улица Волхонка'},
          {'kind': 'house', 'name': '12'}]},
        'AddressDetails': {'Country': {'AddressLine': 'Россия, Москва, улица Волхонка, 12',
          'CountryNameCode': 'RU',
          'CountryName': 'Россия',
          'AdministrativeArea': {'AdministrativeAreaName': 'Москва',
           'Locality': {'LocalityName': 'Москва',
            'Thoroughfare': {'ThoroughfareName': 'улица Волхонка',
             'Premise': {'PremiseNumber': '12',
              'PostalCode': {'PostalCodeNumber': '119019'}}}}}}}}},
      'name': 'улица Волхонка, 12',
      'description': 'Москва, Россия',
      'boundedBy': {'Envelope': {'lowerCorner': '37.601088 55.744961',
        'upperCorner': '37.609299 55.749592'}},
      'uri': 'ymapsbm1://geo?data=Cgg1NjcwOTk5MxI70KDQvtGB0YHQuNGPLCDQnNC-0YHQutCy0LAsINGD0LvQuNGG0LAg0JLQvtC70YXQvtC90LrQsCwgMTIiCg24axZCFTf9XkI,',
      'Point': {'pos': '37.605194 55.747277'}}}]}}}

Давайте создадим DataFrame, в которому будут сохранены адрес, почтовый индекс и координаты

# Достаем блок GeoObject
geo = data['response']['GeoObjectCollection']['featureMember'][0]['GeoObject']

# Разбиваем координаты на долготу и широту
pos = geo['Point']['pos'].split()
lon = float(pos[0])
lat = float(pos[1])

# Достаем адрес
address_meta = geo['metaDataProperty']['GeocoderMetaData']['Address']
components = address_meta['Components']

# Создаем словарь с нужными полями
result = {
    'formatted_address': address_meta['formatted'],
    'postal_code': address_meta.get('postal_code', None),
    'longitude': lon,
    'latitude': lat,
}


# Создаем DataFrame
df = pd.DataFrame([result])


# Смотрим на результат
df
formatted_address postal_code longitude latitude
0 Россия, Москва, улица Волхонка, 12 119019 37.605194 55.747277

1.4 Создаем набор пространственных данных#

На основе полученного dataFrame мы можем создать geoDataFrame, используя координаты объекта

#Создаем geoDataFrame
data_gdf = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df['longitude'], df['latitude']), crs=4326)

#Смотрим на результат
data_gdf.explore()
Make this Notebook Trusted to load map: File -> Trust Notebook

2. Геокодирование данных из таблицы#

У нас довольно редко возникает задача геокодирования только одного конкретного адреса. Намного чаще у нас есть набор адресов, например, из таблицы, которые нужно геокодировать.

Давайте посмотрим на небольшом примере (df_geocode_sample), как это можно сделать.

Изучим имеющийся набор данных

data_sample = pd.read_csv('../data/df_geocode_sample.csv', sep=';', on_bad_lines='skip')

data_sample
id address
0 6482827 обл. Иркутская, г. Братск, ул. 25-летия Братск...
1 6482828 обл. Иркутская, г. Братск, ул. 25-летия Братск...
2 6482829 обл. Иркутская, г. Братск, ул. 25-летия Братск...
3 6690904 обл. Иркутская, г. Братск, ул. 25-летия Братск...
4 9155620 обл. Иркутская, г. Братск, ул. 25-летия Братск...
... ... ...
68 6482906 обл. Иркутская, г. Братск, ул. Студенческая, д...
69 9031632 обл. Иркутская, г. Братск, ул. Студенческая, д...
70 9314793 обл. Иркутская, г. Братск, ул. Студенческая, д...
71 9314801 обл. Иркутская, г. Братск, ул. Студенческая, д...
72 6685100 обл. Иркутская, г. Братск, ул. Студенческая, д...

73 rows × 2 columns

Наша задача остаётся той же, но теперь нам нужно обработать все адреса: отправить для каждого запрос, получить ответ и сохранить координаты.

2.1 Проходим по каждому адресу из таблицы#

# Создаем пустой список для результатов
results = []

# URL для Яндекс Геокодера
url = 'https://geocode-maps.yandex.ru/1.x/'

# Общие параметры
params = {
    'apikey': API_KEY, # Ключ доступа к API
    'format': 'json', # Формат ответа
    'results': 1, # Ограничить количество результатов до 1
    'lang': 'ru_RU', # Язык ответа
}

# Проходим по каждой строке DataFrame
for idx, row in data_sample.iterrows():
    address = row['address']

    # Создаём копию общих параметров и добавляем адрес
    params_request = params.copy()
    params_request['geocode'] = address

    response = requests.get(url, params=params_request)
    print(f"Processing id {row['id']} - status {response.status_code}")

    if response.status_code == 200:
        data = response.json()

        # Достаем блок GeoObject
        geo = data['response']['GeoObjectCollection']['featureMember'][0]['GeoObject']
        
        # Разбиваем координаты на долготу и широту
        pos = geo['Point']['pos'].split()
        lon = float(pos[0])
        lat = float(pos[1])

        # Достаем адрес
        address_meta = geo['metaDataProperty']['GeocoderMetaData']['Address']
        components = address_meta['Components']

        # Создаем словарь с нужными полями
        result = {
            'id': row['id'],
            'input_address': address,
            'formatted_address': address_meta['formatted'],
            'postal_code': address_meta.get('postal_code', None),
            'longitude': lon,
            'latitude': lat,
            }
        
        # Добавлем результат в список
        results.append(result)
        
    else:
        print(f"Ошибка запроса для id {row['id']}")
        results.append({
            'id': row['id'],
            'input_address': address,
            'formatted_address': None,
            'postal_code': None,
            'longitude': None,
            'latitude': None,
        })

    time.sleep(0.2)  # Задержка, чтобы избежать превышения лимитов API

# Создаем финальный DataFrame
final_df = pd.DataFrame(results)
Processing id 6482827 - status 200
Processing id 6482828 - status 200
Processing id 6482829 - status 200
Processing id 6690904 - status 200
Processing id 9155620 - status 200
Processing id 9155592 - status 200
Processing id 6677977 - status 200
Processing id 6482841 - status 200
Processing id 6482819 - status 200
Processing id 8939469 - status 200
Processing id 8939470 - status 200
Processing id 6685120 - status 200
Processing id 8939471 - status 200
Processing id 6482727 - status 200
Processing id 9049835 - status 200
Processing id 9030256 - status 200
Processing id 9049836 - status 200
Processing id 9321126 - status 200
Processing id 9323906 - status 200
Processing id 9323908 - status 200
Processing id 6482860 - status 200
Processing id 6482861 - status 200
Processing id 6482862 - status 200
Processing id 6482863 - status 200
Processing id 6690905 - status 200
Processing id 6482867 - status 200
Processing id 6704186 - status 200
Processing id 6482868 - status 200
Processing id 6482864 - status 200
Processing id 9114502 - status 200
Processing id 6482865 - status 200
Processing id 6482866 - status 200
Processing id 8939472 - status 200
Processing id 8939473 - status 200
Processing id 8939474 - status 200
Processing id 8939475 - status 200
Processing id 8939476 - status 200
Processing id 8939477 - status 200
Processing id 8939478 - status 200
Processing id 8939479 - status 200
Processing id 8939480 - status 200
Processing id 8939481 - status 200
Processing id 6721647 - status 200
Processing id 9155597 - status 200
Processing id 6482851 - status 200
Processing id 8925320 - status 200
Processing id 8925321 - status 200
Processing id 8951561 - status 200
Processing id 6554125 - status 200
Processing id 6554126 - status 200
Processing id 7949790 - status 200
Processing id 7949802 - status 200
Processing id 7949806 - status 200
Processing id 7949793 - status 200
Processing id 7949810 - status 200
Processing id 7742330 - status 200
Processing id 7742331 - status 200
Processing id 7643331 - status 200
Processing id 7643334 - status 200
Processing id 6482903 - status 200
Processing id 9114510 - status 200
Processing id 6482904 - status 200
Processing id 7643335 - status 200
Processing id 9126304 - status 200
Processing id 6483098 - status 200
Processing id 7643330 - status 200
Processing id 7643340 - status 200
Processing id 6482905 - status 200
Processing id 6482906 - status 200
Processing id 9031632 - status 200
Processing id 9314793 - status 200
Processing id 9314801 - status 200
Processing id 6685100 - status 200

2.2 Создаем GeoDataFrame на основе полученных координат#

#Убираем строки с пустыми координатами
final_df_cleaned = final_df.dropna(subset=['longitude', 'latitude'])

#Создаем geoDataFrame
final_gdf = gpd.GeoDataFrame(final_df_cleaned, geometry=gpd.points_from_xy(final_df_cleaned['longitude'], final_df_cleaned['latitude']), crs=4326)

#Смотрим на результат
final_gdf.explore()
Make this Notebook Trusted to load map: File -> Trust Notebook

2.3 Сохраняем результат#

#final_gdf.to_file('../data/data_geocoded.gpkg') 

3. Функция геокодирования#

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

def geocode_addresses(api_key, df, address_column):
    """
    Геокодирование адресов через Yandex API с выводом GeoDataFrame
    
    :param api_key: str, API-ключ Яндекса
    :param df: pandas.DataFrame, таблица с адресами
    :param address_column: str, название столбца с адресами
    :return: geopandas.GeoDataFrame, таблица с координатами и геометрией
    """
    
    results = []
    
    url = 'https://geocode-maps.yandex.ru/1.x/'
    
    # Общие параметры (без адреса)
    base_params = {
        'apikey': api_key,
        'format': 'json',
        'results': 1,
        'lang': 'ru_RU',
    }
    
    for idx, row in df.iterrows():
        address = row[address_column]
        
        params_request = base_params.copy()
        params_request['geocode'] = address
        
        response = requests.get(url, params=params_request)
        print(f"Processing id {row.get('id', idx)} - status {response.status_code}")
        
        if response.status_code == 200:
            data = response.json()
            try:
                geo = data['response']['GeoObjectCollection']['featureMember'][0]['GeoObject']
                
                pos = geo['Point']['pos'].split()
                lon = float(pos[0])
                lat = float(pos[1])
                
                address_meta = geo['metaDataProperty']['GeocoderMetaData']['Address']
                
                result = {
                    'id': row.get('id', idx),
                    'input_address': address,
                    'formatted_address': address_meta.get('formatted', None),
                    'postal_code': address_meta.get('postal_code', None),
                    'longitude': lon,
                    'latitude': lat,
                }
                
            except IndexError:
                print(f"Адрес не найден: {address}")
                result = {
                    'id': row.get('id', idx),
                    'input_address': address,
                    'formatted_address': None,
                    'postal_code': None,
                    'longitude': None,
                    'latitude': None,
                }
                
        else:
            print(f"Ошибка запроса для id {row.get('id', idx)}")
            result = {
                'id': row.get('id', idx),
                'input_address': address,
                'formatted_address': None,
                'postal_code': None,
                'longitude': None,
                'latitude': None,
            }
        
        results.append(result)
        time.sleep(0.2)  # задержка для предотвращения блокировки API
    
    # Создаём финальный DataFrame
    final_df = pd.DataFrame(results)
    
    # Убираем строки с пустыми координатами
    final_df_cleaned = final_df.dropna(subset=['longitude', 'latitude'])
    
    # Создаём GeoDataFrame
    final_gdf = gpd.GeoDataFrame(
        final_df_cleaned,
        geometry=gpd.points_from_xy(final_df_cleaned['longitude'], final_df_cleaned['latitude']),
        crs='EPSG:4326'
    )
    
    return final_gdf

Проверим, как работает наша функция на основе нескольких адресаов

# Создаем словарь с тестовыми адресами
data = {
    'id': [1, 2, 3, 4, 5],
    'address': [
        'Москва, Красная площадь, д.1',
        'Санкт-Петербург, Невский проспект, д.100',
        'Новосибирск, Красный проспект, д.50',
        'Екатеринбург, улица Вайнера, д.10',
        'Казань, улица Баумана, д.15'
    ]
}

# Преобразуем в DataFrame
df_test = pd.DataFrame(data)


# Запусаем функцию

goeocoded = geocode_addresses(API_KEY, df_test, 'address')

#Смотрим на результат
goeocoded.explore()
Processing id 1 - status 200
Processing id 2 - status 200
Processing id 3 - status 200
Processing id 4 - status 200
Processing id 5 - status 200
Make this Notebook Trusted to load map: File -> Trust Notebook

4. Итог#

На примере Yandex API мы научились геокодировать адреса из таблиц, но самое главное – в целом разобрались, как устроен процесс геокодирования. Вы можете использовать такой подход для любых API, изменяя параметры запроса и обработку ответа в соответствии с их документацией. Также не забывайте проверять лимиты на количество бесплатных запросов.

Успехов!