I.Оценка численности населения на основе данных реформы ЖКХ#

Данные о населении — одни из самых важных в городской аналитике. Однако точной информации нет (возможно ли её получить?), и нам остаётся лишь оценивать. Один из возможных параметров для оценки МКД (многоквартирных домов) — общая жилая площадь. Зная (или рассчитав) среднюю обеспеченность мы можем понять количество человек в каждом доме

Реестр многоквартирных домов в рамках реформы ЖКХ#

Реестр многоквартирных домов — это система, содержащая информацию о многоквартирных домах в России. Он был создан для улучшения управления жилым фондом, повышения прозрачности в сфере ЖКХ и упрощения контроля за состоянием зданий (во всяком случае, такие цели)

Что содержится в Реестре МКД:

  • ID дома — уникальный идентификатор.

  • Адрес — регион, город, улица, номер дома.

  • Характеристики дома:

    • Год постройки, этажность, тип дома.

    • Общая плоащдь, жилая площадь, нежилая площадь помещений

    • Состояние здания (аватрийное или нет). и другое

Эти данные мы можем так или иначе использовать для оценки численности населения в городах РФ

Так как данные не содержат координаты объектов, нам нужно их геокодировать - получить координаты на основе адреса

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

import pandas as pd
import geopandas as gpd

1. Оцениваем численность населения в каждом МКД#

Читаем csv табличку с данными об МКД Санкт-Петербурга

mkd = pd.read_csv('../data/spb_mkd_reforma.csv')
mkd = mkd.dropna(subset=['lon', 'lat']) #убираем строки с пустыми координатами
mkd.head()
/var/folders/ry/9bb7wrz54vq_kn2ytlj6ynzm0000gn/T/ipykernel_60044/1018375022.py:1: DtypeWarning: Columns (3,8,9) have mixed types. Specify dtype option on import or set low_memory=False.
  mkd = pd.read_csv('../data/spb_mkd_reforma.csv')
Unnamed: 0 id region_id area_id city_id street_id shortname_region formalname_region shortname_area formalname_area ... cold_water_type sewerage_type sewerage_cesspools_volume gas_type ventilation_type firefighting_type drainage_type build_year lat lon
1 2 8069185 6d1ebb35-70c6-4129-bd55-da3969658f5d 003fc437-dd9c-4c36-b3e3-adab06e5d195 6f354a0e-9b9a-440f-864f-cd96decafa8c 13f6dee0-ad15-4273-8be3-ec82a1bd3b58 обл Ленинградская р-н Тосненский ... Центральное Центральное 0.0 Отсутствует Приточная вентиляция Пожарные гидранты Наружные водостоки 1961.0 59.678426 30.497391
2 3 9360633 c2deb16a-0330-4f05-821f-1d09c93331e6 NaN NaN NaN г Санкт-Петербург NaN NaN ... Не заполнено Не заполнено NaN Не заполнено Не заполнено Не заполнено Не заполнено NaN 59.939095 30.315868
3 4 9373089 c2deb16a-0330-4f05-821f-1d09c93331e6 NaN NaN NaN г Санкт-Петербург NaN NaN ... Не заполнено Не заполнено NaN Не заполнено Не заполнено Не заполнено Не заполнено NaN 59.939095 30.315868
4 5 9382636 c2deb16a-0330-4f05-821f-1d09c93331e6 NaN NaN NaN г Санкт-Петербург NaN NaN ... Не заполнено Не заполнено NaN Не заполнено Не заполнено Не заполнено Не заполнено NaN 59.939095 30.315868
5 6 7033541 c2deb16a-0330-4f05-821f-1d09c93331e6 NaN NaN 87e65156-0f19-4a46-9cd7-510945049fb2 г Санкт-Петербург NaN NaN ... Центральное Центральное 0.0 Центральное Вытяжная вентиляция Отсутствует Наружные водостоки 1862.0 59.938279 30.277860

5 rows × 64 columns

Создадим на основе координат GeoDataFrame и посмотрим на результат

mkd_gdf = gpd.GeoDataFrame(mkd, geometry=gpd.points_from_xy(mkd.lon, mkd.lat), crs="EPSG:4326")

#mkd_gdf.explore(tiles='cartodbpositron')

Пересечем данные об МКД с данными о городских округах города

admin_okrug
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 admin_okrug

NameError: name 'admin_okrug' is not defined
# Читаем слой округов
admin_okrug = gpd.read_file('../data/spb_admin.gpkg', layer='okrug')

# Приводим обе таблицы к одной CRS (наиболее часто используют EPSG:3857 или EPSG:4326)
#    Проверяем текущие CRS:
print("CRS точек МКД:", mkd_gdf.crs)
print("CRS округов:", admin_okrug.crs)

# Если CRS отличаются, то необходимо их привести к единому виду
CRS точек МКД: EPSG:4326
CRS округов: EPSG:4326
# Делаем пространственный join: для каждой точки подбираем тот округ, в который она попадает
#    predicate='within' означает, что точка должна лежать внутри полигона округа
mkd_district = gpd.sjoin(mkd_gdf, admin_okrug, how='left', predicate='within')


# Переименовываем столбцы, чтобы с ними было удобнее работать 

mkd_district = mkd_district.rename(columns={
    'NAME': 'okrug_name',        # новое имя для столбца "название округа"
    'Popul': 'okrug_population'    # новое имя для столбца "численность округа"
})

Вычислим среднюю обеспеченность жилой площадью на основе данных о населении района

# Группируем данные по району и считаем суммарную площадь жилых помещений
# и среднюю обеспеченность жилой площадью на одного жителя в районе
district_stat = mkd_district.groupby('okrug_name').agg(
    total_residential_area=('area_residential', 'sum')
).reset_index()

