Travailler facilement avec les dates sur Pandas

flakito
France School of AI
11 min readJun 17, 2019
La bibliothèque Pandas.

Auteurs : Fatima MOQRAN, Yann Defretin, Arnaud Mercier

Pandas fait partie des bibliothèques indispensables au workflow du parfait développeur souhaitant manipuler des données avec Python. Elle permet, entre autres, de lire des données structurées à partir de fichiers aux formats divers – comme le CSV ou le JSON –, de les manipuler aisément ou encore de les nettoyer.

Dans cet article, nous allons nous concentrer sur l’analyse de données liées au nombre de naissances aux Etats-Unis entre 1969 et 2008 afin de mettre en lumière les fonctionnalités natives de Pandas pour traiter les données et jouer, par exemple, avec es dates afin d’en extraire diverses informations.

Les données utilisées ici sont proposées dans un cours sur OpenClassrooms — elles proviennent en réalité des USA Centers for Disease Control and Prevention. L’objectif de notre exercice est d’arriver à déterminer le nombre de naissances par jour de la semaine, pour chaque décennie. Nous allons donc dans cet article tenter d’arriver jusqu’à ce résultat, moyennant l’application de fonctionnalités de Pandas et nous verrons ensuite quelques pistes permettant d’aller plus loin.

Importer les bibliothèques nécessaires

Avant de travailler avec des bibliothèques comme Pandas ou Numpy, il faut les importer ; et avant même cette étape, il faut installer ces bibliothèques. Si ce n’est pas encore fait sur votre machine, voici donc des instructions pour procéder à l’installation. Une fois que c’est fait, nous pouvons les importer :

import pandas as pd
import numpy as np

Importer les données

Le jeu de données est accessible au format CSV directement via une adresse URL. Cela tombe bien, puisque Pandas permet de télécharger et de lire des données directement depuis une adresse distante :

df = pd.read_csv('https://raw.githubusercontent.com/jakevdp/data-CDCbirths/master/births.csv')df.head(5) # voir les 5 premières lignes
Les données dans leur structure de base.

Nous disposons maintenant des données importées dans un objet de type DataFrame — en gros un tableau ou une table — que nous avons appelé “df” et sur lequel nous allons pouvoir commencer à travailler.

Exploration rapide du jeu de données

Quels sont les noms des colonnes de notre DataFrame?

df.columns

Voyons combien de valeurs il y a dans chaque colonne. La méthode .nunique()nous permet d’afficher le nombre de valeurs uniques pour une Series. Les Seriessont l’autre élément principal de Pandas et sont l’équivalent des colonnes dans un tableau. Chaque colonne consiste en une Series.

df["year"].nunique(), df["month"].nunique(), df["day"].nunique(), df["gender"].nunique()

On peut remarquer tout de suite une erreur dans jeu de données. Il y a 32 valeurs différentes pour la colonne “day”. Les mois ne contenant qu’au maximum 31 jours, il doit y avoir des données à nettoyer. Regardons toutes les journées différentes proposées dans la colonne avec cette fois-ci la méthode .unique() sans “n” donc :

df["day"].unique()

On découvre avec stupeur que dans la liste des jours sont incluses les valeurs “99” et “NaN”, signifiant une valeur manquante. Nous choisissons de purement et simplement retirer les lignes contenant ces valeurs du dataset, puisqu’elles ne sont pas pertinentes dans notre recherche du nombre de naissances pour un jour donné.

Nettoyage des données

Nous ne gardons du jeu de données que les lignes contenant des jours “inférieurs ou égaux à 31” et ne contenant aucune cellule vide :

df = df[ (df["day"] <= 31) & (df["day"].isnull() == False) ]

On va ensuite créer une nouvelle colonne avec la date au format aaaa/mm/jj, de manière à pouvoir travailler avec la bibliothèque Datetime.

df['date'] = pd.to_datetime(df[['year', 'month', 'day']], errors = 'coerce')df.head()
Nouvelle colonne “date” qui regroupe les trois premières colonnes.

On passe au paramètre “errors” de la méthode to_datetimela valeur “coerce”. Cela signifie qu’en cas d’erreur dans la conversion vers un objet datetime, on aura en sortie un NaN — valeur nulle ou manquante — à la place des valeurs non converties.

Analysons le résultat. Quels sont les types des données des colonnes de notre dataset ?

df.dtypes

Il y a-t-il des valeurs manquantes quelque part ? S’il y a eu des erreurs de conversion, to_datetimea dû renvoyer une valeur NaN. Voyons cela :

df.isna().sum()# .sum() compte 1 pour les True et 0 pour les False.
colonne “date” : 24 erreurs

Nous pouvons voir qu’il y a des données manquantes dans la colonne “day”. Cela signifie que des dates n’ont pas pu être converties. En regardant de plus près, on se rend compte que certaines dates étaient effectivement impossibles à transformer. Par exemple, le 31 février, qui n’existe pas ! Il est légitime de se demander d’où viennent de telles erreurs, mais cette investigation sera pour un autre article.

En attendant, effaçons ces données dont nous ne saurions que faire :

