[Из песочницы] Визуальное представление выборов в Санкт-Петербурге — магия накрутки голосов

Привет!

В сентябре этого (2019) года прошли выборы Губернатора Санкт-Петербурга. Все данные о голосовании находятся в открытом доступе на сайте избирательной комиссии, мы не будем ничего ломать, а просто визуализируем информацию с этого сайта www.st-petersburg.vybory.izbirkom.ru в нужном для нас виде, проведем совсем несложный анализ и определим некоторые «волшебные» закономерности.

Обычно для подобных задач я использую Google Colab. Это сервис, который позволяет запускать Jupyter Notebook’и, имея доступ к GPU (NVidia Tesla K80) бесплатно, это заметно ускорит пирсинг данных и их дальнейшую обработку. Мне понадобились некоторые подготовительные работы перед импортом.

%%time 
!apt update
!apt upgrade
!apt install gdal-bin python-gdal python3-gdal 
# Install rtree - Geopandas requirment
!apt install python3-rtree 
# Install Geopandas
!pip install git+git://github.com/geopandas/geopandas.git
# Install descartes - Geopandas requirment
!pip install descartes


Далее импорты.

import requests 
from bs4 import BeautifulSoup 
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import geopandas as gpd
import xlrd


Описание используемых библиотек


  • requests — модуль для запроса на подключение к сайту


  • BeautifulSoup — модуль для парсинга html и xml документов; позволяет получить доступ напрямую к содержимому любых тегов в html


  • numpy — математический модуль с базовым и необходимым набором математических функций


  • pandas — библиотека для анализа данных


  • matplotlib.pyplot — модуль-набор методов построения


  • geopandas — модуль для построения карты выборов


  • xlrd — модуль для чтения табличных файлов


Настал момент собирать сами данные, парсим. Избирком позаботился о нашем времени и предоставил отчетность в таблицах, это удобно.

### Parser

list_of_TIKS = []
for i in range (1, 31):
    list_of_TIKS.append('Территориальная избирательная комиссия №' + str(i))

num_of_voters = []
num_of_voters_voted = []
appearence = []
votes_for_Amosov_percent = []
votes_for_Beglov_percent = []
votes_for_Tikhonova_percent = []

url = "http://www.st-petersburg.vybory.izbirkom.ru/region/region/st-petersburg?action=show&root=1&tvd=27820001217417&vrn=27820001217413®ion=78&global=&⊂_region=78&prver=0&pronetvd=null&vibid=27820001217417&type=222"
response = requests.get(url)
page = BeautifulSoup(response.content, "lxml")

main_links = page.find_all('a')
for TIK in list_of_TIKS:
    for main_tag in main_links:
        main_link = main_tag.get('href')
        if TIK in main_tag:
            current_TIK = pd.read_html(main_link, encoding='cp1251',  header=0)[7]
            
            num_of_voters.extend(int(current_TIK.iloc[0,i]) for i in range (len(current_TIK.columns)))
            num_of_voters_voted.extend(int(current_TIK.iloc[2,i]) + int(current_TIK.iloc[3,i]) for i in range (len(current_TIK.columns)))
            appearence.extend(round((int(current_TIK.iloc[2,i]) + int(current_TIK.iloc[3,i]))/int(current_TIK.iloc[0,i])*100, 2) for i in range (len(current_TIK.columns)))
            
            votes_for_Amosov_percent.extend(round(float(current_TIK.iloc[12,i][-6]+current_TIK.iloc[12,i][-5]+current_TIK.iloc[12,i][-4]+current_TIK.iloc[12,i][-3]+current_TIK.iloc[12,i][-2]),2) for i in range (len(current_TIK.columns)))
            votes_for_Beglov_percent.extend(round(float(current_TIK.iloc[13,i][-6]+current_TIK.iloc[13,i][-5]+current_TIK.iloc[13,i][-4]+current_TIK.iloc[13,i][-3]+current_TIK.iloc[13,i][-2]),2) for i in range (len(current_TIK.columns)))
            votes_for_Tikhonova_percent.extend(round(float(current_TIK.iloc[14,i][-6]+current_TIK.iloc[14,i][-5]+current_TIK.iloc[14,i][-4]+current_TIK.iloc[14,i][-3]+current_TIK.iloc[14,i][-2]),2) for i in range (len(current_TIK.columns)))


Итак, то, о чем и шла речь. Данные в Google Colab собираются шустро, но их не так уж и много.

Прежде, чем строить различные графики и карты, нам хорошо иметь представление, что мы называем «датасетом».

Разбор данных избиркома


По городу Санкт-Петербургу расположены 30 территориальных комиссий к ним же в 31-ый столбец относим цифровые избирательные участки.

image

В каждой территориальной комиссии несколько десятков УИКов (участковых избирательных комиссий).

image

Основное, что нас интересует, это явка на каждом избирательном участке, и какие зависимости мы можем наблюдать. Я буду отталкиваться от следующих:

  • зависимость явки и количества избирательных участков;


  • зависимость процента голосов за кандидатов от явки;


  • зависимость явки от количества избирателей на участке.


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

Постоим то, что придумали.

### Plots Data

#Votes in percent (appearence) - no need in extra computations

#Appearence (num of voters) - no need in extera computations

#Number of UIKs (appearence)
interval = 1

interval_num_of_UIKs = []

