Как стать автором
Обновить

Фальсифицируем выборы в Государственную Думу 2021 года, а потом отменяем фальсификации с помощью машинного обучения

Время на прочтение17 мин
Количество просмотров14K

Предыдущая статья на тему выборов в государственную думу: «Восстанавливаем результаты выборов 2021 с помощью машинного обучения» вызвала интерес. Вместе с тем к статье было много критических комментариев. В некоторых из этих комментариев были подняты спорные вопросы, которые требовали дополнительных исследований. В целом было очень полезно обсудить результаты предыдущей работы с аудиторией Хабра. Поэтому я призываю оставлять читателей комментарии и под этой статьей.

Данное исследование основано на работах, выполненных электоральным аналитиком Сергеем Шпилькиным. Рекомендую посмотреть видео, где он подробно объясняет свою методологию:

Хотя это исследование и использует данные о выборах в государственную думу, эта статья не о политике. Эта статья о работе с открытыми данными. Она демонстрирует, как используя современные и доступные инструменты, такие как python, matplotlib, scikit-learn можно получить дополнительную информацию из этих данных. В данном исследовании используется примитивная модель и малое количество признаков, скорее для демонстрации подхода, чем для каких-либо громких заявлений. Весь исходный код и данные размещены на GitHub. Если у вас после прочтения статьи появятся новые идеи или замечания к коду, буду рад, если вы улучшите его с помощью своего кода. Или можете начать свой собственный проект на его основе.

Для начала опишем некоторые основные принципы проведения голосования в России, важные в контексте данной статьи. Выборы в нашей стране организует не государство, а независимые избирательные комиссии. Комиссии эти формируются из людей, которые имеют разнонаправленные интересы. Всего на выборах федерального уровня формируется примерно 96000 избирательных участков и соответственно комиссий. На участках среднего размера могут проголосовать 1000-2000 человек. В условиях города это 5-10 многоэтажных домов, расположенных вблизи здания, в котором организуют выборы. Вообще говоря, практически любой гражданин России может войти в состав комиссии и организовать выборы, например, по месту своего жительства.

Чьи интересы могут представлять члены избирательных комиссий? Во-первых, интересы различных политических партий, которые участвуют в выборах. Так, например в большом количестве комиссий можно обнаружить представителей двух крупнейших партий России на данный момент – «Единая Россия» и «КПРФ». Кроме того, там могут присутствовать и просто граждане, которые считают важной задачей организацию выборов высокого качества и готовые для этого пожертвовать существенным количеством личного времени. Существуют некоммерческие организации, которые пытаются координировать деятельность таких граждан. Например, общероссийское общественное движение в защиту прав избирателей "Голос", которое было признано иностранным агентом накануне выборов, в августе 2021 года.

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

Однако, чтобы группа людей покрыла своими представителями все участки, необходимо по крайней мере по три человека на участок. В условиях пандемии, сроки проведения голосования, были увеличены до трех дней, что фактически утроило количество необходимых человеко-часов.  То есть каждую из сторон должны были представлять по 300000 человек, которые должны работать примерно по 30 часов каждый. То есть это около 9 млн человеко-часов. При средней зарплате в России в 350 рублей в час, в рублях это более 3-х миллиардов рублей. Ресурсы групп людей, которые организуют выборы не равны. Существует огромный перекос в сторону партии власти. Это понятно, даже если взглянуть на официальный бюджет партии Единая Россия за 2020 год. 9,6 млрд рублей у Единой России против 1,3 млрд у КПРФ. Только партия Единая Россия обладает достаточным бюджетом, чтобы покрыть все участки достаточным количеством своих представителей.  Таким образом существуют участки, где выборы организуют представители только одной из заинтересованных сторон. Не удивительно, что на таких участках качество регистрации результатов может существенно снижаться.

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

Другим решением проблемы могло бы стать электронное голосование в регионах страны с низкой плотностью населения. Как показало предыдущее исследование "Почему на удалении от крупных городов избиратели ходят на участки охотнее и голосуют за партию власти" именно в отдаленных от крупных городов районах качество регистрации результатов голосования снижается. Очень жаль, что реализация электронного голосования на последних выборах сильно дискредитировала саму идею такого проведения голосования. Фактически в данный момент электронное голосование и представляет из себя участки, в которых выборы организуют исключительно представители партии власти. Чтобы электронное голосование было сбалансированным это должен быть проект с открытым исходным кодом, в который могут вносить изменения все заинтересованный стороны. И, прежде чем его тестировать, нужно чтобы между сторонами был консенсус по поводу того, что работа этого кода не создает преимущества ни одной из сторон. Кроме того, код должен исполняться на вычислительных мощностях, подконтрольных каждой из заинтересованных сторон, а не на серверах, принадлежащих государственным структурам.

