Pratique de pandas: un exemple complet

Les exemples de ce TP sont visualisables sous forme de notebooks:

Download nbviewer Onyxia
Binder Open In Colab githubdev

Dans ce tutoriel pandas, nous allons utiliser deux sources de données :

  • Les émissions de gaz à effet de serre estimées au niveau communal par l’ADEME. Le jeu de données est disponible sur data.gouv et requêtable directement dans python avec cet url. pandas offre la possibilité d’importer des données directement depuis un url. C’est l’option prise dans ce tutoriel. Si vous préfèrez, pour des raisons d’accès au réseau ou de performance, importer depuis un poste local, vous pouvez télécharger les données et changer les commandes d’import avec le chemin adéquat plutôt que l’url.

  • Idéalement, on utiliserait les données disponibles sur le site de l’Insee mais celles-ci nécessitent un peu de travail de nettoyage qui n’entre pas dans le cadre de ce TP. Pour faciliter l’import de données Insee, il est recommandé d’utiliser le package pynsee qui simplifie l’accès aux données de l’Insee disponibles sur le site web insee.fr ou via des API.

Nous suivrons les conventions habituelles dans l’import des packages

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pynsee
import pynsee.download

Exploration de la structure des données

Commencer par importer les données de l’Ademe à l’aide du package pandas. Vous pouvez les nommer df.

df = pd.read_csv("https://koumoul.com/s/data-fair/api/v1/datasets/igt-pouvoir-de-rechauffement-global", sep = ";")

Pour les données de cadrage au niveau communal (source Insee), le package pynsee facilite grandement la vie. La liste des données disponibles est ici. En l’occurrence, on va utiliser les données Filosofi (données de revenus) au niveau communal, dans la dernière version disponible. Le point d’entrée principal de la fonction pynsee est la fonction download_file. Le code pour télécharger les données est le suivant :

df_city = pynsee.download.download_file("FILOSOFI_COM_2016")

Note

La fonction download_file attend un identifiant unique pour savoir quelle base de données aller chercher et restructurer depuis le site insee.fr. Pour connaître la liste des bases disponibles, vous pouvez utiliser la fonction pynsee.get_file_list(). Celle-ci renvoie un DataFrame dans lequel on peut rechercher, par exemple grâce à une recherche de mot clé:

meta = pynsee.get_file_list()
meta.loc[meta['label'].str.contains(r"Filosofi.*2016")]

Ici, meta['label'].str.contains(r“Filosofi.*2016”) signifie: *"pandas trouve moi tous les labels où sont contenus les termes Filosofi et 2016_" (.* signifiant *“peu m’importe le nombre de mots ou caractères entre”_)

Exercise

Exercice 1: Afficher des données

L’objectif de cet exercice est de vous amener à afficher des informations sur les données dans un bloc de code (notebook) ou dans la console

Commencer sur df:

* Utiliser les méthodes adéquates pour les 10 premières valeurs, les 15 dernières et un échantillon aléatoire de 10 valeurs
* Tirer 5 pourcent de l'échantillon sans remise
* Ne conserver que les 10 premières lignes et tirer aléatoirement dans celles-ci pour obtenir un DataFrame de 100 données.
* Faire 100 tirages à partir des 6 premières lignes avec une probabilité de 1/2 pour la première observation et une probabilité uniforme pour les autres

Faire la même chose sur df_city

df.head(10)
df.tail(15)
df.sample(10)
df.sample(frac = 0.05)
df[:10].sample(n = 100, replace = True)
df[:6].sample(n = 100, replace = True, weights = [0.5] + [0.1]*5)

df_city.head(10)
df_city.tail(15)
df_city.sample(10)
df_city.sample(frac = 0.05)
df_city[:10].sample(n = 100, replace = True)
df_city[:6].sample(n = 100, replace = True, weights = [0.5] + [0.1]*5)

Cette première approche exploratoire donne une idée assez précise de la manière dont les données sont organisées. On remarque ainsi une différence entre df et df_city quant aux valeurs manquantes: la première base est relativement complète, la seconde comporte beaucoup de valeurs manquantes. Autrement dit, si on désire exploiter df_city, il faut faire attention à la variable choisie.

