Общероссийское голосование по вопросу одобрения изменений, вносимых в Конституцию Российской Федерации, проводилось с 25 июня по 1 июля 2020 года (wikipedia).
Основная цель данной заметки — это продемонстрировать как можно быстро начать работать с данными голосования и показать наличие определенного вида аномалий в них.
Все вычисления, визуализации и парсинг данных приведены в Google Colab, который доступен по этой ссылке Google Colab.
Мы сконцентрируемся на выявлении случаев голосования с нулевой дисперсией, то есть результатов, когда все УИКи внутри одного ТИКа голосуют одинаково или с минимальным разбросом. Данные случаи не имеют естественного объяснения кроме того, что подсчет голосов не осуществлялся.
Примерно это выглядит так (ссылка на страницы ЦИК ТИК 52,ТИК 33,ТИК 42 и ТИК 19):
![](https://habrastorage.org/getpro/habr/post_images/3c5/c84/a30/3c5c84a3092078ae4cef687adaf0285c.png)
![](https://habrastorage.org/getpro/habr/post_images/98d/935/b01/98d935b0154c50a9920a6fa752d47015.png)
![](https://habrastorage.org/getpro/habr/post_images/d64/183/e16/d64183e167f2d6435b7b10e697af0991.png)
![](https://habrastorage.org/getpro/habr/post_images/48c/efd/574/48cefd574cb79f23adacd94802144fdd.png)
![](https://habrastorage.org/getpro/habr/post_images/92e/f14/7fd/92ef147fd3b90e015b9203b0e49898fd.png)
![](https://habrastorage.org/getpro/habr/post_images/797/7e0/677/7977e0677571e4bb8e4cc858bf5b173c.png)
![](https://habrastorage.org/getpro/habr/post_images/eb8/0d4/479/eb80d447922dd0064ad2aeb9903c2b00.png)
![](https://habrastorage.org/getpro/habr/post_images/be8/23e/8c9/be823e8c9d91b6f4bd476d42e4d1d977.png)
![](https://habrastorage.org/getpro/habr/post_images/8b6/693/426/8b66934263c2c1d11ee89764b135567c.png)
![](https://habrastorage.org/getpro/habr/post_images/ee5/9a7/f9a/ee59a7f9ac77ad6c0730e4d40de09ff0.png)
![](https://habrastorage.org/getpro/habr/post_images/8bb/091/574/8bb091574e2e0fee3038ae6a53661077.png)
![](https://habrastorage.org/getpro/habr/post_images/f71/cc1/c24/f71cc1c24edf94f42cd72f5b56c72eab.png)
![](https://habrastorage.org/getpro/habr/post_images/79a/190/5bb/79a1905bb8af657cca5edc0434b199fe.png)
![](https://habrastorage.org/getpro/habr/post_images/a6d/6d7/15e/a6d6d715e063425565bb5faf72ab0ccc.png)
![](https://habrastorage.org/getpro/habr/post_images/113/4e9/51c/1134e951cc2c9f7ca578f89f4b43570a.png)
![](https://habrastorage.org/getpro/habr/post_images/431/1c9/029/4311c9029f45d375b24e909c43696706.png)
![](https://habrastorage.org/getpro/habr/post_images/a50/240/a13/a50240a1322549485ec5dde1151156cf.png)
![](https://habrastorage.org/getpro/habr/post_images/92e/56a/a99/92e56aa9952852617c21a4958bfa6605.png)
![](https://habrastorage.org/getpro/habr/post_images/0af/ebd/825/0afebd8257ca18a7140258dd7dfc68a0.png)
![](https://habrastorage.org/getpro/habr/post_images/d97/005/190/d97005190af76c1511cfbc5db2b7396c.png)
![](https://habrastorage.org/getpro/habr/post_images/ba8/9fc/c00/ba89fcc002e7ea7de545893b932e2c7b.png)
![](https://habrastorage.org/getpro/habr/post_images/487/685/260/487685260cff1369d3fe49a5d254a6f4.png)
![](https://habrastorage.org/getpro/habr/post_images/7d0/9ba/05f/7d09ba05f67521d3dfdd43178c660695.png)
Далее по тексту 'За'='Да', 'Против'='Нет'.
Источники данных
Сведения о результатах голосования предоставляет ЦИК РФ. Разнообразные цифры с разбивкой по округам доступны на странице ЦИК. К сожалению, эти данные сильно фрагментированы и мало приспособлены для машинного анализа.
Сергей Шпилькин одним из первых в нашей стране стал предоставлять данные голосования в режиме реального времени в формате JSON и CSV файлов. За что ему отдельное спасибо (no data, no job!). Соответственно, я использую данные результатов и явки, которые взяты из его Telegram-канала RUElectionData .
Основной результат
- Найдены ТИКи имеющие близкий к нулю разброс явки и результата голосования.
- Найдены ТИКи, где волатильность приращения внутридневной явки между контрольными точками 10.00 12.00 15.00 и 18.00 крайне мала.
- Данные случаи не имеют естественного объяснения кроме того, что подсчет голосов не осуществлялся.
Можно сказать, что общее количество аномальных ТИКов исчисляется сотнями.
Избранные результаты сохранены в архиве. Также их можно посмотреть в (трэш) видео на youtube.
Самый математически красивый результат показал ТИК 18 «Клинцовская городская» из Брянской области. Для всех участков (кроме одного) разница явки между 12.00 и 10.00 часами составила 3%, а разница явки между 15.00 и 12.00 часами составила 5% (ссылка на страницу ЦИК).
![](https://habrastorage.org/getpro/habr/post_images/90f/5fd/485/90f5fd485ef8906720fa1b05d9ba8d91.png)
Но самое интересное, что на УИКах с четными номерами явка составила 91%, а на участках с нечетными номерами — 90% (ссылка на страницу ЦИК).
![](https://habrastorage.org/getpro/habr/post_images/240/252/bc2/240252bc280c623e42e7f7cb65592b24.png)
Описание кода
Весь код можно прогнать через меню Runtime->Run All. Это займет около 15 минут.
Для загрузки данных с Google Drive необходимо иметь Google account. GoogleCredentials выведет ссылку с кодом доступа. Скопировав код доступа в колаб, вы сможете загрузить необходимые данные. Это стандартная процедура для колаба.
В самом конце будет предложена возможность сохранить результаты вычисления на локальном компьютере.
Ниже идет детальное описание основных частей программы.
Начнём с загрузки данных явки и результатов голосования (раздел «Загрузка данных явки и результатов голосования»). Данные соответствуют файлам turnouts_05_Jul_2020_14_56.zip и results_06_Jul_2020_19_05.zip из телеграмм канала RUElectionData.
Далее данные по явке df2 и результатам голосования df1 совмещаются в один дата фрейм df:
df= pd.merge(df1, df2.drop(columns=['reg']), how='inner',on=['tik', 'uik'])
Для транслитерации названий регионов и ТИКов я использую пакет transliterate:
!pip install transliterate
from transliterate import translit
После этого сосчитаем значения явки и результата референдума в процентах:
df['turnout_pct']=df['n_ballots_all']/df['n_registered_voters']*100.
df['yes_pct']=df['yes']/df['n_ballots_polling_station']*100.
df['no_pct']=df['no']/df['n_ballots_polling_station']*100.
df['invalid_pct']=df['n_ballots_invalid']/df['n_ballots_polling_station']*100.
И сделаем резервную копию получившегося дата фрейма:
df_original=df.copy(deep=True)
Теперь можно приступать к анализу. В разделе «Визуализация явки и результатов голосования» приводится визуализация процентов голосов «Да» и «Нет». Предварительно отфильтруем участки с явкой ровно 100%.
df=df[df['turnout_pct']<100.]
ax = df.plot.scatter(x='turnout_pct',y='yes_pct',label='Да (Синий)',c='DarkBlue',s=0.001)
ax = df.plot.scatter(x='turnout_pct',y='no_pct',label='Нет (Красный)',c='DarkRed',s=0.001,ax=ax)
![](https://habrastorage.org/getpro/habr/post_images/23f/6ea/688/23f6ea688f39adec7a20c57ce546f078.png)
Далее, в качестве разогрева, приведем ставшие уже классическими оценки аномальных голосов на графиках голоса «Да» против явки:
![](https://habrastorage.org/getpro/habr/post_images/ae6/6c0/d4d/ae66c0d4d9c46085ad080d9b8792d110.png)
И гистограммы голосов «Да» и явки:
![](https://habrastorage.org/getpro/habr/post_images/33c/693/a84/33c693a8439acd13eb1bb1988df8b515.png)
![](https://habrastorage.org/getpro/habr/post_images/142/4f5/9b1/1424f59b15efb805a231ce61c7824a41.png)
Обратите внимание на пики в районе круглых чисел 80%, 85%, 90% и 95%.
Распределения явки в других странах можно посмотреть здесь. Важно отметить, что аномальное распределение явки в РФ сохраняется и на уровне относительно однородных регионов и, кроме того, форма распределения флуктуирует во времени в зависимости от количества наблюдателей или наличия КОИБов.
На главное блюдо у нас обнаружение аномальных ТИКов с низкой дисперсией. В отличие от многих других методов, данный метод прост (результат и его интерпретация понятна и человеку без специального образования) и свободен от модельных предположений (Помни: A Model Is Only as Good as Its Assumptions).
В разделе «Расчет дисперсии процентов явки и голосов Да» сосчитаны основные статистики и записаны в Excel файл. Для оценки разброса (дисперсии) наиболее подходит робастная версия стандартного отклонения wiki:Median absolute deviation.
Аномальные результаты голосования можно обнаружить, изучая дисперсию голосов «Да» и явки. Данный метод достаточно эффективен, несмотря на свою примитивность. Безусловно, он имеет ошибки 1-го и 2-го рода. Метод лишь детектирует кандидатов в аномальные ТИКи, которые можно далее изучить детально и, пройдя по url ссылке, сравнить приведенные числа с данными ЦИК.
В число аномальных топ 100 по явке и результату ‘Да’ попали 153 ТИКов или 5570 УИКов на которые приходится 6 млн. зарегистрированных избирателей и 5.5 млн. проголосовавших.
Дополнительно я изучил суммы дисперсий голосов «Да» и явки и дисперсии внутридневной явки. Четыре последующих раздела посвящены детальному исследованию каждого случая.
Графики ТИКов с низкой дисперсией голосов или явки выглядят как прямые линии. Графики ТИКов с низкой дисперсией суммы дисперсий голосов и явки выглядят как жирное пятно (окружность).
Поскольку все 4 раздела имеют одну и ту же структуру, я подробно разберу лишь один из них: «Результаты с низкой дисперсией процента голосов Да».
В подразделе «Таблица результатов» приведены топ 50 ТИКов с малой дисперсией по проценту голосов «Да»(«За»).
![](https://habrastorage.org/getpro/habr/post_images/83f/78b/f39/83f78bf39c3126b28f2bc8d3cb830a1f.png)
В подразделе «Детальная визуализация отдельных ТИКов», я привожу пример как можно визуализировать отдельные ТИКи. Для этого надо выбрать соответствующий индекс (цифра в первой колонке) и присвоить переменной id_num её значение. Для примера можно посмотреть id_num 1616, 1995 или 2165.
Значение id_num=1616 соответствует ТИКу 33 Пермь, Орджоникидзевская в Пермском крае
(ссылка на страницу ЦИК). В табличной форме он выглядит так:
![](https://habrastorage.org/getpro/habr/post_images/d1d/a48/93e/d1da4893ebbcb1d1f1ac312216bc1d76.png)
При том что каждый УИК имеет под тысячу избирателей, голоса «Да» (колонка yes_pct) распределились невероятно близко к 71.9%. На графиках это выглядит так:
![](https://habrastorage.org/getpro/habr/post_images/6e4/0a6/684/6e40a66845552abb7c3602a8ca3f8c1c.png)
![](https://habrastorage.org/getpro/habr/post_images/066/bb3/60f/066bb360f9dff92785b0ac485692f5eb.png)
Наконец, в подразделе «Визуализация всех аномальных результатов» отсортируем ТИКи по величине разброса голосов «Да». Затем визуализируем и сохраним plot_top_n_results лучших результатов. Я рекомендую глазами посмотреть от plot_top_n_results=50 до plot_top_n_results=300 результатов.
После проведения всех вычислений есть возможность сжать полученные данные с результатами и сохранить их на локальный компьютер.
import shutil
from google.colab import files
directory='/content/drive/anomaly/dispersion'
shutil.make_archive(directory, 'zip', directory)
files.download('/content/drive/anomaly/dispersion.zip')
Дополнительные ссылки
Исследователь выборов Александр Киреев приводит свой список аномальных ТИКов с оценкой нижней границы аномальных голосов в 10 млн. человек или 10 тыс. избирательных участков.
Много интересных результатов приведено в Facebook'е Сергея Шпилькина и Азата Габдульвалеева.
Визуализация результатов референдума:
https://elections.dekoder.org/ru/russia/constitution/2020/
https://www.electoral.graphics/ru-ru/
Движение в защиту прав избирателей «Голос» https://www.golosinfo.org/.
Ассоциации наблюдателей: https://constitution.observer/
p.s. Спасибо всем, кто прочитал. Надеюсь, вы смогли проверить выкладки автора своими руками, используя открытые код и данные. Отдельная благодарность tyomitch и Lissov за остроумные комментарии.