logo le blog invivoo blanc

Pandas : le datamanagement à la portée de toutes les mains

13 février 2019 | Python | 0 comments

Pandas est un package python qui permet de traiter des jeux de données de manière efficace et compréhensible. De la lecture des fichiers à une première approche statistique.

Pandas est le compagnon de jeu de n’importe quel datascientist pythonicien ! Pandas est souvent accompagné de son ami scikit learn, LE package de machine learning de python. Avant de pouvoir utiliser scikit learn, Pandas intervient dans la première phase d’un projet de datascience, afin de lire, comprendre et préparer la donnée.

Nous vous proposons, à travers un exemple, de passer en revue les principales fonctionnalités de ce package, en suivant les grandes étapes du datamanagement.

Nous allons nous intéresser à l’historique des prénoms de 2004 à 2017 dans l’agglomération parisienne. Et pour les plus bretons d’entre nous, nous étudierons aussi les prénoms rennais. Ces deux datasets nous permettront d’apprendre à utiliser Pandas.

Les plus courageux pourront utiliser le dataset final pour prédire les prénoms les plus populaires dans les années suivantes et, surtout, choisir le prénom de leur futur enfant.

Il est important de mentionner que l’aide de Pandas est très bien documentée. Les fonctions présentées dans ce tutorial sont le plus souvent utilisées dans leur version par défaut, mais de nombreuses options supplémentaires existent et permettent de simplifier les démarches.

Comment lire et stocker mes données ?

La première étape – et parfois la plus compliquée – est de lire les données. Pandas simplifie la lecture de fichiers de tout type : csv, xlsx, txt, json ou encore d’un dictionnaire python. Les données lues sont stockées dans des dataframes.

Un dataframe est une structure de données permettant de stocker les données selon deux dimensions : lignes et colonnes. Les colonnes sont accessibles par les noms de colonne et les lignes sont accessibles par leur index. Ce dernier peut-être un entier, une date, une chaîne de caractères. Pandas gère également le multi-index.



Revenons à notre lecture de fichier. De multiples options permettent de gérer :

  • Les séparateurs
  • L’encodage
  • Le type de chaque colonne
  • La présence ou non d’un en-tête de colonne
  • La façon de traiter les valeurs manquantes
  • La façon de traiter les dates
  • Le nombre de lignes à sauter

Par défaut, pandas interprète le type des données présentes dans chaque colonne en lisant les premières lignes du fichier. Rien ne permet de s’assurer que cette méthode est toujours performante (allez, disons presque toujours). Il est donc préférable de parfaitement préparer la lecture du fichier en précisant un schéma de données (en-tête et type). Évidemment, ami pythonicien, l’ajout de quelques try et autres except est toujours aussi indispensable.

Créer un dataframe après avoir lu les données dans une base SQL, PostgreSQL, Mongo ou tout autre système d’informations est aussi tout à fait possible. En fonction des différents SI, des packages et des drivers additionnelles doivent être installés. Il faut donc utiliser différentes fonctions pour obtenir un dataframe à partir de listes, de dictionnaires, etc. Passons tout de suite à la pratique :

  • Pour le fichier CSV :
df_paris = pd.read_csv('prenoms_paris.csv', sep=';')
  • Pour le fichier json :
with open('prenoms_paris.json') as f:
    data = json.load(f)

prenom_data = []
for d in data:
    prenom_data.append(d['fields'])
df = pd.DataFrame(prenom_data)

Si le fichier est trop gros, l’option chunksize permet de lire le fichier en plusieurs fois pour ensuite les concaténer dans un dataframe :

chunksize = 10 ** 6
data_chunks = list()
for chunk in pd.read_csv('prenoms_paris.csv', chunksize=chunksize, sep = ';'):
    data_chunks.append(chunk)
df = pd.concat(data_chunks)

Explorer mon dataframe

Une fois le fichier lu, il est temps de passer à une première analyse de ces données. Nul besoin d’un autre package, pandas s’occupe de tout.

Afficher un échantillon du dataframe

Généralement, on préfère ne pas afficher le dataframe en entier. On se contentera d’afficher juste un sous ensemble du dataframe en utilisant la fonction head (ou tail par symétrie).

df_paris.head() par défaut retourne les 5 premières lignes.

Filtrer sur une valeur spécifique