Exercise

Exercice 2: structure des données

La première chose à vérifier est le format des données, afin d’identifier des types de variables qui ne conviennent pas. Ici, comme c’est pandas qui a géré automatiquement les types de variables, il y a peu de chances que les types ne soient pas adéquats mais une vérification ne fait pas de mal.

  • Vérifier les types des variables. S’assurer que les types des variables communes aux deux bases sont cohérents.
print(df.dtypes)
print(df_city.dtypes)

Ensuite, on vérifie les dimensions des DataFrames et la structure de certaines variables clés. En l’occurrence, les variables fondamentales pour lier nos données sont les variables communales. Ici, on a deux variables géographiques: un code commune et un nom de commune.

  • Vérifier les dimensions des DataFrames
print(df.shape)
print(df_city.shape)
  • Vérifier le nombre de valeurs uniques des variables géographiques dans chaque base. Les résultats apparaissent-ils cohérents ?
print(df[['INSEE commune', 'Commune']].nunique())
print(df_city[['CODGEO', 'LIBGEO']].nunique())
# Résultats dont l'ordre de grandeur est proche. Dans les deux cas, #(libelles) < #(code)
  • Identifier les libellés pour lesquels on a plusieurs codes communes dans df_city et les stocker dans un vecteur x (conseil: faire attention à l’index de x)
x = df_city.groupby('LIBGEO').count()['CODGEO']
x = x[x>1]
x = x.reset_index()
x

On se focalise temporairement sur les observations où le libellé comporte plus de deux codes communes différents

  • Regarder dans df_city ces observations
df_city[df_city['LIBGEO'].isin(x['LIBGEO'])]
  • Pour mieux y voir, réordonner la base obtenue par order alphabétique
df_city[df_city['LIBGEO'].isin(x['LIBGEO'])].sort_values('LIBGEO')
  • Déterminer la taille moyenne (variable nombre de personnes: NBPERSMENFISC16) et quelques statistiques descriptives de ces données. Comparer aux mêmes statistiques sur les données où libellés et codes communes coïncident
df_city[df_city['LIBGEO'].isin(x['LIBGEO'])]['NBPERSMENFISC16'].describe()
df_city[~df_city['LIBGEO'].isin(x['LIBGEO'])]['NBPERSMENFISC16'].describe()
  • Vérifier sur les grandes villes (plus de 100 000 personnes), la proportion de villes où libellés et codes communes ne coïncident pas. Identifier ces observations.
df_big_city = df_city[df_city['NBPERSMENFISC16']>100000]
df_big_city['probleme'] = df_big_city['LIBGEO'].isin(x['LIBGEO'])
df_big_city['probleme'].mean()
df_big_city[df_big_city['probleme']]
  • Vérifier dans df_city les villes dont le libellé est égal à Montreuil. Vérifier également celles qui contiennent le terme ‘Saint-Denis’
df_city[df_city.LIBGEO == 'Montreuil']
df_city[df_city.LIBGEO.str.contains('Saint-Denis')].head(10)

Ce petit exercice permet de se rassurer car les libellés dupliqués sont en fait des noms de commune identiques mais qui ne sont pas dans le même département. Il ne s’agit donc pas d’observations dupliquées. On se fiera ainsi aux codes communes, qui eux sont uniques.

Les indices

Les indices sont des éléments spéciaux d’un DataFrame puisqu’ils permettent d’identifier certaines observations. Il est tout à fait possible d’utiliser plusieurs indices, par exemple si on a des niveaux imbriqués.

Exercise

Exercice 3: Les indices

A partir de l’exercice précédent, on peut se fier aux codes communes.

  • Fixer comme indice la variable de code commune dans les deux bases. Regarder le changement que cela induit sur le display du dataframe
display(df)
df = df.set_index('INSEE commune')
display(df)

display(df_city)
df_city =  df_city.set_index('CODGEO') 
display(df_city)
  • Les deux premiers chiffres des codes communes sont le numéro de département. Créer une variable de département dep dans df et dans df_city
