Une initiation à pandas (1)

pandas est une des librairies de référence pour le traitement de données avec le langage Python. En tant que journaliste de données, elle constitue une alternative sérieuse à des logiciels comme Excel ou Libre Calc.

pandas pour les nuls

La preuve avec ce carnet de notes qui détaille plusieurs manipulations d'un csv rempli de données électorales !

J'ai pris volontairement un fichier assez classique, pour pouvoir faire des manipulations très basiques comme :

  • des calculs de pourcentages
  • des sélections d'échantillons
  • des créations de nouvelles données à l'aide de tables de correspondance

Les choses un peu plus complexes comme les tableaux croisés dynamiques ne sont pas abordées dans ce carnet de notes. Mais j'essayerais de m'y pencher ultérieurement !

Serie et DataFrame

Avant d'entrer dans le vif du sujet, il faut présenter rapidement les deux format de données prépondérant dans pandas : les Series et les DataFrames.

Pour résumer :

  • les Series correspondent en gros à des tableaux à une dimension (soit à une seule et unique colonne d'un tableur).
  • les DataFrames correspondent quant à elles à des tableaux à plusieurs dimensions, ce qui nous intéresse particulièrement ici

On peut créer une DataFrame à partir de plusieurs Series ou transformer une colonne d'une DataFrame en série, mais ce type de manip' ne constituera pas le coeur de ce carnet de notes ! On va plutôt partir d'un bon vieux fichier csv à l'ancienne :-)

Lire un fichier csv

Commençons par importer le module pandas :

import pandas as pd
from pandas import DataFrame # comme on va bouffer du DataFrame, autant l'importer direct pour gagner en lisibilité

On va ensuite lire un csv avec la fonction read_csv() pour pouvoir le manipuler :

reg015 = pd.read_csv("reg015_grandest.csv")

On peut de suite vérifier que la variable reg015 correspond bien à une DataFrame :

type(reg015)
pandas.core.frame.DataFrame

On peut également obtenir le gabarit de notre DataFrame avec la fonction shape :

reg015.shape
(5189, 8)

Nous avons donc une DataFrame de 5 189 lignes sur huit colonnes.

Pour avoir un rapide aperçu de reg015, on peut utiliser les fonctions head() ou tail(), pour afficher respectivement les cinq premières ou dernières lignes du fichier.

reg015.head()
code insee ville inscrits abstentions exprimes LDVG LFN LUD
0 8001 Acy-Romance 367 120 241 40 97 104
1 8003 Aiglemont 1279 447 803 171 313 319
2 8004 Aire 150 46 99 16 18 65
3 8005 Alincourt 87 25 60 6 12 42
4 8006 Alland'Huy-et-Sausseuil 184 70 108 22 37 49

Manipulation de colonnes

Sélections

Pour afficher une seule colonne de reg015, il suffit de raisonner par nom de colonne :

reg015["ville"].tail()   # cette commande affiche les cinq dernières valeurs de 'ville'

5184         Xamontarupt
5185            Xaronval
5186            Xertigny
5187    Xonrupt-Longemer
5188            Zincourt
Name: ville, dtype: object

On peut s'assurer en deux temps trois mouvements qu'une colonne d'une DataFrame est bien considérée comme une Serie :

type(reg015["ville"])
pandas.core.series.Series

Pour sélectionner plusieurs colonnes, la syntaxe est la suivante :

reg015[['code insee','ville','abstentions']].iloc[:15] # iloc sert à sélectionner une partie des données à partir de leur index
code insee ville abstentions
0 8001 Acy-Romance 120
1 8003 Aiglemont 447
2 8004 Aire 46
3 8005 Alincourt 25
4 8006 Alland'Huy-et-Sausseuil 70
5 8007 Les Alleux 31
6 8008 Amagne 189
7 8009 Amblimont 47
8 8010 Ambly-Fleury 30
9 8011 Anchamps 69
10 8013 Angecourt 108
11 8014 Annelles 23
12 8015 Antheny 44
13 8016 Aouste 72
14 8017 Apremont 23

Tri par colonnes

Il peut être intéressant de trier une colonne d'une DataFrame. On va pas se mentir, c'est même une des manipulations les plus souvent répétées sur ce bon vieil Excel !

Dans notre exemple, si l'on veut trier les communes dans l'ordre décroissant des votes FN, il suffit d'utiliser la fonction sort_values() comme ceci :