Рассмотрим график зависимости результатов партии Единая Россия и КПРФ от явки на всех участках:

На графике можно выделить две характерные области. Плотное ядро из участков, где красные и синие точки практически наслаиваются друг на друга. В этой области результат партии КПРФ очень близок к результату партии Единая Россия. Кроме того, на графике отчетливо видны две области, которые в видео выше Сергей Шпилькин называет «хвостами кометы». Синий хвост направлен в сторону большего результата партии, красный в сторону меньшего результата партии. Это означает, что на участках с большей явкой результат Единой России растет, а КПРФ падает.

Как же выглядит зависимость результатов партий от явки на участках, где выборы были организованы сбалансированными составами комиссий? В предыдущем исследовании "Почему на удалении от крупных городов избиратели ходят на участки охотнее и голосуют за партию власти" было установлено, что в центрах городов с населением свыше 300000 человек качество подсчета голосов было наиболее высокое.  Ниже представлен график зависимости результата партий КПРФ и Единая Россия на участках, расположенных в 43 крупных городах России, на удалении менее 6 км от центров городов. Для Москвы это примерно эквивалентно площади внутри третьего транспортного кольца. Данные о результатах выборов на этих участках сохранены в файл ‘cities_ok.csv’.

Загрузим данные:

#%% Загружаем данные

import numpy as np
import pandas as pd
uiks = pd.read_csv('data/cities_ok.csv', index_col=0)
# uiks['voted'] = uiks['ballots_ok'] + uiks['ballots_spoiled']
uiks['total_voters'].sum()

Проверим сколько избирателей могло участвовать в выборах:

#%% Количество избирателей:
uiks['total_voters'].sum()
12451827

Более 12 миллионов человек. Это более 10% от общего числа избирателей в России.

Теперь построим график зависимости результатов двух крупнейших партий от явки:

#%% График зависимости результата на участках от явки

import matplotlib.pyplot as plt
uiks['er_percent'] = uiks['er'] / (uiks['voted'])
uiks['kprf_percent'] = uiks['kprf'] / (uiks['voted'])

uiks['turnout'] = uiks['voted']/uiks['total_voters']
plt.scatter(uiks['turnout'], uiks['er_percent'], color='blue', s=0.05, label="Единая Россия")
plt.scatter(uiks['turnout'], uiks['kprf_percent'], color='red', s=0.05, label="КПРФ")
lgnd = plt.legend(loc="upper left", scatterpoints=1, fontsize=10)
lgnd.legendHandles[0]._sizes = [30]
lgnd.legendHandles[1]._sizes = [30]
plt.xlabel("явка")
plt.ylabel("результат партии")
plt.xlim(0,1)
plt.ylim(0,1)
plt.show()

На графике можно рассмотреть ядро, в котором красные и синие точки наслаиваются друг на друга. Результат Единой России составил 30.16% и немного превышает результат КПРФ – 25.33%, поэтому синие точки в верхней части графика преобладают над красными.

Сгенерируем таблицу с результатами для участков из выборки с разбивкой по городам:

#%% Итоговый результат для различных городов

cities = uiks['city300'].drop_duplicates()
city_result = pd.DataFrame()
i=0
for city in cities:
    i+=1
    city_data = uiks[uiks['city300'] == city]
    city_er_percent = city_data['er'].sum()/city_data['voted'].sum()
    print(city_er_percent)
    city_kprf_percent = city_data['kprf'].sum()/city_data['voted'].sum()
    print(city_kprf_percent)
    city_result = city_result.append(pd.DataFrame({'name':city,'er_percent': city_er_percent,'kprf_percent': city_kprf_percent}, index=[i]))
city_result

Город

Результат ЕР

Результат КПРФ

Хабаровск

22.59

26.29

Владивосток

24.43

35.47

Киров

24.86

17.35

Ярославль

25.39

25.34

Омск

26.42

