Кому на бюджете жить хорошо?

dieguwa4xh6ychhrpun88wwbf2a.png


ВСТУПЛЕНИЕ

В каком году — рассчитывай,
В какой земле — угадывай,
На столбовой дороженьке
Сошлись семь мужиков:
Семь временнообязанных,
Подтянутой губернии,
Уезда Терпигорева,
Пустопорожней волости,
Из смежных деревень:
Заплатова, Дырявина,
Разутова, Знобишина.
Горелова, Неелова —
Неурожайка тож,
Сошлися — и заспорили:
Кому живется весело,
Вольготно на Руси?

Н.Некрасов

Пару месяцев назад на одном IT мероприятии мне довелось лицезреть в работе Pandas. Парень, который с ним работал не делал ничего особенно удивительного. Но простые сложения значений, вычисления средних, группировки проиводились так виртуозно, что, даже при всей своей предвзятости к Питону, я был очарован. Манипуляции выполнялись на довольно приличных датасетах по данным капитального ремонта за период кажется с 2004 по 2019 год. Сотни тысяч строк, но все работало очень быстро.
В общем когда мне еще через пару месяцев пришлось кое-что анализировать, я решил попробовать сделать это с помощью Pandas. Провозился пару дней с тем, что с помощью Excel я бы смог сделать за день. Тем не менее мне удалось.

С апреля мы все сидим на карантине. Сидел я и думал, что бы мне такое сделать, чтобы не очень сложное и чтобы стильно и модно было. К тому времени я уже видел кучу всякой инфографики про коронавирус, про пожары в лесу, про выборы. Делать то, что уже делали не хотелось, да и браться сразу за сложное не решался, сомневаясь, что смогу закончить. Тут мне попалась какая-то статья про уже отшумевшее явление «barchart race» или по-русски «гонки столбчатых диаграмм». Вы можете подумать, что эта статья будет про barchart race. Да, но только отчасти. Barchart race будет только в конце, а статья скорее о том, как не обладая, какими-то выдающимися способностями и знаниями в области матана и прочей черной магии, можно сделать анализ больших данных и представить результат в доступной для широких масс форме. Итак, поехали.


Идея

Сами «гонки» мне уже не раз попадались в ленте Твиттера, ино тогда я даже не знал, что это вот называется гонками. А узнал я об этом, когда прочитал очередную статью, возможно даже здесь на Хабре. Не помню точно как это было, помню только, что я сильно воодушевился возможностью сорвать покровы и стать первооткрывателем. Идея сделать свою гонку на базе форм бюджетной отчетности по исполнению федерального бюджета. Я довольно давно так или иначе касаюсь сферы госфинансов, но годного сравнительного анализа мне видеть не доводилось, что сильно увеличивало шансы сделать что-то новое. Взять за источник формы бюджетной отчетности круто по двум причинам:


  1. Эти бюрократические бумажки сильно формализованы, что делает их чуть ли не единственным государственным сырьем машиночитаемых данных.
  2. Отчетность делается годами с монотонной регулярностью, те кто эту отчетность готовит в стопятидесятый раз вообще не переживает, что в ней может быть что-то ценное. Поэтому отчетность в свободном доступе на сайте Казначейства и Минфина.
    План был такой:
    Взять как можно более длинный временной диапазон данных об исполнении федерального бюджета России. Посмотреть из каких показателей можно сделать красочную гонку ну и сделать эту гонку.


Коротко о бюджетной классификации

Как говорит наш президент: «Буду краток». Постараюсь только самую суть. Говорим только о бюджетных расходах. Все бюджетные расходы предусматриваются на какую-то конкретную цель. Эта запланированная цель отражается в коде бюджетной классификации.

КБК — 20-ти разрядный цифровой код, отражающий цель расходов, их экономический и функциональный характер. В теории, зная КБК каких-то определенных расходов, можно определить не только их цель, но и способ их расходования (например, заработная плата), отрасль экономики и функцию государства, к которой расход относится и, наконец, ведомство через которое эти расходы будут производится.

По-умному это все называется функциональная, экономическая и ведомственная классификация бюджетных расходов. Структура КБК постоянно меняется, более или менее неизменными являются первые 7 разрядов КБК, а имено первые 3 — код ППП (перечень прямых получателей) разряды 4, 5 — раздел, 6, 7 — подраздел. Остальные разряды КБК меняются так часто, что даже не заглядывая в данные, можно быть уверенным, что сопоставить расходы даже двух соседних лет будет почти невозможно.