# retirons les lignes avec des valeurs manquantes
df.dropna(inplace=True)
# vérifions le résultat
df.isna().sum()

Nous avons désormais un jeu de données tout propre, amputé de quelques centaines de lignes, certes.

Alternativement, nous aurions pu, dès l’ouverture du fichier CSV, créer cette colonne “date” grâce au paramètre “parse_dates” puis retirer les lignes contenant des données invalides ou manquantes en une seule fois :

# réunir les colonnes "year", "month" et "day" dans une nouvelle colonne "date" dès l'ouverture du fichier grâce à parse_datesdf = pd.read_csv('https://raw.githubusercontent.com/jakevdp/data-CDCbirths/master/births.csv', parse_dates={'date': ['year','month','day']})# Convertir la colonne "date" au type datetime si les dates sont valides, sinon supprimer les lignesdef clean(row):
try:
return pd.to_datetime(row['date'], format="%Y %m %d")
except ValueError:
df.drop(row.name, inplace=True, axis=0)
df['date'] = df.apply(clean,axis=1)

Manipulation des données

Voyons à quoi ressemble notre jeu de données désormais et ce qu’il reste à faire pour parvenir à notre but.

df.head()
Notre dataset après nettoyage.

Nous allons désormais créer une colonne contenant le jour de la semaine, en extrayant cette information de l’objet datetime créé plus haut — la colonne “date”.

df["weekday"] = df["date"].dt.day_name()df.head()
Nouvelle colonne “weekday” contenant le jour de la semaine.

Pour résoudre l’exercice, nous allons aussi devoir obtenir l’information de la décennie à laquelle se rattache chaque ligne :

def todecade(y):  return str(y)[2] + '0'df["decade"] = df["year"].apply(todecade)df.head()
Nouvelle colonne “decade” avec la décennie pour chaque ligne.

Les données sont presque prêtes, nous allons désormais les consolider et les représenter au format agrégé qui nous convient.

Agrégation

On crée un nouveau DataFrame à partir d’un objet groupby.

summary = pd.DataFrame(df.groupby(['decade',"weekday"]).mean().astype('int')["births"]) summary

Ce tableau tout en hauteur est un peu difficile à visualiser. Nous pouvons utiliser la méthode .unstack pour faire passer notre index supérieur — on a pour le moment un index hiérarchique à deux niveaux — en index supérieur de colonnes :

summary = summary.unstack(level=0)summary
Tout de suite plus lisible !

Nous préférerions également avoir les jours affichés dans le bon ordre. Pour cela, on peut utiliser la méthode reindex :

week = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]summary = summary.reindex(week)summary
Les jours sont dans le bon ordre désormais.

Visualisation

Nous avons maintenant tous les chiffres nécessaires pour répondre à la question initialement posée qui était : quel est le nombre moyen de naissances par jour de la semaine, pour les décennies 60, 70 et 80 ?

Pour achever le travail, nous trouvons tout de même plus agréable de produire des graphiques permettant de visualiser ces données. On peut choisir de travailler avec la bibliothèque Matplotlib :

import matplotlib.pyplot as plt%matplotlib inline

Il faut savoir que %matplotlib inline permet, lorsqu’on travaille dans Jupyter Notebook, d’afficher les graphiques sans avoir à appeler .show().

Il ne reste plus qu’à afficher les graphiques. On peut les afficher sous différentes formes avec Matplotlib — courbe, histogramme, boîte à moustaches etc. Pour cet article, nous avons choisi de présenter les résultats sous forme de courbe — “line” — et d’histogramme — “bar”.

summary.plot(kind='line', subplots=False)summary.plot(kind='bar', subplots=False)plt.tight_layout() #place les légendes à des endroits plus pertinents
Les naissances par jour de la semaine selon la décennie.

Et voilà, on a répondu à la question posée dans le cours d’OpenClassrooms !

Allons plus loin avec les dates

Pandas permet bien d’autres possibilités lorsqu’il s’agit de travailler avec des dates que nous ne pouvons résumer ici en quelques lignes. La documentation sur les séries temporelles disponible sur le site officiel permet d’entrer dans le vif du sujet de façon plus précise.

Continuons à travailler sur notre jeu de données pour découvrir d’autres fonctionnalités liées aux dates. Par exemple, pour faciliter la gestion des données selon des dates, il pourrait être intéressant d’indexer nos lignes en fonction de la date. Pour cela, nous utilisons la méthode set_index.

df = df.set_index('date')df.head()

Nous pouvons opérer un filtre sur les données en accédant directement à certaines informations contenues dans les objets datetime. Par exemple, on peut accéder au jour du mois d’une date en appelant l’attribut .day de la colonne “date”.

# sélectionnons nos données pour tous les 2 du mois
df[df.index.day == 2]
Afficher uniquement les entrées ayant pour index le deuxième jour d’un mois.

Nous pouvons également accéder au nombre de naissances pour une date précise :

# regardons le nombre de naissance qu'il y a eu le 2 janvier 1988
df.loc['1988-01-02','births']