35.63

Москва

26.47

27.43

Орёл

28.12

23.34

Тверь

28.43

25.50

Липецк

28.47

29.23

Мурманск

28.49

19.86

Барнаул

29.04

31.98

Чебоксары

29.14

24.26

Томск

29.17

23.26

Новосибирск

29.22

28.00

Ульяновск

29.26

37.74

Челябинск

29.90

18.48

Ижевск

29.92

27.66

Красноярск

30.30

21.82

Иваново

30.60

29.66

Пермь

30.71

22.17

Вологда

30.79

21.16

Архангельск

30.83

18.53

Курган

30.85

24.78

Магнитогорск

30.88

22.45

Воронеж

30.97

31.00

Иркутск

31.14

31.18

Рязань

31.31

25.96

Калининград

31.40

24.48

Тольятти

31.49

28.84

Сургут

31.59

19.15

Чита

31.68

23.02

Екатеринбург

31.92

20.22

Калуга

32.53

22.07

Улан-Удэ

32.83

30.24

Череповец

32.94

25.49

Оренбург

32.95

24.75

Нижний Тагил

33.33

21.01

Самара

33.74

29.33

Нижний Новгород

34.09

23.71

Курск

34.12

23.13

Владимир

34.25

26.97

Пенза

34.43

24.74

Смоленск

37.21

24.77

В дальнейшем эта таблица будет служить эталоном для оценки работы алгоритма машинного обучения.

Произведем вброс голосов на случайно выбранные участки в пользу партии Единая Россия:

#%% Осуществляем вброс

from random import uniform
uiks = uiks.sample(frac=1)

uiks['er_fraud'] = uiks['er']
uiks['kprf_fraud'] = uiks['kprf']
uiks['voted_fraud'] = uiks['voted']
uiks['added'] = False

i = 0
er_percent = uiks['er_fraud'].sum()/uiks['voted_fraud'].sum()
for index, row in uiks.iterrows():
    if er_percent < 0.47:
        total_voters = row['total_voters']
        voted = row['voted']
        max_fraud = total_voters - voted
        min_fraud = max_fraud*0
        number = int(uniform(min_fraud, max_fraud))
        uiks.loc[index, 'er_fraud'] = row['er'] + number
        uiks.loc[index, 'voted_fraud'] = row['voted'] + number
        uiks.loc[index,'added'] = True
        er_percent = uiks['er_fraud'].sum()/uiks['voted_fraud'].sum()

uiks['turnout_fraud'] = uiks['voted_fraud']/uiks['total_voters']
uiks['er_percent_fraud'] = uiks['er_fraud']/uiks['voted_fraud']
uiks['kprf_percent_fraud'] = uiks['kprf']/uiks['voted_fraud']

plt.scatter(uiks['turnout_fraud'], uiks['er_percent_fraud'], color='blue', s=0.05)
plt.scatter(uiks['turnout_fraud'], uiks['kprf_percent_fraud'], color='red', s=0.05)
plt.xlim([0,1])
plt.ylim([0,1])
plt.show()
uiks['kprf'].sum()/uiks['voted_fraud'].sum()

В результате вброса появилось два «хвоста» в области больших значений явки. Результат Единой России увеличился c 30 до 47 %.  Однако, для большего сходства с графиком, построенным по результатам ЦИК не хватает участков, где результат Единой России превышает 80%. Вбросом такого результата не достичь, так как явка превысит 100%. Чтобы получить такой результат нужно забрать голоса у других партий и передать их Единой России. Или проще говоря задать результаты выборов на участках псевдослучайным образом для всех партий:

#%% Осуществляем замену голосов

uiks_change = uiks[~uiks['added']]
uiks['changed'] = False
for index, row in uiks_change.iterrows():
    if er_percent < 0.4982:
        total_voters = row['total_voters']
        random_voted = int(uniform(total_voters * 0.8, total_voters))
        voted = random_voted
        random_er = int(uniform(random_voted * 0.8, random_voted))
        uiks.loc[index, 'voted_fraud'] = voted
        uiks.loc[index, 'er_fraud'] = int(random_er)
        uiks.loc[index, 'kprf_fraud'] = int((random_voted - random_er)*0.3)
        uiks.loc[index, 'changed'] = True
        er_percent = uiks['er_fraud'].sum() / uiks['voted_fraud'].sum()

