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})