Результаты выборов в государственную думу, которые проходили 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})
На этой карте участки окрашены в различные цвета, в соответствии с разницей результата КПРФ и Единой России по региону, в котором находится участок(параметр ‘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()
На графике можно выделить две характерные зоны. Плотное ядро в районе явок 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]
Далее будем использовать участки из ядра для того, чтобы обучить модель. В качестве алгоритма будем использовать алгоритм 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()
После применения машинного обучения картина больше похожа на ту, что наблюдалась в регионах севера России, где выборы традиционно проходят на высоком уровне. «Расходящиеся хвосты» в области большой явки больше не наблюдаются. Подсчитаем итоговый результат для партии Единая Россия и КПРФ по результатам полученных данных. Кроме того, установим количество вброшенных голосов:
#%% Считаем итоговый результат после применения машинного обучения
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})
Карта окрасилась в более теплые тона. Во всех регионах результат у партий КПРФ и Единая Россия очень близкий.
В результате моделирования результатов выборов на участках с аномальной явкой можно сделать следующие выводы:
-
Разница количества голосов за партию Единая Россия при подсчетах ЦИК и с использованием модели машинного обучения составила более 14 миллионов.
-
Результат партии Единая Россия составил около 30%
-
Результат партии КПРФ составил 26 %
-
Средняя явка составила около 40%
-
Количество регионов России, в которых результат КПРФ превзошел результат Единой России, увеличилось с четырех до семнадцати.
Автор: Евгений Басистый