Оценка территории по критерию для гексагональной сетки

[ ]:
import warnings
import sys
import os
warnings.filterwarnings("ignore")
sys.stderr = open(os.devnull, 'w')

# local crs
local_crs = 32636


from popframe.models.region import Region

region_model = Region.from_pickle('data/__popframe_model_cache__/3138.pkl')
# grid = gpd.read_file('/Users/mvin/Code/PopFrame/examples/data/level_3.geojson', engine="pyogrio")
# grid
[2]:
import requests
import geopandas as gpd

def fetch_hexagons(
    territory_id: int,
    centers_only: bool = False,
    base_url: str = 'https://urban-api.idu.kanootoko.org/api/v1/territory/{territory_id}/hexagons'
) -> gpd.GeoDataFrame:
    """
    Запрос гексагонов для заданного territory_id и возвращает их в виде GeoDataFrame.

    :param territory_id: ID территории
    :param centers_only: если True — вернуть только центральные точки гексагонов
    :param base_url: шаблон URL с {territory_id}
    :return: GeoDataFrame с колонками geometry, hexagon_id и свойствами
    """
    # Подставляем ID в URL
    url = base_url.format(territory_id=territory_id)
    params = {
        'centers_only': centers_only
    }

    # Выполняем запрос
    response = requests.get(url, params=params)
    response.raise_for_status()  # вызовет исключение, если код != 200

    # Парсим GeoJSON
    geojson = response.json()
    features = geojson.get('features', [])

    # Строим GeoDataFrame
    # from_features автоматически извлечёт geometry и все свойства
    gdf = gpd.GeoDataFrame.from_features(features)
    # Устанавливаем CRS (WGS84)
    gdf = gdf.set_crs(epsg=4326)

    return gdf