Si je veux afficher que les lignes contenant mon prénom :

df_paris [ df_paris [‘Prenoms’]==’Dora’ ] ff

Prenons deux minutes pour comprendre ce que fait Pandas :

  • Pour chaque ligne du dataframe, l’expression entre les crochets est évaluée pour retourner un booléen.
  • Seules lignes avec valeur égale à True sont affichées.

On atteint le résultat suivant :

Description du dataframe

Après avoir affiché notre dataframe, on passe à l’analyse de son contenu :

  • Nombre de lignes
  • Type de colonnes
  • Valeurs possibles pour chaque colonne

Le nombre de lignes est obtenu en utilisant shape : df_paris.shape qui retourne le nombre de lignes et colonnes de notre dataframe. Nous obtenons le type de chaque colonne en utilisant dtypes.

On obtient le résultat suivant :

Les colonnes ‘Sexe’ et ‘Prenoms’ sont des objets et les colonne ‘Annee’ et ‘Nombre’ sont des entiers. Afin d’éviter les éventuels conflits, nous voulons que les valeurs de la colonne ‘Annee’ soient de type str. Pour cela, nous utilisons la fonction astype avec le type voulu en paramètre :

df_paris ['Annee'] = df_paris ['Annee'].astype(str)

Nous pouvons pour chaque colonne obtenir un résumé des données présentes en utilisant la fonction describe() (le nombre de valeurs, la moyenne, l’écart type, le minimum, le maximum et des quantiles). Évidemment, cette fonction ne s’applique que sur les colonnes numériques.

Pour obtenir des informations sur toutes les colonnes, l’option include=’all’ est disponible :

df_paris .describe()
df_paris.describe(include=’all’)

Cette fonction nous permet de découvrir qu’il existe dans le dataset, 2 389 prénoms différents pour des années de 2004 à 2007.

Renommer une colonne

Afin de faciliter la lecture des données par nos amis anglais, nous pouvons aussi renommer une ou plusieurs colonnes en créant un dictionnaire de correspondance avec l’aide de la fonction (attention surprise) rename 

newcols = {
    'Nombre': 'Number',
    'Sexe': 'Sex',
    'Annee': 'Year',
    'Prenoms': 'FirstName'}
df_paris.rename(columns=newcols, inplace=True)

Pour la première fois, une option très importante vient de faire son apparition. L’argument inplace permet de préciser si on veut appliquer sur le dataframe lui-même (inplace = True) ou sur une copie du dataframe (inplace = False). Les deux lignes suivantes sont équivalentes :

df_paris.rename(columns=newcols, inplace=True)

df_renamed = df_paris.rename(columns=newcols, inplace=False)

Gérer les lignes en double

Une étape importante dans l’exploration de nos données est la suppression des lignes dupliquées via la méthode drop_duplicates. Celle-ci retourne un dataframe avec les lignes en double supprimées, avec possibilité de considérer seulement certaines colonnes. Elle prend en argument :

  • Subset : Liste des colonnes sur lesquelles on va vérifier l’existence de lignes en double
  • Option keep qui a trois valeurs possibles : ‘first’ : garder la première occurrence, ‘last’ garder la dernière occurrence et ‘False’ pour supprimer toutes les occurrences

Pour notre cas, nous voulons vérifier sur toutes les colonnes et garder la première occurrence :

df_paris.drop_duplicates(subset=['FirstName', 'Year', 'Sex'], keep='first')

Gérer les valeurs inconnues

On observe que la colonne ‘Sexe’ possède trois valeurs distinctes : F, M et X. La valeur ‘X’ peut avoir plusieurs significations : une valeur manquante, une valeur indéterminée ou même les prénoms qui sont à la fois masculin et féminin. Dans un monde idéal, il faudrait contacter le responsable de l’extraction pour obtenir cette information. Ici, nous ne pouvons effectuer cette démarche, donc nous allons enquêter par nos propres moyens :

On filtre sur les lignes où les valeurs ‘Sexe’ == ‘X’

df_X = df_paris[ df_paris['Sexe']=='X' ]

Ensuite, on récupère la colonne ‘Prenoms ‘ :

df_prenoms_X = df_X[‘Prenoms’]

Puis, on récupère la liste des prénoms distincts :

Prenoms_X = df_prenoms_X.unique()

