Дискложес  ☰      

декларации о доходах

Дискложес     Автоматизированный сбор деклараций о доходах

Переиспользование старых идентификаторов в базе disclosures.ru (наборы эвристик)

Алексей Сокирко, sokirko@yandex.ru, 25 марта 2021 года

Введение

База disclosures.ru содержит информацию о 5 млн. деклараций доходов чиновников. В базе есть два основных класса объектов:

1. Сами декларации (содержат информацию о доходе, недвижимости и автомобилях чиновника и его супруги за какой-то год). Декларации мы еще называем секциями.

2. Чиновники — это созданные автоматически кластера деклараций, которые с большой вероятностью относятся к одному человеку.

В программном коде мы называем кластеризацию деклараций термином «дедупликация» (историческое название). Основная задача дедупликации — правдоподобно объединить декларации одного человека в один кластер с помощью методов машинного обучения. Одна секция должна быть отнесена только к одному кластеру. Кластер может состоять только из одной секции или из многих. У одной секции в sql-базе есть идентификатор section_id (первичный ключ в sql-таблице Section). Идентификатор кластера мы обозначаем через person_id (это первичный ключ в sql-таблице Person). Наша задача поддерживать идентификаторы person_id и section_id как можно более постоянными между перестройками базы, поскольку person_id используется в построении веб-ссылок. Идентификатор section_id тоже можно использовать в веб-ссылках, но главное, что от постоянства section_id зависит постоянство person_id. Постоянные ссылки называют еще «пермалинками». Если в базе идентификаторы будут порождаться каждый раз снова, тогда на страницу с информаций о человеке нельзя будет сослаться с других веб-страниц. Каждый раз при перестройке базы у нас есть старая база, в которой есть свой набор person_id и section_id, надо постараться их сохранить.

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

Пермалинки файлов

Для файлов (исходных документов) мы считаем хеш-код sha256 от содержимого файла. Возникновение разных файлов с одинаковым sha256 маловероятно. Если я правильно понимаю, до сих пор найдено только несколько десятков коллизий для этой хеш-функции.

Гораздо важнее, что около четвери всех файлов записаны в формате html, который был скачен с официальных сайтов. При этом, когда мы попробуем снова скачать эти же файлы через неделю-месяц, файлы, скорее всего, изменятся из-за изменения в дизайне, поскольку в html часто содержится меню сайта, элементы оформления и прочее. Если изменится хотя бы один байт, изменится sha256 и мы будем считать, что перед нами новый файл. Этот файл, действительно, может содержать новую информацию (являться уточнением старого файла), поэтому мы обязаны проанализировать все секции файла, прежде чем объявить, что это полный дубликат. На текущий момент важно понимать, что большое количество входных файлов в базе не является признаком того, что все они содержат новую информацию.

Пермалинки секций

Загрузка секций происходит по офисам (ведомствам). При этом входные файлы одного офиса отсортированы по времени добавления этих сайтов в базу, чтобы сначала были загружены старые файлы, а потом новые. Для входной секции порождается «паспорт», где паспорт — это набор главных характеристик секции. Мы предполагаем, что в базе не могут быть секции с одинаковыми паспортами. Если в старой базе была секция с таким же паспортом, мы будем использовать её идентификатор (section_id). Сейчас в паспорт секции входят следующие характеристики:

office_id (идентификатор офиса);

year (год декларации);

person_name (имя человека);

income_sum (сумма доходов декларанта и супруги);

square_sum (сумма площадей недвижимости);

vehicle_count (число машин).

Для других задач, кроме основного импорта секций, мы можем использовать другие паспорта (более слабые) или даже много паспортов.

Копирование person_id из базы Декларатора

Часть кластеризации уже произведена руками в базе Декларатора, мы должны скопировать эту часть кластеризации в базу disclosures.ru (делается в copy_person_id.py). Кроме ручной кластеризации, в Деклараторе работает еще и автоматическая дедупликация, но ее результаты мы должны игнорировать. На этом же этапе мы можем установить ссылки от объектов базы disclosures.ru к объектам базы Декларатора. Как и при импорте секций, мы сравниваем секции из Декларатора и disclosures.ru по паспортам, которые состоят из трёх элементов:

идентификатор документа;

ФИО человека;

доход человека.

либо по более общему паспорту:

идентификатор документа;

фамилия человека;

доход человека

В отличие от импорта секций здесь мы можем использовать идентификатор документа, из которого была взята эта секций, поскольку все документы Декларатора входят в базу disclosures.ru. Документ обычно связан с конкретным офисом и годом подачи декларации. В самом начале процесса порождаются все возможные паспорта. Если какой-либо паспорт является неоднозначным (указывает на две разные секции), такой паспорт не используется. Если в Деклараторе найдена секция, для которой руками был проставлен person_id, мы проставляем («копируем») этот person_id в базу деклараций. На данный момент таким способом создаются кластера только для 4%, но в эти 4% входят самые большие чиновники России.