reg015 = reg015.sort_values('LFN',ascending=0)
reg015.iloc[:15]
code insee ville inscrits abstentions exprimes LDVG LFN LUD
1319 51454 Reims 98837 47077 50010 9750 14686 25574
4222 67482 Strasbourg 146166 63471 80337 11458 14268 54611
3479 57463 Metz 71132 34191 35671 6512 9812 19347
4516 68224 Mulhouse 53177 26584 25372 4011 7144 14217
4359 68066 Colmar 42485 20330 21212 2580 6089 12543
2341 54395 Nancy 50991 20515 29459 5413 5269 18777
838 10387 Troyes 29096 13443 15123 2805 4656 7662
96 8105 Charleville-Mézières 29398 14411 14420 3046 4331 7043
996 51108 Châlons-en-Champagne 24982 11188 13224 2431 4314 6479
3940 67180 Haguenau 22896 10009 12380 1084 3952 7344
3683 57672 Thionville 26250 11325 14435 2748 3760 7927
1871 52448 Saint-Dizier 16547 8495 7729 1300 3352 3077
4831 88160 Epinal 21185 9321 11389 2228 2785 6376
3976 67218 Illkirch-Graffenstaden 17901 7464 10061 1215 2628 6218
3496 57480 Montigny-lès-Metz 15638 6985 8348 1355 2564 4429

On peut également prendre en compte plusieurs colonnes pour un tri.

Si on veut par exemple trier l'abstention et le FN par ordre décroissant, on peut écrire :

reg015 = reg015.sort_values(['abstentions','LFN'],ascending=[0,0])
reg015.iloc[:15]
code insee ville inscrits abstentions exprimes LDVG LFN LUD
4222 67482 Strasbourg 146166 63471 80337 11458 14268 54611
1319 51454 Reims 98837 47077 50010 9750 14686 25574
3479 57463 Metz 71132 34191 35671 6512 9812 19347
4516 68224 Mulhouse 53177 26584 25372 4011 7144 14217
2341 54395 Nancy 50991 20515 29459 5413 5269 18777
4359 68066 Colmar 42485 20330 21212 2580 6089 12543
96 8105 Charleville-Mézières 29398 14411 14420 3046 4331 7043
838 10387 Troyes 29096 13443 15123 2805 4656 7662
3683 57672 Thionville 26250 11325 14435 2748 3760 7927
996 51108 Châlons-en-Champagne 24982 11188 13224 2431 4314 6479
3940 67180 Haguenau 22896 10009 12380 1084 3952 7344
4831 88160 Epinal 21185 9321 11389 2228 2785 6376
1871 52448 Saint-Dizier 16547 8495 7729 1300 3352 3077
4188 67447 Schiltigheim 17461 8129 8968 1320 2052 5596
3256 57227 Forbach 14059 7591 6276 1087 2545 2644

Création de nouvelles colonnes

On va compléter la DataFrame, par exemple en ajoutant les pourcentages de l'abstention et de chaque liste. La formule pour créer ces nouvelles Series rappelle beaucoup l'incrémentation d'un dictionnaire Python :

reg015['pabs'] = ((reg015['abstentions']/reg015['inscrits'])*100).round(2)
reg015['pldvg'] = ((reg015['LDVG']/reg015['exprimes'])*100).round(2)
reg015['plfn'] = ((reg015['LFN']/reg015['exprimes'])*100).round(2)
reg015['plud'] = ((reg015['LUD']/reg015['exprimes'])*100).round(2)

On peut tout de suite vérifier que la manip' a fonctionné en réutilisant la fonction shape...

reg015.shape
(5189, 12)

...et en affichant les premières valeurs :

reg015 = reg015.sort('code insee')
reg015.head()
code insee ville inscrits abstentions exprimes LDVG LFN LUD pabs pldvg plfn plud
0 8001 Acy-Romance 367 120 241 40 97 104 32.70 16.60 40.25 43.15
1 8003 Aiglemont 1279 447 803 171 313 319 34.95 21.30 38.98 39.73
2 8004 Aire 150 46 99 16 18 65 30.67 16.16 18.18 65.66
3 8005 Alincourt 87 25 60 6 12 42 28.74 10.00 20.00 70.00
4 8006 Alland'Huy-et-Sausseuil 184 70 108 22 37 49 38.04 20.37 34.26 45.37

Résumé statistique

