Les résultats du premier tour de l'élection législative 2017

Ce billet a été écrit à l'aide d'un notebook Jupyter. Son contenu est sous licence BSD. Une vue statique de ce notebook peut être consultée et téléchargée ici : 20170614_Legislatives.ipynb.

Dans ce billet, nous allons nous intéresser aux résultats du premier tour des législatives 2017. Ce billet fait suite à mes billets précédents sur les élections présidentielles (premier tour, deuxième tour) et utilise les mêmes techniques pour aspirer le site du ministère de l'intérieur, disponibles ici : http://elections.interieur.gouv.fr/legislatives-2017.

Une fois les données obtenues, nous essaierons de tracer des courbes intéressantes pour analyser les résultats. En fin de billet, nous proposerons un modèle probabiliste pour donner des bornes sur les résultats à attendre à l'issue du deuxième tour des législatives le 18 juin 2017.

Téléchargement des données depuis le site du ministère de l'intérieur

In [1]:
# import des packages 
from bs4 import BeautifulSoup
import requests
%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn')
import pandas as pd
from collections import OrderedDict
from scipy import stats
import numpy as np

Tout d'abord, allons aspirer le site officiel du ministère de l'intérieur.

In [2]:
url = 'http://elections.interieur.gouv.fr/legislatives-2017/'
In [3]:
soup = BeautifulSoup(requests.get(url).text, 'html.parser')

Les résultats sont organisés par circonscription. Trouvons les liens vers toutes les pages relatives aux circonscriptions. En explorant le code source HTML de la page, j'ai abouti à l'extraction ci-dessous :

In [4]:
links = [url + tag.attrs['href'][2:] for tag in soup.find_all('a', class_='Style6')]

J'introduis une fonction qui garde en mémoire les pages téléchargées depuis le site du ministère avec un cache afin d'accélérer les extractions qui suivront.

In [5]:
from functools import lru_cache

@lru_cache(maxsize=None)
def fetch_page(url):
    "Fetches url for webpage."
    r = requests.get(url)
    return r
In [6]:
circo_links = []
for link in links:
    soup = BeautifulSoup(fetch_page(link).text, 'html.parser')
    circo_links += [url + tag.attrs['href'][3:] for tag in soup.find_all('a') if 'circonscription' in tag.text.lower()]

On vérifie qu'on a le bon nombre de circonscriptions, qui devrait être égal au 577 (nombre de députés à l'assemblée nationale).

In [7]:
len(circo_links)
Out[7]:
577

Maintenant, nous allons pouvoir extraire les tableaux qui figurent sur les différentes pages.

In [8]:
r = fetch_page(circo_links[0])
In [9]:
soup = BeautifulSoup(r.text, 'html.parser')

On peut trouver le titre de la circonscription assez facilement :

In [10]:
soup.find('h3').text.replace('\n', '').replace('\t', '').split(' circonscription')[0]
Out[10]:
'Ain (01) - 1ère'

On peut récuperer une première table sur les stats liés à la circonscription.

In [11]:
table = soup.find_all('tbody')[1]
votes = OrderedDict()
for row in table.find_all('tr'):
    votes[row.td.text] = int(row.td.next_sibling.next_sibling.text.replace(' ', ''))
In [12]:
pd.Series(votes).to_frame()
Out[12]:
0
Inscrits 82694
Abstentions 42063
Votants 40631
Blancs 545
Nuls 155
Exprimés 39931

Ainsi que les résultats par candidat.

In [13]:
table = soup.find_all('tbody')[0]
candidates = OrderedDict()
for row in table.find_all('tr'):
    candidates[row.td.text] = []
    for td in row.find_all('td')[1:]:
        stripped = td.text.strip().replace(',', '.').replace(' ', '')
        candidates[row.td.text].append(stripped)
In [14]:
pd.DataFrame(candidates).transpose()
Out[14]:
0 1 2 3 4
M. Laurent MALLET MDM 13534 16.37 33.89 Ballotage*
M. Xavier BRETON LR 10693 12.93 26.78 Ballotage*
M. Jérôme BUISSON FN 6174 7.47 15.46 Non
Mme Fabrine MARTIN ZEMLIK FI 3874 4.68 9.70 Non
Mme Florence BLATRIX-CONTAT SOC 3687 4.46 9.23 Non
M. Jacques FONTAINE COM 656 0.79 1.64 Non
Mme Laurane RAIMONDO ECO 562 0.68 1.41 Non
Mme Maude LÉPAGNOT EXG 293 0.35 0.73 Non
Mme Marie CARLIER DIV 247 0.30 0.62 Non
M. Gilbert BONNOT DIV 211 0.26 0.53 Non

On peut maintenant écrire une fonction qui rassemble ces extractions.

In [15]:
def extract_circo_data(url):
    "Returns data extracted from url: name of circonscription, candidates, votes."
    r = fetch_page(url)
    soup = BeautifulSoup(r.text, 'html.parser')
    circo_name = soup.find('h3').text.replace('\n', '').replace('\t', '').split(' circonscription')[0]
    table = soup.find_all('tbody')[0]
    candidates = OrderedDict()
    for row in table.find_all('tr'):
        candidates[row.td.text] = []
        for td in row.find_all('td')[1:]:
            stripped = td.text.strip().replace(',', '.').replace(' ', '')
            candidates[row.td.text].append(stripped)
    table = soup.find_all('tbody')[1]
    votes = OrderedDict()
    for row in table.find_all('tr'):
        votes[row.td.text] = int(row.td.next_sibling.next_sibling.text.replace(' ', ''))
    votes = pd.Series(votes).to_frame()
    votes.columns = [circo_name]
    return circo_name, pd.DataFrame(candidates).transpose(), votes

