Доброго времени суток дорогие читатели хабра, 12 августа 2020 года были опубликованы этапы переезда по программе реновации (ознакомиться можно здесь) и мне стало интересно, а как это будет выглядеть, если эти этапы визуализировать. Тут нужно уточнить, что я никак ни связан с правительством Москвы, но являюсь счастливым обладателем квартиры в доме под реновацию, поэтому мне было интересно посмотреть, может даже с некоторой точностью предположить, куда возможно будет двигаться волна реновации в моём случае (а может быть и в вашем, если вас дорогой читатель это заинтересует). Конечно точного прогноза не получится, но хотя-бы можно будет увидеть картину под новым углом.
UPD 28 августа 2020
Получилась полная карта реновации с отмеченными на ней волнами реновации и стартовыми площадками.
Введение
Программа реновации была запущена Правительством Москвы в 2017 году. Благодаря ей 350 тысяч московских семей, то есть более миллиона человек, переедут в новые квартиры с отделкой комфорткласса.
Какие дома войдут в программу, решали сами жители. По итогам голосования в программу было включено 5174 дома.
Участники получат равнозначное жилье в своем районе… (далее можно прочитать здесь)
На основании приказа правительства Москвы от 12 августа 2020 г. № 45/182/ПР-335/20 (прочитать можно здесь) вся программа переселения рассчитана до 2032 года и должна будет пройти в три этапа (три волны):
- первый этап 2020 — 2024гг., в него вошло 930 домов, страницы 3-29 в приказе
- второй этап 2025 — 2028гг., в него вошло 1636 домов, страницы 30-76 в приказе
- третий этап 2029 — 2032гг., в него вошло 1809 домов, страницы 77-128 в приказе
- без определённого этапа (этапы должны будут определиться до конца 1 квартала 2021г.) — 688 домов, страницы 129-148 в приказе
Реализация
Исходный код залит на github и скачать его можно здесь.
- В первой версии кода я использовал геокодер яндекса для определения точных координат домов, входящих в программу реновации. Добавить стартовые площадки через геокодер не удалось, не все координаты определились правильно.
Парсинг данных
Данные я взял из этого приказа, т.к. приказ — это pdf файл с таблицами, то я использовал библиотеку tabula для парсинга pdf файлов.
import pandas as pd
import numpy as np
import requests
from tabula import read_pdf
import json
import os
Первым делом я спарсил одну страницу из этих таблиц, чтобы посмотреть, как дальше чистить данные.
test = read_pdf('prikaz_grafikpereseleniya.pdf', pages='3', pandas_options={'header':None})
test.head()
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | No п/п | АО | Район | NaN | Адрес дома | unom |
1 | 1 | ЦАО | Басманный | Бакунинская ул., д.49 c.4 | NaN | 1316 |
2 | 2 | ЦАО | Басманный | Бакунинская ул., д.77 c.3 | NaN | 1327 |
3 | 3 | ЦАО | Басманный | Балакиревский пер., д.2/26 | NaN | 19328 |
4 | 4 | ЦАО | Басманный | Госпитальный Вал ул., д.3 | NaN | 31354 |
Как видно из того, что получилось спарсить, чтобы очистить данные необходимо удалить лишние колонки и строчки, что и делает функция parse_pdf_table
.
def parse_pdf_table(pages, pdf_file='prikaz_grafikpereseleniya.pdf'):
df = read_pdf(pdf_file, pages=pages, pandas_options={'header':None})
# удаляем не нужные строки
df = df[~(df.iloc[:,0] == 'No п/п')]
# оставляем только нужные колонки
df = df.iloc[:,1:4]
df.columns = ['AO', 'district', 'address']
return df
Каждая волна находится в своём диапазоне страниц, парсим их и проверяем по документу, т.е. количество строк должно совпадать с тем, что есть в pdf файле. (Также сразу добавляем к данным номер волны, т.к. это пригодится в будущем)
wave_1 = parse_pdf_table('3-29') # 2020 - 2024
wave_1['wave'] = 1
wave_1.shape
(930, 4)
wave_2 = parse_pdf_table('30-76') # 2025 - 2028
wave_2['wave'] = 2
wave_2.shape
(1636, 4)
wave_3 = parse_pdf_table('77-128') # 2029 - 2032
wave_3['wave'] = 3
wave_3.shape
(1809, 4)
unknown = parse_pdf_table('129-148')
unknown['wave'] = 0
unknown.shape
(688, 4)
Обработка данных
Обрабатывать данные будем на пандасе (pandas), для этого соберём все волны в один датафрейм df
.
df = pd.concat([wave_1, wave_2, wave_3, unknown], ignore_index=True)
Выделим своим цветом метки каждой волны.
df['marker-color'] = df['wave'].map({1:'#0ACF00', # зеленый
2:'#1142AA', # синий
3:'#FFFD00', # жёлтый
0:'#FD0006'}) # красный
Также подпишем каждую метку в зависимости от волны.
df['iconContent'] = df['wave'].map({1:'1',
2:'2',
3:'3',
0:''})
В описание метки добавим адрес.
df['description'] = df['address']
Если не уточнить город — Москва, то по данным, полученным из геокодера получится, что реновация началась по всей стране, да что там, во всём мире. (Даёшь реновацию во всём мире! :)
def add_city(x):
if x['AO'] == 'ЗелАО':
return 'Зеленоград, ' + x['address']
return 'Москва, ' + x['address']
df['address'] = df[['AO', 'address']].apply(add_city, axis=1)
Для определения координат каждого дома по адресу я использовал геокодер яндекса, что очень удобно, т.к. он бесплатный и на него есть очень хорошая документация. Если нужно будет пересчитать координаты, то не забудьте ввести свой ключ.
def geocoder(addr, key='введи свой ключ'):
url = 'https://geocode-maps.yandex.ru/1.x'
params = {'format':'json', 'apikey': key, 'geocode': addr}
response = requests.get(url, params=params)
try:
coordinates = response.json()["response"]["GeoObjectCollection"]["featureMember"][0]["GeoObject"]["Point"]["pos"]
lon, lat = coordinates.split(' ')
except:
lon, lat = 0, 0
return lon, lat
%%time
df['longitude'], df['latitude'] = zip(*df['address'].apply(geocoder))
CPU times: user 2min 11s, sys: 4.31 s, total: 2min 15s
Wall time: 15min 14s
Все координаты определились удачно (именно удачно, т.к. нет гарантий, что геокодер спарсил адрес так как нам нужно), другими словами он хотя-бы что-то вернул.
len(df[df['longitude'] == 0])
0
Сохраним полученные данные.
df.to_csv('waves.csv')
#df = pd.read_csv('waves.csv')
Формирование карты волн реновации
Для отображения полученных данных на карте я использовал формат GeoJSON.
def df_to_geojson(df, properties, lat='latitude', lon='longitude'):
geojson = {'type':'FeatureCollection', 'features':[]}
for _, row in df.iterrows():
feature = {'type':'Feature',
'properties':{},
'geometry':{'type':'Point',
'coordinates':[]}}
feature['geometry']['coordinates'] = [row[lon],row[lat]]
for prop in properties:
feature['properties'][prop] = row[prop]
geojson['features'].append(feature)
return geojson
Т.к. меток получилось очень много, то полная карта может медленно работать на слабом ПК, поэтому я разделил данные по округам Москвы для удобства.
properties = ['marker-color', 'iconContent', 'description']
if not os.path.exists('data'):
os.makedirs('data')
for ao, data in df.groupby('AO'):
geojson = df_to_geojson(data, properties)
with open('data/' + ao + '.geojson', 'w') as f:
json.dump(geojson, f, indent=2)
Полученные данные в формате .geojson
я сохранил в папку data
. В файле ВСЕ_ОКРУГА.geojson
записаны данные по всем округам вместе.
geojson = df_to_geojson(df, properties)
with open('data/ВСЕ_ОКРУГА.geojson', 'w') as f:
json.dump(geojson, f, indent=2)
ссылка на полную карту (может работать медленно) здесь.
В целом получилось не плохо, все метки внутри границ Москвы, однако, есть и несколько ошибок, как например недалеко от Сергиева Посада — Пролетарий СНТ территория (п.Вороновское), д.1 или в окрестностях Орехово-Зуево — Гаражный пер. (пос.ДСК Мичуринец, п.Внуковское), д.8/КБ/Н. (Честно говоря я бы и сам не сразу понял, где это находится)
Что хотелось сделать, но не получилось :(
Официальный список стартовых площадок находится здесь.
Также на карту волн реновации я хотел добавить стартовые площадки, однако это не получилось сделать. Проблема даже не в том, что нормально спарсить список не удалось, это можно было бы решить, проблема в том, что геокодер не может точно определить координаты по владению, например, Шмитовский проезд, вл. 39, Мукомольный проезд, вл. 6, или где находится этот адрес — район Южное Медведково, мкр. 1, 2, 3, корп. 38.
Таким образом единственный источник данных это официальная карта реновации (находится здесь), а как получить из неё координаты я не знаю, если кто знает, как получить координаты стартовых площадок, напишите пожалуйста в комментах.
Однако не всё так плохо и выход всё же есть — можно добавить эти метки вручную!
Видео-инструкция о том, как это сделать есть в исходном коде проекта, а также её можно посмотреть/скачать здесь.
- Вторая версия карты получилась полной и точной, на неё удалось добавить как дома по реновации, так и стартовые площадки. Вместо геокодера я использовал данные, которые удалось спарсить пользователю PbIXTOP, за что ему большое спасибо.
Волны московской реновации 2.0
import pandas as pd
import numpy as np
import json
from tabula import read_pdf
from tqdm.notebook import tqdm
import os
Адреса и локации домов по реновации
with open('renovation_address.txt') as f:
bounded_addresses = json.load(f)
def parse_pdf_table(pages, pdf_file='prikaz_grafikpereseleniya.pdf'):
df = read_pdf(pdf_file, pages=pages, pandas_options={'header':None})
# удаляем не нужные строки
df = df[~(df.iloc[:,0] == 'No п/п')]
df['unom'] = df.iloc[:,-1].combine_first(df.iloc[:,-2])
# оставляем только нужные колонки
df = df.iloc[:,[1, 2, 3, -1]]
df.columns = ['AO', 'district', 'description', 'unom']
return df
wave_1 = parse_pdf_table('3-29') # 2020 - 2024
wave_1['wave'] = 1
wave_2 = parse_pdf_table('30-76') # 2025 - 2028
wave_2['wave'] = 2
wave_3 = parse_pdf_table('77-128') # 2029 - 2032
wave_3['wave'] = 3
unknown = parse_pdf_table('129-148')
unknown['wave'] = 0
df = pd.concat([wave_1, wave_2, wave_3, unknown], ignore_index=True)
df['marker-color'] = df['wave'].map({1:'#0ACF00', # зеленый
2:'#1142AA', # синий
3:'#FFFD00', # жёлтый
0:'#FD0006'}) # красный
df['iconContent'] = df['wave'].map({1:'1',
2:'2',
3:'3',
0:''})
df['longitude'] = 0
df['latitude'] = 0
for i in tqdm(bounded_addresses):
unom = i['unom']
coordinates = i['center']['coordinates']
df.loc[df['unom']==unom, 'longitude'] = coordinates[1]
df.loc[df['unom']==unom, 'latitude'] = coordinates[0]
HBox(children=(FloatProgress(value=0.0, max=5152.0), HTML(value='')))
# Объеденим ТАО и НАО в ТиНАО, т.к. в стартовых площадках есть только ТиНАО
df.loc[(df['AO'] == 'ТАО') | (df['AO'] == 'НАО'), 'AO'] = 'ТиНАО'
df[df['longitude'] == 0]
AO | district | description | unom | wave | marker-color | iconContent | longitude | latitude | |
---|---|---|---|---|---|---|---|---|---|
917 | ТиНАО | поселение Михайлово-Ярцевское | Армейский пос. (п.Михайлово-Ярцевское), д.11 | 15000016 | 1 | #0ACF00 | 1 | 0.0 | 0.0 |
918 | ТиНАО | поселение Михайлово-Ярцевское | Армейский пос. (п.Михайлово-Ярцевское), д.13 | 15000015 | 1 | #0ACF00 | 1 | 0.0 | 0.0 |
919 | ТиНАО | поселение Михайлово-Ярцевское | Армейский пос. (п.Михайлово-Ярцевское), д.3 | 15000013 | 1 | #0ACF00 | 1 | 0.0 | 0.0 |
925 | ТиНАО | поселение Михайлово-Ярцевское | Армейский пос. (п.Михайлово-Ярцевское), д.4 | 15000012 | 1 | #0ACF00 | 1 | 0.0 | 0.0 |
926 | ТиНАО | поселение Михайлово-Ярцевское | Армейский пос. (п.Михайлово-Ярцевское), д.6 | 15000014 | 1 | #0ACF00 | 1 | 0.0 | 0.0 |
4883 | ТиНАО | поселение Внуковское | Гаражный пер. (пос.ДСК Мичуринец, п.Внуковское)... | 4405823 | 0 | #FD0006 | 0.0 | 0.0 | |
4945 | ТиНАО | поселение Мосрентген | Теплый Стан ул. (п.Мосрентген, в/г), д.51 | 20000002 | 0 | #FD0006 | 0.0 | 0.0 | |
4946 | ТиНАО | поселение Мосрентген | Теплый Стан ул. (п.Мосрентген, в/г), д.52 | 20000003 | 0 | #FD0006 | 0.0 | 0.0 | |
4947 | ТиНАО | поселение Мосрентген | Теплый Стан ул. (п.Мосрентген, в/г), д.53 | 20000001 | 0 | #FD0006 | 0.0 | 0.0 | |
4948 | ТиНАО | поселение Мосрентген | Теплый Стан ул. (п.Мосрентген, в/г), д.85 | 20000000 | 0 | #FD0006 | 0.0 | 0.0 | |
4995 | ТиНАО | поселение Вороновское | Пролетарий СНТ территория (п.Вороновское), д.1 | 20000004 | 0 | #FD0006 | 0.0 | 0.0 |
Добавляем вручную дома, которые не удалось спарсить
df.loc[917, ['longitude', 'latitude']] = 37.204805, 55.385382
df.loc[918, ['longitude', 'latitude']] = 37.205255, 55.385367
df.loc[919, ['longitude', 'latitude']] = 37.201518, 55.385265
df.loc[925, ['longitude', 'latitude']] = 37.201545, 55.384927
df.loc[926, ['longitude', 'latitude']] = 37.204151, 55.384576
df.loc[4883, ['longitude', 'latitude']] = 37.321218, 55.661308
df.loc[4945, ['longitude', 'latitude']] = 37.476896, 55.604153
df.loc[4946, ['longitude', 'latitude']] = 37.477406, 55.603895
df.loc[4947, ['longitude', 'latitude']] = 37.476546, 55.602729
df.loc[4948, ['longitude', 'latitude']] = 37.477568, 55.604659
df.loc[4995, ['longitude', 'latitude']] = 37.176806, 55.341541
Стартовые площадки
with open('start_area.txt') as f:
end = json.load(f)
data = {
'AO':[],
'district':[],
'longitude':[],
'latitude':[],
'description':[]
}
for i in end['response']:
data['AO'].append(i['OKRUG'])
data['district'] = i['AREA']
coordinates = i['geoData']['coordinates']
data['longitude'].append(coordinates[1])
data['latitude'].append(coordinates[0])
description = i['Address']
if 'StartOfRelocation' in i:
if i['StartOfRelocation'] is not None:
description += '\n' + i['StartOfRelocation']
data['description'].append(description)
df_start_area = pd.DataFrame(data)
df_start_area['marker-color'] = '#7D3E00' # коричневый цвет
df_start_area['iconContent'] = '0'
df_start_area['unom'] = None
df_start_area['wave'] = -1
Объеденяем метки домов по реновации и стартовых площадок
df = pd.concat([df, df_start_area], ignore_index=True)
Формирование карты реновации
def df_to_geojson(df, properties, lat='latitude', lon='longitude'):
geojson = {'type':'FeatureCollection', 'features':[]}
for _, row in df.iterrows():
feature = {'type':'Feature',
'properties':{},
'geometry':{'type':'Point',
'coordinates':[]}}
feature['geometry']['coordinates'] = [row[lon],row[lat]]
for prop in properties:
feature['properties'][prop] = row[prop]
geojson['features'].append(feature)
return geojson
properties = ['marker-color', 'iconContent', 'description']
Разделяем данные по округам.
if not os.path.exists('data'):
os.makedirs('data')
for ao, data in df.groupby('AO'):
geojson = df_to_geojson(data, properties)
with open('data/' + ao + '.geojson', 'w') as f:
json.dump(geojson, f, indent=2)
Полная карта (может работать медленно)
geojson = df_to_geojson(df, properties)
with open('data/ВСЕ_ОКРУГА.geojson', 'w') as f:
json.dump(geojson, f, indent=2)
Выводы
В целом можно сказать, что затея удалась, однако ещё раз повторю, выводы, которые вы можете получить исходя из этих данных носят лишь примерный характер, даже в самом приказе написано, что сроки указанные в нём являются ориентировочными и могут быть скорректированы, к тому же значительное число домов пока ещё даже не распределено.
UPD 28 августа 2020
Полная карта реновации с отмеченными на ней волнами реновации и стартовыми площадками.
Спасибо пользователю PbIXTOP за данные, спарсенные с официальной карты.
ВСЕ ОКРУГА (Может работать медленно)
ВАО
ЗАО
ЗелАО
САО
СВАО
СЗАО
ТиНАО
ЦАО
ЮАО
ЮВАО
ЮЗАО
UPD 1 сентября 2020
Добавил актуальный код для формирования карты, скрыл реализацию, т.к. большинство читателей статьи интересуется только картой.
Спасибо за внимание.
VarLegovar
Судя по полученной карте основная реновация- это неопределенная дата этой реновации