Ainsi, on peut concaténer les étapes a, b et c en une seule ligne :

df_paris[df_paris['Sexe']=='X']['Prenoms'].unique() 

On obtient les prénoms suivants :

Au vu des prénoms affichés, l’hypothèse principale est que ce X représente en fait une valeur manquante. Nous choisissons donc de filtrer ces lignes.

df_paris = df_paris[ df_paris['Sexe'] != 'X' ]

Encore plus d’options

Pour faciliter la lecture des données, il est parfois utile de ranger les valeurs d’une colonne. ff00000000

df.sort_values(by='Nombre', ascending = False)

On observe que sur les 11 premières lignes, on retrouve 9 Gabriel et 2 Adam surtout sur les dernières années. Dans le top 15, on retrouve un seul prénom féminin. Pour étudier une seule colonne ou quelques valeurs, de nombreuses possibilités sont à notre disposition :

  • Une colonne : df[‘Annee’] L
  • Quelques lignes : df[0:3]
  • Pour obtenir toutes les valeurs de la ligne 3 : df.iloc[3]
  • Pour obtenir la valeur de la colonne A correspondant à une valeur particulière : df.loc[dates[0],’A’]

Attention à ne pas confondre : loc et iloc !

Modifier mon dataframe

Gérer les valeurs manquantes

Une problématique, d’une grande importance pour les data scientists, est la gestion des valeurs manquantes. La méthode la plus simple, mais rarement la plus efficace, est d’enlever toutes les lignes ayant au moins une valeur manquante :

df_paris.dropna(how='any')

Il est tout aussi facile de remplacer les valeurs manquantes par une valeur fixe ou par une valeur moyenne :

