Общероссийское голосование по вопросу одобрения изменений, вносимых в Конституцию Российской Федерации, проводилось с 25 июня по 1 июля 2020 года (wikipedia).
Основная цель данной заметки — это продемонстрировать как можно быстро начать работать с данными голосования и показать наличие определенного вида аномалий в них.
Все вычисления, визуализации и парсинг данных приведены в Google Colab, который доступен по этой ссылке Google Colab.
Мы сконцентрируемся на выявлении случаев голосования с нулевой дисперсией, то есть результатов, когда все УИКи внутри одного ТИКа голосуют одинаково или с минимальным разбросом. Данные случаи не имеют естественного объяснения кроме того, что подсчет голосов не осуществлялся.
Примерно это выглядит так ('За'='Да', 'Против'='Нет'):
Источники данных
Сведения о результатах голосования предоставляет ЦИК РФ. Разнообразные цифры с разбивкой по округам доступны на странице ЦИК. К сожалению, эти данные сильно фрагментированы и мало приспособлены для машинного анализа.
Сергей Шпилькин одним из первых в нашей стране стал предоставлять данные голосования в режиме реального времени в формате JSON и CSV файлов. За что ему отдельное спасибо (no data, no job!). Соответственно, я использую данные результатов и явки, которые взяты из его телеграм-канала RUElectionData .
Основной результат
- Найдены ТИКи имеющие близкий к нулю разброс явки и результата голосования.
- Найдены ТИКи, где волатильность приращения внутридневной явки между контрольными точками 10.00 12.00 15.00 и 18.00 крайне мала.
- Данные случаи не имеют естественного объяснения кроме того, что подсчет голосов не осуществлялся.
Можно сказать, что общее количество аномальных ТИКов исчисляется сотнями.
Избранные результаты сохранены в архиве. Также их можно посмотреть в (трэш) видео на youtube.
Самый математически красивый результат показал ТИК 18 «Клинцовская городская» из Брянской области. Для всех участков (кроме одного) разница явки между 12.00 и 10.00 часами составила 3%, а разница явки между 15.00 и 12.00 часами составила 5% (ссылка на страницу ЦИК).
Но самое интересное, что на УИКах с четными номерами явка составила 91%, а на участках с нечетными номерами — 90%.
Описание кода
Весь код можно прогнать через меню 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)
Далее, в качестве разогрева, приведем ставшие уже классическими оценки аномальных голосов на графиках голоса «За» против явки:
И гистограммы голосов «За» и явки:
Обратите внимание на пики в районе круглых чисел 80%, 85%, 90% и 95%.
На главное блюдо у нас обнаружение аномальных ТИКов с низкой дисперсией. В отличие от многих других методов, данный метод прост (результат и его интерпретация понятна и человеку без специального образования) и свободен от модельных предположений (Помни: A Model Is Only as Good as Its Assumptions).
В разделе «Расчет дисперсии процентов явки и голосов За» сосчитаны основные статистики и записаны в Excel файл. Для оценки разброса (дисперсии) наиболее подходит робастная версия стандартного отклонения wiki:Median absolute deviation.
Аномальные результаты голосования можно обнаружить изучая дисперсию голосов «За» и явки. Данный метод достаточно эффективен, несмотря на свою примитивность. Безусловно, он имеет ошибки 1-го и 2-го рода. Метод лишь детектирует кандидатов в аномальные ТИКи, которые можно далее изучить детально и, пройдя по url ссылке, сравнить приведенные числа с данными ЦИК.
Дополнительно я изучил суммы дисперсий голосов «За» и явки и дисперсии внутри-дневной явки. Четыре последующих раздела посвящены детальному исследованию каждого случая.
Графики ТИКов с низкой дисперсией голосов или явки выглядят как прямые линии. Графики ТИКов с низкой дисперсией суммы дисперсий голосов и явки выглядят как жирное пятно (окружность).
Поскольку все 4 раздела имеют одну и ту же структуру, я подробно разберу лишь один из них: «Результаты с низкой дисперсией процента голосов 'За'».
В подразделе «Таблица результатов» приведены топ 50 ТИКов с малой дисперсией по проценту голосов 'За'.
В подразделе «Детальная визуализация отдельных ТИКов», я привожу пример как можно визуализировать отдельные ТИКи. Для этого надо выбрать соответствующий индекс (цифра в первой колонке) и присвоить переменной id_num её значение. Для примера можно посмотреть id_num 1616, 1995 или 2165.
Значение id_num=1616 соответствует ТИКу 33 Пермь, Орджоникидзевская в Пермском крае
(ссылка на страницу ЦИК). В табличной форме он выглядит так:
При том что каждый УИК имеет под тысячу избирателей, голоса «За» (колонка yes_pct) распределились невероятно близко к 71.9%. На графиках это выглядит так:
Наконец, в подразделе «Визуализация всех аномальных результатов» отсортируем ТИКи по величине разброса голосов «За». Затем визуализируем и сохраним 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://www.golosinfo.org/.
Визуализация результатов референдума:
https://elections.dekoder.org/ru/russia/constitution/2020/
https://www.electoral.graphics/ru-ru/
Ассоциации наблюдателей: https://constitution.observer/
p.s. Спасибо всем кто прочитал. Надеюсь, вы смогли проверить выкладки автора своими руками, используя открытые код и данные. Отдельная благодарность tyomitch и Lissov за остроумные комментарии.
Biga
А можете сделать такие же графики для «против»? Не будет ли там таких же аномалий?
masai
Какие именно?
Гистограмму? Она будет точно такой же, только симметрично отражённой. То есть, перекос по-прежнему в сторону «за».
В остальных графиках есть и «за», и «против».
Biga
Не очень понятен ваш ответ.
Вот есть график «плотность распределения 'за'» к «проценту явки». То есть в каждой колонке доля «за» для данной явки из общего количества «за». По идее, из такого графика никак не вывести такой же график для «против». Поэтому хотелось бы его увидеть.
По логике, если голоса «против» не вбрасывались, то график должен быть без аномалий. А если голоса «против» тоже вбрасывались для увеличения явки, но, в меньшем соотношении, например, 25/75 для сохранения более высокого процента «за», то мы увидим аномалии, но меньшего размера.
Надеюсь, я правильно всё понимаю.
masai
Это был не ответ, а уточняющий вопрос. :) Вообще, такие графики очень легко строить, если есть опыт работы с Pandas, так как в начале статьи есть ссылка на тетрадку с расчётами.
Нет такого графика или я не понимаю о чём вы.
Есть гистограммы для «За» и для явки (после слов
И гистограммы голосов «За» и явки
). Это именно гистограммы. То есть, высота столбика — это количество «за»/значений явки, попавших в диапазон. Для «против» график просто симметрично отразится.Есть график с точками % за / % явки (для ТИК 33 Пермь). Для «против» он тоже будет симметрично отражён, так как % против = 100 — % за.
anonymous Автор
biga Интересующие вас пропорции 'Да' и 'Нет' в добавленных бюллетенях иногда можно оценить из сообщений наблюдателей.
Наблюдатели Петербурга задокументировали приписку ~1000 голосов на УИК 2167
(протокол). Проведя всю арифметику, получается что доля ‘Да’ в добавленных голосах 90%, а доля 'Нет' 10%. Та же пропорция на УИК 1407 в Спб. Все прилично. Культурная столица как-никак.
Про феномен приписки 1000 голосов можно почитать в Yaroslav Sobolev blog или как еще один частный случай тут .