Восстанавливаем результаты выборов в Государственную думу 2021 года с помощью машинного обучения

image-loader.svg

Результаты выборов в государственную думу, которые проходили 17–19 сентября 2021 вызывают сомнения у многих экспертов. Независимый электоральный аналитик Сергей Шпилькин оценил количество голосов, вброшенных за партию власти, примерно в 14 миллионов. В данной работе применены методы машинного обучения для того, чтобы выявить избирательные участки, на которых подсчет голосов происходил без нарушений и установить истинный результат на тех участках, где, предположительно, были зарегистрированы ошибочные данные.

Результаты выборов можно найти на сайте ЦИК. Кроме того, результаты были выгружены с сайта и помещены в телеграмм канал RuElectionData. В рамках данной работы исследуются результаты выборов для партий «Единая Россия» и «КПРФ», которые по результатам, опубликованным ЦИК, получили 49,82 и 18,93 процента голосов избирателей. В данном исследовании в качестве источника результатов используется часть данных, которые были сохранены в файл «edata.csv». Этот файл можно скачать совместно с исходным кодом с GitHub.

Для начала загрузим данные и проверим их полноту:

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

import pandas as pd
uiks = pd.read_csv('data/edata.csv', index_col=0)

name

region

kprf

er

voted

total_voters

lat

lon

0

УИК №592

Алтайский край

57

49

178

385

51.885025

85.307478

1

УИК №593

Алтайский край

189

174

569

1515

51.934707

85.326494

2

УИК №594

Алтайский край

157

141

464

1175

51.930130

85.333621

3

УИК №595

Алтайский край

303

339

962

2257

51.943233

85.336853

4

УИК №596

Алтайский край

264

282

843

1924

51.961639

85.335227

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

#%% Итоговый результат КПРФ

kprf = uiks['kprf'].sum()/uiks['voted'].sum()

0.18925488494610923

#%% Итоговый результат Единой России

er = uiks['er'].sum()/uiks['voted'].sum()

0.4982132868119814

Итоговый результат совпадает с результатом на сайте ЦИК, будем считать данные полными.

Как видно из результатов ЦИК, Единая Россия опередила КПРФ более чем в два раза. Однако есть регионы, где КПРФ одержала победу. Для каждого из участков добавим параметр 'k-e' , который равен разнице результата Единой России и КПРФ в регионе, в котором находится участок. Кроме того, создадим таблицу с регионами, где победу одержала КПРФ:

uiks['k-e'] = 0.0
regions = uiks['region'].drop_duplicates()
reg = pd.DataFrame()
for region in regions:
   region_data = uiks[uiks['region'] == region]
   voted = region_data['voted'].sum()
   kprf_total = region_data['kprf'].sum()
   kprf_percent = kprf_total/voted
   er_total = region_data['er'].sum()
   er_percent = er_total/voted
   uiks.loc[uiks['region'] == region, 'k-e'] = kprf_percent-er_percent
   if er_total>kprf_total:
       uiks.loc[uiks['region'] == region, 'color'] = 'blue'
   else:
       uiks.loc[uiks['region'] == region, 'color'] = 'red'
   reg = reg.append(pd.DataFrame({'name': region,'kprf':[kprf_total], 'kprf_percent':[kprf_percent],'er':[er_total],'er_percent':[er_percent]}), ignore_index=True)
reg[reg['kprf']>reg['er']]

name

kprf

kprf_percent

er

er_percent

1

Ненецкий автономный округ

4917

0.319763

4469

0.290629

2

Республика Марий Эл

89018

0.362999

81969

0.334255

3

Республика Саха (Якутия)

118683

0.351483

112160

0.332165

4

Хабаровский край

113691

0.265075

105112

0.245072

Нанесем участки на карту России с помощью библиотеки plotly.express.

import plotly.express as px
fig = px.scatter_mapbox(uiks, #our data set
                        lat="lat",
                        lon="lon",
                        color="k-e",
                        range_color = (-0.5,0.5),
                        zoom=2,
                        width=1200, height=800,
                        center = {'lat':60,'lon':105},
                        title =  'По данным ЦИК')
fig.update_layout(mapbox_style="open-street-map")
fig.update_traces(marker=dict(size=5))
fig.show(config={'scrollZoom': True})

image-loader.svg

На этой карте участки окрашены в различные цвета, в соответствии с разницей результата КПРФ и Единой России по региону, в котором находится участок (параметр «k-e»). Цвет может меняться от темно синего (Результат Единой России на 50% и более выше, чем у КПРФ) до желтого (результат КПРФ на 50% выше, чем у Единой России). На карте преобладают холодные тона. Преимущество Единой России очевидно.