uiks['turnout_fraud'] = uiks['voted_fraud']/uiks['total_voters']
uiks['er_percent_fraud'] = uiks['er_fraud']/uiks['voted_fraud']
uiks['kprf_percent_fraud'] = uiks['kprf_fraud']/uiks['voted_fraud']

plt.scatter(uiks['turnout_fraud'], uiks['er_percent_fraud'], color='blue', s=0.05)
plt.scatter(uiks['turnout_fraud'], uiks['kprf_percent_fraud'], color='red', s=0.05)
plt.xlim([0,1])
plt.ylim([0,1])

plt.show()
uiks['kprf_fraud'].sum()/uiks['voted_fraud'].sum()

После генерации результатов в область явки свыше 80 процентов и результата Единой России свыше 80 процентов заполнилась точками. Результат Единой России достиг 49.83 %, результат КПРФ снизился до 18.16%.

Попробуем теперь вернуть участки из «хвостов» обратно в «ядро» с помощью инструментов библиотеки scikit-learn. 

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

#%% Выделяем кластер с нормальной и аномальной явкой

from sklearn.cluster import DBSCAN
er = uiks[['turnout_fraud', 'er_percent_fraud']]
er = er.to_numpy()
db = DBSCAN(eps=0.045, min_samples=200).fit(er)
plt.scatter(er[:, 0], er[:, 1], c=db.labels_, s=0.01)
plt.show()
uiks['db'] = db.labels_
uiks_normal = uiks[uiks['db'] == 0]
uiks_abnormal = uiks[uiks['db'] != 0]
plt.scatter(uiks_normal['turnout_fraud'], uiks_normal['er_percent_fraud'], color='blue', s=0.05)
plt.scatter(uiks_abnormal['turnout_fraud'], uiks_abnormal['er_percent_fraud'], color='red', s=0.05)
plt.show()

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

Установим общее число участков с правильными результатами:

#%% Количество участков без фальсификаций

len(uiks)-len(uiks[uiks['added']])-len(uiks[uiks['changed']])
3768

Общее количество участков в ядре:

#%% Количество участков в ядре
len(uiks_normal)
4044

Количество участков со вбросами в ядре:

#%% Количество участков с вбросами в ядре

len(uiks_normal[uiks_normal['added']])
353

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

Далее используем кластер участков без фальсификаций для обучения модели и восстановления результатов партии Единая Россия:

#%% Создаем pipeline для машинного обучения

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsRegressor
pipe = Pipeline([("scale", StandardScaler()), ("model", KNeighborsRegressor(weights='distance'))])

#%% Обучаем модель и делаем предсказание

from sklearn.model_selection import GridSearchCV
mod = GridSearchCV(estimator=pipe, param_grid={'model__n_neighbors': [1,2,3,4,5,6,7,10,11,12,13,14,15,16,17,18,19,20]}, cv=3)
X = uiks_normal[['kprf_fraud', 'total_voters', 'lat', 'lon']]
y = uiks_normal['er_fraud']
Xx = uiks_abnormal[['kprf_fraud', 'total_voters', 'lat', 'lon']]
mod.fit(X, y)
prediction = mod.predict(Xx)
uiks_abnormal['er_predicted'] = prediction.round()
pd.DataFrame(mod.cv_results_)

Теперь, когда у нас есть восстановленные результаты для партии Единая Россия, вычислим явку и процент проголосовавших за партии:

#%% Вычисляем явку по результатам машинного обучения

uiks_abnormal['voted_predicted'] = uiks_abnormal['voted_fraud'] - uiks_abnormal['er_fraud'] + uiks_abnormal['er_predicted']
uiks_abnormal['turnout_predicted'] = uiks_abnormal['voted_predicted'] / uiks_abnormal['total_voters']
uiks_abnormal['er_percent_predicted'] = uiks_abnormal['er_predicted'] / uiks_abnormal['voted_predicted']
uiks_abnormal['kprf_percent_predicted'] = uiks_abnormal['kprf'] / uiks_abnormal['voted_predicted']

uiks_normal['er_predicted'] = uiks_normal['er_fraud']
uiks_normal['voted_predicted'] = uiks_normal['voted_fraud']
uiks_normal['turnout_predicted'] = uiks_normal['turnout_fraud']
uiks_normal['er_percent_predicted'] = uiks_normal['er_percent_fraud']
uiks_normal['kprf_percent_predicted'] = uiks_normal['kprf_percent_fraud']