pandas inclut une fonction describe(), qui fournit un résumé statistique d'une DataFrame.

reg015.describe()
code insee inscrits abstentions exprimes LDVG LFN LUD pabs pldvg plfn plud
count 5189.000000 5189.000000 5189.000000 5189.000000 5189.000000 5189.000000 5189.000000 5189.000000 5189.000000 5189.000000 5189.000000
mean 52218.089420 748.866448 306.898054 422.042976 65.476200 152.277125 204.289651 35.063469 14.315629 41.653419 44.030798
std 22246.978603 3288.345087 1495.242562 1739.999508 289.796553 441.425121 1044.662815 7.601217 6.859461 9.639802 10.584106
min 8001.000000 6.000000 0.000000 3.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 6.450000
25% 51432.000000 111.000000 36.000000 70.000000 9.000000 30.000000 29.000000 30.530000 9.380000 35.290000 36.880000
50% 55058.000000 236.000000 82.000000 144.000000 20.000000 61.000000 61.000000 35.230000 13.640000 41.320000 43.330000
75% 67127.000000 553.000000 203.000000 336.000000 45.000000 136.000000 153.000000 40.000000 18.540000 47.830000 50.790000
max 88532.000000 146166.000000 63471.000000 80337.000000 11458.000000 14686.000000 54611.000000 62.790000 50.000000 87.100000 100.000000

Valeurs minimale et maximale, déviation standard, moyenne, nombre d'unités (assez inutile pour le coup)... Y a pas mal de choses !

Par défaut, le tableau descriptif comprend les quartiles, mais on peut les changer à l'envie.

Par exemple, si on veut directement paramétrer des cartes par déciles avec neuf classes de couleurs, on peut procéder comme suit :

reg015.describe(percentiles=[.11, .22, .33, .44, .55, .66, .77, .88]) # chaque niveau représente un seuil de classe sur la carte
code insee inscrits abstentions exprimes LDVG LFN LUD pabs pldvg plfn plud
count 5189.000000 5189.000000 5189.000000 5189.000000 5189.000000 5189.000000 5189.000000 5189.000000 5189.000000 5189.000000 5189.000000
mean 52218.089420 748.866448 306.898054 422.042976 65.476200 152.277125 204.289651 35.063469 14.315629 41.653419 44.030798
std 22246.978603 3288.345087 1495.242562 1739.999508 289.796553 441.425121 1044.662815 7.601217 6.859461 9.639802 10.584106
min 8001.000000 6.000000 0.000000 3.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 6.450000
11% 10113.680000 68.000000 20.000000 44.000000 5.000000 18.000000 17.000000 26.050000 6.546800 30.430000 31.966800
22% 51263.360000 101.000000 33.000000 64.000000 8.000000 27.000000 27.000000 29.770000 8.790000 34.480000 35.900000
33% 52247.040000 141.000000 48.000000 88.000000 12.000000 38.000000 36.000000 32.140000 10.710000 37.290000 39.040000
44% 54333.720000 196.000000 68.000000 121.000000 17.000000 52.000000 50.000000 34.170000 12.500000 40.000000 41.764400
50% 55058.000000 236.000000 82.000000 144.000000 20.000000 61.000000 61.000000 35.230000 13.640000 41.320000 43.330000
55.0% 55369.400000 272.000000 95.000000 166.000000 23.000000 70.000000 71.000000 36.080000 14.544000 42.494000 44.584000
66% 57403.080000 396.000000 143.000000 239.000000 32.000000 99.000000 104.000000 38.040000 16.570000 45.340800 47.834800
77% 67238.760000 612.520000 221.000000 366.000000 50.000000 150.000000 168.000000 40.487600 18.970000 48.337600 51.750000
88% 68274.440000 1189.000000 447.440000 712.000000 98.000000 269.000000 323.000000 43.838800 22.284400 52.602000 56.850000
max 88532.000000 146166.000000 63471.000000 80337.000000 11458.000000 14686.000000 54611.000000 62.790000 50.000000 87.100000 100.000000

Sélections et expressions logiques

pandas offre également la possibilité de faire des sélections sur une DataFrame pour renvoyer une nouvelle DataFrame en résultat.

Par exemple, si on veut sélectionner les villes où le FN obtient la majorité absolue, il suffira d'entrer :

