В этой статье мы рассмотрим решение задачи поиска оптимальной локации для электрозарядных станций с помощью открытых данных, методов геоаналитики и алгоритмов классического машинного обучения.
Что такое оптимальная локация?
Под оптимальной локацией для размещения любого объекта инфраструктуры понимают такую локацию, где она будет пользоваться спросом у конечных потребителей.
По опросам BCG EV Charging Survey респонденты ожидают найти станцию электрозарядки на территории существующих площадок АЗС, а также хотели бы их видеть на улицах и общественных пространствах, возле супермаркетов, на парковках, на местах отдых возле шоссе, около ресторанов и кафе. Причем предпочтения носят региональных характер, например в странах Азии это паркинги ЖК, в Америке - АЗС, в Европе - больше на общественных пространствах.
В Москве новые станции электрозарядки рекомендовано размещать возле торговых и деловых центров, кафе и ресторанов, около продуктовых магазинов и жилых домов.
Если ориентироваться на тип ЭЗС, то различают быстрые зарядки (полный заряд от получаса) и медленные (заряд за несколько часов). Быстрые ЭЗС будут пользоваться спросом возле кафе, ресторанов и продуктовых магазинов, медленные - возле деловых центров и жилых домов, где потенциальные пользователи электромобилей проводят значительную часть времени днем или ночью.
Где вероятнее найти станцию зарядки электромобилей?
Предлагаю посмотреть на рисунок 1, где изображены две локации из разных частей города.

Изначально, когда я составлял вопрос о наиболее вероятной локации, где может быть расположена станция электрозарядки, я однозначно подразумевал локацию 1. Это центр города, судя по плотности и характеру застройки, наличию деловой и развлекательной инфраструктуры, которая привлекает владельцев электрокаров в дневные часы.
Когда я задавал этот же вопрос для нескольких аудиторий, то в этот момент возникали обсуждения и споры, почему вторая локация не такое "удачное" место для размещения ЭЗС, ведь здесь проходит магистральная дорога с высоким автомобильным трафиком.
В действительности именно на первой локации уже размещены несколько станций зарядки, а во второй пока нет ни одной. Другими словами вероятность найти ЭЗС в первой локации будет выше.
Как использовать данные о структуре локации или наборе ее функций для ранжирования набора локаций между собой, например для задачи выбора оптимальных локаций? Для этого мы будем использовать геоэмбеддинги.
Что такое геоэмбеддинг
Понятие геоэмбеддинга (spatial embedding или geoembedding) это производное от широко используемых эмбеддингов. В NLP эмбеддинг - это процесс или результат процесса преобразования языковой сущности - слова, предложения, текста - в числовой вектор.
Для геоэмбеддинга используется цифровое описание пространственных свойств объектов, например расположен в пределах другого объекта, расположен рядом с объектом (на расстоянии 100 метров), занимает количественно описанную долю от всей площади объекта и т.д.
По аналогии с word2vec в геоэмбеддингах встречаются понятия loc2vec, hex2vec, tile2vec, где различаются цифровое описание самих локаций.

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

