Loi de Benford et suspicion de fraudes aux présidentielles des USA en 2016
1 Loi de Benford et suspicion de fraudes aux présidentielles des USA en 2016
Source : problème 9 de “Python & Pandas et les 36 problèmes de Data Science”, Frédéric Bro et Chantal Remy
La loi de Benford est une loi empirique qui concerne la distribution des premiers chiffres significatifs dans de nombreux ensembles de données réels. Elle prédit que, dans de nombreux ensembles de données réels, le premier chiffre significatif suit une distribution logarithmique, c’est-à-dire que le chiffre 1 apparaît en premier 30% du temps, le chiffre 2 17,6% du temps, le chiffre 3 12,5% du temps, etc. Une loi analogue est valable pour le second chiffre significatif. De nos jours, pour vérifier l’authenticité des données, on peut vérifier si elles suivent la loi de Benford. Si ce n’est pas le cas, cela peut être un signe de fraude.
Lors des élections présidentielles américaines de 2016, des chercheurs ont appliqué la loi de Benford aux résultats des élections dans les différents États. Ils ont constaté que les résultats des élections dans certains États ne suivaient pas la loi de Benford et ont suggéré que cela pourrait être dû à des fraudes électorales. Ceci incita les candidats Hilary Clinton et Jill Stein à demander un comptage manuel dans certains comtés.
Dans cet exercice, vous allez vérifier si les résultats des élections dans les différents États des États-Unis en 2016 suivent la loi de Benford. Pour cela, vous allez utiliser les données des élections présidentielles américaines de 2016, disponibles dans le fichier benford_usa.csv
.
Quelques liens pour en savoir plus sur la loi de Benford :
1.1 Exercice
Loi de Benford
Tracer la fréquence relative du second chiffre significatif \(c\in\{0,1,\dots,9\}\) dans la représentation en base \(10\) prédite par la loi de Benford qui est donnée par la formule suivante : \[ p(c) = \sum_{i=1}^9 \log_{10}\left(1+\frac{1}{10i+c}\right). \]Ouverture et séléction des données.
- Le fichier
benford_usa.csv
contient le nombre de voix remportées par les 3 candidats dans chaque districts des États-Unis. Importer le fichier, et analyser brièvement les données. Bonus : retrouver le résultats des élections en combinant avec les nombre de grands électeurs par état. Cf. wikipedia - Ne garder ensuite que les lignes correspondantes aux comptés du Michigan, de Pennsylvanie et du Wisconsin. Parmis ces lignes, ne garder que celles où chaque candidat à obtenu plus de 10 voix. C’est ces données que nous allons analyser.
- Le fichier
Analyse des données
En théorie, la répartition des fréquences du second chiffre des nombres de voix suit la loi de Benford. Pour chaque candidat, on va vérifier si c’est le cas.
Calculer la fréquence du second chiffre des nombres de voix pour chaque candidat et comparer avec la loi de Benford (on crééera un nouveau DataFrame où chaque ligne correspond à un chiffre et où chaque colonne correspond à un candidat; on ajoutera de plus la colonne des fréquences théoriques).
Afficher la distribution de chaque colonne.
Bonus : simulation pour la détection d’éventuelles anomalies
Ces graphes peuvent susciter des doutes quant à la réguarité des résultats. Pour aller plus loin nous allons introduire un indicateur syntetique pour résumer ces analyse: on calcule la distance euclidienne entre la colonne théoriques et la colonne effective pour chqua candidat.
On remaqruqe que l’indicateur atteste d’un comportement “extreme” mais est-ce si rare ? Pour cela, nous allons simuler \(10^4\) votations qui suivent la distributions théorique de Benford et nous allons vérifier si ce résultat est effectivement très rare ou non.
Importation des librairies nécessaires.
1.2 Loi de Benford
La fonction théorique \(p\) qui donne la fréquence du second chiffre significatif dans la représentation en base \(10\) prédite par la loi de Benford est donnée par:
# Générer les chiffres de 0 à 9
cc = np.arange(0,10)
# Fonction pour calculer la probabilité
p = lambda c : sum( [ np.log10( 1+1/(10*i+c)) for i in range( 1 , 10 ) ] )
# Calculer les probabilités pour chaque valeur de cc
FB = [ p(c) for c in cc ]
# Affichage des résultats dans un DataFrame
df = pd.DataFrame({'Chiffre':cc,'Probabilité':FB})
# Transformer les valeurs en DataFrame et utiliser la première colonne comme index
df = df.set_index('Chiffre') # Notez que set_index renvoie un nouveau DataFrame
display(df)
# Vérification que FB est normalisée
display(df['Probabilité'].sum())
# Tracer les résultats
sns.barplot(x='Chiffre',y='Probabilité',data=df);
Probabilité | |
---|---|
Chiffre | |
0 | 0.119679 |
1 | 0.113890 |
2 | 0.108821 |
3 | 0.104330 |
4 | 0.100308 |
5 | 0.096677 |
6 | 0.093375 |
7 | 0.090352 |
8 | 0.087570 |
9 | 0.084997 |
0.9999999999999997
1.3 Ouverture et séléction des données
On importe les données à partir d’un fichier csv
. Par défault le séparateur d’éléments est “,
” mais dans ce fichier c’est “;
” qui a été utilisé, il faut alors l’indiquer explicitement.
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3143 entries, 0 to 3142
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 State 3143 non-null object
1 ST 3143 non-null object
2 Fips 3143 non-null int64
3 County 3143 non-null object
4 Trump 3143 non-null int64
5 Clinton 3143 non-null int64
6 Stein 3143 non-null int64
dtypes: int64(4), object(3)
memory usage: 172.0+ KB
State | ST | Fips | County | Trump | Clinton | Stein | |
---|---|---|---|---|---|---|---|
0 | Georgia | GA | 13089 | DeKalb County, Georgia | 47531 | 239131 | 0 |
1 | Texas | TX | 48487 | Wilbarger County, Texas | 3166 | 807 | 13 |
2 | Virginia | VA | 51111 | Lunenburg County, Virginia | 3206 | 2226 | 25 |
3 | Georgia | GA | 13297 | Walton County, Georgia | 31093 | 8279 | 0 |
4 | North Carolina | NC | 37011 | Avery County, North Carolina | 6226 | 1670 | 0 |
5 | North Carolina | NC | 37069 | Franklin County, North Carolina | 16320 | 12811 | 0 |
6 | Michigan | MI | 26037 | Clinton County, Michigan | 21635 | 16490 | 379 |
7 | Arkansas | AR | 5045 | Faulkner County, Arkansas | 29170 | 14525 | 507 |
8 | North Carolina | NC | 37025 | Cabarrus County, North Carolina | 53224 | 35048 | 0 |
9 | Idaho | ID | 16025 | Camas County, Idaho | 410 | 110 | 15 |
Les colonnes “ST”, “Fips” et “County” ne sont pas utiles pour notre analyse, on peut les supprimer.
State | Trump | Clinton | Stein | |
---|---|---|---|---|
0 | Georgia | 47531 | 239131 | 0 |
1 | Texas | 3166 | 807 | 13 |
2 | Virginia | 3206 | 2226 | 25 |
3 | Georgia | 31093 | 8279 | 0 |
4 | North Carolina | 6226 | 1670 | 0 |
... | ... | ... | ... | ... |
3138 | Tennessee | 9525 | 2425 | 36 |
3139 | Michigan | 224589 | 176238 | 3886 |
3140 | West Virginia | 3516 | 1315 | 47 |
3141 | North Carolina | 6225 | 1585 | 0 |
3142 | Maryland | 7938 | 5695 | 94 |
3143 rows × 4 columns
Combien d’états sont dans le fichier ? Lesquels ? Affichons la liste des états (sans doublons):
array(['Georgia', 'Texas', 'Virginia', 'North Carolina', 'Michigan',
'Arkansas', 'Idaho', 'Florida', 'Tennessee', 'Kentucky', 'Alabama',
'Colorado', 'Missouri', 'Ohio', 'Pennsylvania', 'South Dakota',
'Louisiana', 'Wisconsin', 'Minnesota', 'Indiana', 'Maryland',
'Kansas', 'New York', 'Mississippi', 'North Dakota', 'Iowa',
'California', 'Wyoming', 'New Mexico', 'Montana', 'Utah',
'Oklahoma', 'Alaska', 'Oregon', 'Illinois', 'Hawaii',
'South Carolina', 'West Virginia', 'Arizona', 'Maine',
'New Jersey', 'Connecticut', 'Nebraska', 'Washington', 'Nevada',
'Massachusetts', 'Rhode Island', 'New Hampshire', 'Delaware',
'Vermont', 'District of Columbia'], dtype=object)
Combien de voix ont été obtenues par chaque candidat au total ?
# On affiche le nombre total de votes pour chaque candidat, i.e. la somme des colonnes Trump, Clinton et Stein
cols = ['Trump','Clinton','Stein']
pour_affichage = pd.DataFrame( Tot[cols].sum().sort_values(ascending=False),columns=['Nombre de votes'])
pour_affichage['Pourcentage'] = pour_affichage / pour_affichage.sum() * 100
# pour un affichage plus joli
pour_affichage = pour_affichage.style.format({'Pourcentage':'{:.2f} %'})
pour_affichage
Nombre de votes | Pourcentage | |
---|---|---|
Clinton | 62426228 | 50.02 % |
Trump | 61064602 | 48.93 % |
Stein | 1311595 | 1.05 % |
1.4 Bonus : pourquoi une fraude éventuelle dans ces trois états aurait changé le résultat de l’élection ?
Aux USA, ce n’est pas la majorité qui gagne mais le candidat qui obtient le plus de grands électeurs. Clinton avait obtenu plus de voix que Trump mais c’est ce dernier qui a été élu. En effet, chaque état a un nombre de grands électeurs qui lui est attribué et c’est le candidat qui remporte la majorité des voix dans cet état qui obtient tous les grands électeurs de cet état.
Voici le bilan des voix puis des grands électeurs obtenue par chaque candidat :
# Agréger les votes par état
votes_by_state = Tot.pivot_table(index='State', values=['Trump', 'Clinton', 'Stein'], aggfunc='sum')
votes_by_state
Clinton | Stein | Trump | |
---|---|---|---|
State | |||
Alabama | 718084 | 9287 | 1306925 |
Alaska | 0 | 0 | 0 |
Arizona | 936250 | 25255 | 1021154 |
Arkansas | 378729 | 9837 | 677904 |
California | 7362490 | 220312 | 3916209 |
Colorado | 1212209 | 33147 | 1137455 |
Connecticut | 884432 | 22793 | 668266 |
Delaware | 235581 | 6100 | 185103 |
District of Columbia | 260223 | 3995 | 11553 |
Florida | 4485745 | 64019 | 4605515 |
Georgia | 1837300 | 0 | 2068623 |
Hawaii | 266827 | 12727 | 128815 |
Idaho | 189677 | 8464 | 407199 |
Illinois | 2977498 | 74112 | 2118179 |
Indiana | 1031953 | 0 | 1556220 |
Iowa | 650790 | 11119 | 798923 |
Kansas | 414788 | 22698 | 656009 |
Kentucky | 628834 | 13913 | 1202942 |
Louisiana | 779535 | 14018 | 1178004 |
Maine | 354873 | 14075 | 334838 |
Maryland | 1497951 | 31839 | 873646 |
Massachusetts | 1964768 | 46910 | 1083069 |
Michigan | 2268193 | 50700 | 2279805 |
Minnesota | 1366676 | 36957 | 1322891 |
Mississippi | 462001 | 3580 | 678457 |
Missouri | 1054889 | 25086 | 1585753 |
Montana | 174521 | 7669 | 274120 |
Nebraska | 273858 | 8346 | 485819 |
Nevada | 537753 | 0 | 511319 |
New Hampshire | 348521 | 6416 | 345789 |
New Jersey | 2021756 | 35949 | 1535513 |
New Mexico | 380724 | 9729 | 315875 |
New York | 4143874 | 99895 | 2640570 |
North Carolina | 2162074 | 0 | 2339603 |
North Dakota | 93526 | 3769 | 216133 |
Ohio | 2317001 | 44310 | 2771984 |
Oklahoma | 419788 | 0 | 947934 |
Oregon | 934631 | 45132 | 742506 |
Pennsylvania | 2844705 | 48912 | 2912941 |
Rhode Island | 249902 | 6155 | 179421 |
South Carolina | 849469 | 12917 | 1143611 |
South Dakota | 114938 | 0 | 227460 |
Tennessee | 867110 | 15919 | 1517402 |
Texas | 3867816 | 71307 | 4681590 |
Utah | 274188 | 7695 | 452086 |
Vermont | 178179 | 6748 | 95053 |
Virginia | 1916845 | 27272 | 1731156 |
Washington | 1610524 | 51066 | 1129120 |
West Virginia | 187457 | 8000 | 486198 |
Wisconsin | 1380823 | 30934 | 1403694 |
Wyoming | 55949 | 2512 | 174248 |
On constate que pour l’Alaska les résultats des trois candidats (Trump, Clinton et Stein) sont tous à 0, cela signifie qu’il n’y a pas de votes enregistrés pour ces états dans le fichier. Y a-t-il d’autres états pour lesquels les résultats sont manquants ? Si oui, lesquels ? Pour détecter ces états spécifiquement, on peut filtrer les lignes où les totaux de votes pour ces candidats sont égaux à zéro.
# Filtrer les lignes où les votes sont tous à zéro pour Trump, Clinton et Stein
# On utilise sum(axis=1) qui calcule la somme des votes pour chaque ligne (chaque état)
mask = votes_by_state[['Trump', 'Clinton', 'Stein']].sum(axis=1) == 0
states_with_no_votes = votes_by_state[mask]
# Afficher les états avec 0 votes
states_with_no_votes
Clinton | Stein | Trump | |
---|---|---|---|
State | |||
Alaska | 0 | 0 | 0 |
Nous constatons que les résultats pour 1 état sont absents dans notre jeu de données. Cependant, nous poursuivrons l’analyse en supposant que toutes les données nécessaires sont présentes, sans tenir compte de cette omission.
Ajoutons maintenant le nombre de grands électeurs par état. Pour cela, nous allons utiliser un dictionnaire qui contient le nombre de grands électeurs par état.
# Dictionnaire des grands électeurs par état en 2016
electoral_votes = {
"Georgia": 16,
"Texas": 38,
"Virginia": 13,
"North Carolina": 15,
"Michigan": 16,
"Arkansas": 6,
"Idaho": 4,
"Florida": 29,
"Tennessee": 11,
"Kentucky": 8,
"Alabama": 9,
"Colorado": 9,
"Missouri": 10,
"Ohio": 18,
"Pennsylvania": 20,
"South Dakota": 3,
"Louisiana": 8,
"Wisconsin": 10,
"Minnesota": 10,
"Indiana": 11,
"Maryland": 10,
"Kansas": 6,
"New York": 29,
"Mississippi": 6,
"North Dakota": 3,
"Iowa": 6,
"California": 55,
"Wyoming": 3,
"New Mexico": 5,
"Montana": 3,
"Utah": 6,
"Oklahoma": 7,
"Alaska": 3,
"Oregon": 7,
"Illinois": 20,
"Hawaii": 4,
"South Carolina": 9,
"West Virginia": 5,
"Arizona": 11,
"Maine": 4,
"New Jersey": 14,
"Connecticut": 7,
"Nebraska": 5,
"Washington": 12,
"Nevada": 6,
"Massachusetts": 11,
"Rhode Island": 4,
"New Hampshire": 4,
"Delaware": 3,
"Vermont": 3,
"District of Columbia": 3,
}
# Ajouter les grands électeurs au DataFrame
votes_by_state['ElectoralVotes'] = electoral_votes
votes_by_state.head()
Clinton | Stein | Trump | ElectoralVotes | |
---|---|---|---|---|
State | ||||
Alabama | 718084 | 9287 | 1306925 | 9 |
Alaska | 0 | 0 | 0 | 3 |
Arizona | 936250 | 25255 | 1021154 | 11 |
Arkansas | 378729 | 9837 | 677904 | 6 |
California | 7362490 | 220312 | 3916209 | 55 |
# Trouver le gagnant pour chaque état
votes_by_state['Winner'] = votes_by_state[['Trump', 'Clinton', 'Stein']].idxmax(axis=1)
votes_by_state.head()
Clinton | Stein | Trump | ElectoralVotes | Winner | |
---|---|---|---|---|---|
State | |||||
Alabama | 718084 | 9287 | 1306925 | 9 | Trump |
Alaska | 0 | 0 | 0 | 3 | Trump |
Arizona | 936250 | 25255 | 1021154 | 11 | Trump |
Arkansas | 378729 | 9837 | 677904 | 6 | Trump |
California | 7362490 | 220312 | 3916209 | 55 | Clinton |
# Résultats finaux
final_results = votes_by_state.pivot_table(index='Winner', values='ElectoralVotes', aggfunc='sum')
final_results
ElectoralVotes | |
---|---|
Winner | |
Clinton | 233 |
Trump | 305 |
On voit que Trump a obtenu 305 grands électeurs contre 233 pour Clinton. Il a donc été élu président des USA. Cependant, si les résultats dans les états du Michigan, de Pennsylvanie et du Wisconsin avaient été différents, Clinton aurait obtenu plus de grands électeurs (et Trump moins). Aurait-elle été élue ?
# Calcul de la somme des grands électeurs des états impliqués dans la triche éventuelle
si_triche_verifiee = electoral_votes['Michigan'] + electoral_votes['Wisconsin'] + electoral_votes['Pennsylvania']
# Ajuster les résultats de Trump et Clinton
final_results.loc['Trump'] -= si_triche_verifiee # Déduire les grands électeurs pour Trump
final_results.loc['Clinton'] += si_triche_verifiee # Ajouter les grands électeurs à Clinton
# Afficher les résultats ajustés
final_results
ElectoralVotes | |
---|---|
Winner | |
Clinton | 279 |
Trump | 259 |
1.5 Séléction des données dans les états du Michigan, de Pennsylvanie et du Wisconsin
Nous allons regarder les données relatives aux états du Michigan, du Wisconsin et de Pennsylvanie. De plus, nous nous intéressons aux comtés où les candidats ont eu plus de 10 voix.
mask1 = (Tot['State'] == 'Michigan') | (Tot['State'] == 'Wisconsin') | (Tot['State'] == 'Pennsylvania')
mask2 = (Tot['Trump'] >= 10) & (Tot['Clinton'] >= 10) & (Tot['Stein'] >= 10)
# Nouveau DataFrame avec les lignes sélectionnées
Sel = Tot[mask1 & mask2].copy()
Sel
State | Trump | Clinton | Stein | |
---|---|---|---|---|
6 | Michigan | 21635 | 16490 | 379 |
12 | Michigan | 2158 | 1156 | 49 |
20 | Pennsylvania | 22676 | 6849 | 136 |
26 | Wisconsin | 17310 | 18524 | 583 |
33 | Wisconsin | 31044 | 17391 | 449 |
... | ... | ... | ... | ... |
3057 | Wisconsin | 39010 | 26476 | 641 |
3090 | Pennsylvania | 257488 | 363017 | 5021 |
3105 | Wisconsin | 46620 | 42506 | 834 |
3125 | Michigan | 7228 | 3973 | 116 |
3139 | Michigan | 224589 | 176238 | 3886 |
220 rows × 4 columns
Combien de bureaux de vote sont concernés par ces critères ?
Combien de voix ont été obtenues par chaque candidat au total dans ces trois états ?
cols = ['Trump','Clinton','Stein']
pour_affichage = pd.DataFrame( Sel[cols].sum().sort_values(ascending=False),columns=['Nombre de votes'])
pour_affichage['Pourcentage'] = pour_affichage / pour_affichage.sum() * 100
# pour un affichage plus joli
pour_affichage = pour_affichage.style.format({'Pourcentage':'{:.2f} %'})
pour_affichage
Nombre de votes | Pourcentage | |
---|---|---|
Trump | 6594676 | 49.89 % |
Clinton | 6492249 | 49.12 % |
Stein | 130535 | 0.99 % |
1.6 Analyse des données
Combien de votes aux total par candidat dans ces trois états ?
# Sel_sum = Sel.groupby('State')[cols].sum()
Sel_sum = Sel.pivot_table(index='State',values=cols,aggfunc='sum')
Sel_sum
Clinton | Stein | Trump | |
---|---|---|---|
State | |||
Michigan | 2268193 | 50700 | 2279805 |
Pennsylvania | 2844236 | 48908 | 2911446 |
Wisconsin | 1379820 | 30927 | 1403425 |
# Pourcentage de la distribution des votes dans chaque état
Sel_sum.div(Sel_sum.sum(axis=1), axis=0) * 100
Clinton | Stein | Trump | |
---|---|---|---|
State | |||
Michigan | 49.322504 | 1.102486 | 49.575010 |
Pennsylvania | 48.999774 | 0.842575 | 50.157651 |
Wisconsin | 49.031118 | 1.098973 | 49.869908 |
Nous allons ajouter une colonne par candidat qui contient le second chiffre du nombres de votes :
for candidat in ["Trump", "Clinton", "Stein"]:
Sel.loc[:, candidat + '_2chiffre'] = Sel[candidat].apply(lambda x: int(str(x)[1]))
Sel.head()
State | Trump | Clinton | Stein | Trump_2chiffre | Clinton_2chiffre | Stein_2chiffre | |
---|---|---|---|---|---|---|---|
6 | Michigan | 21635 | 16490 | 379 | 1 | 6 | 7 |
12 | Michigan | 2158 | 1156 | 49 | 1 | 1 | 9 |
20 | Pennsylvania | 22676 | 6849 | 136 | 2 | 8 | 3 |
26 | Wisconsin | 17310 | 18524 | 583 | 7 | 8 | 8 |
33 | Wisconsin | 31044 | 17391 | 449 | 1 | 7 | 4 |
On crée un DataFrame avec l’histogramme des secondes chiffres par candidat. Autrement dit, on compte le nombre de fois où chaque chiffre est apparu en deuxième position dans les nombres de votes.
Pour cela on utilise apply
qui applique une fonction donnée (ici, pd.Series.value_counts
) à chaque colonne du DataFrame résultant. La fonction pd.Series.value_counts
compte les occurrences uniques des valeurs dans une série (ici une colonne) et renvoie une série dont les indices sont les valeurs uniques présentes dans la colonne d’origine. Ces valeurs sont triées par défaut en ordre décroissant de fréquence.
# Première méthode
# Trump_histo = Sel['Trump_2chiffre'].value_counts()
# Clinton_histo = Sel['Clinton_2chiffre'].value_counts()
# Stein_histo = Sel['Stein_2chiffre'].value_counts()
Trump_histo = Sel['Trump_2chiffre'].value_counts(normalize=True)
Clinton_histo = Sel['Clinton_2chiffre'].value_counts(normalize=True)
Stein_histo = Sel['Stein_2chiffre'].value_counts(normalize=True)
His = pd.DataFrame({'Trump_histo': Trump_histo, 'Clinton_histo': Clinton_histo, 'Stein_histo': Stein_histo})
His = His.rename_axis('Chiffre')
display(His)
# Deuxième méthode
# cols = ['Trump_2chiffre', 'Clinton_2chiffre', 'Stein_2chiffre']
# # His = Sel[cols].apply(pd.Series.value_counts) / nb_bureaux
# His = Sel[cols].apply(lambda x: x.value_counts(normalize=True))
# His = His.rename_axis('Chiffre')
# His = His.rename(columns={"Trump_2chiffre": "Trump_histo", "Clinton_2chiffre": "Clinton_histo", "Stein_2chiffre": "Stein_histo"})
# display(His)
Trump_histo | Clinton_histo | Stein_histo | |
---|---|---|---|
Chiffre | |||
0 | 0.100000 | 0.086364 | 0.109091 |
1 | 0.140909 | 0.113636 | 0.136364 |
2 | 0.104545 | 0.122727 | 0.063636 |
3 | 0.109091 | 0.090909 | 0.095455 |
4 | 0.113636 | 0.104545 | 0.163636 |
5 | 0.104545 | 0.090909 | 0.027273 |
6 | 0.109091 | 0.122727 | 0.109091 |
7 | 0.095455 | 0.109091 | 0.063636 |
8 | 0.063636 | 0.095455 | 0.104545 |
9 | 0.059091 | 0.063636 | 0.127273 |
On ajoute la colonne Benford
avec les effectifs théoriques des seconde chiffres attendus par la loi de Benford :
# On ajoute la colonne Benford avec les effectifs théoriques
# His['Benford'] = { Chiffre : round(p(Chiffre)*nb_bureaux) for Chiffre in His.index}
His['Benford'] = { Chiffre : p(Chiffre) for Chiffre in His.index}
His
Trump_histo | Clinton_histo | Stein_histo | Benford | |
---|---|---|---|---|
Chiffre | ||||
0 | 0.100000 | 0.086364 | 0.109091 | 0.119679 |
1 | 0.140909 | 0.113636 | 0.136364 | 0.113890 |
2 | 0.104545 | 0.122727 | 0.063636 | 0.108821 |
3 | 0.109091 | 0.090909 | 0.095455 | 0.104330 |
4 | 0.113636 | 0.104545 | 0.163636 | 0.100308 |
5 | 0.104545 | 0.090909 | 0.027273 | 0.096677 |
6 | 0.109091 | 0.122727 | 0.109091 | 0.093375 |
7 | 0.095455 | 0.109091 | 0.063636 | 0.090352 |
8 | 0.063636 | 0.095455 | 0.104545 | 0.087570 |
9 | 0.059091 | 0.063636 | 0.127273 | 0.084997 |
On affiche la distribution des seconds chiffres par candidat.
# His.plot(figsize=(15,5))
# plt.xticks(range(10));
# His.plot.bar(figsize=(15,5));
plt.figure(figsize=(10,5))
plt.subplot(2,2,1)
sns.barplot(data=His, x=His.index, y='Trump_histo', color='blue', alpha=0.5)
plt.subplot(2,2,2)
sns.barplot(data=His, x=His.index, y='Clinton_histo', color='red', alpha=0.5)
plt.subplot(2,2,3)
sns.barplot(data=His, x=His.index, y='Stein_histo', color='green', alpha=0.5)
plt.subplot(2,2,4)
sns.barplot(data=His, x=His.index, y='Benford', color='black', alpha=0.5)
plt.tight_layout();
Pour chaque candidat, on calcule la distance euclidienne entre les effectifs théoriques et les effectifs de chaque candidats. Si A et B sont deux listes de même longueur, la distance euclidienne entre A et B est donnée par : \[\text{dist}(A,B) = || A-B ||_2 =\sqrt{\sum_{c=0}^9 \left( A_c - B_c \right)^2}.\] Dans numpy, cela se calcule simplement par np.linalg.norm(A-B)
.
# Dictionnaire pour stocker les distances
D = {}
# Pour chaque candidat, calculer la norme euclidienne entre les colonnes
for candidat in ["Trump_histo", "Clinton_histo", "Stein_histo"]:
D[candidat] = np.linalg.norm(His[candidat] - His['Benford'])
D
{'Trump_histo': 0.05398891120857391,
'Clinton_histo': 0.05714784909840473,
'Stein_histo': 0.12083530757488783}
Selon ces valeurs, le candidat Stein semble légitime à contester les résultats. Mais est-ce effectivement si rare d’obtenir une telle distance ?
1.7 Bonus : Simulation pour la détection d’éventuelles anomalies
Pour commencer, on simule 10 votations qui suivent la loi de Benford.
# Créer une copie de Sel et ajouter une colonne avec des chiffres simulés selon FB
Sel_copy = Sel.copy()
for test in ["test"+str(i) for i in range(1,11)]:
# Générer nb_bureaux chiffres aléatoires selon la distribution FB
Sel_copy[test] = np.random.choice(range(10), size=nb_bureaux, p=FB)
# Calculer les effectifs pour la colonne simulée
# His[test] = Sel_copy[test].value_counts()
His[test] = Sel_copy[test].value_counts(normalize=True)
# Calculer la norme entre les effectifs simulés et ceux de Benford
D[test] = np.linalg.norm(His[test] - His['Benford'])
# Affichage des distances un peu plus joli
pd.DataFrame( index=D.keys(), data=D.values(), columns=['Distance']).sort_values(by='Distance')
Distance | |
---|---|
Trump_histo | 0.053989 |
test8 | 0.056365 |
Clinton_histo | 0.057148 |
test3 | 0.057500 |
test10 | 0.058188 |
test6 | 0.063843 |
test7 | 0.064035 |
test2 | 0.073833 |
test9 | 0.074478 |
test4 | 0.074537 |
test1 | 0.074738 |
test5 | 0.074909 |
Stein_histo | 0.120835 |
Bien que la valeur semble effectivement élevée pour Stein, il est possible que cette valeur ne soit pas si rares. En effet, dans nos 10 simulations, nous avons parfois obtenu des valeurs de distance euclidienne presque aussi élevées que celles observées pour Stein. Pour comprendre s’il s’agit d’evenement rares, on simule \(10^4\) fois pour voir combien de fois D['test']>D[candidat]
:
distance_Stein = D['Stein_histo']
distance_Clinton = D['Clinton_histo']
distance_Trump = D['Trump_histo']
cpt_Stein = 0
cpt_Clinton = 0
cpt_Trump = 0
N = 10_000
for i in range(N):
test = pd.Series(np.random.choice(range(10), size=nb_bureaux, p=FB)) # le chiffre i apparait avec une probabilité FB[i] et ce pour 220 bureaux de vote
# test_histo = test.value_counts()
test_histo = test.value_counts(normalize=True)
D_test = np.linalg.norm( test_histo - His['Benford'])
if D_test > distance_Stein:
cpt_Stein += 1
if D_test > distance_Clinton:
cpt_Clinton += 1
if D_test > distance_Trump:
cpt_Trump += 1
print(f"""Sur {N} simulations,
{cpt_Stein} fois on a obtenu une distance supérieure à celle de Stein, soit {cpt_Stein/N*100:.2f}% des fois;
{cpt_Clinton} fois on a obtenu une distance supérieure à celle de Clinton, soit {cpt_Clinton/N*100:.2f}% des fois;
{cpt_Trump} fois on a obtenu une distance supérieure à celle de Trump, soit {cpt_Trump/N*100:.2f}% des fois.
""")
Sur 10000 simulations,
5 fois on a obtenu une distance supérieure à celle de Stein, soit 0.05% des fois;
6154 fois on a obtenu une distance supérieure à celle de Clinton, soit 61.54% des fois;
6944 fois on a obtenu une distance supérieure à celle de Trump, soit 69.44% des fois.
1.8 Conclusion
La visualisation de la distribution des effectifs du second chiffre des résultats de vote dans les bureaux des états du pour chaque candidat à montré une anomalie. Les résultats ne sont pas conforme à la loi théorique de Benford. De plus, très peu de simulations ont donné des écarts encore plus importants pour le candidat Stein, autrement dit c’est très rare de voir une telle distribution.