uiks_predicted = uiks_normal.append(uiks_abnormal)

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

Вот как выглядела зависимость результатов партий от явки до фальсификаций:

#%% Строим график зависимости результатов на участках со вбросами до фальсификаций
uiks_added = uiks_abnormal[uiks_abnormal['added']]
er_string = str(round(100*uiks_added['er'].sum()/uiks_added['voted'].sum(),2)) + '%'
kprf_string = str(round(100*uiks_added['kprf'].sum()/uiks_added['voted'].sum(),2))+ '%'
plt.scatter(uiks_added['turnout'], uiks_added['er_percent'], color='blue', s=0.05, label="Единая Россия " + er_string)
plt.scatter(uiks_added['turnout'], uiks_added['kprf_percent'], color='red', s=0.05, label="КПРФ " + kprf_string)
plt.xlim([0, 1])
plt.ylim([0, 1])
lgnd = plt.legend(loc="upper left", scatterpoints=1, fontsize=10)
lgnd.legendHandles[0]._sizes = [30]
lgnd.legendHandles[1]._sizes = [30]
plt.xlabel("явка")
plt.ylabel("результат партии")
plt.title("До фальсификаций")
plt.show()

После фальсификаций в виде вброса голосов ядро рассыпалось и превратилось в два «хвоста»:

#%% Строим график зависимости результатов на участках от явки со вбросами c фальсификациями
er_string = str(round(100*uiks_added['er_fraud'].sum()/uiks_added['voted_fraud'].sum(),2)) + '%'
kprf_string = str(round(100*uiks_added['kprf_fraud'].sum()/uiks_added['voted_fraud'].sum(),2))+ '%'
plt.scatter(uiks_added['turnout_fraud'], uiks_added['er_percent_fraud'], color='blue', s=0.05, label="Единая Россия " + er_string)
plt.scatter(uiks_added['turnout_fraud'], uiks_added['kprf_percent_fraud'], color='red', s=0.05, label="КПРФ " + kprf_string)
plt.xlim([0, 1])
plt.ylim([0, 1])
lgnd = plt.legend(loc="upper left", scatterpoints=1, fontsize=10)
lgnd.legendHandles[0]._sizes = [30]
lgnd.legendHandles[1]._sizes = [30]
plt.xlabel("явка")
plt.ylabel("результат партии")
plt.title("С фальсификациями")
plt.show()

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

#%% Строим график зависимости результатов на участках с заменой до фальсификаций

uiks_changed = uiks_abnormal[uiks_abnormal['changed']]
er_string = str(round(100*uiks_changed['er'].sum()/uiks_changed['voted'].sum(),2)) + '%'
kprf_string = str(round(100*uiks_changed['kprf'].sum()/uiks_changed['voted'].sum(),2))+ '%'
plt.scatter(uiks_changed['turnout'], uiks_changed['er_percent'], color='blue', s=0.05, label="Единая Россия " + er_string)
plt.scatter(uiks_changed['turnout'], uiks_changed['kprf_percent'], color='red', s=0.05, label="КПРФ " + kprf_string)
plt.xlim([0, 1])
plt.ylim([0, 1])
lgnd = plt.legend(loc="upper left", scatterpoints=1, fontsize=10)
lgnd.legendHandles[0]._sizes = [30]
lgnd.legendHandles[1]._sizes = [30]
plt.xlabel("явка")
plt.ylabel("результат партии")

plt.show()

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

#%% Строим график зависимости результатов на участках с заменой до фальсификаций

uiks_changed = uiks_abnormal[uiks_abnormal['changed']]
er_string = str(round(100*uiks_changed['er'].sum()/uiks_changed['voted'].sum(),2)) + '%'
kprf_string = str(round(100*uiks_changed['kprf'].sum()/uiks_changed['voted'].sum(),2))+ '%'
plt.scatter(uiks_changed['turnout'], uiks_changed['er_percent'], color='blue', s=0.05, label="Единая Россия " + er_string)
plt.scatter(uiks_changed['turnout'], uiks_changed['kprf_percent'], color='red', s=0.05, label="КПРФ " + kprf_string)
plt.xlim([0, 1])
plt.ylim([0, 1])
lgnd = plt.legend(loc="upper left", scatterpoints=1, fontsize=10)
lgnd.legendHandles[0]._sizes = [30]
lgnd.legendHandles[1]._sizes = [30]
plt.xlabel("явка")
plt.ylabel("результат партии")
plt.title("До фальсификаций (участки с заменой)")
plt.show()