Il est également possible de sélectionner une période. Par exemple, si nous voulons retrouver les lignes correspondant à la plage allant du 2 janvier au 2 mars 1988 :

df.loc['1988-01-02':'1988-03-02']
Aperçu des naissances entre le 2 janvier 1988 et le 2 mars 1988.

Faisons quelques statistiques. Calculons par exemple la moyenne des naissances par mois en utilisant la méthode .resample. Cette méthode permet d’ échantillonner nos données.

df.resample('M').mean().head()
Moyenne de naissances par mois.

Ici, nous avons les moyennes par mois — “M”. Nous pourrions aussi bien les calculer par jour ou par année — respectivement “D” ou “Y”.

Il est également possible de calculer le minimum, le maximum, la somme, l’écart-type…

df.resample('M').max().head()
Valeur maximum pour chaque colonne par mois.

Pour faire des statistiques avec le nombre total de naissances — filles et garçons — , nous pourrions créer une nouvelle colonne qui englobe les naissances filles et garçons à cette date :

df['total_births']=df.groupby('date').births.sum()
df.head()

Jusqu’à maintenant, nous avons vu seulement une façon de gérer les dates avec Pandas qui s’appelle timestamp . Il indique une valeur à un point du temps et on peut le créer avec date_rangeou encoreto_datetime.

Il en existe d’autres, notamment le timespan qui représente une période comme un jour, un mois une année. Dans bien des cas, c’est une manière plus naturelle d’envisager les dates. La méthode utilisée est Period (documentation).

Les périodes

Transformons notre DataFrame pour le classer par trimestre. Pour changer l’index en index period, on utilise la méthode to_period avec le paramètre freq='Q’ (pour quarter) :

df = df.to_period(freq='Q')

Par défaut, les trimestres commencent à partir du 1er janvier.

Vérifions les dates de début et de fin de chaque trimestre en ajoutant une colonne “date de début” et “date de fin” :

df['start_date'] = df.index.map(lambda x : x.start_time)
df['end_date'] = df.index.map(lambda x : x.end_time.date)

Lorsqu’on utilise la commande to_datetime pour créer des dates, Pandas manipule les données d’entrées pour les faire correspondre au bon format. Ainsi pour une entrée de type “01/12/1989”, Pandas va “comprendre” que le premier nombre est un jour, le second un mois et le dernier une année. Il transforme alors les données au format “aaaa-mm-jj”. S’il ne trouve pas de bon format, il indique une erreur.

Ce comportement de Pandas peut entraîner une mauvaise interprétation des données. Par exemple, une date erronée de type “02/14/2012” — le 2ème jour du 14ème mois n’existe pas — sera interprétée par Pandas comme le “14–02–2012”. Pour empêcher ce comportement, on peut indiquer le format des dates d’entrées. Pandas soulèvera alors une erreur si la date n’existe pas dans le format indiqué. Il est alors judicieux de déterminer l’opération automatiquement effectuée dans ces cas d’erreurs au moyen du paramètre errors=....

pd.to_datetime('2/14/2012', dayfirst=True)
# Résultat : 2012-02-14. Indiquer l'ordre avec 'dayfirst' n’empêche pas la sur-interprétation
pd.to_datetime('2/14/2012', format='%d/%m/%Y')
# Soulève une erreur, ce qui est l'effet recherché

Si les données correspondant à des dates sont dans un format connu (par exemple JJ/MM/AAAA) et sans erreurs, Pandas peut les formater très rapidement avec le paramètre infer_datetime_format :

# Créons une série de dates dans un format "correct"
series_of_dates = []
for i in range(1900, 2000, 1):
for j in range(1, 12, 1):
series_of_dates.append('1'+'/'+str(j)+'/'+str(i))
# Calculons le temps de calcul
%timeit pd.to_datetime(serie_de_dates, infer_datetime_format=False)
%timeit pd.to_datetime(serie_de_dates, infer_datetime_format=True)

Il est parfois plus aisé de travailler avec un timestampdémarrant à partir d’un certaine date. Par défaut, Pandas utilise le timecode d’Unix — le nombre de secondes depuis le 1er janvier 1970.

pd.to_datetime(0, unit='s')

Il est possible de définir notre propre timecode :

pd.to_datetime(0, unit='s', origin=pd.Timestamp('1980/12/4'))

Conclusion

Pandas est une bibliothèque très fournie dont les possibilités sont immenses et les fonctionnalités liées aux dates ne représentent qu’une infime partie de ses capacités.

A travers cet article et cet exercice, nous avons pu aborder les différentes façons de manier et de travailler des jeux de données à l’aide de Pandas et plus particulièrement ceux où les dates jouent un rôle prépondérant.

Nous avons appris à lire un jeu de données, le comprendre, le structurer ainsi que le nettoyer dans le but de l’analyser plus efficacement et atteindre notre objectif initial qui était de représenter le nombre de naissances par semaine et par décennie sous forme de graphiques.

Pour aller plus loin, la documentation officielle est un bon début à laquelle vous pouvez greffer des exercices sous forme de notebook Jupyter réalisés par Guilherme Samora : https://github.com/guipsamora/pandas_exercises.

--

--