Сейчас структура КБК расходов федерального бюджета выглядит так:
fadyjrlsbgjayme7y2b9cqbmjtk.png


  • Ведомственная классификация
    • ППП — код ведомства
  • Функциональная классификация
    • РЗ — раздел
    • ПР — подраздел
    • ЦСР — целевая статья расходов
      • ГП — госпрограмма
      • ПП — подпрограмма
      • ОМ — основное мероприятие
      • НП — направление расходов
  • Экономическая класссификация
    • ВР — вид расходов

Классификация представляет собой иерархическую структуру, чем глубже ты погружаешься, тем более конкретными становятся расходы.

Мой первоначальный план был в том, чтобы сгруппировать расходы на уровне раздела и подраздела. Я наивно полагал, что бюрократам будет лень выдумывать новые коды и они из года в год будут использовать одни и те же. Я был прав только отчасти. Почти неизменными являются только коды ведомств. Пока ведомство существует оно имеет один и тот же код, когда ведомство умирает, его код уже не присваивается новому ведомству. Всего из 3 десятичных разрядов можно получить 10 в третьей степени вариантов, то есть тысячу комбинаций. Сейчас максимальный код у Министерства спорта. Номерок блатной три семерочки под силу пробить не каждому министру. Тут код пробивал еще сам Мутко. В бытность Госкомспорта это ведомство носило скромный номер 164. Именно с приходом Мутко Минспорта получило счастливые три семерки.


Часть первая. Наивная лобовая атака.

Довольно быстро мне удалось собрать у Казначейства и Минфина различные формы бюджетной отчетности по исполнению бюджета. Мой выбор пал на отчет по форме ОКУД 0507011, отчетность по этой форме мне удалось найти с 2002 по 2019. Остальные формы отчета об исполнении бюджета начинались где-то с 2007 года.

Попытка сопоставить коды сразу по подразделам с треском провалилась, мне не удалось найти ни одного кода подраздела, который бы не изменил своего содержания с 2002 по 2019 год.

Например, код РЗПР 0501 в 2003 был кодом «Органы внутренних дел», а в 2006 уже «Жилищное хозяйство». Даже относительно стабильные коды в 01 разделе менялись. 0103 в 2002 году был «Функционирование исполнительных органов государственной власти», а в 2005 стал уже «Функционирование законодательных (представительных) органов государственной власти и местного самоуправления».

С разделами не получилось, но я был готов к такому повороту. Следующая попытка была сгруппироваться уровнем выше, по разделам, но и здесь меня ждал неприятный сюрприз. Раздел 02 успел побыть национальной обороной и судебной властью, а раздел 03 был и международной и правоохранительной деятельностью. Менялись не только коды, но и названия поэтому привязаться по названиям тоже не получалось. Такого поворота я не ждал, работа остановилась на несколько дней.
Поковыряв матчасть я наткнулся на очень интересные международные документы. Оказывается международное сообщество, в лице ООН, ОЭСР и МВФ озабоченные необходимостью сбора и анализа данных по государственным финансам разработали и в 1986 году издали эпохальный документ под названием A Manual on Government Finance Statistics (GFSM 1986). https://www.imf.org/external/pubs/ft/gfs/manual/1986/eng/index.htm
Труд оказался настолько удачным, что изменения в него вносились лишь дважды: в 2001 году и в 2014 году. Именно GFSM 2014 является действующей редакцией.

В составе этого мануала в 4 части есть глава «Functional classification», в которой сформулирована функциональная классификация государственных расходов. Называется она Classicifation of the Functions of Government или сокращенно COFOG. По-русски это назвали Классификацией функций органов государственного управления (КФОГУ). КФОГУ это достаточно общая, чтобы быть универсальной для любой страны иерархическая классификация. Коды КФОГУ состоят из 4 разрядов: первые 2 разряда — раздел, 3 разряд это глава и 4 разряд это класс. Судя по некоторому сходству и в нумерации и по содержанию российского КБК с международным КФОГУ, наш Минфин пытается приблизить КБК к международной классификации. Попытки продолжаются как минимум с 2002 года. Минфин с завидным упороством год, через год меняет содержание разделов и подразделов КБК, не говоря уже обо всем остальном.

