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>
output_165_1
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>
output_169_1

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.

comments powered by Disqus