fn_vainqueur = reg015[reg015['plfn']>50]  # cette syntaxe ne marchera qu'avec une seule et unique expression logique
fn_vainqueur.head()
code insee ville inscrits abstentions exprimes LDVG LFN LUD pabs pldvg plfn plud
9 8011 Anchamps 164 69 89 26 51 12 42.07 29.21 57.30 13.48
12 8015 Antheny 98 44 50 3 27 20 44.90 6.00 54.00 40.00
13 8016 Aouste 167 72 89 17 46 26 43.11 19.10 51.69 29.21
20 8023 Artaise-le-Vivier 59 13 44 3 23 18 22.03 6.82 52.27 40.91
23 8026 Aubigny-les-Pothées 224 69 148 18 83 47 30.80 12.16 56.08 31.76

Rien n'empêche de pimenter la chose en accumulant les sélections. Si on veut ne retenir que les villes où :

  • la liste de droite obtient la majorité absolue
  • la liste de gauche est seconde

On peut écrire :

ud_victoire = reg015['plud']> 50
dvg_second = reg015['pldvg']> reg015['plfn']

tierce = reg015[ud_victoire & dvg_second]
tierce.head()
code insee ville inscrits abstentions exprimes LDVG LFN LUD pabs pldvg plfn plud
16 8019 Les Grandes-Armoises 42 14 26 6 4 16 33.33 23.08 15.38 61.54
111 8123 Chuffilly-Roche 58 15 41 13 7 21 25.86 31.71 17.07 51.22
394 8432 Sury 84 20 62 17 10 35 23.81 27.42 16.13 56.45
529 10071 Chacenay 55 29 26 7 1 18 52.73 26.92 3.85 69.23
1172 51294 Hourges 67 23 44 9 5 30 34.33 20.45 11.36 68.18

A noter qu'on peut aussi rassembler les sélections directement ainsi (attention aux parenthèses) :

tierce = reg015[(reg015['plud']>50) & (reg015['pldvg']> reg015['plfn'])]

Chacune des DataFrames ainsi créées peut être par la suite enregistrée dans un nouveau fichier. Mais avant cela...

Quelques fonctions intermédiaires

Il est très courant lors d'une soirée électorale de vouloir très rapidement avoir le nom du candidat ou de la liste arrivée en tête dans tel territoire.

Une fonction de pandas toute désignée pour ça est idxmax(). On va l'utiliser en prenant en compte les voix brutes de la droite, de la gauche et de l'extrême droite pour remplir une nouvelle colonne de la DataFrame nommée 'vainqueur' :

reg015['vainqueur'] = reg015[['LDVG','LFN','LUD']].idxmax(axis=1)
reg015.iloc[:6]
code insee ville inscrits abstentions exprimes LDVG LFN LUD pabs pldvg plfn plud vainqueur
0 8001 Acy-Romance 367 120 241 40 97 104 32.70 16.60 40.25 43.15 LUD
1 8003 Aiglemont 1279 447 803 171 313 319 34.95 21.30 38.98 39.73 LUD
2 8004 Aire 150 46 99 16 18 65 30.67 16.16 18.18 65.66 LUD
3 8005 Alincourt 87 25 60 6 12 42 28.74 10.00 20.00 70.00 LUD
4 8006 Alland'Huy-et-Sausseuil 184 70 108 22 37 49 38.04 20.37 34.26 45.37 LUD
5 8007 Les Alleux 63 31 31 5 13 13 49.21 16.13 41.94 41.94 LFN

Les plus observateurs auront remarqué un souci : la dernière ville donne le FN vainqueur, alors qu'il est à égalité avec la droite. Moyen moins...

Il faut tricher un peu pour inclure le cas d'égalité. Voici une manip' gracieusement proposée sur StackOverflow, qui consiste en gros à appliquer un calque sur les valeurs initiales de 'vainqueur'.

Les étapes pour y arriver seront :

  • créer une nouvelle Serie, disons 'somme', pour paramétrer le calque
  • additionner à l'intérieur de cette Serie le nombre de maximum (!) de chaque commune

Une fois que la Serie sera prête, on la superposera à la Serie 'vainqueur' pour que :

  • si la valeur de somme est égale à 1, le calque ne s'applique pas
  • en revanche, si cette valeur est supérieure à 1, on pose un masque dessus en la remplaçant par 'Egalite'

Concrètement, ça donne :