Построим график зависимости результатов партий от явки для участков, где была замена голосов:

#%% Строим график зависимости результатов на участках от явки с заменой c фальсификациями
er_string = str(round(100*uiks_changed['er_fraud'].sum()/uiks_changed['voted_fraud'].sum(),2)) + '%'
kprf_string = str(round(100*uiks_changed['kprf'].sum()/uiks_changed['voted_fraud'].sum(),2))+ '%'
plt.scatter(uiks_changed['turnout_fraud'], uiks_changed['er_percent_fraud'], color='blue', s=0.05)
plt.scatter(uiks_changed['turnout_fraud'], uiks_changed['kprf_percent_fraud'], color='red', s=0.05)
plt.xlim([0, 1])
plt.ylim([0, 1])
lgnd = plt.legend(loc="upper left", scatterpoints=1, fontsize=10)
lgnd.legendHandles[0]._sizes = [30]
lgnd.legendHandles[1]._sizes = [30]
plt.xlabel("явка")
plt.ylabel("результат партии")
plt.title("С фальсификациями (участки с заменой)")
plt.show()

После фальсификаций результаты партий концентрируются в прямоугольных областях от 80% до 100% для Единой России и от 5% до 0% для КПРФ.

            Оценим результат применения модели к данным участков, где была замена голосов:

#%% Строим график зависимости результатов на участках от явки с заменой после машинного обучения
er_string = str(round(100*uiks_changed['er_predicted'].sum()/uiks_changed['voted_predicted'].sum(),2)) + '%'
kprf_string = str(round(100*uiks_changed['kprf_fraud'].sum()/uiks_changed['voted_predicted'].sum(),2))+ '%'
plt.scatter(uiks_changed['turnout_predicted'], uiks_changed['er_percent_predicted'], color='blue', s=0.05, label="Единая Россия " + er_string)
plt.scatter(uiks_changed['turnout_predicted'], uiks_changed['kprf_percent_predicted'], color='red', s=0.05, label="КПРФ " + kprf_string)
plt.xlim([0, 1])
plt.ylim([0, 1])
lgnd = plt.legend(loc="upper left", scatterpoints=1, fontsize=10)
lgnd.legendHandles[0]._sizes = [30]
lgnd.legendHandles[1]._sizes = [30]
plt.xlabel("явка")
plt.ylabel("результат партии")
plt.show()

Из графика видно, что модель работает не удовлетворительно. Вместо ядра получился «шлейф» в области низкой явки. Это и не удивительно. Ведь при заменен голосов были полностью фальсифицированы результаты для всех партий. А в нашей модели мы предполагали, что результат КПРФ был зафиксирован верно. К счастью, количество участков, где мы симулировали замену голосов, было не велико. Поэтому на итоговом результате эти искажения скажутся не сильно. Здесь интересно заметить, что в предыдущей статье «Восстанавливаем результаты выборов в государственную думу 2021 года с помощью машинного обучения» в итоговых результатах наблюдается очень похожий «хвост» в области низкой явки. Можно предположить, что в него попали итоги голосования на участках, где не были достоверно зарегистрированы результаты ни для одной из партий.

 

Изначально мы ставили задачу восстановить итоговый результат выборов в отдельных городах. Проверим как с этим справилось машинное обучение:

#%% Итоговый результат для городов

cities = uiks_predicted['city300'].drop_duplicates()
city_result_predicted = pd.DataFrame()
i=0
for city in cities:
    i+=1
    city_data = uiks_predicted[uiks_predicted['city300'] == city]
    city_er_percent = city_data['er'].sum()/city_data['voted'].sum()
    city_er_percent_fraud = city_data['er_fraud'].sum()/city_data['voted_fraud'].sum()
    city_er_percent_predicted = city_data['er_predicted'].sum()/city_data['voted_predicted'].sum()
    er_error = city_er_percent_predicted - city_er_percent
    city_result_predicted = city_result_predicted.append(pd.DataFrame({'name':city,'er_percent_fraud': city_er_percent_fraud,'er_percent_predicted': city_er_percent_predicted, 'er_percent': city_er_percent,'er_error': er_error}, index=[i]))