В результате в России нет ни собственной функциональной классификации, ни международной.
В общем я решил привязать подразделы наших КБК к этой международной классификации COFOG, чтобы потом сгруппировать расходы именно по разделам COFOG.
Учитывая, что разделы и подразделы в российском КБК постоянно меняются, привязывать коды COFOG я решил не к кодам КБК, а к названиям. Для этого я собрал перечень всех уникальных названий подразделов отечественного КБК и сматчил их через самое короткое расстояние Левенштейна. Результат получился не очень точный, ошибки пришлось исправлять вручную. Пару дней я возился расставляя в таблице названий российских подразделов КБК правильные коды КФОГУ. Как же я обломался, когда стало понятно, что сделать это вообще невозможно, потому что в России по подразделу «Другие расходы» могут проходить и расходы на армию и на экологию, а по разделу 01 (Общие расходы) прохотят расходы из тематических разделов. Понять из каких именно тематических разделом эти расходы можно только, если каждый подраздел рассматривать отдельно по каждому ведомству, что кратно увеличивает количество. Но главное, что я уже начал сопоставление без кодов ведомств. Тут я совсем приуныл. Анализ расходов нашего государства на решение своих социально-экономических задач не удался. Снова приуныл.
Думаю: «Гори оно огнем, сделаю хоть что-то!»


Часть вторая. О том как что-то получилось.

Сделал я в итоге так. Собрал все расходы бюджета по ведомствам, сделал outer join всех лет по кодам ППП. Получилась таблица вида:
nniacmxk2duxx-m6nuimjjc8ijo.png

Дальше была довольно долгая и неприятная возня с названиями. В отчете об исполнении бюджета используются только полные наименования, поэтому даже у относительно коротких названий вроде Министерство спорта был хвост Российской Федерации. Просто менять Российской Федерации на РФ я не стал и названия вручную менял с официальные сокращенные. Пришлось посидеть, но зато я теперь я гораздо лучше ориентируюсь в российкой бюрократии. В период с 2002 по 2019 год существует или существовало 240 ведомств, там есть и отдельные бюджетные учреждения вроде Большого театра или Эрмитажа, но в основном там ведомства. Российская бюрократия каждый год представлена в ассортименте около 95 наименований. Если с 2002 их было 240, то каждый год за 17 лет менялось примерно 8 ведомств. Из вышепредставленной таблицы гонка получилась довольно быстро.

Посмотреть ее можете по ссылке:


Или в виде html тут:
https://vneberu.ru/

Видео с ведомственной группировкой получилось хорошим, но Минфин, который выполняет функции передаста по почти всем межбюджетным трансфертам просто затмевает всех остальных. А что же там у него внутри увидеть на уровне ведомства невозможно. Поэтому я отдохнул немного и снова взялся за дело.


Часть третья. Кому на нашем бюджете живется лучше других?

Пока я разбирался в хитросплетениях российского КБК я заметил 1 интересную особенность. Почти во всех ведомствах присутствовали расходы по 01 разделу, а этот раздел наиболее стабильный Владимир Путин с расходами на себя занимает в нем гордое первое место. Расходы на Президента всегда проходят по подразделу 0101.

За 17 лет его название менялось все однажды, а по содержанию это всегда расходы на Президента. Я подумал, что раз Минфин так любит проводить расходы на содержание самих ведомств по 01 разделу, то я смогу оценить т.н. «Затраты на руководство и управление» государственных функций просто сгруппировав подразделы по разделу 01. Впоследствии оказалось, что расходы на содержание ведомств проходят по 01, но так происходит далеко не всегда. Очень часто расходы проходят так, как это предусмотрено GFSM 2014, а именно расходы на управление какой-то функцией проходят по соответствующему тематическому разделу. Например, расходы на руководство полицией классифицируются по коду 70310 Руководство деятельностью полиции и связанными с ней услугами, включая регистрацию иностранцев, выдачу рабочих и проездных документов иммигрантам, ведение документации на арестованных и связанной с работой полиции статистики, регулирование дорожного движения и контроль за ним, предупреждение контрабандной деятельности и контроль за морским и океаническим рыболовным промыслом;

В отдельные годы расходы на управление проходили по 01 разделу, а в другие по какому-то тематическому. Я снова застрял в начале пути. Приходилось бросать сделанное и придумывать что-то другое. Была нужна какая-то более изощренная модель группировки. К тому времени я уже собрал для себя некоторый набор полезных функций для Pandas, которые постоянно использовал в работе. Я уже упомянал про levenshtein_merge (). С помощью этой функции можно сделать join двух Pandas DataFrame через нечеткое соответствие, а именно чере самое короткое расстояние Левенштейна. Функций становилось больше, я решил собрать их в библиотеку. Когда я начал свой бюджетный квест, это был просто класс в том же файле, что и основной проект, но сейчас это вполне годная самостоятельная библиотека, которую можно поставить через pip install и сильно облегчить себе жизнь при работе с Pandas DataFrame. Библиотеку я назвал полуфабрикат, что в переводе на английский будет prepack.
Сейчас без четверти 2 и я уже немного утомился, поэтому постараюсь продолжить статью в формате комментариев кода jupyter notebook в котором я и проводил все манипуляции.

# Сначала импорт нужных модулей
import pandas as pd
import numpy as np
import os, sys

# это мой модуль полуфабрикат
from prepack import prepack as pp
# Это настройки отображения данных в блокноте
pd.options.display.max_rows = 2000
pd.options.display.max_columns = 200
pd.options.display.max_colwidth = 500
pd.options.display.min_rows = 40

А вот здесь начинает работать полуфабрикат prepack. Этой функцией считывается zip архив. Содержимое архива возвращается в виде перечня имен файлов из архива и file-like объектов,
которые можно обрабатывать дальше. К примеру через pd.read_excel (), если в архиве файлы excel.

names, files = pp.read_zip("raw_data.zip") 

Эта простая функция отличается от pd.read_excel () только тем, что меняет параметры открытия excel по умолчанию
Мой read_excel читает весь excel как текст, и не пытается взять заголовки колонок из первой строки.
Я открываю начало файла, чтобы правильно задать параметры для массового чтения всех файлов в архиве. По структуре файлы
не отличаются. Я задаю параметры для одного файла, а подходят они и ко всем остальным.

pp.read_excel(files[0]).iloc[8:13,:]

Эта функцию я очень люблю, она сильно экономит время. Делает она вот что. Читает сразу несколько файлов excel, соединяя их
в 1 большой DataFrame, сразу задавая правильные заголовки и убирая мусор из файла, оставляя только данные.
Функции нужно минимум 3 первых аргумента: files, columns, fltr. Первый аргумент это список excel файлов, второй аргумент — горизонтальные границы данных в файле, нужно указать первый и последний столбец с данными. Третий аргумент — фильтры, которые
будут применены ко всем файлам, чтобы убрать пустые строки в конце и заголовки в начале каждого файла файла. 4 аргумент
опциональный. С его помощью можно укзаать в каких столбах первого файла есть заголовки.

Изучив первый файл, который я открыл выше можно видеть, что данные с 0 по 11 столбец, а данные только в тех строках, где
в первом столбце текст, а во втором число. Этих двух фильтров достаточно, чтобы убрать весь мусок. Заголовки у нас 3 столбцах:
8,9,10

# считывает все наши данные в 1 большой DataFrame
df = pp.parse_excels(files, columns=[0,12], fltr={0: 'istext',1: 'isnum'}, header=[8,9,10])

#смотрим, что получилось
df.head(5)

Еще 1 полезная функция из библиотеки, это обертка для pickle, начиная с версии prepack 0.4.2 умеет читать и писать в pkl.gz

# сохраняем в файл
df.to_csv('raw_data.csv.gz')
pp.save(df, 'raw_data.pkl.gz')

Одна из основных функций prepack. С ее помощью можно фильтровать DataFrame, получаяя вектор с булевой маской
Первым аргументом задается сам фильтруемый DataFrame, второым задается фильтр, а третий аргумент определяет, будут
ли использованы названия столбцов или их индексы. Сейчас я адресуюсь по индексам, поэтому 3 аргумент iloc=True.

Первое что нужно сделать это получить список всех названий ведомств, для этого я получу из массива строки, где
заполнено «наименоавние показателя» и «код ППП», остальные части КБК должны быть пустыми. Именно в этих строках записаны
названия ведомств

# тут будут только названия получателей и их коды ППП
f = pp.df_filter_and(df, {0: 'istext', 1: 'isnum', 2: 'isblank', 3: 'isblank'}, iloc=True)
ppp = df[f]
idx = ppp['Наименование показателя'].index
ppp = ppp.loc[idx,:]