df_paris.fillna(value=5)
df_paris.fillna(subset=[‘Nombre’], value=df_paris.Nombre.mean()

Créer une nouvelle colonne

On entend souvent que les prénoms courts sont à la mode. Nous nous proposons de vérifier cette assertion. Pour cela, nous allons créer une nouvelle colonne grâce à une des nombreuses méthodes possibles. En utilisant le concept de list comprehension, on peut créer une colonne de la façon suivante :

df_paris ['NumberLetter'] = [len(x) for x in df_paris ['FirstName']]

Ceci nous permet de trouver un prénom de deux lettres : El qui est donné régulièrement ces dernières années.

On peut choisir de créer une nouvelle règle basée sur des règles de gestion. Nous proposons une toute première approche pour estimer le sexe d’un basé sur la dernière lettre du prénom ou la longueur du prénom. Le package numpy peut aussi être utile avec sa fonction where :

import numpy as np
df_paris ['SexePredicted'] = np.where(df_paris .FirstName.str[-1:] == 'a', 'F',
                       np.where(df_paris.NumberLetter < 2,'M',
                   np.where(df_paris.FirstName.str.contains('john'),'M','F')))

Modifier une colonne

Il peut être utile aussi de ‘recoder’ une colonne. Souvent, il existe de multiples valeurs correspondantes à la même occurrence (exemple : H pour Homme).

df_paris ['Sex'] = df_paris ['Sex'].apply({'M':'Masculin', 'F':'Féminin'}.get)

Ici on transforme le ‘M’ en masculin et le ‘F’ en Féminin

Ne pas oublier de checker si la colonne “Sexe” ne présente pas d’autres valeurs qu’on aurait oublié de traiter. On peut même appliquer une fonction custom et l’appliquer à une colonne de notre dataframe :

lower = lambda x: x.lower()
df_paris ['FirstName'] = df_paris['FirstName'].apply(lower)

Filtrer des lignes

On peut utiliser des méthodes pandas ou des méthodes plus pythoniques :

df_paris [np.max(df['Number']) == df_paris ['Number']]
df_paris [(df.Number == 1) & (df_paris.NumberLetter == 6)]
df_paris [(df[‘Number’] <= 1) & (df_paris [‘NumberLetter’] > 6)]

df_paris.pipe(lambda d: d[d[' Number '] == np.max(df_paris ['Number'])])
df_paris.query('Number>0').query('0<NumberLetter<2')

L’avantage des deux dernières est qu’elles permettent de chainer un grand nombre de lignes de codes pour rendre le code plus compact.

Groupby

Pour finir ce chapitre, il nous reste à vous présenter le groupby qui fonctionne de la même façon que le groupby en SQL. Si on veut sommer les prénoms donnés sur toutes les années, on regroupe par nom et on somme sur le nombre :

df_paris.groupby(['FirstName'])['Number'].sum().reset_index(drop=False).sort_values('Number').tail(5

NB : en sortie du groupby, nous obtenons un multi index, il faut donc utiliser la fonction reset_index pour transformer le multi-index en colonne.

Si on oublie l’option reset_index , on obtient le résultat suivant :

Les colonnes ‘Firstname’ et ‘Number’ sont passées en index.

Revenons à notre groupby. Nous obtenons les prénoms les plus données depuis 2004 dans Paris. Les vainqueurs sont Louise et Emma pour les filles et Gabriel et Raphaël pour les garçons. Nous remarquons que le prénom Camille est aussi dans le top 10 des prénoms les plus données. [SV1] 

Vous pouvez ajouter la colonne ‘Sex’ au groupby pour avoir les top prénoms féminins et masculins.

  • Les valeurs NA ne sont pas toujours à supprimer, tout dépend de leurs significations et de ce qu’on veut faire avec le dataframe.
  • Faire attention aux indexes lorsqu’on effectue des transformations sur notre dataframe

Joindre plusieurs dataframes

En datamanagement, une des actions les plus courantes est de joindre plusieurs tables pour regrouper les données en un seul dataframe. Nous avons à notre disposition un second dataframe, l’équivalent du premier, mais avec les prénoms donnés dans la ville de Rennes. Avant de pouvoir joindre ces deux dataframes, nous devons unifier les différentes colonnes.

df_rennes = pd.read_csv('prenoms-a-rennes.csv', sep =';')
df_rennes ['Année de naissance '] = df_rennes [''Année de naissance '].astype(str)
newcols = {
    'Nombre': 'Number',
    'Sexe': 'Sex',
    'Année de naissance': 'Year',
    'Prénom': 'FirstName'
}
df_rennes.rename(columns=newcols, inplace=True)
df_rennes.drop_duplicates(subset=['FirstName', 'Year', 'Sex'], keep='first')
df_rennes['Sex'] = df_rennes['Sex'].apply({'Masculin':'M', 'Féminin':'F'}.get)
df_rennes = df_rennes[ df_rennes['Sex'] != 'X' ]
df_rennes['FirstName'] = df_rennes['FirstName'].apply(lower)

Nous pouvons maintenant joindre ces fichiers :

col_to_keep_paris = ['FirstName', 'Year', 'Sex', 'Number']
col_to_keep_rennes = ['FirstName', 'Year', 'Sex', 'Number']
dfrennes_merged = df_rennes[col_to_keep_rennes].merge(df_paris[col_to_keep_paris], how='inner', on = ['FirstName', 'Year', 'Sex'])

Nous avons deux colonnes qui portent le même nom ‘Number’ mais qui n’a pas été utilisé pour la jointure. Après la jointure, nous récupérons alors deux colonnes ‘Number_x’ et ‘Number_y’. Pour éviter les confusions, nous devons renommer les colonnes avant la jointure.

dfrennes_merged.sort_values(['NumberRennes','NumberParis'], ascending=False)

Nous retrouvons les mêmes prénoms dans les tops prénoms pour la ville de Rennes. Evidemment, les valeurs dans la colonne NumberRennes sont plus petites que pour Paris. On peut imaginer trouver un indicateur permettant de renormaliser ces valeurs permettant de les comparer.[SV1] 

Les différentes possibilités pour l’argument how sont :

  • Left : toutes les lignes du dataframe de gauche sont gardés auxquels sont rajoutés les informations du dataframe de droite
  • Right : symétrique à left
  • Outer : garde toutes les lignes des deux dataframes
  • Inner : gardes uniquement les lignes communs dans les deux dataframes

Pour conclure, nous pouvons chercher les prénoms les plus données sans avoir été donnée dans l’autre ville :

dfrennes_merged = df_rennes[col_to_keep_rennes].merge(df[col_to_keep_paris], how='outer', on = ['FirstName', 'Year', 'Sex'])
dfrennes_merged[np.isnan(dfrennes_merged['NumberParis'])].sort_values(['NumberRennes']).tail()

Pivoter un dataframe

Pandas vous permet également de transformer (reshapping) votre dataframe. Pivoter les colonnes en lignes, ou inversement, les lignes en colonnes, se fait avec deux fonctions qui fonctionnent de manière symétrique : pivot et melt.

La fonction pivot

La fonction pivot prend trois arguments qui déterminent la façon dont le dataframe va pivoter :

  • Values : les valeurs du dataframe original que vous voulez voir dans la table pivot.
  • Columns : cet argument permet de spécifier ce qui deviendra la colonne dans la table pivot.
  • Index : cet argument permet de spécifier ce qui deviendra l’index dans la table pivot.

Afin de mieux évaluer les tendances annuelles et l’évolution du choix des prénoms, nous pouvons utiliser cette fonction pour pivoter notre dataframe

df_pivot = df_paris.pivot_table(values = 'Number', columns = 'Year', index = ['FirstName', 'Sex'])

Le prénom et le sexe sont en index, et les valeurs de la colonnes year sont les noms des nouvelles colonnes. Nous pouvons alors étudier l’évolution du prénom, Hugo, par exemple :

df_pivot.query("FirstName == 'hugo'")

Des agrégations peuvent aussi être effectuées :

df_pivot = df_paris.pivot_table(values = 'Number', columns = 'Year', index = 'FirstName', aggfunc='sum')

Cette dernière ligne de code permet de sommer le nombre d’occurrences des prénoms mixtes en une seule et même ligne.

Nous avons maintenant le dataframe df_pivot (ou pivot_table). Dans le cas où les données d’une ou plusieurs colonnes sont des variables identifiantes, tandis que toutes les autres colonnes sont des valeurs numériques, nous pouvons alors transformer plusieurs colonnes en une seule qui prendre des valeurs différentes. Dans ce cas, le nouveau dataframe aura plus de lignes et moins de colonnes.

La fonction melt

La fonction melt est la fonction inverse de la fonction pivot (ou pivot_table). Dans le cas où les données d’une ou plusieurs colonnes sont des variables identifiantes, tandis que toutes les autres colonnes sont des valeurs numériques, Nous pouvons alors transformer plusieurs colonnes en une seule qui prendra des valeurs différentes. Dans ce cas, le nouveau dataframe aura plus de lignes et moins de colonnes.

df_pivot.reset_index(drop=False,inplace=False).melt(id_vars=['FirstName'],value_vars=df_pivot.columns.tolist())

Nous avons recréé le dataframe d’origine, à une exception près. Nous avons maintenant toutes les combinaisons possibles FirstName/Year, même celles qui n’ont aucune valeur.

Et pour finir, comment sauvegarder un dataframe ?

Une fois retravaillée, nous pouvons sauvegarder notre dataframe sous différents formats, en fonction de l’objectif métier derrière cette automatisation : csv, xlsx, json ou même remplir automatiquement une base SQL-like ou PostgreSQL. Pour les deux premiers, pandas propose de manière native des fonctions qui permettent de le faire à peu de frais :

df.to_csv('myDataFrame.csv', sep='\t', encoding='utf-8')
df.to_excel('myDataFrame.csv', encoding='utf-8')

De nombreuses options permettent d’améliorer la lecture des fichiers de sortie, de l’encodage (toujours un piège), au séparateur des decimals en passant par l’ajout ou non de l’index dans le fichier de sortie (drop = True).

En bonus, le package xlswriter permet d’ajouter couleurs, format de cellule, graphiques, etc. Dans les cas où le fichier de sortie a pour objectif une lecture facile pour les gens du métier, ce package leur change la vie.

Pour les sorties au format json, une fonction existe aussi dans le package pandas. L’argument le plus important est orient qui permet de définir les clés du json. L’index, les colonnes et les valeurs peuvent passer sur plusieurs formats :

with open('df.json', 'w', encoding='utf-8') as file:
    df.to_json(file, force_ascii=False)

La dernière possibilité évoquée ici est de pouvoir remplir une base base SQL-like ou même une base PostgreSQL en itérant sur les lignes de notre dataframe (avec l’aide de psycopg2 par exemple). Pour ceci, rien de plus simple, comme souvent avec python, la fonction iterrows() nous permet d’itérer par ligne en donnant des tuples (index, rows).