Дедупликация

Процесс дедупликации не описан в этом документе. Здесь нас интересуют эвристики для создания пермалинков. На вход ему подаются секции и кластера, уже построенные в базе Декларатор (см. выше copy_person_id.py). На выходе каждой секции нужно приписать один номер кластера (person_id). Дедупликация построена на машинном обучении, поэтому ее результаты не стабильны. Имея старую базу, надо сделать так, чтобы person_id не менялся «без причины». Формального определения термина «без причины» у нас нет, есть зато примеры. Например, если в старой базе был кластер из пяти секций, а в новой базе из этого кластера «выпала» одна секция, лучше сохранить тот же person_id у нового кластера из 4 секций. С другой стороны, если был кластер из двух секций и он развалился на две части, тогда лучше переиспользовать старый person_id, но приписать его можно любому из двух односекционных кластеров, для другого придётся использовать новый идентификатор. Можно ли это обобщить? Пусть есть новый кластер, рассмотрим следующие случаи:

1. Если в кластере есть секции, которые уже были приписаны какому-то кластеру из Декларатора, проставим его person_id остальным секции, никаких новых person_id создавать не будем.

2. Та же ситуация, что и выше, но в кластере есть два или более person_id, которые пришли из Декларатора. В таком случае автоматическая дедупликация предлагает объединить два кластера, сделанных руками. Делать это без дополнительной проверки нельзя, поэтому эти секции останутся без person_id (не будут кластеризованы).

3. В кластере нет секций, которым проставлены person_id из Декларатора. В таком случаем мы точно должны создавать новую запись в таблице Person, но идентификатор person_id может быть взять из старой базы или создан новый.

Процесс заимствования person_id напоминает определение наследника после смерти короля. У нас было несколько варианты этой процедуры наследования. Ниже мы опишем все три, чтобы было, зачем это все происходит:

1. Самое простое правило может быть таким. Пусть есть новый кластер из k секций, ищем кластер в старой базе с таким же набором секций. Если такой есть, берем его person_id, если нет, создаем новый person_id. Это правило очень жёсткое. Каждый год, когда чиновники будут публиковать новые декларации, person_id будет меняться, что, конечно, не является допустимым.

2. Рассмотрим более мягкий вариант. Возьмем все секции нового кластера и посмотрим, какой у них был person_id в старой базе. Пусть person_idbest — это старый идентификатор, которые в стречается чаще всего в новом кластере, пусть Cnew — количество секций в новом кластере, которые были приписаны person_idbest . Пуcть Cold - размер кластера person_idbest в старой базе. Если 2 * Cnew > Cold, тогда больше половины секций старого кластера оказалось в новом кластере. Это означает, что у нас не будет другого такого нового кластера, который больше будет похож на старый person_idbest. Значит мы можем использовать person_idbest для этого кластера. Это правило лучше, но, например, если кластер разбился на две одинаковые части, никакая часть не получит старого person_id.

3. Максимально мягкий вариант, который мы пока можем придумать, выглядит так. Мы идём по всем старым кластерам. Для каждого старого кластера мы берём новые кластера, в которые вошли секции из старого кластера. Упорядочиваем новые кластеры по количеству представителей из старого кластера. Чем больше, тем лучше. Если количество одинаковое, упорядочиваем еще по максимальному номеру секции (это технический момент нужен для стабильности сортировки). Выбираем кластер, где больше всего представителей старого кластера. Этот способ сохранит много старых person_id, но не все. Например, старый кластер теперь может пропасть, если пропали все секции этого кластера (например, мы улучшили алгоритм первоначального импорта и все секции старого кластера не прошли по сравнению паспортов). Кроме этого, может случиться следующее. Старый кластер разбился на мелкие кусочки (например, по одной секции) и разошёлся в другие кластера, где были выбраны другие (старые) person_id. Проблема этого метода в том, что нам необходимо получить «все старые кластера» в одном списке. В программной реализации нет такого списка, поскольку программа сначала делит всю базу на части по «минимальному ФИО», где минимальное ФИО — это фамилия и инициалы (например, Иванов И. И.). Реальная кластеризация с машинным обучением происходит внутри всех секций с одинаковым минимальным ФИО.

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

Метрики

Для проверки постоянства пермалинков кластеров (основные объекты индексирования для поисковых систем) у нас есть одно ручное множество на 76 person_id ФИО (data/external_links.json). Для этих person_id мы каждый раз должны проверять, что person_id сохранился и у него похожее ФИО.

Кроме этого, мы проверям процент person_id, которые были переиспользованы в новой базе (scripts/check_person_id_permanence.py).