Теперь я сделаю функции, которые позволят мне вырезать из DataFrame нужные строки по фильтру, а с полученными строками делать небольшой постпроцессинг.

#эта функция позволяет вырезать строки из фрейма по фильтру, сохраняя вырезанное отдельно
def df_filter(df, fltr):
    f = pp.df_filter_and(df, fltr)
    res = df[f]
    df.drop(res.index, axis=0, inplace=True)
    return res

Этой функцией я буду готовить наборы критериев, по которым буду выбирать из общего DataFrame нужные строки
На входе общих датафрейм, фильтр для фильтрации нужных названий ведомств, чтобы записать их коды ППП. Дальше передается
список подразделов КБК, которые нужно брать в расчет. Последний аргумент label задает дополнительный критерий для
поиска по целевым статьям. Это нужно для того, чтобы выбрать такие расходы на содержание ведомства, которые только часть
подраздела, что не позволяет включать такой родительский подраздел полностью.

def df_filter_post_proc(df, fltr, rzpr, label):
    df_ = df_filter(df, fltr)
    codes = df_['Код по бюджетной классификации ППП'].unique()
    names = df_['Наименование показателя'].unique()
    return {'name': list(names), 'ppp': list(codes), 'rzpr': list(rzpr), 'label': list(label)}

Здесь функция, в которой происходит основная работа. Получилось довольно громоздко, но оптимизировать не стану, все равно функция только на 1 раз. Делает она вот что. Сначала получаем срез массива df по кодам ППП у группы и кодам подразделов. Потому получаем срез по тем же заданным кодам ППП, но уже те строки, где указаны целевые статьи, ищем нужные целевые статьи по их характерным названиям. Дальше можно было бы просто сложить эти срезы и отправить в результат по группе для последующего сложения сумм. Но ведь у нас иерархическая структура. Если найденная целевая статья будет из найденного подраздела, то сумма по этой целевой статье посчитается дважды: 1 раз по строке самой целевой статьи, а второй раз в составе суммы по подразделу. Поэтому нужно найти пересечение двух этих срезов. В случае если есть пересечение по коду ППП и подразделу, тогда из второго среза такую строку убираем.

То что останется во втором срезе вместе с первым срезом записываем в результат для этой группы и переходим к следующей группе.

# основная функция в которой и проиходит выборка нужных строк для каждой нужной нам функции
def groups_fill(df, groups):
    res = pd.DataFrame([])
    for g in groups:
        el = groups[g]

        ppp = el['ppp']
        rzpr = el['rzpr']
        label = el['label']

        #Если кодов ppp нет, то искать нечего - пропускаем цикл
        if len(ppp) == 0:
            continue

        #тут 1 фильтр по кодам
        f1 = {'Код по бюджетной классификации ППП': ppp}
        #тут 2 фильтр по тем подразделам, которые можно считать расходами на управление
        f2 = {'Коды ФКР РзПр': rzpr}
        #тут 3 фильтр, что убрать лишние строки с кодами статей
        f3 = {'Коды ЦСР ЦСР': 'isblank'}
        #тут 3 фильтр, что убрать лишние строки с видами расходов
        f4 = {'Коды ВР ВР': 'isblank'}

        #маски по фильтрам
        m1 = pp.df_filter_or(df, f1)
        m2 = pp.df_filter_or(df, f2)
        m3 = pp.df_filter_or(df, f3)
        m4 = pp.df_filter_or(df, f4)
        #соединим все маски в одну через логическое и
        df1 = df[m1 & m2 & m3 & m4]

        #Второй срез будет по кодам ppp, кодам ВР и label (наименованию показателя)
        #по кодам ppp и кодам ВР маски уже есть
        df_ = df[m1 & m4]
        f5 = {'Наименование показателя': label}
        m5 = pp.df_filter_or(df_, f5)
        df2 = df_[m5].copy()

        #для исключения дублирования двух срезов надо убрать те позиции в label (df2), раздел/подраздел которых
        #совпадает с заданными разделами/подразделами в rzpr (df1). Это делается потому что если label указан для ЦСР,
        #а РЗ/ПР совпадает, то общая сумма по РЗ/ПР уже включает в себя сумму, которая указана в строке с ЦСР.
        #пересечение ищем только для совпадений сразу по 3 параметрам: ППП, РЗПР, год (src_filename)

        #сначала найдем пересечение df1 и df2
        #нам нужны будут индексы из df2, поэтому сохраним их
        df2['idx'] = df2.index
        intersect = pd.merge(df1.loc[:,['Код по бюджетной классификации ППП','Коды ФКР РзПр', 'src_filename']],
                             df2.loc[:,['Код по бюджетной классификации ППП','Коды ФКР РзПр', 'src_filename','idx']],
                             on=['Код по бюджетной классификации ППП','Коды ФКР РзПр', 'src_filename'], how='inner')

        #удалим найденные строки из df2 по индексу
        df2_filtered = df2.drop(intersect['idx'], axis=0)

        #теперь то, что есть в df1 и df2 соединяем в 1 срез, через копирование данных
        df3 = pd.concat([df1, df2_filtered], axis=0)
        v = df3[['Наименование показателя',
                 'Код по бюджетной классификации ППП',
                 'Коды ФКР РзПр',
                 '(тыс. руб.) Исполнено',
                 'src_filename']].copy()
        #добавим столбец имя
        v['name'] = g
        #добавим все в итоговый датафрейм
        res = pd.concat([res, v], axis=0)
    return res