Построим теперь график зависимости результатов Единой России и КПРФ от явки c помощью matplotlib:

import matplotlib.pyplot as plt
uiks = uiks[uiks['kprf']>10]
uiks = uiks[uiks['er']>10]
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.01)
plt.scatter(uiks['turnout'], uiks['kprf_percent'], color='red', s=0.01)
plt.show()

image-loader.svg

На графике можно выделить две характерные зоны. Плотное ядро в районе явок 0.2–0.6 и расходящиеся «хвосты» в районе явок свыше 0.6. В своих работах, независимые электоральные аналитики показывают, что подобная картина может наблюдаться при вбросе голосов за партию, результат которой растет с явкой. Причем в ядре находятся участки с «нормальной явкой», на которых не было фальсификаций, а хвосты соответствуют участкам с «аномальной явкой», где результаты выборов недостоверны.

Отделим участки с нормальным голосованием от участков с аномальным голосованием.

Чтобы выделить участки в ядре используем алгоритм DBSCAN (Density Based Scan) из библиотеки scikit-learn. Этот алгоритм выделяет кластеры, в которых для каждой точи в радиусе «eps» имеется количество точек равное «min_samples». Хороший результат дает eps = 0.009 и min_samples = 175:

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

from sklearn.cluster import DBSCAN
er = uiks[['turnout', 'er_percent']]
er = er.to_numpy()
db = DBSCAN(eps=0.009, min_samples=175).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]

image-loader.svg

Далее будем использовать участки из ядра для того, чтобы обучить модель. В качестве алгоритма будем использовать алгоритм k ближайших соседей. В sklearn он реализован в виде класса KNeighborsRegressor. Кроме того, мы создадим объект класса Pipeline, чтобы автоматически нормализовать данные с помощью StandardScaler.

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

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsRegressor

pipe = Pipeline([("scale", StandardScaler()), ("model", KNeighborsRegressor())])
pipe.get_params()

Для обучения модели мы разделим участки в ядре на три части (cv = 3) и проведем оптимизацию результатов по количеству ближайших соседей ('model__n_neighbors'):

#%% Задаем параметры кросс валидации

from sklearn.model_selection import GridSearchCV
mod = GridSearchCV(estimator=pipe, param_grid={'model__n_neighbors': [45, 50, 55,60,65,70,75,80,85,90]}, cv=3)

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

#%% Обучаем модель

X = uiks_normal[['kprf', 'total_voters', 'lat', 'lon']]
y = uiks_normal['er']
Xx = uiks_abnormal[['kprf', 'total_voters', 'lat', 'lon']]
mod.fit(X, y)

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

#%% Рассчитываем результат Единой России используя модель

prediction = mod.predict(Xx)
uiks_abnormal['prediction'] = prediction
uiks_abnormal['er_predicted'] = prediction.round()

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

#%% Корректируем результаты, так как предполагаем, что за Единую Россию не было вбросов

for index, row in uiks_abnormal.iterrows():
    if row['er'] < row['prediction']:
        uiks_abnormal.loc[index, 'er_predicted'] = row['er']
uiks_normal['er_predicted'] = uiks_normal['er']

Теперь, когда у нас есть расчетные результаты партии Единая Россия на аномальных участках, можно пересчитать явку и другие параметры. Кроме того, создадим объект  uiks_predicted, который будет содержать результаты выборов на участках с нормальным и аномальным голосованием:

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

uiks_abnormal['voted_predicted'] = uiks_abnormal['voted'] - uiks_abnormal['er'] + uiks_abnormal['er_predicted']
uiks_normal['voted_predicted'] = uiks_normal['voted']
uiks_abnormal['turnout_predicted'] = uiks_abnormal['voted_predicted'] / uiks_abnormal['total_voters']
uiks_normal['turnout_predicted'] = uiks_normal['turnout']
uiks_abnormal['er_percent_predicted'] = uiks_abnormal['er_predicted'] / uiks_abnormal['voted_predicted']
uiks_normal['er_percent_predicted'] = uiks_normal['er_percent']
uiks_abnormal['kprf_percent_predicted'] = uiks_abnormal['kprf'] / uiks_abnormal['voted_predicted']
uiks_normal['kprf_percent_predicted'] = uiks_normal['kprf_percent']
uiks_predicted = uiks_normal.append(uiks_abnormal)