df['dep'] = df.index.str[:2]
df_city['dep'] = df_city.index.str[:2]
  • Calculer les émissions totales par secteur pour chaque département. Mettre en log ces résultats dans un objet df_log. Garder 5 départements et produire un barplot
df_log = df.groupby('dep').sum().apply(np.log)
df_log.sample(5).plot(kind = "bar")
  • Repartir de df. Calculer les émissions totales par département et sortir la liste des 10 principaux émetteurs de CO2 et des 5 départements les moins émetteurs. Sans faire de merge, regarder les caractéristiques de ces départements (population et niveau de vie)
## Emissions totales par département (df)
df_emissions = df.reset_index().set_index(['INSEE commune','dep']).sum(axis = 1).groupby('dep').sum()
#df.reset_index().groupby('dep').sum().sum(axis = 1).head() #version simplifiee ?
gros_emetteurs = df_emissions.sort_values(ascending = False).head(10)
petits_emetteurs = df_emissions.sort_values().head(5)


## Caractéristiques des départements (df_city)
print(df_city[df_city['dep'].isin(gros_emetteurs.index)][['NBPERSMENFISC16','MED16']].sum())
print(df_city[df_city['dep'].isin(gros_emetteurs.index)][['NBPERSMENFISC16','MED16']].mean())
print(df_city[df_city['dep'].isin(petits_emetteurs.index)][['NBPERSMENFISC16','MED16']].sum())
print(df_city[df_city['dep'].isin(petits_emetteurs.index)][['NBPERSMENFISC16','MED16']].mean())

Exercise

Exercice 4: performance des indices

Un des intérêts des indices est qu’ils permettent des agrégations efficaces.

  • Repartir de df et créer une copie df_copy = df.copy() et df_copy2 = df.copy() afin de ne pas écraser le DataFrame df
df_copy = df.copy()
df_copy2 = df.copy()
  • Utiliser la variable dep comme indice pour df_copy et retirer tout index pour df_copy2
df_copy = df_copy.set_index('dep')
df_copy2 = df_copy2.reset_index()
  • Importer le module timeit et comparer le temps d’exécution de la somme par secteur, pour chaque département, des émissions de CO2
%timeit df_copy.drop('Commune', axis = 1).groupby('dep').sum()
%timeit df_copy2.drop('Commune', axis = 1).groupby('dep').sum()

Restructurer les données

On présente généralement deux types de données:

* format __wide__: les données comportent des observations répétées, pour un même individu (ou groupe), dans des colonnes différentes 
* format __long__: les données comportent des observations répétées, pour un même individu, dans des lignes différentes avec une colonne permettant de distinguer les niveaux d'observations

Un exemple de la distinction entre les deux peut être pris à l’ouvrage de référence d’Hadley Wickham, R for Data Science:

L’aide mémoire suivante aidera à se rappeler les fonctions à appliquer si besoin:

Le fait de passer d’un format wide au format long (ou vice-versa) peut être extrêmement pratique car certaines fonctions sont plus adéquates sur une forme de données ou sur l’autre. En règle générale, avec python comme avec R, les formats long sont souvent préférables.

Exercise

Exercice 5: Restructurer les données: wide to long

  • Créer une copie des données de l’ADEME en faisant df_wide = df.copy()
df_wide = df.copy()
df_wide[['Commune','dep', "Agriculture", "Tertiaire"]].head() 
  • Restructurer les données au format long pour avoir des données d’émissions par secteur en gardant comme niveau d’analyse la commune (attention aux autres variables identifiantes).
df_wide.reset_index().melt(id_vars = ['INSEE commune','Commune','dep'],
                          var_name = "secteur", value_name = "emissions")
  • Faire la somme par secteur et représenter graphiquement
(df_wide.reset_index()
 .melt(id_vars = ['INSEE commune','Commune','dep'],
                          var_name = "secteur", value_name = "emissions")
 .groupby('secteur').sum().plot(kind = "barh")
)
  • Garder, pour chaque département, le secteur le plus polluant
(df_wide.reset_index().melt(id_vars = ['INSEE commune','Commune','dep'],
                          var_name = "secteur", value_name = "emissions")
 .groupby(['secteur','dep']).sum().reset_index().sort_values(['dep','emissions'], ascending = False).groupby('dep').head(1)
)