city_result_predicted.sort_values('er_percent')

Город

Результат ЕР с фальсификациями

Результат ЕР после машинного обучения

Результат ЕР до фальсификаций

Ошибка модели

Хабаровск

40.33

24.10

22.59

1.51

Владивосток

48.85

27.46

24.43

3.03

Киров

41.44

26.31

24.86

1.45

Ярославль

50.26

27.87

25.39

2.48

Омск

48.10

27.78

26.42

1.36

Москва

46.39

27.49

26.47

1.02

Орёл

48.25

29.14

28.12

1.02

Тверь

45.31

29.39

28.43

0.96

Липецк

47.87

30.36

28.47

1.89

Мурманск

49.66

29.74

28.49

1.25

Барнаул

48.86

30.44

29.04

1.40

Чебоксары

43.76

29.48

29.14

0.34

Томск

52.42

30.55

29.17

1.38

Новосибирск

52.77

30.06

29.22

0.84

Ульяновск

52.61

31.43

29.26

2.17

Челябинск

47.48

30.80

29.90

0.90

Ижевск

50.06

31.30

29.92

1.38

Красноярск

51.22

31.02

30.30

0.72

Иваново

51.37

31.65

30.60

1.05

Пермь

54.06

32.04

30.71

1.33

Вологда

46.55

30.80

30.79

0.01

Архангельск

48.04

32.13

30.83

1.30

Курган

47.86

31.53

30.85

0.68

Магнитогорск

47.71

31.29

30.88

0.41

Воронеж

52.25

31.96

30.97

0.99

Иркутск

49.23

31.90

31.14

0.76

Рязань

49.04

31.90

31.31

0.59

Калининград

49.63

32.20

31.40

0.80

Тольятти

50.02

32.96

31.49

1.47

Сургут

51.57

32.45

31.59

0.86

Чита

60.76

33.03

31.68

1.35

Екатеринбург

48.60

32.60

31.92

0.68

Калуга

52.28

32.17

32.53

-0.36

Улан-Удэ

53.60

33.62

32.83

0.79

Череповец

47.23

33.21

32.94

0.27

Оренбург

50.70

33.30

32.95

0.35

Нижний Тагил

45.81

32.44

33.33

-0.89

Самара

55.76

34.85

33.74

1.11

Нижний Новгород

56.50

34.77

34.09

0.68

Курск

55.74

33.61

34.12

-0.51

Владимир

53.26

33.65

34.25

-0.60

Пенза

52.99

34.43

34.43

0.00

Смоленск

69.93

36.24

37.21

-0.97

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

 

Для визуализации качества работы модели построим график зависимости восстановленных результатов партии «Единая Россия» в городах от истинных результатов:

#%% Строим график зависимости результатов городов сгенерированных от первоначальных

plt.scatter(city_result_predicted['er_percent'], city_result_predicted['er_percent'], color='red', label = "Истинные результаты")
plt.scatter(city_result_predicted['er_percent'], city_result_predicted['er_percent_predicted'], color='blue', label = "Результаты машинного обучения")
lgnd = plt.legend(loc="upper left", scatterpoints=1, fontsize=10)
plt.xlabel("Результат ЕР в городах")
plt.ylabel("Результат ЕР в городах")
plt.show()

По результатам исследования можно сделать следующие выводы:

  1. Фальсификации результатов выборов на участках в виде вбросов бюллетеней формируют на графике зависимости результатов партий от явки характерные области, которые тянутся расходящимися «хвостами» из ядра. С помощью фальсификаций в виде вбросов не возможно добиться результатов близких к 100% ни за одну из партий.

  2. Результат близкий к 100 % может свидетельствовать о другом типе фальсификаций: на участках происходила замена бюллетеней с голосами за одну из партий на бюллетени с голосами за другую партию или генерация итогового результата случайным образом.

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

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

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

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

Какие выводы после прочтения статьи можете сделать вы? Напишите, пожалуйста в комментариях.

Теги:
Хабы:
Всего голосов 39: ↑38 и ↓1+37
Комментарии26

Публикации

Истории

Работа

Python разработчик
125 вакансий
Data Scientist
56 вакансий

Ближайшие события

Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн
Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург