Analyse et Modélisation de données en Python avec Pandas
Dans cet article, nous allons explorer comment faire de l’analyse de données en Python, en utilisant la librairie Pandas. L’article est assez long, car j’ai essayé d’explorer toutes les possibilités offertes par Pandas.
Avant de commencer, il est important de noter que Pandas a pour objectif d’effectuer de l’analyse et de la modélisation de données. Par contre, il n’implémente pas un grand nombre de fonctionnalités de modélisation en dehors de la régression linéaire. Pour ce type d’analyses avancées, il faut plutôt regarder statsmodel et scikit-learn.
Les exemples de codes Python que vous trouverez dans cet article sont pensés pour être utilisés dans une session de l’interpréteur Python. Je recommande d’utiliser ipython ou mieux, un notebook Jupyter.
Commençons!
Les structures de données Pandas
-
Les Series pour les données 1D labélisées et homogènement typées (ce sont des conteneurs pour des données scalaires). La taille des séries est immutable.
-
Les DataFrame pour les données 2D labélisées, de taille mutable, tabulaires et potentiellement de types hétérogènes (ce sont des conteneurs de Séries)
Bon à savoir: la grande majorité des méthodes des structures Pandas produisent de nouveaux objets et ne modifient pas les objets d’entrée.
Créer une structure de données Pandas
Pandas est souvent utilisé de concert avec Numpy et matplotlib. Nous commençons donc par importer ces 3 librairies:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
Quand on crée une série en passant une liste de scalaires au constructeur, Pandas crée un index automatiquement (des entiers, comme pour les index des listes):
s = pd.Series([1, 3, 5, np.nan, 6, 8])
s
0 1.0
1 3.0
2 5.0
3 NaN
4 6.0
5 8.0
dtype: float64
On peut créer un index de dates avec pandas, en précisant la date de début et le nombre de dates qu’on veut avoir dans l’index, sous la forme d’un nombre de jours consécutifs à partir du jour précisé dans la date de début.
Dans le code qui suit, on crée un index de dates commençant le 30 mars 2019, et s’étendant sur 6 jours:
dates = pd.date_range('20190330', periods=6)
dates
DatetimeIndex(['2019-03-30', '2019-03-31', '2019-04-01', '2019-04-02',
'2019-04-03', '2019-04-04'],
dtype='datetime64[ns]', freq='D')
On peut construire un DataFrame à partir d’un array numpy de dimension 2, en précisant les colonnes et les index.
df = pd.DataFrame(np.random.randn(6, 4), index=dates, columns=list('ABCD'))
df
A | B | C | D | |
---|---|---|---|---|
2019-03-30 | -2.759782 | -0.754355 | 0.636811 | -0.412057 |
2019-03-31 | -0.153566 | 0.246851 | 1.359295 | -0.276277 |
2019-04-01 | -0.698618 | 0.809926 | 0.687781 | 0.694458 |
2019-04-02 | -0.350507 | 1.054788 | -0.461713 | 0.801541 |
2019-04-03 | 0.369715 | 1.549302 | 0.642595 | -0.136862 |
2019-04-04 | 0.437396 | 0.310099 | -0.169594 | 0.391921 |
On peut construire un DataFrame à partir d’un dictionnaire. Les clés deviennent alors les colonnes (dimensions du dataframe) et les valeurs sont répétées si nécessaire (si ce sont des scalaires), dans le cas où l’une des colonnes contient une Série ou un array numpy de taille supérieure à 1.
df2 = pd.DataFrame({
'A': 1.,
'B': pd.Timestamp('20190221'),
'C': pd.Series(1, index=list(range(4))),
'D': np.array([3] * 4, dtype='int32'),
'E': pd.Categorical(['test', 'train', 'test', 'train']),
'F': 'foo'
})
df2
A | B | C | D | E | F | |
---|---|---|---|---|---|---|
0 | 1.0 | 2019-02-21 | 1 | 3 | test | foo |
1 | 1.0 | 2019-02-21 | 1 | 3 | train | foo |
2 | 1.0 | 2019-02-21 | 1 | 3 | test | foo |
3 | 1.0 | 2019-02-21 | 1 | 3 | train | foo |
La preuve que les dataframe peuvent avoir des données de types hétérogènes:
df2.dtypes
A float64
B datetime64[ns]
C int64
D int32
E category
F object
dtype: object
Voir les données
On peut voir les premières lignes, et les dernières lignes, l’index, les colonnes, la représentation Numpy des données sous-jacentes, des statistiques de base des données. On peut également accéder à la transposée des données et les ordonner soit par index, soit par valeurs en précisant la colonne à partir de laquelle les données seront ordonnées.
df.head(3)
A | B | C | D | |
---|---|---|---|---|
2019-03-30 | -2.759782 | -0.754355 | 0.636811 | -0.412057 |
2019-03-31 | -0.153566 | 0.246851 | 1.359295 | -0.276277 |
2019-04-01 | -0.698618 | 0.809926 | 0.687781 | 0.694458 |
df.tail(5)
A | B | C | D | |
---|---|---|---|---|
2019-03-31 | -0.153566 | 0.246851 | 1.359295 | -0.276277 |
2019-04-01 | -0.698618 | 0.809926 | 0.687781 | 0.694458 |
2019-04-02 | -0.350507 | 1.054788 | -0.461713 | 0.801541 |
2019-04-03 | 0.369715 | 1.549302 | 0.642595 | -0.136862 |
2019-04-04 | 0.437396 | 0.310099 | -0.169594 | 0.391921 |
df.index
DatetimeIndex(['2019-03-30', '2019-03-31', '2019-04-01', '2019-04-02',
'2019-04-03', '2019-04-04'],
dtype='datetime64[ns]', freq='D')
df.columns
Index(['A', 'B', 'C', 'D'], dtype='object')
df2.to_numpy() # Méthode non disponible dans la version 0.23 de Pandas
array([[1.0, Timestamp('2019-02-21 00:00:00'), 1, 3, 'test', 'foo'],
[1.0, Timestamp('2019-02-21 00:00:00'), 1, 3, 'train', 'foo'],
[1.0, Timestamp('2019-02-21 00:00:00'), 1, 3, 'test', 'foo'],
[1.0, Timestamp('2019-02-21 00:00:00'), 1, 3, 'train', 'foo']],
dtype=object)
df.describe()
A | B | C | D | |
---|---|---|---|---|
count | 6.000000 | 6.000000 | 6.000000 | 6.000000 |
mean | -0.525894 | 0.536102 | 0.449196 | 0.177121 |
std | 1.176395 | 0.796318 | 0.658857 | 0.520569 |
min | -2.759782 | -0.754355 | -0.461713 | -0.412057 |
25% | -0.611590 | 0.262663 | 0.032008 | -0.241423 |
50% | -0.252036 | 0.560012 | 0.639703 | 0.127529 |
75% | 0.238895 | 0.993572 | 0.676485 | 0.618823 |
max | 0.437396 | 1.549302 | 1.359295 | 0.801541 |
df.T
2019-03-30 00:00:00 | 2019-03-31 00:00:00 | 2019-04-01 00:00:00 | 2019-04-02 00:00:00 | 2019-04-03 00:00:00 | 2019-04-04 00:00:00 | |
---|---|---|---|---|---|---|
A | -2.759782 | -0.153566 | -0.698618 | -0.350507 | 0.369715 | 0.437396 |
B | -0.754355 | 0.246851 | 0.809926 | 1.054788 | 1.549302 | 0.310099 |
C | 0.636811 | 1.359295 | 0.687781 | -0.461713 | 0.642595 | -0.169594 |
D | -0.412057 | -0.276277 | 0.694458 | 0.801541 | -0.136862 | 0.391921 |
df.sort_index(axis=0, ascending=False)
A | B | C | D | |
---|---|---|---|---|
2019-04-04 | 0.437396 | 0.310099 | -0.169594 | 0.391921 |
2019-04-03 | 0.369715 | 1.549302 | 0.642595 | -0.136862 |
2019-04-02 | -0.350507 | 1.054788 | -0.461713 | 0.801541 |
2019-04-01 | -0.698618 | 0.809926 | 0.687781 | 0.694458 |
2019-03-31 | -0.153566 | 0.246851 | 1.359295 | -0.276277 |
2019-03-30 | -2.759782 | -0.754355 | 0.636811 | -0.412057 |
df.sort_values(by='B')
A | B | C | D | |
---|---|---|---|---|
2019-03-30 | -2.759782 | -0.754355 | 0.636811 | -0.412057 |
2019-03-31 | -0.153566 | 0.246851 | 1.359295 | -0.276277 |
2019-04-04 | 0.437396 | 0.310099 | -0.169594 | 0.391921 |
2019-04-01 | -0.698618 | 0.809926 | 0.687781 | 0.694458 |
2019-04-02 | -0.350507 | 1.054788 | -0.461713 | 0.801541 |
2019-04-03 | 0.369715 | 1.549302 | 0.642595 | -0.136862 |
Sélectionner les données
Il est recommandé d’utiliser les méthodes optimisées d’accès
aux données spécifiques de Pandas telles que .at
, .iat
,
.loc
et .iloc
pour les codes déployés en production, et
de se contenter des expressions de sélection standards de
Python et Numpy pour les analyses simples de données.
Récupérer les données
Les dataframes sont comme des dictionnaires:
df['A']
2019-03-30 -2.759782
2019-03-31 -0.153566
2019-04-01 -0.698618
2019-04-02 -0.350507
2019-04-03 0.369715
2019-04-04 0.437396
Freq: D, Name: A, dtype: float64
La structure renvoyée est une Series
(une colonne de
dataframe)
type(df['A'])
pandas.core.series.Series
On peut sélectionner un sous-ensemble du dataframe en appliquant un slice:
df[0:3]
A | B | C | D | |
---|---|---|---|---|
2019-03-30 | -2.759782 | -0.754355 | 0.636811 | -0.412057 |
2019-03-31 | -0.153566 | 0.246851 | 1.359295 | -0.276277 |
2019-04-01 | -0.698618 | 0.809926 | 0.687781 | 0.694458 |
Le type retourné est un DataFrame
type(df[0:3])
pandas.core.frame.DataFrame
Sélection par Label
On peut récupérer une série indexée par les colonnes du
DataFrame
(une ligne):
df.loc[dates[0]]
A -2.759782
B -0.754355
C 0.636811
D -0.412057
Name: 2019-03-30 00:00:00, dtype: float64
Ou un DataFrame
en récupérant plusieurs lignes
df.loc[:, ['A', 'B']]
A | B | |
---|---|---|
2019-03-30 | -2.759782 | -0.754355 |
2019-03-31 | -0.153566 | 0.246851 |
2019-04-01 | -0.698618 | 0.809926 |
2019-04-02 | -0.350507 | 1.054788 |
2019-04-03 | 0.369715 | 1.549302 |
2019-04-04 | 0.437396 | 0.310099 |
Pour le slicing par Label des lignes, les 2 labels des extrémités sont inclus (à la différence du slicing des listes Python)
df.loc['20190330':'20190331', ['A', 'B']]
A | B | |
---|---|---|
2019-03-30 | -2.759782 | -0.754355 |
2019-03-31 | -0.153566 | 0.246851 |
Pour récupérer un scalaire, il faut sélectionner le Label (la ligne) et la colonne:
df.loc[dates[0], 'A']
-2.759782433495169
L’accès est beaucoup plus rapide si on utilise la méthode
.at
df.at[dates[0], 'A']
-2.759782433495169
Sélection par position
La sélection par position se fait en utilisant les méthode
préfixées .i
df.iloc[3]
A -0.350507
B 1.054788
C -0.461713
D 0.801541
Name: 2019-04-02 00:00:00, dtype: float64
df.iloc[3:5, 0:2]
A | B | |
---|---|---|
2019-04-02 | -0.350507 | 1.054788 |
2019-04-03 | 0.369715 | 1.549302 |
Remarquez que dans le cas précédent, on retrouve le comportement du slicing des listes en Python (l’extrême droite n’est pas inclu).
On peut passer une liste des indices qu’on veut sélectionner pour les lignes ou pour les colonnes (cherry-pick):
df.iloc[[1, 2, 4], [0, 2]]
A | C | |
---|---|---|
2019-03-31 | -0.153566 | 1.359295 |
2019-04-01 | -0.698618 | 0.687781 |
2019-04-03 | 0.369715 | 0.642595 |
Pour obtenir un slice uniquement sur les lignes:
df.iloc[1:3, :]
A | B | C | D | |
---|---|---|---|---|
2019-03-31 | -0.153566 | 0.246851 | 1.359295 | -0.276277 |
2019-04-01 | -0.698618 | 0.809926 | 0.687781 | 0.694458 |
Pour obtenir un slice uniquement sur les colonnes:
df.iloc[:, 1:3]
B | C | |
---|---|---|
2019-03-30 | -0.754355 | 0.636811 |
2019-03-31 | 0.246851 | 1.359295 |
2019-04-01 | 0.809926 | 0.687781 |
2019-04-02 | 1.054788 | -0.461713 |
2019-04-03 | 1.549302 | 0.642595 |
2019-04-04 | 0.310099 | -0.169594 |
Pour récupérer une valeur scalaire explicitement:
df.iloc[1, 1]
0.24685134417220603
Ou encore plus rapide:
df.iat[1, 1]
0.24685134417220603
Indexation booléenne
On peut récupérer les lignes pour lesquelles une certaine condition est vérifiée sur une colonne:
df[df.A > 0]
A | B | C | D | |
---|---|---|---|---|
2019-04-03 | 0.369715 | 1.549302 | 0.642595 | -0.136862 |
2019-04-04 | 0.437396 | 0.310099 | -0.169594 | 0.391921 |
Ou alors récupérer les lignes pour lesquelles une condition est vérifiée pour au moins une colonne dans la ligne:
df[df > 0]
A | B | C | D | |
---|---|---|---|---|
2019-03-30 | NaN | NaN | 0.636811 | NaN |
2019-03-31 | NaN | 0.246851 | 1.359295 | NaN |
2019-04-01 | NaN | 0.809926 | 0.687781 | 0.694458 |
2019-04-02 | NaN | 1.054788 | NaN | 0.801541 |
2019-04-03 | 0.369715 | 1.549302 | 0.642595 | NaN |
2019-04-04 | 0.437396 | 0.310099 | NaN | 0.391921 |
On peut sélectionner les lignes pour lesquelles les valeurs d’une certaine colonne sont contenue dans une collection de valeurs:
df2 = df.copy()
df2['E'] = ['one', 'one', 'two', 'three', 'four', 'three']
df2
A | B | C | D | E | |
---|---|---|---|---|---|
2019-03-30 | -2.759782 | -0.754355 | 0.636811 | -0.412057 | one |
2019-03-31 | -0.153566 | 0.246851 | 1.359295 | -0.276277 | one |
2019-04-01 | -0.698618 | 0.809926 | 0.687781 | 0.694458 | two |
2019-04-02 | -0.350507 | 1.054788 | -0.461713 | 0.801541 | three |
2019-04-03 | 0.369715 | 1.549302 | 0.642595 | -0.136862 | four |
2019-04-04 | 0.437396 | 0.310099 | -0.169594 | 0.391921 | three |
df2[df2['E'].isin(['two', 'four'])]
A | B | C | D | E | |
---|---|---|---|---|---|
2019-04-01 | -0.698618 | 0.809926 | 0.687781 | 0.694458 | two |
2019-04-03 | 0.369715 | 1.549302 | 0.642595 | -0.136862 | four |
Assignation de valeurs
Quand on crée une nouvelle colonne, les données sont automatiquement alignées sur l’index déjà en place (les valeurs associées à l’index déjà en place sont conservées, celles en trop dans l’ordre lexicographique sont ignorées et celles manquantes sont mises à NAN)
s1 = pd.Series([1, 2, 3, 4, 5, 6], index=pd.date_range('20190331', periods=6))
s1
2019-03-31 1
2019-04-01 2
2019-04-02 3
2019-04-03 4
2019-04-04 5
2019-04-05 6
Freq: D, dtype: int64
df['F'] = s1
df
A | B | C | D | F | |
---|---|---|---|---|---|
2019-03-30 | -2.759782 | -0.754355 | 0.636811 | -0.412057 | NaN |
2019-03-31 | -0.153566 | 0.246851 | 1.359295 | -0.276277 | 1.0 |
2019-04-01 | -0.698618 | 0.809926 | 0.687781 | 0.694458 | 2.0 |
2019-04-02 | -0.350507 | 1.054788 | -0.461713 | 0.801541 | 3.0 |
2019-04-03 | 0.369715 | 1.549302 | 0.642595 | -0.136862 | 4.0 |
2019-04-04 | 0.437396 | 0.310099 | -0.169594 | 0.391921 | 5.0 |
On peut assigner des valeurs en indexant le dataframe par des labels ou par des entiers:
df.at[dates[0], 'A'] = 0
df
A | B | C | D | F | |
---|---|---|---|---|---|
2019-03-30 | 0.000000 | -0.754355 | 0.636811 | -0.412057 | NaN |
2019-03-31 | -0.153566 | 0.246851 | 1.359295 | -0.276277 | 1.0 |
2019-04-01 | -0.698618 | 0.809926 | 0.687781 | 0.694458 | 2.0 |
2019-04-02 | -0.350507 | 1.054788 | -0.461713 | 0.801541 | 3.0 |
2019-04-03 | 0.369715 | 1.549302 | 0.642595 | -0.136862 | 4.0 |
2019-04-04 | 0.437396 | 0.310099 | -0.169594 | 0.391921 | 5.0 |
df.iat[0, 1] = 0
df
A | B | C | D | F | |
---|---|---|---|---|---|
2019-03-30 | 0.000000 | 0.000000 | 0.636811 | -0.412057 | NaN |
2019-03-31 | -0.153566 | 0.246851 | 1.359295 | -0.276277 | 1.0 |
2019-04-01 | -0.698618 | 0.809926 | 0.687781 | 0.694458 | 2.0 |
2019-04-02 | -0.350507 | 1.054788 | -0.461713 | 0.801541 | 3.0 |
2019-04-03 | 0.369715 | 1.549302 | 0.642595 | -0.136862 | 4.0 |
2019-04-04 | 0.437396 | 0.310099 | -0.169594 | 0.391921 | 5.0 |
On peut aussi assigner une nouvelle colonne en utilisant un ndarray Numpy. Il faut par contre que la taille du ndarray soit la même que celle du dataframe (son nombre de lignes)
df.loc[:, 'D'] = np.array([5] * len(df))
df
A | B | C | D | F | |
---|---|---|---|---|---|
2019-03-30 | 0.000000 | 0.000000 | 0.636811 | 5 | NaN |
2019-03-31 | -0.153566 | 0.246851 | 1.359295 | 5 | 1.0 |
2019-04-01 | -0.698618 | 0.809926 | 0.687781 | 5 | 2.0 |
2019-04-02 | -0.350507 | 1.054788 | -0.461713 | 5 | 3.0 |
2019-04-03 | 0.369715 | 1.549302 | 0.642595 | 5 | 4.0 |
2019-04-04 | 0.437396 | 0.310099 | -0.169594 | 5 | 5.0 |
On peut modifier des valeurs dans le dataframe aux endroits
vérifiant un condition (une clause WHERE
):
df2 = df.copy()
df2[df2 > 0] = -df2
df2
A | B | C | D | F | |
---|---|---|---|---|---|
2019-03-30 | 0.000000 | 0.000000 | -0.636811 | -5 | NaN |
2019-03-31 | -0.153566 | -0.246851 | -1.359295 | -5 | -1.0 |
2019-04-01 | -0.698618 | -0.809926 | -0.687781 | -5 | -2.0 |
2019-04-02 | -0.350507 | -1.054788 | -0.461713 | -5 | -3.0 |
2019-04-03 | -0.369715 | -1.549302 | -0.642595 | -5 | -4.0 |
2019-04-04 | -0.437396 | -0.310099 | -0.169594 | -5 | -5.0 |
Données manquantes
Les données manquantes dans Pandas sont représentées par
np.nan
. Ces données manquantes ne sont pas incluses dans
les calculs par défaut.
Pour changer, ajouter ou effacer des index sur des axes
spécifiques, on applique l’opération .reindex
en précisant
le nouvel index (pour les lignes) et les nouvelles colonnes:
df1 = df.reindex(index=dates[0:4], columns=list(df.columns) + ['E'])
df1.loc[dates[0]:dates[1], 'E'] = 1
df1
A | B | C | D | F | E | |
---|---|---|---|---|---|---|
2019-03-30 | 0.000000 | 0.000000 | 0.636811 | 5 | NaN | 1.0 |
2019-03-31 | -0.153566 | 0.246851 | 1.359295 | 5 | 1.0 | 1.0 |
2019-04-01 | -0.698618 | 0.809926 | 0.687781 | 5 | 2.0 | NaN |
2019-04-02 | -0.350507 | 1.054788 | -0.461713 | 5 | 3.0 | NaN |
Pour supprimer les lignes qui ont des données manquantes, il
faut utiliser .dropna
df1.dropna(how='any')
A | B | C | D | F | E | |
---|---|---|---|---|---|---|
2019-03-31 | -0.153566 | 0.246851 | 1.359295 | 5 | 1.0 | 1.0 |
Pour remplacer les valeurs manquantes par une valeur par
défaut, il faut utiliser .fillna
df1.fillna(value=5)
A | B | C | D | F | E | |
---|---|---|---|---|---|---|
2019-03-30 | 0.000000 | 0.000000 | 0.636811 | 5 | 5.0 | 1.0 |
2019-03-31 | -0.153566 | 0.246851 | 1.359295 | 5 | 1.0 | 1.0 |
2019-04-01 | -0.698618 | 0.809926 | 0.687781 | 5 | 2.0 | 5.0 |
2019-04-02 | -0.350507 | 1.054788 | -0.461713 | 5 | 3.0 | 5.0 |
On peut obtenir un masque de booléens, qui indique les
endroits où il manque des données avec pd.isna
pd.isna(df1)
A | B | C | D | F | E | |
---|---|---|---|---|---|---|
2019-03-30 | False | False | False | False | True | False |
2019-03-31 | False | False | False | False | False | False |
2019-04-01 | False | False | False | False | False | True |
2019-04-02 | False | False | False | False | False | True |
Opérations
En général, les opérations exclues les données manquantes.
Statistiques
Pour obtenir une moyenne colonne par colonne:
df.mean()
A -0.065930
B 0.661828
C 0.449196
D 5.000000
F 3.000000
dtype: float64
Pour obtenir une moyenne ligne par ligne:
df.mean(1)
2019-03-30 1.409203
2019-03-31 1.490516
2019-04-01 1.559818
2019-04-02 1.648513
2019-04-03 2.312322
2019-04-04 2.115580
Freq: D, dtype: float64
Le 1
dans le code précédent indique suivant quel axe on
veut faire la moyenne, sachant que 0
(ou pas d’argument)
correspond à l’axe des lignes et 1
à l’axe des colonnes.
On peut faire des opérations entre 2 structures de données de dimensions différentes (dataframe et series):
s = pd.Series([1, 3, 5, np.nan, 6, 8], index=dates).shift(2)
s
2019-03-30 NaN
2019-03-31 NaN
2019-04-01 1.0
2019-04-02 3.0
2019-04-03 5.0
2019-04-04 NaN
Freq: D, dtype: float64
df.sub(s, axis='index')
A | B | C | D | F | |
---|---|---|---|---|---|
2019-03-30 | NaN | NaN | NaN | NaN | NaN |
2019-03-31 | NaN | NaN | NaN | NaN | NaN |
2019-04-01 | -1.698618 | -0.190074 | -0.312219 | 4.0 | 1.0 |
2019-04-02 | -3.350507 | -1.945212 | -3.461713 | 2.0 | 0.0 |
2019-04-03 | -4.630285 | -3.450698 | -4.357405 | 0.0 | -1.0 |
2019-04-04 | NaN | NaN | NaN | NaN | NaN |
Application
On peut appliquer des fonctions aux données colonne par colonne (le paramètre de la fonction est une colonne)
df.apply(np.cumsum)
A | B | C | D | F | |
---|---|---|---|---|---|
2019-03-30 | 0.000000 | 0.000000 | 0.636811 | 5 | NaN |
2019-03-31 | -0.153566 | 0.246851 | 1.996106 | 10 | 1.0 |
2019-04-01 | -0.852184 | 1.056777 | 2.683887 | 15 | 3.0 |
2019-04-02 | -1.202691 | 2.111565 | 2.222174 | 20 | 6.0 |
2019-04-03 | -0.832976 | 3.660867 | 2.864769 | 25 | 10.0 |
2019-04-04 | -0.395580 | 3.970966 | 2.695176 | 30 | 15.0 |
df.apply(lambda x: x.max() - x.min())
A 1.136014
B 1.549302
C 1.821009
D 0.000000
F 4.000000
dtype: float64
Histogrammes
Pour obtenir un histogramme (le compte valeur par valeur),
on dispose de la méthode .value_counts
s = pd.Series(np.random.randint(0, 7, size=10))
s
0 0
1 2
2 0
3 5
4 5
5 4
6 2
7 0
8 0
9 1
dtype: int64
s.value_counts()
0 4
5 2
2 2
4 1
1 1
dtype: int64
Méthodes sur les chaînes de caractères
Ces méthodes sont rassemblées dans l’attribut str
des
structures de données Pandas, et permettent d’effectuer des
opérations sur les valeurs de la structure qui sont des
chaînes de caractères.
s = pd.Series(['A', 'B', 'C', 'Aaba', 'Baca', np.nan, 'CABA', 'dog', 'cat'])
s.str.lower()
0 a
1 b
2 c
3 aaba
4 baca
5 NaN
6 caba
7 dog
8 cat
dtype: object
Combinaisons
Pandas fournit différentes façons de combiner aisément les
Series
et les DataFrame
. On peut le faire en utilisant
différents types de logiques d’ensembles pour les index et
d’algèbre relationnelle pour les opérations de type jointure
/ fusion.
Concaténer
La concaténation de 2 objets Pandas se fait grâce à la
fonction pd.concat
df = pd.DataFrame(np.random.randn(10, 4))
df
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | -1.729973 | 0.058934 | -0.434967 | 1.108158 |
1 | -0.933198 | -1.116243 | 0.398964 | -1.573282 |
2 | 0.023501 | 0.625520 | -0.154714 | -0.654235 |
3 | 0.344503 | -0.249123 | -1.504876 | -1.203312 |
4 | -0.244597 | 0.115774 | -0.830470 | 2.052748 |
5 | -1.340891 | 0.584198 | 0.238852 | -0.596003 |
6 | 3.031119 | -1.437290 | -0.581537 | -1.749066 |
7 | 0.224386 | 0.097499 | 0.577294 | -0.069991 |
8 | 0.292925 | -0.606479 | 1.050523 | -1.607612 |
9 | -0.878079 | -1.290651 | -0.628939 | -0.334491 |
pieces = [df[:3], df[3:7], df[7:]]
pd.concat(pieces)
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | -1.729973 | 0.058934 | -0.434967 | 1.108158 |
1 | -0.933198 | -1.116243 | 0.398964 | -1.573282 |
2 | 0.023501 | 0.625520 | -0.154714 | -0.654235 |
3 | 0.344503 | -0.249123 | -1.504876 | -1.203312 |
4 | -0.244597 | 0.115774 | -0.830470 | 2.052748 |
5 | -1.340891 | 0.584198 | 0.238852 | -0.596003 |
6 | 3.031119 | -1.437290 | -0.581537 | -1.749066 |
7 | 0.224386 | 0.097499 | 0.577294 | -0.069991 |
8 | 0.292925 | -0.606479 | 1.050523 | -1.607612 |
9 | -0.878079 | -1.290651 | -0.628939 | -0.334491 |
Joindre
Il s’agit de l’opération analogue en SQL. Pour ce faire, il
faut utilser la fonction pd.merge
left = pd.DataFrame({'key': ['foo', 'foo'], 'lval': [1, 2]})
right = pd.DataFrame({'key': ['foo', 'foo'], 'rval': [4, 5]})
left
key | lval | |
---|---|---|
0 | foo | 1 |
1 | foo | 2 |
right
key | rval | |
---|---|---|
0 | foo | 4 |
1 | foo | 5 |
pd.merge(left, right, on='key')
key | lval | rval | |
---|---|---|---|
0 | foo | 1 | 4 |
1 | foo | 1 | 5 |
2 | foo | 2 | 4 |
3 | foo | 2 | 5 |
Le pd.merge
effectue une union des données des 2
structures en se basant sur la clé passée en paramètre.
left = pd.DataFrame({'key': ['foo', 'bar'], 'lval': [1, 2]})
right = pd.DataFrame({'key': ['foo', 'bar'], 'rval': [4, 5]})
left
key | lval | |
---|---|---|
0 | foo | 1 |
1 | bar | 2 |
right
key | rval | |
---|---|---|
0 | foo | 4 |
1 | bar | 5 |
pd.merge(left, right, on='key')
key | lval | rval | |
---|---|---|---|
0 | foo | 1 | 4 |
1 | bar | 2 | 5 |
Annexer
On peut rajouter des lignes à un DataFrame
df = pd.DataFrame(np.random.randn(8, 4), columns=['A', 'B', 'C', 'D'])
df
A | B | C | D | |
---|---|---|---|---|
0 | 0.620042 | 0.222848 | -1.542318 | 0.265275 |
1 | -0.035750 | -0.484962 | 1.657518 | 0.920229 |
2 | 1.610796 | -0.123767 | 3.037619 | -1.396509 |
3 | 0.047643 | -0.687491 | -0.944381 | 3.190332 |
4 | -0.780613 | 0.409961 | -0.673624 | 0.905534 |
5 | -1.532464 | -0.396571 | -0.105000 | -0.054862 |
6 | 1.699681 | -0.072909 | -0.830766 | -0.770954 |
7 | -1.513352 | 0.328086 | 0.934592 | -0.124238 |
s = df.iloc[3]
df.append(s, ignore_index=True)
A | B | C | D | |
---|---|---|---|---|
0 | 0.620042 | 0.222848 | -1.542318 | 0.265275 |
1 | -0.035750 | -0.484962 | 1.657518 | 0.920229 |
2 | 1.610796 | -0.123767 | 3.037619 | -1.396509 |
3 | 0.047643 | -0.687491 | -0.944381 | 3.190332 |
4 | -0.780613 | 0.409961 | -0.673624 | 0.905534 |
5 | -1.532464 | -0.396571 | -0.105000 | -0.054862 |
6 | 1.699681 | -0.072909 | -0.830766 | -0.770954 |
7 | -1.513352 | 0.328086 | 0.934592 | -0.124238 |
8 | 0.047643 | -0.687491 | -0.944381 | 3.190332 |
Grouper
L’opération .grouby
est une procédure impliquant une ou
plusieurs des actions suivantes:
-
Séparer les données en groupes suivant un certain critère
-
Appliquer une fonction à chaque groupe indépendamment
-
Combiner le résultat dans une structure de donnée
df = pd.DataFrame({'A': ['foo', 'bar', 'foo', 'bar',
'foo', 'bar', 'foo', 'foo'],
'B': ['one', 'one', 'two', 'three',
'two', 'two', 'one', 'three'],
'C': np.random.randn(8),
'D': np.random.randn(8)})
df
A | B | C | D | |
---|---|---|---|---|
0 | foo | one | 0.647957 | -0.725883 |
1 | bar | one | 0.376489 | -0.385296 |
2 | foo | two | -0.399363 | -1.342656 |
3 | bar | three | 0.458775 | 1.688988 |
4 | foo | two | 1.076639 | 0.131735 |
5 | bar | two | -1.100755 | -0.753938 |
6 | foo | one | 0.084555 | 1.546450 |
7 | foo | three | 1.105213 | -1.946767 |
df.groupby('A').sum()
C | D | |
---|---|---|
A | ||
bar | -0.265491 | 0.549754 |
foo | 2.515001 | -2.337120 |
Grouper par plusieurs colonnes forme un index hiérarchique
df.groupby(['A', 'B']).sum()
C | D | ||
---|---|---|---|
A | B | ||
bar | one | 0.376489 | -0.385296 |
three | 0.458775 | 1.688988 | |
two | -1.100755 | -0.753938 | |
foo | one | 0.732512 | 0.820567 |
three | 1.105213 | -1.946767 | |
two | 0.677276 | -1.210921 |
Modification de la forme des structures de données
Empilement
La méthode .stack
“comprime” un niveau dans les colonnes
du DataFrame (i.e les colonnes deviennent un niveau
supplémentaire d’indexation à la ligne des données)
tuples = list(zip(*[['bar', 'bar', 'baz', 'baz',
'foo', 'foo', 'qux', 'qux'],
['one', 'two', 'one', 'two',
'one', 'two', 'one', 'two']]))
index = pd.MultiIndex.from_tuples(tuples, names=['first', 'second'])
df = pd.DataFrame(np.random.randn(8, 2), index=index, columns=['A', 'B'])
df2 = df[:4]
df2
A | B | ||
---|---|---|---|
first | second | ||
bar | one | -0.030511 | 0.412391 |
two | -0.790359 | -1.050496 | |
baz | one | -0.641583 | 0.139852 |
two | -0.621345 | 0.009085 |
stacked = df2.stack()
stacked
first second
bar one A -0.030511
B 0.412391
two A -0.790359
B -1.050496
baz one A -0.641583
B 0.139852
two A -0.621345
B 0.009085
dtype: float64
L’opération inverse de .stack
c’est .unstack
. La
structure d’entrée doit contenir un MultiIndex
comme index
pour que ça marche:
stacked.unstack()
A | B | ||
---|---|---|---|
first | second | ||
bar | one | -0.030511 | 0.412391 |
two | -0.790359 | -1.050496 | |
baz | one | -0.641583 | 0.139852 |
two | -0.621345 | 0.009085 |
Si une valeur numérique est passée en paramètre, elle indique suivant quel niveau d’indexation il faut déplier les données:
stacked.unstack(1)
second | one | two | |
---|---|---|---|
first | |||
bar | A | -0.030511 | -0.790359 |
B | 0.412391 | -1.050496 | |
baz | A | -0.641583 | -0.621345 |
B | 0.139852 | 0.009085 |
stacked.unstack(0)
first | bar | baz | |
---|---|---|---|
second | |||
one | A | -0.030511 | -0.641583 |
B | 0.412391 | 0.139852 | |
two | A | -0.790359 | -0.621345 |
B | -1.050496 | 0.009085 |
Tableaux croisés dynamiques (Pivot tables)
D’après Wikipédia, un tableau croisé
dynamique
est une synthèse d’une table de données brutes. Dans Pandas,
une telle table s’obtient à l’aide de la fonction
pd.pivot_table
df = pd.DataFrame({'A': ['one', 'one', 'two', 'three'] * 3,
'B': ['A', 'B', 'C'] * 4,
'C': ['foo', 'foo', 'foo', 'bar', 'bar', 'bar'] * 2,
'D': np.random.randn(12),
'E': np.random.randn(12)})
df
A | B | C | D | E | |
---|---|---|---|---|---|
0 | one | A | foo | -1.637443 | -0.198674 |
1 | one | B | foo | 0.538599 | 0.902056 |
2 | two | C | foo | -2.065462 | -0.817254 |
3 | three | A | bar | 0.298942 | 0.411964 |
4 | one | B | bar | 1.100118 | 2.218368 |
5 | one | C | bar | 0.198245 | -0.056927 |
6 | two | A | foo | 0.917070 | -0.406791 |
7 | three | B | foo | -1.003526 | -2.001790 |
8 | one | C | foo | -0.379830 | -0.396019 |
9 | one | A | bar | 0.406914 | 1.897216 |
10 | two | B | bar | 1.009347 | -0.139946 |
11 | three | C | bar | -0.182149 | -1.637063 |
pd.pivot_table(df, values='D', index=['A', 'B'], columns=['C'])
C | bar | foo | |
---|---|---|---|
A | B | ||
one | A | 0.406914 | -1.637443 |
B | 1.100118 | 0.538599 | |
C | 0.198245 | -0.379830 | |
three | A | 0.298942 | NaN |
B | NaN | -1.003526 | |
C | -0.182149 | NaN | |
two | A | NaN | 0.917070 |
B | 1.009347 | NaN | |
C | NaN | -2.065462 |
Séries temporelles
Pandas fournit un moyen simple, puissant et efficient d’effectuer du re-échantillonage (par exemple, passer de données variant toutes les secondes à des données variant toutes les 5 minutes)
rng = pd.date_range('1/1/2012', periods=100, freq='S')
ts = pd.Series(np.random.randint(0, 500, len(rng)), index=rng)
ts.resample('5Min').sum()
2012-01-01 29667
Freq: 5T, dtype: int64
Pandas permet de représenter les zones temporelles (TimeZones)
rng = pd.date_range('3/6/2012 00:00', periods=5, freq='D')
ts = pd.Series(np.random.randn(len(rng)), rng)
ts
2012-03-06 -1.802124
2012-03-07 1.500321
2012-03-08 -0.263506
2012-03-09 -1.027141
2012-03-10 -2.014588
Freq: D, dtype: float64
Le résultat ci-dessus n’est pas “localisé” (i.e ne contient
pas les “vraie” date et heure associée à un pays). Pour ce
faire, il suffit d’appeler la méthode tz_localize
:
ts_utc = ts.tz_localize('UTC')
ts_utc
2012-03-06 00:00:00+00:00 -1.802124
2012-03-07 00:00:00+00:00 1.500321
2012-03-08 00:00:00+00:00 -0.263506
2012-03-09 00:00:00+00:00 -1.027141
2012-03-10 00:00:00+00:00 -2.014588
Freq: D, dtype: float64
On peut ainsi convertir d’une timezone à l’autre
ts_utc.tz_convert('US/Eastern')
2012-03-05 19:00:00-05:00 -1.802124
2012-03-06 19:00:00-05:00 1.500321
2012-03-07 19:00:00-05:00 -0.263506
2012-03-08 19:00:00-05:00 -1.027141
2012-03-09 19:00:00-05:00 -2.014588
Freq: D, dtype: float64
On peut convertir entre différentes étendues
temporelles. Créons une série temporelle s’étendant sur 5
mois (freq="M"
ci-dessous):
rng = pd.date_range('1/1/2012', periods=5, freq='M')
ts = pd.Series(np.random.randn(len(rng)), index=rng)
ts
2012-01-31 0.441643
2012-02-29 -0.711276
2012-03-31 0.784683
2012-04-30 -0.984221
2012-05-31 0.061811
Freq: M, dtype: float64
Ci-dessus, on a débuté l’index temporel au 1er Janvier 2012. Comme on a précisé qu’on voulait une fréquence mensuelle, Pandas a automatiquement converti l’index en prenant les fins de mois de la période.
Avec la méthode to_period
, on peut ne retenir que le mois
(dans le cadre d’une fréquence mensuelle):
ps = ts.to_period()
ps
2012-01 0.441643
2012-02 -0.711276
2012-03 0.784683
2012-04 -0.984221
2012-05 0.061811
Freq: M, dtype: float64
Avec la méthode to_timestamp
, Pandas respecte la date de
début de la période. C’est donc le 1er jour de chaque mois
de la période qui est choisi:
ps.to_timestamp()
2012-01-01 0.441643
2012-02-01 -0.711276
2012-03-01 0.784683
2012-04-01 -0.984221
2012-05-01 0.061811
Freq: MS, dtype: float64
Convertir entre période et timestamp permet d’appliquer certaines fonctions arithmétiques utiles. Par exemple, pour convertir d’une fréquence de 1/4 d’années avec l’année se terminant en Novembre en une fréquence de 1/4 d’années chaque quart se terminant à 9h du matin de la fin du mois suivant la fin du 1/4, on fait ceci:
prng = pd.period_range('1990Q1', '2000Q4', freq='Q-NOV')
ts = pd.Series(np.random.randn(len(prng)), prng)
ts.index = (prng.asfreq('M', 'e') + 1).asfreq('H', 's')
ts.head()
1990-03-01 00:00 -0.182267
1990-06-01 00:00 0.342509
1990-09-01 00:00 -0.179646
1990-12-01 00:00 0.147279
1991-03-01 00:00 0.938427
Freq: H, dtype: float64
Catégories
Pandas peut inclure des données de catégories dans un DataFrame
df = pd.DataFrame({'id': [1, 2, 3, 4, 5, 6],
'raw_grade': ['a', 'b', 'b', 'a', 'a', 'e']})
Convertir les moyennes brûtes en données de type catégories:
df['grade'] = df['raw_grade'].astype('category')
df['grade']
0 a
1 b
2 b
3 a
4 a
5 e
Name: grade, dtype: category
Categories (3, object): [a, b, e]
Renommer les catégories en des noms plus significatifs:
df['grade'].cat.categories = ['very good', 'good', 'very bad']
Réordonner les catégories et ajouter simultanément les catégories manquantes:
df['grade'] = df['grade'].cat.set_categories(['very bad', 'bad', 'medium', 'good', 'very good'])
df['grade']
0 very good
1 good
2 good
3 very good
4 very good
5 very bad
Name: grade, dtype: category
Categories (5, object): [very bad, bad, medium, good, very good]
Affichages graphiques
La méthode .plot
est une méthode utile pour afficher sur
un graphique toutes les colonnes avec les labels:
ts = pd.Series(np.random.randn(1000),
index=pd.date_range('1/1/2000', periods=1000))
ts = ts.cumsum()
ts.plot()
<matplotlib.axes._subplots.AxesSubplot at 0x7fc3a2db3748>
df = pd.DataFrame(np.random.randn(1000, 4), index=ts.index,
columns=['A', 'B', 'C', 'D'])
df = df.cumsum()
plt.figure()
df.plot()
<matplotlib.axes._subplots.AxesSubplot at 0x7fc3a2a7d470>
Gestion des entrées / sorties des données
CSV
utiliser .to_csv
pour écrire dans un CSV, .read_csv
pour
lire un CSV.
HDF5
Utiliser .to_hdf
pour écrire dans un HDFStore et
.read_hdf
pour en lire un.
Excel
Utiliser .to_excel
pour écrire et .read_excel
pour la
lecture.
Pour conclure, comme vous pouvez le voir, Pandas offre un panel bien large de possibilité pour l’analyse des données avec Python.