Cette routine d'extraction permet d'obtenir, pour chaque circonscription, la table des résultats par candidat, le nom de la circonscription et les données statistiques sur les votants (inscrits, abstentions, votants, blancs, nuls, exprimés). Prenons un exemple avec la 2ème circonscription de la Haute-Marne.

In [16]:
circo_name, candidates, votes = extract_circo_data(circo_links[245])
In [17]:
circo_name
Out[17]:
'Haute-Marne (52) - 2ème'
In [18]:
votes
Out[18]:
Haute-Marne (52) - 2ème
Inscrits 61604
Abstentions 32008
Votants 29596
Blancs 441
Nuls 145
Exprimés 29010
In [19]:
candidates
Out[19]:
0 1 2 3 4
M. François CORNUT-GENTILLE LR 9808 15.92 33.81 Ballotage*
M. Frédéric FABRE FN 8431 13.69 29.06 Ballotage*
M. Vincent BERTHET REM 6148 9.98 21.19 Non
M. Daniel MONNIER FI 2380 3.86 8.20 Non
M. Antoine DESFRETIER SOC 966 1.57 3.33 Non
Mme Valérie ROFFIDAL ECO 466 0.76 1.61 Non
M. Edouard GONZALEZ COM 414 0.67 1.43 Non
Mme Anne HALIN EXG 214 0.35 0.74 Non
Mme Laurence OLIVIER DIV 183 0.30 0.63 Non

Maintenant passons aux graphiques que nous allons pouvoir tracer à partir de ces données brutes par circonscription.

Analyse des statistiques du vote par circonscription

Réunissons dans un premier temps tous les tableaux de données obtenus concernants les votes.

In [20]:
all_votes_data = [extract_circo_data(url)[2] for url in circo_links]
In [21]:
all_votes = pd.concat(all_votes_data, axis=1)
In [22]:
all_votes
Out[22]:
Ain (01) - 1ère Ain (01) - 2ème Ain (01) - 3ème Ain (01) - 4ème Ain (01) - 5ème Aisne (02) - 1ère Aisne (02) - 2ème Aisne (02) - 3ème Aisne (02) - 4ème Aisne (02) - 5ème ... Français établis hors de France (99) - 2ème Français établis hors de France (99) - 3ème Français établis hors de France (99) - 4ème Français établis hors de France (99) - 5ème Français établis hors de France (99) - 6ème Français établis hors de France (99) - 7ème Français établis hors de France (99) - 8ème Français établis hors de France (99) - 9ème Français établis hors de France (99) - 10ème Français établis hors de France (99) - 11ème
Inscrits 82694 93520 75614 89390 75359 72345 73981 68099 79116 82223 ... 75029 120696 122765 91374 127486 105955 121399 107796 99374 92766
Abstentions 42063 47291 41131 45625 38409 36770 39857 35369 43878 41864 ... 63414 95202 94943 76810 101742 78999 109986 92085 79955 67141
Votants 40631 46229 34483 43765 36950 35575 34124 32730 35238 40359 ... 11615 25494 27822 14564 25744 26956 11413 15711 19419 25625
Blancs 545 471 359 521 374 517 685 639 530 609 ... 36 43 68 71 47 50 41 140 175 93
Nuls 155 160 116 211 168 173 270 287 208 251 ... 79 81 90 48 92 332 72 122 79 152
Exprimés 39931 45598 34008 43033 36408 34885 33169 31804 34500 39499 ... 11500 25370 27664 14445 25605 26574 11300 15449 19165 25380

6 rows × 577 columns

Commençons par agréger les résultats en les sommant.

In [23]:
all_votes.sum(axis=1).to_frame(name='France entière')
Out[23]:
France entière
Inscrits 47570988
Abstentions 24403480
Votants 23167508
Blancs 357018
Nuls 156326
Exprimés 22654164

On peut représenter ceci sous forme d'un graphique.

In [24]:
all_votes.sum(axis=1).to_frame(name='France entière').plot.bar()
Out[24]:
<matplotlib.axes._subplots.AxesSubplot at 0x113248e10>

Comme cela a été signalé dans la presse, il y a eu plus d'abstentions que de votants. On remarque qu'il y a beaucoup moins de blancs et de nuls que lors de la présidentielle (2,5 millions de blancs au deuxième tour de la présidentielle). 10 millions de Français de plus se sont abstenus d'aller voter qu'au deuxième tour de la présidentielle.

Regardons maintenant quel graphique nous obtenons si nous faisons un graphique pour toutes les circonscriptions.

In [25]:
fig, ax = plt.subplots(figsize=(8, 75), dpi=100)
all_votes.transpose()[['Abstentions', 'Blancs', 'Nuls', 'Exprimés']].iloc[::-1].plot.barh(stacked=True, ax=ax, width=1.0)
Out[25]:
<matplotlib.axes._subplots.AxesSubplot at 0x113292f98>