Теперь зададим сами группы и критерии для включения записей в них.
Концепция работы следущая:


  1. для каждой группы формируем список ведомств, входящих в эту группу.
  2. Дальше выбираем там расходы из подразделов с 0101 по 0107 в этих подразделах в период с 2002 по 2019 только расходы на содержание ведомств.
  3. Задаем ключевые слова для поиска целевых статей, которые помимо подразделов с 0101 по 0107 будут включены в расходы. В основном это будут общие ключевые слова, но для некоторых групп задаются дополнительные.

lst = {}

#это стандартные подразделы для расходов на собственное содержание ОИВ
rzpr_base = ['0101','0102','0103','0104','0105','0106','0107']

#Это стандартная ЦСР для расходов на содержание
label_base = ['Руководство и управление в сфере установленных функций',
              r'contains=.*(?:Обеспечение.*аппарата).*',
              r'contains=.*(?:обеспечение функций государственных органов).*',
              r'contains=.*(?:обеспечение функций органов.*власти).*'
             ]

# Тут идут сами группы и заданные для них параметры для фильтрации
f = {'Наименование показателя': r'contains=.*(?:избирательная комиссия).*'}
rzpr = ['isblank']
df_slice = df_filter_post_proc(ppp, f, rzpr, label_base)
lst['выборы'] = df_slice