somme = reg015[['LDVG','LFN','LUD']].eq(reg015[['LDVG','LFN','LUD']].max(axis=1), axis=0).sum(axis=1)
somme[:6]
0    1
1    1
2    1
3    1
4    1
5    2
dtype: int64

On voit bien qu'à l'indice 5, celui correspondant aux Alleux, le nombre de maximum est supérieur à 1.

On peut alors procéder au changement de valeurs comme suit :

reg015['vainqueur'] = reg015['vainqueur'].mask(somme > 1, 'Egalite')
reg015[:6]
code insee ville inscrits abstentions exprimes LDVG LFN LUD pabs pldvg plfn plud vainqueur
0 8001 Acy-Romance 367 120 241 40 97 104 32.70 16.60 40.25 43.15 LUD
1 8003 Aiglemont 1279 447 803 171 313 319 34.95 21.30 38.98 39.73 LUD
2 8004 Aire 150 46 99 16 18 65 30.67 16.16 18.18 65.66 LUD
3 8005 Alincourt 87 25 60 6 12 42 28.74 10.00 20.00 70.00 LUD
4 8006 Alland'Huy-et-Sausseuil 184 70 108 22 37 49 38.04 20.37 34.26 45.37 LUD
5 8007 Les Alleux 63 31 31 5 13 13 49.21 16.13 41.94 41.94 Egalite

Une fois ceci fait, pourquoi ne pas ajouter une couleur en fonction du parti arrivé en tête dans chaque commune ?

pandas est une librairie Python, on pourrait donc règler l'affaire en utilisant des boucles if/elif/else... mais on peut aussi vouloir utiliser une table de correspondance, par exemple :

correspondance = pd.read_csv("table_couleurs.csv")
correspondance
vainqueur couleur
0 LFN #454552
1 LUD #4ea1d3
2 LDVG #e85a71

Pour être parfaitement raccord à la colonne 'vainqueur' de notre grosse DataFrame, il faudrait ajouter une ligne avec le cas d'égalité.

Une DataFrame fonctionne à peu près comme une liste Python classique, à l'intérieur de laquelle on range un dictionnaire.

La commande sera plus parlante :

correspondance = correspondance.append([{'vainqueur':'Egalite','couleur':'#c0c0c0'}])
correspondance
vainqueur couleur
0 LFN #454552
1 LUD #4ea1d3
2 LDVG #e85a71
0 Egalite #c0c0c0

Pour faire la correspondance, la fonction merge() fait le job (ça rappellera sûrement des trucs aux utilisateurs de Google Fusion Table).

Dans notre cas, ça va donner :

reg015 = pd.merge(reg015, correspondance, how='left')
reg015[:6]
code insee ville inscrits abstentions exprimes LDVG LFN LUD pabs pldvg plfn plud vainqueur couleur
0 8001 Acy-Romance 367 120 241 40 97 104 32.70 16.60 40.25 43.15 LUD #4ea1d3
1 8003 Aiglemont 1279 447 803 171 313 319 34.95 21.30 38.98 39.73 LUD #4ea1d3
2 8004 Aire 150 46 99 16 18 65 30.67 16.16 18.18 65.66 LUD #4ea1d3
3 8005 Alincourt 87 25 60 6 12 42 28.74 10.00 20.00 70.00 LUD #4ea1d3
4 8006 Alland'Huy-et-Sausseuil 184 70 108 22 37 49 38.04 20.37 34.26 45.37 LUD #4ea1d3
5 8007 Les Alleux 63 31 31 5 13 13 49.21 16.13 41.94 41.94 Egalite #c0c0c0

Transformer une DataFrame en fichier

Un instinct primaire encouragerait à tenter une fonction to_csv() pour transformer une DataFrame, et il aurait raison !

reg015.to_csv("reg015_def.csv")

Pandas obéit par défaut à une logique anglo-saxonne : les séparateurs de données sont des virgules, ce qui tombe bien puisque les décimales sont représentées par des points.

Mais rien n'interdit d'ajouter quelques paramètres pour avoir à l'arrivée un csv lisible avec une configuration francophone !

reg015.to_csv("reg015_deffr.csv", sep = ";", decimal = ",")

C'est tout pour aujourd'hui !

Point Github

Les CSV utilisés pour cette démo ont été publiés via mon compte Github, avec un carnet de notes qui en détaille les grandes lignes !

Par Raphaël da Silva dans la catégorie
Tags : #python, #pandas, #statistiques,