Петербургский Фонд капитального ремонта опубликовал документы, в которых указана задолженность за каждую квартиру в городе по итогам 2024 года. Мы изучили эти файлы, чтобы ответить на вопрос: где и почему хуже всего платят за ремонт в своём доме.
Я занимаюсь анализом данных и дата-журналистикой в газете «Деловой Петербург». Расскажу о том, как объединяли информацию из множества локальных html-таблиц и приведу примеры кода на «Питоне».

Документы — это более 20 тыс. отчётов (по одному отчёту на каждый многоквартирный жилой дом в Петербурге), собранных в папки по восемнадцати районам города. Очевидно, это результат автоматической выгрузки из базы данных.
Часто журналисты не раскрывают свои источники. Но в этом случае данные открыты, хоть и расположены не в самом очевидном месте (вообще, опыт показывает: то, что прямо называется «открытыми данными», часто особого интереса обычно не представляет). Всё выглядит легальным: организация сама заявляет, что публикует эти документы на основании одного из приказов Минстроя.
Архив за последний год (а при желании, и архивы за последние несколько лет) можно загрузить с файлового сервера Фонда капремонта. Распакуем его в каталог «Долги» в папке нашего проекта. Каждый отчёт в архиве — это html-файл. Кто-то скажет: наверное, намного легче было бы написать SQL-запросы, чтобы получить нужную информацию, чем копаться в этих файлах. Но, увы, к самой базе у нас доступа нет, так что будем работать с отчётами.