Exercise

Exercice 6: long to wide

Cette transformation est moins fréquente car appliquer des fonctions par groupe, comme nous le verrons par la suite, est très simple.

  • Repartir de `df_wide = df.copy()
df_wide = df.copy()
  • Reconstruire le DataFrame, au format long, des données d’émissions par secteur en gardant comme niveau d’analyse la commune puis faire la somme par département et secteur
df_long_agg = (df_wide.reset_index()
 .melt(id_vars = ['INSEE commune','Commune','dep'],
                          var_name = "secteur", value_name = "emissions").groupby(["dep", "secteur"]).sum()
)

df_long_agg.head()
  • Passer au format wide pour avoir une ligne par secteur et une colonne par département
df_wide_agg = df_long_agg.reset_index().pivot_table(values = "emissions", index = "secteur", columns = "dep")

df_wide_agg.head()
  • Calculer, pour chaque secteur, la place du département dans la hiérarchie des émissions nationales
df_wide_agg.rank(axis = 1)
  • A partir de là, en déduire le rang médian de chaque département dans la hiérarchie des émissions et regarder les 10 plus mauvais élèves, selon ce critère.
df_wide_agg.rank(axis = 1).median().nlargest(10)

Combiner les données

Une information que l’on cherche à obtenir s’obtient de moins en moins à partir d’une unique base de données. Il devient commun de devoir combiner des données issues de sources différentes. Nous allons ici nous focaliser sur le cas le plus favorable qui est la situation où une information permet d’apparier de manière exacte deux bases de données (autrement nous serions dans une situation, beaucoup plus complexe, d’appariement flou). La situation typique est l’appariement entre deux sources de données selon un identifiant individuel ou un identifiant de code commune, ce qui est notre cas.

Il est recommandé de lire ce guide assez complet sur la question des jointures avec R qui donne des recommandations également utiles en python.

On utilise de manière indifférente les termes merge ou join. Le deuxième terme provient de la syntaxe SQL. En pandas, dans la plupart des cas, on peut utiliser indifféremment df.join et df.merge

Exercise

Exercice 7: Calculer l’empreinte carbone par habitant

  • Créer une variable emissions qui correspond aux émissions totales d’une commune
df['emissions'] = df.sum(axis = 1)
  • Faire une jointure à gauche entre les données d’émissions et les données de cadrage. Comparer les émissions moyennes des villes sans match (celles dont des variables bien choisies de la table de droite sont NaN) avec celles où on a bien une valeur correspondante dans la base Insee
df_merged = df.merge(df_city, how = "left", left_index = True, right_index = True)

print(df_merged[df_merged['LIBGEO'].isna()]['emissions'].mean())
print(df_merged[~df_merged['LIBGEO'].isna()]['emissions'].mean())
  • Faire un inner join puis calculer l’empreinte carbone (l’émission rapportée au nombre de ménages fiscaux) dans chaque commune. Sortir un histogramme en niveau puis en log et quelques statistiques descriptives sur le sujet.
df_merged = df.merge(df_city, left_index = True, right_index = True)
df_merged['empreinte'] = df_merged['emissions']/df_merged['NBPERSMENFISC16']

df_merged['empreinte'].plot(kind ="hist")
np.log(df_merged['empreinte']).plot.hist()
df_merged['empreinte'].describe()
  • Regarder la corrélation entre les variables de cadrage et l’empreinte carbone. Certaines variables semblent-elles pouvoir potentiellement influer sur l’empreinte carbone ?
df_merged.corr()['empreinte'].nlargest(10)
# Les variables en lien avec le transport. 

Exercices bonus

Les plus rapides d’entre vous sont invités à aller un peu plus loin en s’entraînant avec des exercices bonus qui proviennent du site de Xavier Dupré. 3 notebooks en lien avec numpy et pandas vous y sont proposés :

  1. Calcul Matriciel, Optimisation : énoncé / corrigé
  2. DataFrame et Graphes : énoncé / corrigé
  3. Pandas et itérateurs : énoncé / corrigé
Previous
Next