Для применения инструментов машинного обучения сетка гексагонов h3 - лучший вариант, учитывая перечисленные на рисунке преимущества. Отсутствие официальных статистических данных для гексагонов постепенно компенсируется наличием смоделированных данных, например по численности населения (Kontur Population data).
Варианты геоэмбеддингов
Рассмотрим самый простой вариант геоэмбеддинга - эмбеддинг количества объектов в локации. Для этого выполним следующие три шага:
территорию города разобьем на гексагоны h3 (разрешения 8, площадь 0,73 кв км):
# импорт библиотек from srai.regionalizers import H3Regionalizer, geocode_to_region_gdf # выбор параметров h3_res = 9 city_name = "Челябинск" # загрузка данных area = geocode_to_region_gdf(city_name) regionalizer = H3Regionalizer(resolution=h3_res) regions = regionalizer.transform(area)загрузим данные о расположении ЭЗС из открытых источников (например, Openstreetmap.org, тэг "charging_station"):
# импорт библиотек from srai.loaders import OSMOnlineLoader from srai.plotting import plot_regions from srai.regionalizers import geocode_to_region_gdf import folium from folium import plugins # загрузка данных query = {"amenity": "charging_station"} area = geocode_to_region_gdf(city_name) loader = OSMOnlineLoader() cs_gdf = loader.load(area, query)рассчитаем количество ЭЗС в пределах каждого гексагона и проранжируем все гексагоны территории по этому параметру:
# импорт библиотек from srai.regionalizers import H3Regionalizer, geocode_to_region_gdf # загрузка данных area = geocode_to_region_gdf(city_name) regionalizer = H3Regionalizer(resolution=h3_res) regions = regionalizer.transform(area) # построение карты map = folium.Map(location=[area.centroid.y.iloc[0], area.centroid.x.iloc[0]], zoom_start = 11, attributionControl=0, tiles='cartodbpositron') folium.GeoJson(area, style_function=lambda feature: {'fillOpacity': 0.01,'color': 'grey'}).add_to(map) folium.GeoJson(regions, style_function=lambda feature: {'weight': 1,'color': 'rosybrown'}).add_to(map) folium.GeoJson(cs_gdf, control = False, marker = folium.CircleMarker(radius = 4, weight = 0.2, fill_color = 'orange', fill_opacity = 1)).add_to(map) map
Здесь в коде использована готовая библиотека для работы с геоэмбеддингами (SRAI). В ней собраны те же инструменты для работы с геоданными, что есть в geopandas, shapely, folium, osmnx и другие.
Результат выведен на карту (рисунок 3) со шкалой ранжирования гексагонов по количеству ЭЗС.

Добавляя географических закономерностей, можно использовать в геоэмбеддинге количества функцию расстояния. Эта функция будет зависеть от параметра - расстояние, в пределах которого локации будут считаться соседними. Таким образом, комплексный индекс локации будет отображать первый закон географии по Тоблеру (все связано со всем остальным, но близкие объекты связаны сильнее, чем далекие).
Далее в примере мы будем учитывать несколько категорий объектов (ранее упомянутые кафе, рестораны, парковки, АЗС и прочие объекты), которые предполагают наличие ЭЗС в их окружении. Другими словами, основной задачей будет сформировать числовой вектор локации.
Поиск локаций, где уже есть станции зарядки для электромобилей
Проанализируем город, где уже есть разветвленная и успешная сеть электрозарядных станций. В нашем случае "успешная сеть ЭЗС" - это допущение. Рассмотрим на примере города Кельн, Германия:
выгрузим территорию города, проиндексируем ее гексагонами h3 разрешения 10 и выгрузим все существующие ЭЗС в пределах города:
# импорт библиотек from srai.embedders import Hex2VecEmbedder from srai.joiners import IntersectionJoiner from srai.loaders.osm_loaders.filters import HEX2VEC_FILTER from srai.neighbourhoods.h3_neighbourhood import H3Neighbourhood from srai.regionalizers import H3Regionalizer, geocode_to_region_gdf from srai.plotting import plot_regions, plot_numeric_data from srai.loaders import OSMOnlineLoader # выбор параметров source_city = "Cologne, Germany" H3_resolution = 10 # выбор типа объекта charging_stations_osm_tag = {"amenity": "charging_station"} # загрузка данных source_area = geocode_to_region_gdf(source_city) loader = OSMOnlineLoader() stations = loader.load(source_area, charging_stations_osm_tag)
Рисунок 4. Желтыми точками отмечено расположение ЭЗС на территории города Кельн выгрузим все объекты, которые могут влиять на потенциальное размещение ЭЗС. Мы использовали доступный справочник тэгов для поиска нужных категорий:
# выбор параметров data = np.array(['building_retail', 'building_supermarket', 'building_office', 'building_commercial', 'shop_mall', 'shop_department_store', 'tourism_hotel', 'amenity_restaurant', 'amenity_cafe', 'amenity_fuel', 'amenity_car_rental', 'amenity_car_sharing', 'amenity_parking']) # подготовка словаря категорий POI output_features = pd.Series(data) embedder_osm_tags = {} for element in output_features: if element == 'amenity_charging_stations': continue key,value = element.split('_', 1) if key not in embedder_osm_tags: embedder_osm_tags[key] = [value] else: embedder_osm_tags[key].append(value)Для каждого гексагона h3 или локации рассчитаем количество объектов каждой категории и приведем структуру данных к виду как на рисунке 5.
train_features = OSMOnlineLoader().load(source_area, embedder_osm_tags)
train_regions = H3Regionalizer(resolution=H3_resolution).transform(source_area)
train_joint = IntersectionJoiner().transform(train_regions, train_features)
train_embeddings = embedder.transform(train_regions, train_features, train_joint)
train_embeddings.head()
Далее сформируем две выборки с позитивными и негативными локациями. Позитивные локации - это те гексагоны h3, где размещены ЭЗС, негативные - там, где нет ЭЗС.
Сбалансируем две выборки, таким образом чтобы количество негативных локаций было в три раза больше позитивных. Негативные локации выбираются случайным образом.
# пересечение слоя ЭЗС с гексагонами
cs_joint = IntersectionJoiner().transform(train_regions, stations)
# выборка позитивных гексагонов (где есть ЭЗС)
positive_samples = train_regions.join(cs_joint, how='inner')
positive_samples = positive_samples.reset_index().drop(columns=['feature_id']).set_index('region_id')
positive_samples['is_positive'] = True
len(positive_samples)
# выборка негативных гексагонов (где нет ЭЗС)
negative_samples = train_regions.copy()
negative_samples['is_positive'] = False
negative_samples.loc[positive_samples.index, "is_positive"]= True
negative_samples = negative_samples[~negative_samples["is_positive"]]
# балансировка - выберете нужное количество из датасета
negative_undersampled = negative_samples.sample(n=3 * len(positive_samples))
# построение карты
map = folium.Map(location=[source_area.centroid.y.iloc[0], source_area.centroid.x.iloc[0]], zoom_start = 11, attributionControl=0, tiles='cartodbpositron')
folium.GeoJson(positive_samples, style_function=lambda feature: {'fillOpacity': 0.01,'color': "green"}).add_to(map) # где есть ЭЗС
folium.GeoJson(negative_undersampled, style_function=lambda feature: {'fillOpacity': 0.01,'color': "red"}).add_to(map) # где нет ЭЗС
folium.GeoJson(source_area, style_function=lambda feature: {'fillOpacity': 0.01,'color': 'grey'}).add_to(map)
map
К этому моменту мы подготовили данные для модели классификации с обучающей выборкой: у нас есть локации, их признаки (количество объектов категорий) и разметка на позитивное и негативное значение.
Используем один из стандартных методов классического машинного обучения, например, SVM, для построения модели классификации с учителем:
train_data = pd.concat([positive_samples, negative_undersampled])
from sklearn.model_selection import train_test_split
X = train_embeddings.loc[train_data.index].to_numpy()
y = train_data["is_positive"].astype(int).to_numpy()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
from sklearn import svm
from sklearn.metrics import classification_report
classifier = svm.SVC(probability=True)
classifier.fit(X_train, y_train)
y_pred = classifier.predict(X_test)
y_pred_proba = classifier.predict_proba(X_test)
print(classification_report(y_test, y_pred))
Полученные значения метрик позволяют нам использовать модель для дальнейшей работы. Как повысить эти значения, мы рассмотрим уже в конце.
Как применить модель, основанную на геоэмбеддингах?
Возьмем другой город, где мы хотим разместить сеть ЭЗС, например, Казань. Пройдем следующие шаги:
загрузим территорию города, проиндексируем ее гексагонами h3 разрешения 10, то есть точно так же как мы сделали для первого города
выгрузим все объекты категорий, которые могут влиять на потенциальное размещение ЭЗС
Для каждой локации рассчитаем количество объектов каждой категории:
# выбор параметров target_city = "Kazan, Russia" H3_resolution = 10 # загрузка данных target_area = geocode_to_region_gdf(target_city) # загрузка данных для целевого города (target_city) target_regions = H3Regionalizer(resolution = H3_resolution).transform(target_area) target_features = OSMOnlineLoader().load(target_area, embedder_osm_tags) target_joint = IntersectionJoiner().transform(target_regions, target_features) target_embeddings = embedder.transform(target_regions, target_features, target_joint)Рассчитаем вероятность размещения ЭЗС на основе обученной ранее модели классификации:
# применение обученного классификатора на целевом городе (target_city) station_probas = classifier.predict_proba(target_embeddings.to_numpy()) target_regions['station_proba'] = station_probas[:, 1] target_regions_json = target_regions.to_json() target_regions_upd = target_regions.reset_index() target_regions_upd['id'] = target_regions_upd['region_id']Визуализируем результат на карте:
# построение карты map = folium.Map(location=[target_area.centroid.y.iloc[0], target_area.centroid.x.iloc[0]], zoom_start = 11, attributionControl=0, tiles='cartodbpositron') folium.GeoJson(target_area, style_function=lambda feature: {'fillOpacity': 0.01,'color': 'grey'}).add_to(map) folium.Choropleth( geo_data=target_regions_json, name="choropleth", data=target_regions_upd, columns=["id", "station_proba"], key_on="feature.id", fill_color="Spectral_r", fill_opacity=0.5, legend_name="Number of charging stations per hexagon", line_weight=0.1, ).add_to(map) map

