
Доброго времени суток дорогие читатели хабра, 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
Добавил актуальный код для формирования карты, скрыл реализацию, т.к. большинство читателей статьи интересуется только картой.
Спасибо за внимание.