district_stat.head()
okrug_name total_residential_area
0 Адмиралтейский округ 877337.29
1 Александровская 1733.60
2 Балканский округ 1266607.95
3 Белоостров 11588.22
4 Васильевский округ 1822414.17
# Объединяем эти данные с исходным DataFrame mkd_district по полю 'okrug_name'
mkd_district = mkd_district.merge(district_stat[['okrug_name', 'total_residential_area']], on='okrug_name', how='inner')

# Считаем обеспеченность
mkd_district['avg_area_per_person'] = mkd_district['total_residential_area'] / mkd_district['okrug_population']

На основне оценочной обеспеченности населения жилой площадью, вычисляем количество жителей в каждом доме

# Оцениваем кол-во человек на основе обеспеченности
mkd_district['estimated_population'] = mkd_district['area_residential'] / mkd_district['avg_area_per_person']

# Смотрим на результат
mkd_district.head()
Unnamed: 0 id region_id area_id city_id street_id shortname_region formalname_region shortname_area formalname_area ... build_year lat lon geometry index_right okrug_name okrug_population total_residential_area avg_area_per_person estimated_population
0 3 9360633 c2deb16a-0330-4f05-821f-1d09c93331e6 NaN NaN NaN г Санкт-Петербург NaN NaN ... NaN 59.939095 30.315868 POINT (30.31587 59.93910) 65.0 Дворцовый округ 6887.0 393274.37 57.103873 NaN
1 4 9373089 c2deb16a-0330-4f05-821f-1d09c93331e6 NaN NaN NaN г Санкт-Петербург NaN NaN ... NaN 59.939095 30.315868 POINT (30.31587 59.93910) 65.0 Дворцовый округ 6887.0 393274.37 57.103873 NaN
2 5 9382636 c2deb16a-0330-4f05-821f-1d09c93331e6 NaN NaN NaN г Санкт-Петербург NaN NaN ... NaN 59.939095 30.315868 POINT (30.31587 59.93910) 65.0 Дворцовый округ 6887.0 393274.37 57.103873 NaN
3 6 7033541 c2deb16a-0330-4f05-821f-1d09c93331e6 NaN NaN 87e65156-0f19-4a46-9cd7-510945049fb2 г Санкт-Петербург NaN NaN ... 1862.0 59.938279 30.277860 POINT (30.27786 59.93828) 49.0 округ № 7 40859.0 3325797.15 81.396930 27.530522
4 7 9058812 c2deb16a-0330-4f05-821f-1d09c93331e6 NaN NaN 87e65156-0f19-4a46-9cd7-510945049fb2 г Санкт-Петербург NaN NaN ... 1862.0 59.938279 30.277860 POINT (30.27786 59.93828) 49.0 округ № 7 40859.0 3325797.15 81.396930 28.539160

5 rows × 71 columns

2. Создаем карту плотности населения#

Создаем функцию построения регулярной сетки для агрегирования данных

from shapely.geometry import Polygon

def create_regular_grid(gdf, square_size):
    #вычислеяем utm зоны для набора данных 
    utm_zone = gdf.estimate_utm_crs()
    #перепроецируем набор данных
    gdf = gdf.to_crs(utm_zone)
    minX, minY, maxX, maxY = gdf.total_bounds
    
    grid_cells = []
    x, y = minX, minY

    while y <= maxY:
        while x <= maxX:
            geom = Polygon([(x, y), (x, y + square_size), (x + square_size, y + square_size), (x + square_size, y), (x, y)])
            grid_cells.append(geom)
            x += square_size
        x = minX
        y += square_size

    fishnet = gpd.GeoDataFrame(geometry=grid_cells, crs=utm_zone)
    fishnet['grid_id'] = fishnet.index
    return fishnet

Создаем сетку для Санкт-Петербурга 1км^2

grid = create_regular_grid(mkd_district, 1000)

Определяем систему координат для перепроецирования данных МКД

utm_crs = mkd_district.estimate_utm_crs()
mkd_district_utm = mkd_district.to_crs(utm_crs)

Рассчитаем плотность населения в каждой ячейке

mkd_district_utm = mkd_district_utm.drop(columns=['index_right'])

# Выполняем пространственное соединение: сопоставляем каждую точку дома с тем квадратом сетки, 
# в котором она находится.  
msk_in_grid = gpd.sjoin(mkd_district_utm, grid, predicate='within')
# Группируем результирующий набор msk_in_grid по полю 'id' (это идентификатор ячейки сетки), 
# и суммируем для каждой ячейки численность населения (столбец 'estimated_population').  
pop_grid = msk_in_grid.groupby('grid_id')['estimated_population'].sum()

# Преобразуем Series pop_grid в DataFrame: сбрасываем индекс, 
# даём новой колонке имя 'pop_sum' (это суммарное население в ячейке).
# Теперь pop_grid_df имеет два столбца: 'id' и 'pop_sum'.
pop_grid_df = pop_grid.reset_index(name='pop_sum')

# Объединяем исходный GeoDataFrame grid с таблицей pop_grid_df по столбцу 'id' (в сетке) и 'id_right' (в слое с перечечением).  
pop_grid_gdf = grid.merge(pop_grid_df,left_on='grid_id', right_on='grid_id', how='left')

# Вычисляем плотность населения для каждой ячейки: кол-во человек на квадратны километр
pop_grid_gdf['pop_density'] = pop_grid_gdf['pop_sum']/(pop_grid_gdf.geometry.area/1000000 )

Визуализируем результат

pop_grid_gdf.explore(column='pop_density', cmap='YlGnBu', tiles='cartodbpositron', scheme='NaturalBreaks', k=5, legend=True, missing_kwds={'color': '#ffffff00','fillOpacity': 0})
Make this Notebook Trusted to load map: File -> Trust Notebook