Используем данные из uiks_predicted для построения графика зависимости результатов на участках от явки.

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

plt.scatter(uiks_predicted['turnout_predicted'], uiks_predicted['er_percent_predicted'], color='blue', s=0.01)
plt.scatter(uiks_predicted['turnout_predicted'], uiks_predicted['kprf_percent_predicted'], color='red', s=0.01)
plt.show()

image-loader.svg

После применения машинного обучения картина больше похожа на ту, что наблюдалась в регионах севера России, где выборы традиционно проходят на высоком уровне. «Расходящиеся хвосты» в области большой явки больше не наблюдаются. Подсчитаем итоговый результат для партии Единая Россия  и КПРФ по результатам полученных данных. Кроме того, установим количество вброшенных голосов:

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

er_real = uiks_predicted['er_predicted'].sum() //12155992.0
kprf_real = uiks_predicted['kprf'].sum() //10610737
voted_real = uiks_predicted['voted_predicted'].sum() 40145581.0
er_real_percent = er_real / voted_real //0.30279775998259933
kprf_real_percent = kprf_real / voted_real //0.26430647497666054
fake_votes = uiks_predicted['er'].sum() - uiks_predicted['er_predicted'].sum() //14595980.0

Таким образом, после восстановления результатов с помощью модели машинного обучения Единая Россия набирает около 30 процентов при средней явке 40 процентов. Разница количества голосов за Единую Россию в исходных данных и данных, полученных в результате моделирования составляет 14595980. КПРФ набирает 26 процентов.  Посмотрим, изменился ли состав регионов, в которых лидирует КПРФ:

#%% Таблица регионов, где победила КПРФ после применения машинного обучения

reg_true = pd.DataFrame()
for region in regions:
   region_data = uiks_predicted[uiks_predicted['region'] == region]
   kprf_total = region_data['kprf'].sum()
   er_total = region_data['er_predicted'].sum()
   voted = region_data['voted'].sum()
   kprf_percent = kprf_total/voted
   er_percent = er_total/voted
   uiks_predicted.loc[uiks['region'] == region, 'k-e'] = kprf_percent-er_percent
   if er_total>kprf_total:
       uiks_predicted.loc[uiks_predicted['region'] == region, 'color'] = 'blue'
   else:
       uiks_predicted.loc[uiks_predicted['region'] == region, 'color'] = 'red'
   reg_true = reg_true.append(pd.DataFrame({'name': region,'kprf':[kprf_total],'er':[er_total]}), ignore_index=True)
reg_true[reg_true['kprf']>reg_true['er']]

name

kprf

er

1

Алтайский край

224806

205960

2

Ивановская область

84969

84383

3

Кабардино-Балкарская Республика

77074

72990

4

Костромская область

57588

56026

5

Ненецкий автономный округ

4863

3883

6

Омская область

190454

162625

7

Приморский край

173429

142138

8

Республика Алтай

22244

20992

9

Республика Калмыкия

25485

24826

10

Республика Марий Эл

89013

69266

11

Республика Саха (Якутия)

118362

87085

12

Ростовская область

333737

333235

13

Сахалинская область

43471

38457

14

Ульяновская область

147069

120774

15

Хабаровский край

113312

85935

16

Ярославская область

98695

90208

17

город Москва

871223

529986

Количество регионов, где КПРФ одержала победу над Единой Россией, увеличилось с четырех до семнадцати. Проверим, как изменилась раскраска регионов на карте России:

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

fig = px.scatter_mapbox(uiks_predicted, #our data set
                        lat="lat",
                        lon="lon",
                        color='k-e',
                        range_color = (-0.5,0.5),
                        zoom=2,
                        width=1200, height=800,
                        center = {'lat':60,'lon':105},
                        title =  'После машинного обучения')
fig.update_layout(mapbox_style="open-street-map")
fig.update_traces(marker=dict(size=5))
fig.show(config={'scrollZoom': True})

image-loader.svg

Карта окрасилась в более теплые тона. Во всех регионах результат у партий КПРФ и Единая Россия очень близкий.

В результате моделирования результатов выборов на участках с аномальной явкой можно сделать следующие выводы:

·      Разница количества голосов за партию Единая Россия при подсчетах ЦИК и с использованием модели машинного обучения составила более 14 миллионов.

·      Результат партии Единая Россия составил около 30%

·      Результат партии КПРФ составил 26%

·      Средняя явка составила около 40%

·      Количество регионов России, в которых результат КПРФ превзошел результат Единой России, увеличилось с четырех до семнадцати.

© Habrahabr.ru