territory_id = 3138  # ID территории для Санкт-Петербурга
gdf_hex = fetch_hexagons(territory_id, centers_only=False)
gdf_hex
[2]:
geometry hexagon_id properties
0 POLYGON ((30.06574 59.67090, 30.07379 59.67194... 157558 {}
1 POLYGON ((30.08770 60.08939, 30.09047 60.09382... 157559 {}
2 POLYGON ((30.37948 59.79270, 30.38759 59.79372... 157560 {}
3 POLYGON ((30.66727 59.76362, 30.66198 59.76704... 157561 {}
4 POLYGON ((30.24918 60.02467, 30.25198 60.02910... 157562 {}
... ... ... ...
2529 POLYGON ((30.45791 60.02272, 30.46608 60.02376... 160087 {}
2530 POLYGON ((30.23231 59.84322, 30.23764 59.83983... 160088 {}
2531 POLYGON ((29.50097 60.20315, 29.49286 60.20206... 160089 {}
2532 POLYGON ((30.74536 59.75700, 30.74007 59.76043... 160090 {}
2533 POLYGON ((30.17198 60.10435, 30.17477 60.10878... 160091 {}

2534 rows × 3 columns

[3]:
import requests
import geopandas as gpd

def fetch_mo(
    territory_id: int,
    centers_only: bool = False,
    base_url: str = 'https://urban-api.idu.kanootoko.org/api/v1/territory/indicator_values'
) -> gpd.GeoDataFrame:
    """
    Запрос гексагонов для заданного territory_id и возвращает их в виде GeoDataFrame.

    :param territory_id: ID территории
    :param centers_only: если True — вернуть только центральные точки гексагонов
    :param base_url: шаблон URL с {territory_id}
    :return: GeoDataFrame с колонками geometry, hexagon_id и свойствами
    """
    # Подставляем ID в URL
    url = base_url.format(territory_id=territory_id)
    params = {
        'parent_id': territory_id,  # ID родительской территории (1 для МО)
        'indicator_ids' : 1
    }

    # Выполняем запрос
    response = requests.get(url, params=params)
    response.raise_for_status()  # вызовет исключение, если код != 200

    # Парсим GeoJSON
    geojson = response.json()
    features = geojson.get('features', [])

    # Строим GeoDataFrame
    # from_features автоматически извлечёт geometry и все свойства
    gdf = gpd.GeoDataFrame.from_features(features)
    # Устанавливаем CRS (WGS84)
    gdf = gdf.set_crs(epsg=4326)

    return gdf

territory_id = 3138  # ID территории для Санкт-Петербурга
gdf_mo = fetch_mo(territory_id, centers_only=False)
gdf_mo['population'] = gdf_mo['indicators'].apply(lambda lst: lst[0].get('value'))


gdf_mo
[3]:
geometry territory_id name indicators population
0 MULTIPOLYGON (((30.25023 59.90129, 30.25112 59... 3153 Адмиралтейский район [{'name_full': 'Численность населения', 'measu... 155981.0
1 MULTIPOLYGON (((30.17787 59.94437, 30.17790 59... 3145 Василеостровский район [{'name_full': 'Численность населения', 'measu... 206680.0
2 MULTIPOLYGON (((30.07078 60.09625, 30.07421 60... 3144 Выборгский район [{'name_full': 'Численность населения', 'measu... 541590.0
3 MULTIPOLYGON (((30.34398 59.97727, 30.34399 59... 3152 Калининский район [{'name_full': 'Численность населения', 'measu... 536794.0
4 MULTIPOLYGON (((30.16152 59.87428, 30.16152 59... 3151 Кировский район [{'name_full': 'Численность населения', 'measu... 335774.0
5 MULTIPOLYGON (((30.43875 59.82757, 30.44219 59... 3156 Колпинский район [{'name_full': 'Численность населения', 'measu... 186169.0
6 MULTIPOLYGON (((30.39428 59.92902, 30.39591 59... 3150 Красногвардейский район [{'name_full': 'Численность населения', 'measu... 366971.0
7 MULTIPOLYGON (((30.03496 59.71785, 30.04047 59... 3155 Красносельский район [{'name_full': 'Численность населения', 'measu... 431546.0
8 MULTIPOLYGON (((29.54157 60.04248, 29.54184 60... 3141 Кронштадтский район [{'name_full': 'Численность населения', 'measu... 44414.0
9 MULTIPOLYGON (((29.42576 60.19074, 29.42719 60... 3140 Курортный район [{'name_full': 'Численность населения', 'measu... 83491.0
10 MULTIPOLYGON (((30.19877 59.80161, 30.19888 59... 3148 Московский район [{'name_full': 'Численность населения', 'measu... 335221.0
11 MULTIPOLYGON (((30.36331 59.91387, 30.36350 59... 3146 Невский район [{'name_full': 'Численность населения', 'measu... 547896.0
12 MULTIPOLYGON (((30.21013 59.97234, 30.21024 59... 3142 Петроградский район [{'name_full': 'Численность населения', 'measu... 115757.0
13 MULTIPOLYGON (((29.64754 59.92986, 29.64792 59... 3147 Петродворцовый район [{'name_full': 'Численность населения', 'measu... 134148.0
14 MULTIPOLYGON (((29.95527 60.03782, 29.95528 60... 3139 Приморский район [{'name_full': 'Численность населения', 'measu... 699243.0
15 MULTIPOLYGON (((30.21822 59.67307, 30.22257 59... 3149 Пушкинский район [{'name_full': 'Численность населения', 'measu... 263732.0
16 MULTIPOLYGON (((30.33258 59.91305, 30.33284 59... 3154 Фрунзенский район [{'name_full': 'Численность населения', 'measu... 413983.0
17 MULTIPOLYGON (((30.30827 59.94106, 30.30908 59... 3143 Центральный район [{'name_full': 'Численность населения', 'measu... 200654.0
[4]:
import pandas as pd
all_gdfs = []

# 2. Перебираем все territory_id из уже имеющегося gdf_mo:
for parent_tid in gdf_mo['territory_id']:
    # вызываем вашу функцию fetch_mo для каждого района:
    child_gdf = fetch_mo(parent_tid, centers_only=False)

    # При желании можно сохранить «родительский» ID (т. е. ID района из gdf_mo),
    # чтобы потом понимать, к какому району относится каждая запись:
    child_gdf['parent_territory_id'] = parent_tid

    # При необходимости можно добавить и название «родительского» района:
    # например, найдём строку в gdf_mo с данным parent_tid и вытащим её name:
    parent_name = gdf_mo.loc[gdf_mo['territory_id'] == parent_tid, 'name'].iloc[0]
    child_gdf['parent_name'] = parent_name

    # Добавляем полученный GeoDataFrame в список:
    all_gdfs.append(child_gdf)

# 3. После цикла объединяем все GeoDataFrame в один:
#    ignore_index=True — чтобы сбросить индексы и получить «плоскую» таблицу.
combined_gdf = pd.concat(all_gdfs, ignore_index=True)
combined_gdf['population'] = combined_gdf['indicators'].apply(lambda lst: lst[0].get('value'))
combined_gdf

[4]:
geometry territory_id name indicators parent_territory_id parent_name population
0 MULTIPOLYGON (((30.28180 59.93306, 30.28272 59... 3249 Адмиралтейский округ [{'name_full': 'Численность населения', 'measu... 3153 Адмиралтейский район 24189.0
1 MULTIPOLYGON (((30.25023 59.90129, 30.25112 59... 3252 Екатерингофский округ [{'name_full': 'Численность населения', 'measu... 3153 Адмиралтейский район 21890.0
2 MULTIPOLYGON (((30.29074 59.90313, 30.29090 59... 3251 округ Измайловское [{'name_full': 'Численность населения', 'measu... 3153 Адмиралтейский район 32807.0
3 MULTIPOLYGON (((30.26049 59.91726, 30.26152 59... 3253 округ Коломна [{'name_full': 'Численность населения', 'measu... 3153 Адмиралтейский район 34026.0
4 MULTIPOLYGON (((30.31777 59.92170, 30.31781 59... 3250 округ Семеновский [{'name_full': 'Численность населения', 'measu... 3153 Адмиралтейский район 22414.0
... ... ... ... ... ... ... ...
136 MULTIPOLYGON (((30.30827 59.94106, 30.30908 59... 3169 Дворцовый округ [{'name_full': 'Численность населения', 'measu... 3143 Центральный район 7348.0
137 MULTIPOLYGON (((30.33489 59.95015, 30.33642 59... 3166 Литейный округ [{'name_full': 'Численность населения', 'measu... 3143 Центральный район 40151.0
138 MULTIPOLYGON (((30.31013 59.93647, 30.31023 59... 3165 округ № 78 [{'name_full': 'Численность населения', 'measu... 3143 Центральный район 9085.0
139 MULTIPOLYGON (((30.35063 59.91524, 30.35178 59... 3168 округ Лиговка-Ямская [{'name_full': 'Численность населения', 'measu... 3143 Центральный район 19563.0
140 MULTIPOLYGON (((30.35920 59.94408, 30.35920 59... 3170 округ Смольнинское [{'name_full': 'Численность населения', 'measu... 3143 Центральный район 74205.0

141 rows × 7 columns

[5]:
from popframe.method.city_evaluation import CityPopulationScorer

scorer = CityPopulationScorer(combined_gdf, gdf_hex)


output_list = scorer.run()
scores_df = pd.DataFrame(output_list)
gdf_hex['score'] = scores_df['score']
gdf_hex
[5]:
geometry hexagon_id properties score
0 POLYGON ((30.06574 59.67090, 30.07379 59.67194... 157558 {} 2.0
1 POLYGON ((30.08770 60.08939, 30.09047 60.09382... 157559 {} 1.0
2 POLYGON ((30.37948 59.79270, 30.38759 59.79372... 157560 {} 3.0
3 POLYGON ((30.66727 59.76362, 30.66198 59.76704... 157561 {} 3.0
4 POLYGON ((30.24918 60.02467, 30.25198 60.02910... 157562 {} 3.0
... ... ... ... ...
2529 POLYGON ((30.45791 60.02272, 30.46608 60.02376... 160087 {} 2.0
2530 POLYGON ((30.23231 59.84322, 30.23764 59.83983... 160088 {} 4.0
2531 POLYGON ((29.50097 60.20315, 29.49286 60.20206... 160089 {} 1.0
2532 POLYGON ((30.74536 59.75700, 30.74007 59.76043... 160090 {} 3.0
2533 POLYGON ((30.17198 60.10435, 30.17477 60.10878... 160091 {} 1.0

2534 rows × 4 columns

[6]:
scores_df
[6]:
hexagon_id project average_population_density total_population score interpretation
0 157558 None 774.7 58652.0 2.0 Территория отличается относительно низкой числ...
1 157559 None 43.9 6021.0 1.0 Территория имеет низкиие показателяи численнос...
2 157560 None 318.5 132978.0 3.0 Территория имеет средние показатели численност...
3 157561 None 631.2 143914.0 3.0 Территория имеет средние показатели численност...
4 157562 None 2387.6 114257.0 3.0 Территория имеет средние показатели численност...
... ... ... ... ... ... ...
2529 160087 None 904.0 75180.0 2.0 Территория отличается относительно низкой числ...
2530 160088 None 4223.5 71589.0 4.0 Территория имеет высокие показатели численност...
2531 160089 None 33.4 1668.0 1.0 Территория имеет низкиие показателяи численнос...
2532 160090 None 631.2 143914.0 3.0 Территория имеет средние показатели численност...
2533 160091 None 43.9 6021.0 1.0 Территория имеет низкиие показателяи численнос...

2534 rows × 6 columns

[7]:
import matplotlib.pyplot as plt
gdf_hex.plot(
    column='score',
    legend=True,
    figsize=(15,15),
    cmap='RdYlGn'
).set_axis_off()
plt.title(' Санкт-Петербурга (оценка населения)')

plt.show()
../_images/examples_criterion_hex_7_0.png

Население

[ ]:
from popframe.method.territory_evaluation import TerritoryEvaluation

evaluation = TerritoryEvaluation(region=region_model)

results = evaluation.population_criterion(territories_gdf=gdf_hex)
scores_df = pd.DataFrame(results)
scores_df
[ ]:
gdf_hex['score'] = scores_df['score']
gdf_hex
geometry hexagon_id properties score
0 POLYGON ((30.06574 59.6709, 30.07379 59.67194,... 157558 {} 4.0
1 POLYGON ((30.0877 60.08939, 30.09047 60.09382,... 157559 {} 5.0
2 POLYGON ((30.37948 59.7927, 30.38759 59.79372,... 157560 {} 5.0
3 POLYGON ((30.66727 59.76362, 30.66198 59.76704... 157561 {} 5.0
4 POLYGON ((30.24918 60.02467, 30.25198 60.0291,... 157562 {} 5.0
... ... ... ... ...
2529 POLYGON ((30.45791 60.02272, 30.46608 60.02376... 160087 {} 0.0
2530 POLYGON ((30.23231 59.84322, 30.23764 59.83983... 160088 {} 0.0
2531 POLYGON ((29.50097 60.20315, 29.49286 60.20206... 160089 {} 2.0
2532 POLYGON ((30.74536 59.757, 30.74007 59.76043, ... 160090 {} 5.0
2533 POLYGON ((30.17198 60.10435, 30.17477 60.10878... 160091 {} 5.0

2534 rows × 4 columns

[ ]:
import matplotlib.pyplot as plt

# допустим, ваш GeoDataFrame называется gdf и в нём есть колонки 'geometry' и 'score'
fig, ax = plt.subplots(1, 1, figsize=(10, 8))

gdf_hex.plot(
    column='score',          # раскраска по score
    cmap='RdYlGn',           # палитра от Red → Yellow → Green
    linewidth=0.5,           # толщина границ
    edgecolor='grey',        # цвет границ
    legend=True,             # вывод легенды
    legend_kwds={
        'label': "Оценка score",
        'shrink': 0.6
    },
    ax=ax
)

ax.set_title("Санкт-Петербург: оценка территорий по критерию населения", fontsize=15)
ax.set_axis_off()  # убираем оси

plt.show()


../_images/examples_criterion_hex_11_0.png
[ ]:

[ ]:
# grid.to_file("grid_population_score.geojson", driver="GeoJSON")
[ ]:
from popframe.method.territory_evaluation import TerritoryEvaluation

evaluation = TerritoryEvaluation(region=region_model)

results = evaluation.evaluate_territory_location(territories_gdf=gdf_hex)
scores_df = pd.DataFrame(results)
scores_df
territory score interpretation closest_settlement closest_settlement1 closest_settlement2
0 None 0 Территория находится за границей агломерации None None None
1 None 5 Территория находится внутри или непосредственн... поселок Левашово None None
2 None 7 Территория находится внутри или непосредственн... поселок Шушары None None
3 None 7 Территория находится внутри или непосредственн... город Колпино None None
4 None 1 Территория находится между основными ядрами си... None поселок Парголово поселок Лисий Нос
... ... ... ... ... ... ...
2529 None 0 Территория находится за границей агломерации None None None
2530 None 1 Территория находится между основными ядрами си... None поселок Стрельна поселок Александровская
2531 None 3 Территория находится внутри или непосредственн... поселок Молодежное None None
2532 None 3 Территория находится внутри или непосредственн... поселок Саперный None None
2533 None 5 Территория находится внутри или непосредственн... поселок Левашово None None

2534 rows × 6 columns

[ ]:
gdf_hex['score_2'] = scores_df['score']
gdf_hex
geometry hexagon_id properties score score_2
0 POLYGON ((30.06574 59.6709, 30.07379 59.67194,... 157558 {} 4.0 0
1 POLYGON ((30.0877 60.08939, 30.09047 60.09382,... 157559 {} 5.0 5
2 POLYGON ((30.37948 59.7927, 30.38759 59.79372,... 157560 {} 5.0 7
3 POLYGON ((30.66727 59.76362, 30.66198 59.76704... 157561 {} 5.0 7
4 POLYGON ((30.24918 60.02467, 30.25198 60.0291,... 157562 {} 5.0 1
... ... ... ... ... ...
2529 POLYGON ((30.45791 60.02272, 30.46608 60.02376... 160087 {} 0.0 0
2530 POLYGON ((30.23231 59.84322, 30.23764 59.83983... 160088 {} 0.0 1
2531 POLYGON ((29.50097 60.20315, 29.49286 60.20206... 160089 {} 2.0 3
2532 POLYGON ((30.74536 59.757, 30.74007 59.76043, ... 160090 {} 5.0 3
2533 POLYGON ((30.17198 60.10435, 30.17477 60.10878... 160091 {} 5.0 5

2534 rows × 5 columns

[ ]:
import geopandas as gpd
import matplotlib.pyplot as plt

# допустим, ваш GeoDataFrame называется gdf и в нём есть колонки 'geometry' и 'score'
fig, ax = plt.subplots(1, 1, figsize=(10, 8))

gdf_hex.plot(
    column='score_2',          # раскраска по score
    cmap='RdYlGn',           # палитра от Red → Yellow → Green
    linewidth=0.5,           # толщина границ
    edgecolor='grey',        # цвет границ
    legend=True,             # вывод легенды
    legend_kwds={
        'label': "Оценка score",
        'shrink': 0.6
    },
    ax=ax
)

ax.set_title("Карта оценки score_2")
ax.set_axis_off()  # убираем оси

plt.show()

../_images/examples_criterion_hex_16_0.png