II. Геокодирование#
Простыми словами, геокодирование – это процесс, когда мы берём адрес или название места и превращаем его в точку на карте. (Если делать наоборот – получать адрес по координатам, это называется обратное геокодирование.)
Чаще всего геокодирование делается с помощью специального сервиса, к которому мы обращаемся через API.
🔗 API (application programming interface) – это набор инструкций и стандартов, которые позволяют разным программам взаимодействовать друг с другом. Проще говоря, API – это посредник, который даёт возможность использовать функционал другой программы.
–
Процесс геокодирования можно свести к нескольким шагам:
Пишем запрос к сервису через API (указываем адрес и другие важные параметры)
Получаем ответ
Обрабатываем ответ
–
На 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()
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()
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
4. Итог#
На примере Yandex API мы научились геокодировать адреса из таблиц, но самое главное – в целом разобрались, как устроен процесс геокодирования. Вы можете использовать такой подход для любых API, изменяя параметры запроса и обработку ответа в соответствии с их документацией. Также не забывайте проверять лимиты на количество бесплатных запросов.
Успехов!