for i in range (int(100/interval+1/interval)):
    interval_num_of_UIKs.append(0)

for i in range (0, int(100/interval+1/interval), interval):
    for j in range (len(appearence)):
        if appearence[j] < (i + interval/2) and appearence[j] >= (i - interval/2):
            interval_num_of_UIKs[i] = interval_num_of_UIKs[i] + 1
### Plotting

#Number of UIKs (appearence)

plt.figure(figsize=(10, 6))
plt.plot(interval_num_of_UIKs)
plt.axis([0, 100, 0, 200])
plt.ylabel('Number of UIKs in a 1% range')
plt.xlabel('Appearence')
plt.show()

#Votes in percent (appearence)

plt.figure(figsize=(10, 10))
plt.scatter(appearence, votes_for_Amosov_percent, c = 'g', s = 6)
plt.scatter(appearence, votes_for_Beglov_percent, c = 'b', s = 6)
plt.scatter(appearence, votes_for_Tikhonova_percent, c = 'r', s = 6)
plt.ylabel('Votes in % for each candidate')
plt.xlabel('Appearence')
plt.show()

#Appearence (num of voters)

plt.figure(figsize=(10, 6))
plt.scatter(num_of_voters, appearence, c = 'y', s = 6)
plt.ylabel('Appearence')
plt.xlabel('Number of voters registereg in UIK')
plt.show()


Зависимость явки и количества избирательных участков

image

Зависимость процента голосов за кандидатов от явки

  • «зеленый» — голоса за Амосова


  • «синий» — за Беглова


  • «красный» — за Тихонову


image

Зависимость явки от количества избирателей на участке

image

Построения вполне сносные, но в ходе работы выяснилось, что в среднем на участке 400 человек и процент за Беглова от 50 до 70, но есть два участка с явкой >1200 чел и процентом 90±0.2. Интересно, что такое произошло на этих участках. Какие-то фантастические агитаторы поработали? Или просто подвезли 10 автобусов людей и заставили голосовать? Так или иначе, мы взволнованы, небольшое такое расследование получается. Но нам еще карты рисовать. Продолжим.

Визуальное представление и работа с geopandas

### Extra data for visualization: appearence and number of voters by municipal districts

current_UIK = pd.read_html(url, encoding='cp1251',  header=0)[7]

num_of_voters_dist = []
num_of_voters_voted_dist = []
appearence_dist = []

for j in [num_of_voters_dist, num_of_voters_voted_dist, appearence_dist]:
    j.extend(0 for i in range (18))

districts = {
    '0' : [1], #Адмиралтейский
    '1' : [2], #Василеостровский 
    '2' : [18], #Петроградский
    '3' : [16, 30], #Центральный
    '4' : [10, 14, 22], #Выборгский
    '5' : [11, 17], #Калининский
    '6' : [4, 25], #Красногвардейский
    '7' : [5, 24], #Невский
    '8' : [23, 29], #Фрунзенский
    '9' : [9, 12, 28], #Приморский
    '10' : [13], #Курортный
    '11' : [15], #Кронштадтский
    '12' : [21], #Колпинский
    '13' : [20], #Пушкинский
    '14' : [19, 27], #Московский
    '15' : [3, 7], #Кировский
    '16' : [6, 26], #Красносельский
    '17' : [8] #Петродворцовый
}

for i in districts.keys():
    for k in range (1, 31):
        if k in districts[i]:
            num_of_voters_dist[int(i)]= num_of_voters_dist[int(i)] + int(current_UIK.iloc[0,k-1])
            num_of_voters_voted_dist[int(i)] = num_of_voters_voted_dist[int(i)] + int(current_UIK.iloc[2,k-1]) + int(current_UIK.iloc[3,k-1])

for i in range (18):
    appearence_dist[i] = round(num_of_voters_voted_dist[i]/num_of_voters_dist[i]*100, 2)
### GeoDataFrame 

SPb_shapes= gpd.read_file('./shapes/Administrative_Discrits.shp', encoding='cp1251')

temp = pd.DataFrame({'Количество избирателей': num_of_voters_dist, 'Явка':appearence_dist })
temp['Район'] = SPb_shapes[['Район']]
temp['geometry'] = SPb_shapes[['geometry']]
SPB_elections_visualization = gpd.GeoDataFrame(temp)
### Colored districts

SPB_elections_visualization.plot(column = 'Район', linewidth=0, cmap='plasma', legend=True, figsize=[15,15])


gqaewvkqs-avcijtoydeotxdkkq.png

Окрасили административные районы города и подписали их, выглядит привычно, похоже на Питер, но Невы все-таки не хватает.

Количество избирателей

### Number of voters gradient

SPB_elections_visualization.plot(column = 'Количество избирателей', linewidth=0, cmap='plasma', legend=True, figsize=[15,15])


6tjjrtsv8inbg4gjzuvyjcwayty.png

Явка

### Appearence gradient

SPB_elections_visualization.plot(column = 'Явка', linewidth=0, cmap='plasma', legend=True, figsize=[15,15])


c2eesdfpqud3miyz5tlkhxmc6js.png

Заключение


Можно долго развлекаться с данными, использовать их в разных сферах и, конечно же, получать определенную пользу, для этого они и существуют. С помощью простых и сложных инструментов визуализации геоданных можно делать прекрасные вещи. О своих успехах пишите в коменты!

© Habrahabr.ru