Итак, у нас есть 18 папок, которые нужно пройти, в каждой более 1 тыс. документов, всё вместе содержит информацию о более чем 2 млн помещений (квартир и нежилых помещений). Автоматически прочитать таблицу из html-документа можно при помощи метода Pandas read_html(). Нам нужно прочитать их все и объединить содержащиеся в файлах таблицы.
Прямолинейное решение
Первое, что приходит в голову — объединить маленькие таблицы в большую средствами самой библиотеки Pandas:
#Это пример кода на языке Python 3 #В нём собираем нужную информацию из html-таблиц в одну базу данных #Предполагается, что выгруженные с сервера каталоги с #html-файлами дежат в папке data нашего проекта. import pandas as pd #Наш главный помощник - библиотека Pandas. #Не забудьте установить её, если будете запускать этот код. from pathlib import Path #Для подсчёта числа файлов import os #Для прохода по списку файлов from tqdm import tqdm #Необязательно: используем, чтобы #следить за ходом выполнения программы. #Этот модуль тоже устанавливается отдельно. dfconcat=pd.DataFrame() #Готовим пустую базу данных path='Долги' #Путь к папке, где лежат файлы отчётов files_count=len(list(Path(path).rglob("*"))) #Подсчитаем общее число файлов pbar=tqdm(total=files_count) for foldername, subfolders, filenames in os.walk(path): #Проходим по всем подкаталогам for filename in (filenames): #И берём каждый файл filepath=os.path.join(foldername, filename) #Получаем полный путь к файлу try: #По очень небольшому числу домов нет данных о долгах, потому что #их жители собирают взносы на отдельных счетах. Пропускаем эти дома. df=pd.read_html(filepath, thousands='.', decimal=',') #Считываем таблицы #из html-файла except: continue #Каждый отчёт содержит несколько таблиц. Для нашего примера #мы будем брать только строчки с долгами по каждому помещению в доме debts=df[3].iloc[4:-2] dfconcat=dfconcat._append (debts, ignore_index=True) #Присоединяем собранные #строки к единой таблице #Можно также #использовать похожий #метод concat pbar.update() pbar.close() print (dfconcat) #Выводим результат в командную строку
Вначале всё идёт хорошо, и ожидаемое время завершения формирования единой таблицы, как показывает индикатор полосы прогресса tqdm, составляет около 5 минут. Но с каждым присоединением промежуточной таблицы оно растёт. Когда индикатор хода выполнения задачи подходит к 20 %, прогнозируемое время выполнения её приближается уже к 30 минутам, а скорость падает с 30 до 8 итераций в секунду. Затем прогноз оставшегося времени и вовсе приближается к 2 часам.
Объединение таблиц средствами «Питона»
Причина, судя по всему, в медленной работе метода append в Pandas. Попробуем обойтись без него. Следующее решение, предполагающее двойное преобразование, я нашёл на Stack Owerflow. Подготовим пустой словарь-накопитель. Каждую маленькую таблицу, прочитанную из html-файла, будем преобразовывать в словарь и добавлять к словарю-накопителю. Затем уже преобразуем словарь-накопитель обратно в таблицу Pandas, чтобы можно было его анализировать.
from pathlib import Path import pandas as pd import os from tqdm import tqdm path='Долги' files_count=len(list(Path(path).rglob("*"))) pbar=tqdm(total=files_count) d = {} #Готовим словарь-накопитель i = 0 #Итератор for foldername, subfolders, filenames in os.walk(path): for filename in (filenames): filepath=os.path.join(foldername, filename) try: df=pd.read_html(filepath, thousands='.', decimal=',') except: continue debts=df[3].iloc[4:-2] #Убираем лишние колонки, которые не несут смысловой нагрузки debts.drop(debts.columns[[-1,-2,-3,-4,-5,-6]],axis=1,inplace=True) #Дадим колонкам понятные названия debts.columns=['Номер помещения', 'Задолженность на начало периода', 'Аванс на начало периода', 'Начислено нарастающим итогом', 'Уплачено нарастающим итогом', 'Засчитано за оказанные услуги', 'Задолженность на конец периода', 'Аванс на конец периода', 'Уплачено пени'] data = debts.to_dict('records') #Преобразуем таблицу в словарь for entry in data: #И добавляем каждую строчку к словарю-накопителю d[i] = {"Номер помещения": entry["Номер помещения"], "Задолженность на начало периода": entry["Задолженность на начало периода"], "Аванс на начало периода":entry["Аванс на начало периода"], "Начислено нарастающим итогом":entry["Начислено нарастающим итогом"], "Уплачено нарастающим итогом":entry["Уплачено нарастающим итогом"], "Засчитано за оказанные услуги":entry["Засчитано за оказанные услуги"], "Задолженность на конец периода":entry["Задолженность на конец периода"], "Аванс на конец периода":entry["Аванс на конец периода"], 'Уплачено пени':entry["Уплачено пени"]} # increment the counter i = i + 1 pbar.update() pbar.close() dfconcat = pd.DataFrame.from_dict(d, "index") #Обратное преобразование из словаря #в таблицу print (dfconcat)
Вышло около 13 минут, что значительно лучше. Но и этот способ оказался не самым быстрым.
Объединение таблиц через базу данных
Чуть быстрее на моём компьютере оказалось загружать извлечённую из html-файлов табличную информацию в базу данных. Хотя для работы c SQL базами данных из «Питона» часто используют библиотеку SQLAlchemy, здесь это не строго обязательно. Мы будем напрямую отправлять несложные SQL команды из Pandas во входящий в стандартный пакет поставки «Питона» модуль SQLite.
#Это пример кода похож на предыдущий, но таблицы #из html-файлов объединяются при помощи SQLite import pandas as pd import os import sqlite3 #На этот раз будем использовать SQL-базу для накопления данных from pathlib import Path from tqdm import tqdm # Подключаем файл базы данных и создаём таблицу con=sqlite3.connect('est.db') cur=con.cursor() cur.execute("DROP TABLE IF EXISTS result;") #На всякий случай, если прошлый #проход программы был прерван cur.execute("CREATE TABLE result (foldername, filename, num_of_ap, \ start_debt, start_avans, accrued, paid, counted, final_debt, \ final_avans, peni);") path='Долги' files_count=len(list(Path(path).rglob("*"))) pbar=tqdm(total=files_count) #Эта функция вызывается для каждой строки таблицы, считанной Pandas, #и записывает эту строку в базу SQLite def addrows (row): params=(foldername, filename, row['Номер помещения'], row['Задолженность на начало периода'], row['Аванс на начало периода'], row['Начислено нарастающим итогом'], row['Уплачено нарастающим итогом'], row['Засчитано за оказанные услуги'], row['Задолженность на конец периода'], row['Аванс на конец периода'], row['Уплачено пени']) cur.execute('INSERT INTO result VALUES (?,?,?,?,?,?,?,?,?,?,?);', params) for foldername, subfolders, filenames in os.walk(path): for filename in (filenames): filepath=os.path.join(foldername, filename) try: df=pd.read_html(filepath, thousands='.', decimal=',') except: continue debts=df[3].iloc[4:-2] debts.drop(debts.columns[[-1,-2,-3,-4,-5,-6]],axis=1,inplace=True) #Дадим понятные названия колонкам, чтобы потом записать по ним их в #базу в функции addrows debts.columns=['Номер помещения', 'Задолженность на начало периода', 'Аванс на начало периода', 'Начислено нарастающим итогом', 'Уплачено нарастающим итогом', 'Засчитано за оказанные услуги', 'Задолженность на конец периода', 'Аванс на конец периода', 'Уплачено пени'] debts.apply(addrows, axis=1) pbar.update() pbar.close() dfconcat = pd.read_sql_query("SELECT * FROM result", con) print (dfconcat)
Выполнение этой программы заняло около 12 минут, что намного быстрее первоначального варианта, и чуть быстрее варианта со словарём. На моём компьютере она выполнялась со скоростью около 30 операций в секунду.
Результаты и выводы
Так можно получить базу данных, с которой, правда, потребуется ещё поработать, чтобы увидеть в ней интересные закономерности. Нормализация заключается, в частности, в приведении столбцов к числовым типам. А для того, чтобы построить интерактивную карту, нужно ещё получить координаты по адресам. Но геокодинг — это отдельная тема, которой возможно, как-нибудь посвятим публикацию.
В итоге, мы обнаружили, что чаще всего за капитальный ремонт не платит бизнес. То есть владельцы нежилых (коммерческих) помещений в многоквартирных жилых домах. Так как в центре Петербурга, по сравнению с другими частями нашего города, таких помещений больше всего, то и должников там значительно больше. По словам экспертов, если собственник не платит за капитальный ремонт, то и другие жилищно-коммунальные услуги он, скорее всего, не будет аккуратно оплачивать. Поэтому можно предполагать, что существенная часть долга за ЖКУ — это долги бизнеса.
Итоговый материал можно прочитать на сайте газеты.
ссылка на оригинал статьи https://habr.com/ru/articles/891704/
Добавить комментарий