rzpr = ['0101','0102','0103','0104','0105','0106','0107','0201']
f = {'Наименование показателя': r'contains=.*(?:армии|оборон|внешней разведки|боеприпас|вооружен|военн).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['армия'] = df_slice

rzpr = ['0101','0102','0103','0104','0105','0106','0107','0201']
f = {'Наименование показателя': r'contains=.*(?:чрезвычайн|медико-биологическ|государственным резервам).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['ГО ЧС'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:надзор|контрол|инспекц).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['надзор и контроль'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:космос|космическ).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['космос'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:государственная дума|совет федерации).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['федеральное собрание'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:счетная палата).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['счетная палата'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:культур|художеств|зодчеств|живопис|большой театр|эрмитаж|фонд кинофильмов).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['культура'] = df_slice 

f = {'Наименование показателя': r'contains=.*(?:наук|научн|фундаментальных исследований|курчатовский институт|образован|просвещения|университет).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['наука и образование'] = df_slice 

f = {'Наименование показателя': r'contains=.*(?:налог).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['налоги'] =df_slice

f = {'Наименование показателя': r'contains=.*(?:имуществ).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['имущество'] =df_slice

f = {'Наименование показателя': r'contains=.*(?:финансов|казначейство|ценных бумаг|финансовых).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['финансы'] =df_slice

f = {'Наименование показателя': r'contains=.*(?:метролог|стандартизац|статистик|архив).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['статистика, стандартизация и архив'] =df_slice

f = {'Наименование показателя': r'contains=.*(?:аккредитац|интеллектуальной собственности|алкогольного рынка|тариф|патент|лицензирован|антимонопольн|экономик|экономическими зонами|экономического развития|экономических реформ|содействия развитию|экономической конъюнктуры).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['экономика, тарифы, патенты, лицензии'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:таможн|таможенная).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['таможня'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:кадастр|картограф|регистрацион).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['кадастр и картография'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:судостроен|промышленности|энергетик|энергетическая|атомной).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['промышленность и энергетика'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:транспорт|навигацион|дорожн).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['транспорт'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:телеграф|связи|коммуникац|информационным технолог|системам управления|фельдъегер).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['связь'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:правительство).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['правительство'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:президент).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['президент'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:прокуратур|следственный комитет|внутренних дел|миграцион|национальной гвардии|служба безопасности|служба охраны).*'}
label = label_base.copy()
label = label + ['Органы прокуратуры','Военная реформа']
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['прокуратура, полиция и спецслужбы'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:конституционный|верховный суд|арбитражный суд|судебный департамент).*'}
label = label_base.copy()
label = label + ['Судебная власть']
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label)
lst['суды'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:пристав).*'}
label = label_base.copy()
label = label + [r'contains=.*судебных приставов.*']
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label)
lst['судебные приставы'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:юстиции|исполнения наказаний).*'}
label = label_base.copy()
label = label + ['Уголовно-исполнительная система']
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label)
lst['тюрьмы'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:сельск|рыболовств).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['сельское хозяйство'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:водных ресурсов|недропользован|природных|экологии|лесного хозяйства|метеоролог).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['экология, вода, недра, метеорология'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:жилищн|регионального развития|строительств).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['ЖКХ'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:здравохранения|здравоохранен|медицинс|социальной защиты|социального развития).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['здравохранение и соцзащита'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:спорт).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['спорт'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:труд[ау]|занятост).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['занятость'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:пенсионный фонд|фонд социального страхования).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['пенсионный фонд и фонд соцстрахования'] = df_slice

f = {'Наименование показателя': r'contains=.*(?:иностранных дел).*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['пенсионный фонд и фонд соцстрахования'] = df_slice

#прочие министерства агентства и службы
f = {'Наименование показателя': r'contains=.*министерство.*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['прочие министерства'] = df_slice

f = {'Наименование показателя': r'contains=.*агентств.*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['прочие агентства'] = df_slice

f = {'Наименование показателя': r'contains=.*служба.*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['прочие службы'] = df_slice

# все что осталось
f = {'Наименование показателя': r'contains=.*'}
df_slice = df_filter_post_proc(ppp, f, rzpr_base, label_base)
lst['прочие'] = df_slice

Когда группы и фильтр в них настроены, проверим, что получилось.

# посмотрим, сколько ведомств получилось по каждой группе
for group in lst:
    el = lst[group]
    print(len(el['name']), group, 'ppp:', '|'.join(el['ppp']))
print('осталось', len(ppp))
1 выборы ppp: 308
11 армия ppp: 125|126|177|464|722|184|187|186|721|185
3 ГО ЧС ppp: 160|171|388
23 надзор и контроль ppp: 083|316|497|204|048|059|060|077|079|081|093|106|141|151|498|587|086|085|087|096
3 космос ppp: 258|259|730
2 федеральное собрание ppp: 330|333
1 счетная палата ppp: 305
20 культура ppp: 056|164|166|409|424|425|591|597|057|058|167|175|054
35 наука и образование ppp: 075|139|190|226|319|386|401|423|486|494|573|589|677|693|073|074|144|385|595|384|007
3 налоги ppp: 181|205|182
3 имущество ppp: 163|166|167
7 финансы ppp: 092|520|720|723|521|724|100
6 статистика, стандартизация и архив ppp: 149|154|159|155|157|172
13 экономика, тарифы, патенты, лицензии ppp: 140|162|165|197|263|161|168|307|142|139|160
1 таможня ppp: 153
6 кадастр и картография ppp: 071|156|072|173|321
10 промышленность и энергетика ppp: 020|099|129|306|021|023|101|143|022|725
7 транспорт ppp: 029|104|103|107|108|109|110|179
12 связь ppp: 078|089|128|134|397|201|070|084|088|135|071
0 правительство ppp: 
6 президент ppp: 206|303|352|588|304
10 прокуратура, полиция и спецслужбы ppp: 153|188|415|189|202|192|416|417|180
4 суды ppp: 434|436|437|438
1 судебные приставы ppp: 322
2 тюрьмы ppp: 318|320
4 сельское хозяйство ppp: 076|082|080|085
7 экология, вода, недра, метеорология ppp: 050|158|049|052|053|169|051
6 ЖКХ ppp: 133|279|132|309|360|069
7 здравохранение и соцзащита ppp: 054|148|387|055|061|064|056|149
2 спорт ppp: 777
1 занятость ppp: 150
1 пенсионный фонд и фонд соцстрахования ppp: 310
5 прочие министерства ppp: 022|350|340|370
5 прочие агентства ppp: 174|091|095|260|380
0 прочие службы ppp: 
3 прочие ppp: 152|302|090
осталось 0
# посмотрим сколько ведомств не попалу ни в одну из тематических групп, но попало сюда
lst['прочие агентства']
{'name': ['Федеральное агентство по туризму',
  'Федеральное агентство по делам молодежи',
  'Федеральное агентство по делам Содружества Независимых Государств, соотечественников, проживающих за рубежом, и по международному гуманитарному сотрудничеству',
  'Федеральное агентство по обустройству государственной границы Российской Федерации',
  'Федеральное агентство по делам национальностей'],
 'ppp': ['174', '091', '095', '260', '380'],
 'rzpr': ['0101', '0102', '0103', '0104', '0105', '0106', '0107'],
 'label': ['Руководство и управление в сфере установленных функций',
  'contains=.*(?:Обеспечение.*аппарата).*',
  'contains=.*(?:обеспечение функций государственных органов).*',
  'contains=.*(?:обеспечение функций органов.*власти).*']}

Теперь запустим основную функцию, которая соберет данные по созданным группам.

# запустим основную функцию, передав ей в качестве аргументов список с группами и настройками фильтров к ним
df2 = groups_fill(df,lst)
# Смотрим, что получилось
df2

2970 rows × 6 columns

# тут мы поменяем тип данных в столбце с суммой
df3 = df2.loc[:,['(тыс. руб.) Исполнено', 'src_filename', 'name']]
#новые названия столбцов
df3.columns = ['value', 'year', 'name']

#индекс тех строк, где пусто
idx = df3[df3.loc[:,'value'] == ''].index

#меняем на 0
df3.loc[idx, 'value'] = '0.0'

#ставим тип float64
df3 = df3.astype({'value': 'float64'})
# группируем данные по году и названию группы
df4 = df3.groupby(['year','name']).sum().reset_index()
#функция для создания таблицы с группами и расходами по годам
def manuscript(df):
    res = pd.DataFrame([])
    for i in df.index:
        n = df.loc[i,'name']
        y = df.loc[i,'year']
        v = df.loc[i,'value']
        res.loc[n,y] = v
    res['name'] = res.index
    #так я перемещу последний столбец в начало
    last_idx = res.shape[1] - 1
    order = [last_idx]
    order = order + list(range(0, last_idx))
    res = res.iloc[:,order]
    return res
# запускаем функцию и смотрим результат ее работы
df5 = manuscript(df4)
df5
# Эта функция подготовит данные для их загрузки d3.js скрипту для визуализации гонок
def prep_data(df):
    lst = []
    size = df.shape[0]
    for i in range(0, size):
        row = df.iloc[i,:]
        name = row['name']

        #skip first column
        row_ = row[1:]
        for k, y in enumerate(row_.index):
            begin = float(row_[k]) # current year
            try:
                end = float(row_[k + 1]) # next year
            except:
                end = float(row_[k]) 

            range_ = end - begin
            step = range_ / 10

            cur = begin
            for n in range(0,10):
                last = cur
                cur = begin + step * (n+1)
                if cur < 0:
                    cur = 0.0
                lst.append({'name': name,
                            'value': round(cur, 2),
                            'year': float(str(y) + '.' + str(n)),
                            'lastValue': round(last, 2),
                            'rank': 0})

    df2 = pd.DataFrame(lst)
    df2 = df2.sort_values(by=['year','value'])
    df2.reset_index(drop=True, inplace = True)
    df2['rank'] = range(1,df2.shape[0]+1)
    return df2
# делаем данные
data = prep_data(df5)
data

6120 rows × 5 columns

# сохраняем все в csv для загрузки в d3.js
data.to_csv('data_groups.csv', index=False)

Смотрим полученные гонки. Результаты гонки говорят сами за себя.

Тут в html:
https://vneberu.ru/barchart-race2.html

Тут видео на youtube:


Спасибо за внимание, добро пожаловать в комментарии.


ССЫЛКИ

Библиотеку prepack можно посмотреть тут: https://github.com/legale/prepack
Собранные бандлы с гонками можно скачать по ссылкам выше, но можно тут: https://github.com/legale/d3.js-portable


© Habrahabr.ru