На рисунке 8 цветом проранжированы локации, где наиболее вероятно размещение ЭЗС, учитывая успешный опыт другого города - гексагоны красного цвета наиболее подходящие варианты локаций.
Как проанализировать результаты модели, основанной на геоэмбеддингах?
Безусловно, в Казани уже есть сеть станций зарядки для электромобилей. Текущее расположение доступно в том числе в открытых д��нных Минэнерго России. Сопоставим координаты текущих ЭЗС с рекомендованными моделью локациями. Рекомендованной локацией мы будем считать ту, для которой рассчитанная вероятность более 0,8.
# импорт библиотек
from srai.loaders import OSMOnlineLoader
from srai.plotting import plot_regions
from srai.regionalizers import geocode_to_region_gdf
import folium
from folium import plugins
# загрузка данных
suggested_locations = target_regions_upd[target_regions_upd.station_proba > 0.8]
query = {"amenity": "charging_station"}
area = geocode_to_region_gdf(target_city)
loader = OSMOnlineLoader()
cs_gdf = loader.load(area, query)
# построение карты
map = folium.Map(location=[target_area.centroid.y.iloc[0], target_area.centroid.x.iloc[0]], zoom_start = 11, attributionControl=0, tiles='cartodbpositron')
folium.GeoJson(suggested_locations, style_function=lambda feature: {'fillOpacity': 0.4,'color': "orange"}).add_to(map)
folium.GeoJson(cs_gdf, control = False, marker = folium.CircleMarker(radius = 4, weight = 0.2, fill_color = 'red', fill_opacity = 1)).add_to(map)
folium.GeoJson(area, style_function=lambda feature: {'fillOpacity': 0.01,'color': 'grey'}).add_to(map)
map
На рисунке 9 изображены желтые гексагоны - это рекомендованные локации для размещения ЭЗС, красные точки - это существующие локации ЭЗС в Казани (данные openstreetmap.org)
На данном этапе мы можем исключить часть рекомендованных локаций, так как в них уже есть работающая ЭЗС. Дополнительно можно расширить рекомендованную локацию до соседних гексагонов, так ка�� не везде технически реально разместить новую станцию.
# импорт библиотек
from srai.neighbourhoods import H3Neighbourhood
from srai.regionalizers import H3Regionalizer
# поиск соседних гексагонов к рекомендованным
target_area_hex = H3Regionalizer(resolution=H3_resolution).transform(target_area)
neighbourhood_with_regions = H3Neighbourhood(target_area_hex)
neighbours_list = []
for index, row in suggested_locations.iterrows():
for hex in neighbourhood_with_regions.get_neighbours(row['region_id']):
if hex not in neighbours_list:
neighbours_list.append(hex)
neighbours_hex = pd.DataFrame(neighbours_list, columns=['region_id'])
neighbours_hex_df = target_area_hex.merge(neighbours_hex, left_on='region_id', right_on='region_id')
# пространственное пересечение
suggested_locations_with_neighbours = pd.concat([suggested_locations, neighbours_hex_df])
suggested_location_exist = cs_gdf.sjoin(suggested_locations_with_neighbours, how='inner', predicate='intersects')
# построение карты
map = folium.Map(location=[target_area.centroid.y.iloc[0], target_area.centroid.x.iloc[0]], zoom_start = 11, attributionControl=0, tiles='cartodbpositron')
folium.GeoJson(neighbours_hex_df, style_function=lambda feature: {'fillOpacity': 0.4,'color': "moccasin"}).add_to(map)
folium.GeoJson(suggested_locations, style_function=lambda feature: {'fillOpacity': 0.4,'color': "orange"}).add_to(map)
folium.GeoJson(cs_gdf, control = False, marker = folium.CircleMarker(radius = 4, weight = 0.2, fill_color = 'red', fill_opacity = 1)).add_to(map)
folium.GeoJson(suggested_location_exist, control = False, marker = folium.CircleMarker(radius = 4, weight = 0.2, fill_color = 'black', fill_opacity = 1)).add_to(map)
folium.GeoJson(area, style_function=lambda feature: {'fillOpacity': 0.01,'color': 'grey'}).add_to(map)
map
На рисунке 10 желтые гексагоны - это рекомендованные локации для размещения ЭЗС, бледно-желтые - соседние к рекомендованным локации, красные точки - это текущие локации ЭЗС в Казани (openstreetmap.org). Черные точки - это существующие ЭЗС, которые попали в рекомендованные локации.
Как улучшить модель?
Мы попытались решить задачу поиска оптимальной локации для размещения новой ЭЗС с использованием только открытых данных и в сильно упрощенном варианте. Для ее совершенствования можно добавить следующие шаги:
добавить новые признаки для обучения классификатора. Это могут быть дополнительные тэги из набора данных OSM или объекты из других источников данных
использовать для обучающей выборки несколько городов или городских агломераций, желательно с сопоставимой плотностью населения, уровнем жизни и спросом на использование ЭЗС
убрать из обучающих выборок изначально нерелевантные локации. Например, где уже есть ЭЗС, где нет технической возможности установить ЭЗС, леса, поля, водные акватории и т.п.
уточнить какой тип ЭЗС планируется разместить - быстрые или медленные. Для каждого типа определяется характерный список объектов
добавить данные о реальных пользователях электрокаров. Это относится к коммерческим данным: пользователи релевантных приложений, программ лояльности ЭЗС, сегмента, заинтересованного в пользовании электрокаров и другие
получить данные о проходимости локаций и суточной динамики численности населения этих локаций или отдельно пользователей электрокаров из предыдущего пункта
реализовать алгоритмы Feature selection: корреляция, последовательный отбор/исключение признаков, улучшение обучения классификатора с помощью L1 регуляризации/ Random Forest и другие. То есть использовать настройку параметров модели
Вместо заключения
Аналогичное решение может быть расширено для оптимизации размещения сети объектов другого типа, например, велопарковки, кикшеринг и т.п. Больше о возможностях применения геоэмбеддингов можно узнать по ссылкам ниже:
EuroSciPy 2023 Introduction to Geospatial Machine Learning with SRAI
Loc2Vec: Self-supervised metric learning through triplet-loss
Data Fest 2024 Геоэмбеддинги: универсальные представления для локаций
Data Fest 2024 Мультимодальные геоэмбеддинги: методы, результаты и внедрение
