4. Pandas

Python et son écosystème sont parfaitement adaptés au traitement des données (Data Science) au point d’en être devenu l’un des deux standards. L’autre étant le langage R issu du monde des statisticiens.

Le package de référence est Pandas.

Il s’installe avec la commande:

$ python -m pip install pandas

Ce qui va suivre est très largement inspiré de la documentation officielle. Il s’agit d’une introduction au concept de data frame dans l’environnement Python. Pour une documentation exhaustive, on consultera le User Guide.

Pour démarrer, la première chose à faire est d’importer le module pandas. L’alias pd est traditionnellement utilisé:

import pandas as pd

4.1. Lecture / écriture de données

pandas est capable de lire un grand nombre de données structurées avec la méthode read_*(). L’écriture de données dans un fichier utilise la méthode to_*().

../_images/14_io_readwrite1.svg

Pour cette découverte, nous allons travailler avec les Iris de Fisher. Le jeu de données comprend 50 échantillons de chacune des trois espèces d’iris (Iris setosa, Iris virginica et Iris versicolor). Quatre caractéristiques ont été mesurées à partir de chaque échantillon : la longueur et la largeur des sépales et des pétales.

Les données stockées au format csv sont disponibles dans le fichier iris.csv. La lecture se fait avec la méthode read_csv() qui prend en argument un fichier csv et retourne une data frame:

>>> iris = pd.read_csv("iris.csv")
>>> type(iris)
<class 'pandas.core.frame.DataFrame'>

4.2. La DataFrame

Pandas permet l’utilisation de structures de données bi dimensionnelles, de type tableur. Ces structures s’appellent des DataFrame.

../_images/14_table_dataframe1.svg

La méthode describe() renseigne sur les données contenues dans cette data frame. Les statistiques descriptives sont utilisées pour les données numériques:

>>> iris.describe()
    sepal.length  sepal.width  petal.length  petal.width
count    150.000000   150.000000    150.000000   150.000000
mean       5.843333     3.057333      3.758000     1.199333
std        0.828066     0.435866      1.765298     0.762238
min        4.300000     2.000000      1.000000     0.100000
25%        5.100000     2.800000      1.600000     0.300000
50%        5.800000     3.000000      4.350000     1.300000
75%        6.400000     3.300000      5.100000     1.800000
max        7.900000     4.400000      6.900000     2.500000

L’attribut values() retourne l’ensemble structuré des valeurs de la data frame sous la forme d’un numpy.ndarray.

>>> v = iris.values
>>> type(v)
<class 'numpy.ndarray'>

En utilisant l’opérateur d’indexation:

>>> v[0:10]
array([[5.1, 3.5, 1.4, 0.2, 'Setosa'],
    [4.9, 3.0, 1.4, 0.2, 'Setosa'],
    [4.7, 3.2, 1.3, 0.2, 'Setosa'],
    [4.6, 3.1, 1.5, 0.2, 'Setosa'],
    [5.0, 3.6, 1.4, 0.2, 'Setosa'],
    [5.4, 3.9, 1.7, 0.4, 'Setosa'],
    [4.6, 3.4, 1.4, 0.3, 'Setosa'],
    [5.0, 3.4, 1.5, 0.2, 'Setosa'],
    [4.4, 2.9, 1.4, 0.2, 'Setosa'],
    [4.9, 3.1, 1.5, 0.1, 'Setosa']], dtype=object)

L’attribut dtypes() renseigne sur le type des données contenues dans la data frame.

>>> iris.dtypes
sepal.length    float64
sepal.width     float64
petal.length    float64
petal.width     float64
variety          object
dtype: object

L’attribut axes() contient la nature et la dimension des axes de la data frame.

>>> iris.axes
[RangeIndex(start=0, stop=150, step=1), Index(['sepal.length', 'sepal.width', 'petal.length', 'petal.width',
    'variety'],
    dtype='object')]

Les attributs ndim(), size() et shape() renseignent également sur la structure de la dataframe.

>>> iris.ndim, iris.size, iris.shape
(2, 750, (150, 5))

4.2.1. Création

Les données sont le plus souvent contenues dans des fichiers externes que l’on lit avec la méthode read_*(). Il est quelquefois nécessaire de créer la data frame à partir de données calculées.

Une façon simple est d’utiliser le constructeur auquel on passe un dictionnaire pour lequel:

  • les clés sont les variables d’observation

  • les valeurs sont les observations structurées sous forme de séquences.

    >>> df = pd.DataFrame({
    ...                     "Name": ["Braund, Mr. Owen Harris",
    ...                             "Allen, Mr. William Henry",
    ...                             "Bonnell, Miss. Elizabeth"],
    ...                     "Age": [22, 35, 58],
    ...                     "Sex": ["male", "male", "female"]}
    ... )
    
    >>> df
                        Name  Age     Sex
    0   Braund, Mr. Owen Harris   22    male
    1  Allen, Mr. William Henry   35    male
    2  Bonnell, Miss. Elizabeth   58  female
    

La variable entière qui précède chaque ligne est appelée index ou label.

4.3. Les Series

Dans la terminologie Pandas, chaque colonne est une Series et est accessible avec l’opérateur d’indexation. Contrairement à une simple liste Python, une Series est une structure de données unidimensionnelle pour laquelle tous les éléments ont le même type (dtype est unique: int64, float64, object, …), ce qui rend les opérations d’accès et de calcul très efficaces. En fait Pandas est construit sur le module Numpy.

Une data frame peut être vue comme une juxtaposition de Series de même dimension.

../_images/14_table_series.svg

Extrayons une colonne de la data frame iris.

>>> pl = iris["petal.length"]
>>> pl
0      1.4
1      1.4
2      1.3
3      1.5
4      1.4
    ...
145    5.2
146    5.0
147    5.2
148    5.4
149    5.1
Name: petal.length, Length: 150, dtype: float64

La colonne est bien de type Series.

>>> type(pl)
<class 'pandas.core.series.Series'>

Une Series est un objet à part entière et, comme la data frame peut être éventuellement créé avec le constructeur.

>>> ages = pd.Series([22, 35, 58], name="Age")
>>> ages
0    22
1    35
2    58
Name: Age, dtype: int64
>>> type(ages)
<class 'pandas.core.series.Series'>

4.3.1. Les attributs

Les attributs renseignent sur la taille, le type, le contenu, etc… de la Series.

>>> pl.size, pl.dtype
(150, dtype('float64'))
>>> pl.values
array([1.4, 1.4, 1.3, 1.5, 1.4, 1.7, 1.4, 1.5, 1.4, 1.5, 1.5, 1.6, 1.4,
    1.1, 1.2, 1.5, 1.3, 1.4, 1.7, 1.5, 1.7, 1.5, 1. , 1.7, 1.9, 1.6,
    1.6, 1.5, 1.4, 1.6, 1.6, 1.5, 1.5, 1.4, 1.5, 1.2, 1.3, 1.4, 1.3,
    1.5, 1.3, 1.3, 1.3, 1.6, 1.9, 1.4, 1.6, 1.4, 1.5, 1.4, 4.7, 4.5,
    4.9, 4. , 4.6, 4.5, 4.7, 3.3, 4.6, 3.9, 3.5, 4.2, 4. , 4.7, 3.6,
    4.4, 4.5, 4.1, 4.5, 3.9, 4.8, 4. , 4.9, 4.7, 4.3, 4.4, 4.8, 5. ,
    4.5, 3.5, 3.8, 3.7, 3.9, 5.1, 4.5, 4.5, 4.7, 4.4, 4.1, 4. , 4.4,
    4.6, 4. , 3.3, 4.2, 4.2, 4.2, 4.3, 3. , 4.1, 6. , 5.1, 5.9, 5.6,
    5.8, 6.6, 4.5, 6.3, 5.8, 6.1, 5.1, 5.3, 5.5, 5. , 5.1, 5.3, 5.5,
    6.7, 6.9, 5. , 5.7, 4.9, 6.7, 4.9, 5.7, 6. , 4.8, 4.9, 5.6, 5.8,
    6.1, 6.4, 5.6, 5.1, 5.6, 6.1, 5.6, 5.5, 4.8, 5.4, 5.6, 5.1, 5.1,
    5.9, 5.7, 5.2, 5. , 5.2, 5.4, 5.1])

4.3.2. Les opérations vectorisées

pandas autorise la « vectorisation » des opérations sur les séquences, ce qui assure:

  • une écriture compacte et lisible

  • une grande efficacité algorithmique

Une liste exhaustive de ce type d’opération ici.

Pour illustrer le concept d’opération vectorisée, on souhaite normaliser les données (cm) pour les rendres compatibles avec le Système International, il faut les passer en mètres. La vectorisation évite d’itérer sur l’ensemble des éléments.

>>> pl_m = pl.mul(0.01)
>>> pl_m
0      0.014
1      0.014
2      0.013
3      0.015
4      0.014
    ...
145    0.052
146    0.050
147    0.052
148    0.054
149    0.051
Name: petal.length, Length: 150, dtype: float64

4.3.3. La conversion de type

La lecture de données hétérogènes conduit parfois à un type qui n’est pas représentatif de la nature de ces données. Un cas courant est de récupérer des données numériques sous la forme de chaines de caractères qui ne se prêtent donc pas aux opérations numériques.

Pour pandas, une chaine de caractères est de type object.

>>> ages = pd.Series(["22", "35", "58"], name="Age")
>>> ages
0    22
1    35
2    58
Name: Age, dtype: object

Les données ont été lues comme des chaînes de caractères et ne sont pas éligibles à un traitement numérique. Pire, l’opération peut se dérouler sans erreur et retourner une valeur aberrante :

>>> ages.mean()
74519.33333333333

Avant traitement, il faut donc convertir le type object en un type numérique.

Les opérations de conversion sont décrites ici.

>>> ages = ages.astype('int64')
>>> ages
0    22
1    35
2    58
Name: Age, dtype: int64

Le type des données a bien été modifié et maintenant les opérations numériques sont légitimes:

>>> ages.mean()
38.333333333333336

4.3.4. Indexation et itération

L’indexation est l’opération qui permet de sélectionner un sous ensemble des données d’une Series. L’itération est l’opération qui permet de parcourir tout ou partie des éléments d’une Series.

Les opérations d’indexation et d’itération sont décrites ici.

>>> pl.at[100]
6.0

Pour cet exemple les index et les positions se confondent et les opérations de type at() ou iat() conduisent au même résultat.

On peut également accéder à un sous ensemble de valeurs. Attention, la convention de slice utilisée ici est différente de celle de Python : le dernier indice est inclus.

>>> pl.loc[40:50]
40    1.3
41    1.3
42    1.3
43    1.6
44    1.9
45    1.4
46    1.6
47    1.4
48    1.5
49    1.4
50    4.7
Name: petal.length, dtype: float64

items() retourne un objet zip constitué de paires (index, value) sur lequel on peut itérer:

>>> for index, value in pl.loc[40:50].items():
...     print(index, '-', value)
...
40 - 1.3
41 - 1.3
42 - 1.3
43 - 1.6
44 - 1.9
45 - 1.4
46 - 1.6
47 - 1.4
48 - 1.5
49 - 1.4
50 - 4.7

keys() retourne la séquence d’index.

>>> pl.keys()
RangeIndex(start=0, stop=150, step=1)

4.3.5. Application d’une fonction

apply() permet d’appliquer la fonction passée en argument à chaque élément de la Series.

>>> import math
>>> pl.loc[40:50].apply(math.sqrt)
40    1.140175
41    1.140175
42    1.140175
43    1.264911
44    1.378405
45    1.183216
46    1.264911
47    1.183216
48    1.224745
49    1.183216
50    2.167948
Name: petal.length, dtype: float64

4.3.6. Maths & Stats

La vectorisation est disponible pour les opérations mathématiques et statistiques.

Les opérations mathématiques et statistiques sont décrites ici.

>>> pl.sum()
563.7
>>> pl.cumsum()
0        1.4
1        2.8
2        4.1
3        5.6
4        7.0
    ...
145    543.0
146    548.0
147    553.2
148    558.6
149    563.7
Name: petal.length, Length: 150, dtype: float64
>>> pl.max(), pl.min()
(6.9, 1.0)
>>> pl.mean(), pl.median()
(3.7580000000000005, 4.35)

4.4. Sélection de données

Le traitement des données implique des mécanismes de sélection d’un sous ensemble de celles ci. Une information complète sur ce mécanisme est fournie ici.

4.4.1. Sélection de variables

../_images/14_subset_columns.svg

Il s’agit ici de sélectionner une ou plusieurs variables (les colonnes de la dataframe) pour l’ensemble des observations.

La sélection par colonne utilise l’opérateur d’indexation. On passe le nom de colonne (ou la séquence de noms) en argument.

>>> iris["petal.length"]
0      1.4
1      1.4
2      1.3
3      1.5
4      1.4
    ...
145    5.2
146    5.0
147    5.2
148    5.4
149    5.1
Name: petal.length, Length: 150, dtype: float64

>>> iris[ ["petal.length", "petal.width"]]
    petal.length  petal.width
0             1.4          0.2
1             1.4          0.2
2             1.3          0.2
3             1.5          0.2
4             1.4          0.2
..            ...          ...
145           5.2          2.3
146           5.0          1.9
147           5.2          2.0
148           5.4          2.3
149           5.1          1.8

[150 rows x 2 columns]

On peut forger une séquence de noms de colonnes avec un prédicat. La list comprehension de Python est particulièrement efficace :

>>> c = [ name for name in iris.columns if "petal" in name ]
>>> c
['petal.length', 'petal.width']
>>> iris[c]
    petal.length  petal.width
0             1.4          0.2
1             1.4          0.2
2             1.3          0.2
3             1.5          0.2
4             1.4          0.2
..            ...          ...
145           5.2          2.3
146           5.0          1.9
147           5.2          2.0
148           5.4          2.3
149           5.1          1.8

[150 rows x 2 columns]

4.4.2. Sélection d’observations

../_images/14_subset_rows.svg

Il s’agit ici de sélectionner les observations (les lignes de la dataframe) qui correspondent à un ou plusieurs critères sur les variables.

L’opérateur d’indexation est utilisé. Pour une sélection inconditionnelle, on passe le range d’index en argument.

>>> iris[::2]
    sepal.length  sepal.width  petal.length  petal.width    variety
0             5.1          3.5           1.4          0.2     Setosa
2             4.7          3.2           1.3          0.2     Setosa
4             5.0          3.6           1.4          0.2     Setosa
6             4.6          3.4           1.4          0.3     Setosa
8             4.4          2.9           1.4          0.2     Setosa
..            ...          ...           ...          ...        ...
140           6.7          3.1           5.6          2.4  Virginica
142           5.8          2.7           5.1          1.9  Virginica
144           6.7          3.3           5.7          2.5  Virginica
146           6.3          2.5           5.0          1.9  Virginica
148           6.2          3.4           5.4          2.3  Virginica

[75 rows x 5 columns]

Pour une sélection conditionnelle, on passe un prédicat en argument. Ici on sélectionne toutes les observations (les lignes) pour lesquelles la longueur du pétale est supérieure à 6 :

>>> iris[ iris["petal.length"] > 6 ]
    sepal.length  sepal.width  petal.length  petal.width    variety
105           7.6          3.0           6.6          2.1  Virginica
107           7.3          2.9           6.3          1.8  Virginica
109           7.2          3.6           6.1          2.5  Virginica
117           7.7          3.8           6.7          2.2  Virginica
118           7.7          2.6           6.9          2.3  Virginica
122           7.7          2.8           6.7          2.0  Virginica
130           7.4          2.8           6.1          1.9  Virginica
131           7.9          3.8           6.4          2.0  Virginica
135           7.7          3.0           6.1          2.3  Virginica

Un prédicat complexe peut être utilisé. Ici on sélectionne toutes les observations (les lignes) pour lesquelles la longueur du pétale est supérieure à 6 et la largeur du sépale inférieure à 3 :

>>> iris[ (iris["petal.length"] > 6) & (iris["sepal.width"] < 3)  ]
    sepal.length  sepal.width  petal.length  petal.width    variety
107           7.3          2.9           6.3          1.8  Virginica
118           7.7          2.6           6.9          2.3  Virginica
122           7.7          2.8           6.7          2.0  Virginica
130           7.4          2.8           6.1          1.9  Virginica

On peut imaginer grouper la sélection des lignes et des colonnes en une seule opération. Mais attention, le faire sans précaution peut conduire à un message d’alerte, même si le résultat est correct:

>>> iris.loc[110:130][ iris["petal.length"] > 6 ]
__main__:1: UserWarning: Boolean Series key will be reindexed to match DataFrame index.
    sepal.length  sepal.width  petal.length  petal.width    variety
117           7.7          3.8           6.7          2.2  Virginica
118           7.7          2.6           6.9          2.3  Virginica
122           7.7          2.8           6.7          2.0  Virginica
130           7.4          2.8           6.1          1.9  Virginica

L’origine de ce warning est que les tailles des structures de données mises en jeu diffèrent.

L’expression iris["petal.length"] > 6 est un masque booléen de taille la longueur de la data frame iris:

>>> mask = iris["petal.length"] > 6
>>> mask.dtype
dtype('bool')
>>> len(mask)
150

L’expression iris.loc[110:130] est un sous ensemble (de taille différente) de la data frame iris:

>>> len(iris.loc[110:130])
21

On préfèrera traiter le problème en séparant distinctement les deux opérations

>>> subset = iris.loc[110:130]
>>> subset[subset["petal.length"] > 6]
    sepal.length  sepal.width  petal.length  petal.width    variety
117           7.7          3.8           6.7          2.2  Virginica
118           7.7          2.6           6.9          2.3  Virginica
122           7.7          2.8           6.7          2.0  Virginica
130           7.4          2.8           6.1          1.9  Virginica

4.4.3. La méthode query()

La sélection d’observations est parfaitement réalisée avec l’opérateur d’indexation comme on l’a vu précédemment. Mais la syntaxe est lourde :

>>> iris[ (iris["petal.length"] > 6) & (iris["sepal.width"] < 3)  ]
    sepal.length  sepal.width  petal.length  petal.width    variety
107           7.3          2.9           6.3          1.8  Virginica
118           7.7          2.6           6.9          2.3  Virginica
122           7.7          2.8           6.7          2.0  Virginica
130           7.4          2.8           6.1          1.9  Virginica

On lui préférera l’utilisation de la méthode query() à laquelle on passe le prédicat exprimé dans une chaîne de caractères.

La chaine de caractère passée en argument est interprétée comme une expression Python. Ici le nom des variables originales est un problème puisque le . séparateur a une signification en Python. Evaluer l’expression petal.length > 6 signifierait un accès à l’attribut length de l’objet petal et conduirait à une erreur.

Renommons les variables (les colonnes) de façon non ambigües :

>>> iris.rename(columns={   "sepal.length": "sepal_length",
...                         "sepal.width": "sepal_width",
...                         "petal.length": "petal_length",
...                         "petal.width": "petal_width"},
                            inplace = true)

Le paramètre inplace permet de créer une nouvelle DataFrame lorsqu’il est False. C’est la valeur par défaut. S’il est True la DataFrame sur laquelle la méthode query() est appelée est modifiée.

>>> iris
    sepal_length  sepal_width  petal_length  petal_width    variety
0             5.1          3.5           1.4          0.2     Setosa
1             4.9          3.0           1.4          0.2     Setosa
2             4.7          3.2           1.3          0.2     Setosa
3             4.6          3.1           1.5          0.2     Setosa
4             5.0          3.6           1.4          0.2     Setosa
..            ...          ...           ...          ...        ...
145           6.7          3.0           5.2          2.3  Virginica
146           6.3          2.5           5.0          1.9  Virginica
147           6.5          3.0           5.2          2.0  Virginica
148           6.2          3.4           5.4          2.3  Virginica
149           5.9          3.0           5.1          1.8  Virginica

[150 rows x 5 columns]

La sélection d’observation est maintenant plus compacte, donc plus lisible :

>>> iris.query(" petal_length > 6 & sepal_width <  3")
    sepal_length  sepal_width  petal_length  petal_width    variety
107           7.3          2.9           6.3          1.8  Virginica
118           7.7          2.6           6.9          2.3  Virginica
122           7.7          2.8           6.7          2.0  Virginica
130           7.4          2.8           6.1          1.9  Virginica

Par la suite on privilégiera donc la méthode query() à l’opérateur d’indexation.

Note

l’utilisation de variables est autorisée à l’intérieur de la chaine qui exprime la requête en préfixant celle ci de @.

4.5. Création de colonne

Il est parfois nécessaire de construire des données additionnelles à celles lues avec la méthode read_*() et d’intégrer celles ci à la data frame initiale. Ici encore l’opérateur d’indexation est utilisé.

A titre d’exemple on peut ajouter un rapport de forme pour les pétales et les sépales :

>>> iris["sepal_rf"] = iris["sepal_length"] / iris["sepal_width"]
>>> iris
    sepal_length  sepal_width  petal_length  petal_width    variety  sepal_rf
0             5.1          3.5           1.4          0.2     Setosa  1.457143
1             4.9          3.0           1.4          0.2     Setosa  1.633333
2             4.7          3.2           1.3          0.2     Setosa  1.468750
3             4.6          3.1           1.5          0.2     Setosa  1.483871
4             5.0          3.6           1.4          0.2     Setosa  1.388889
..            ...          ...           ...          ...        ...       ...
145           6.7          3.0           5.2          2.3  Virginica  2.233333
146           6.3          2.5           5.0          1.9  Virginica  2.520000
147           6.5          3.0           5.2          2.0  Virginica  2.166667
148           6.2          3.4           5.4          2.3  Virginica  1.823529
149           5.9          3.0           5.1          1.8  Virginica  1.966667

[150 rows x 6 columns]

On peut également renommer une ou plusieurs colonnes avec la méthode rename().

4.6. Statistiques descriptives

Les méthodes statistiques évoquées pour les Series s’appliquent évidemment aux data frames.

Lorsqu’on sélectionne une seule colonne, on retrouve le comportement évoqué plus haut :

>>> iris["petal_length"].mean()
3.7580000000000005

Lorsqu’on sélectionne plusieurs colonnes, la méthode statistique est appliquée à chacune :

>>> iris[ ["petal_length", "petal_width"] ].mean()
petal_length    3.758000
petal_width     1.199333
dtype: float64

Rappelons que la méthode describe() applique un ensemble de méthodes statistiques sur les valeurs numériques d’une data frame, ou d’une portion de cette data frame.

>>> iris[ ["petal_length", "petal_width"] ].describe()
    petal_length  petal_width
count    150.000000   150.000000
mean       3.758000     1.199333
std        1.765298     0.762238
min        1.000000     0.100000
25%        1.600000     0.300000
50%        4.350000     1.300000
75%        5.100000     1.800000
max        6.900000     2.500000

La méthode agg() permet de personnaliser les opérations appliquées sur chacune des colonnes :

>>> iris.agg( { "petal_length" : [ 'min', 'max'],
...             "petal_width" : [ 'mean', 'median']})
        petal_length  petal_width
max              6.9          NaN
mean             NaN     1.199333
median           NaN     1.300000
min              1.0          NaN

On note ici l’utilisation de NaN (Not a Number) lorsque les résultats ne sont pas disponibles. Il est employé par exemple lorsqu’une donnée est manquante. NaN fait l’objet d’un traitement particulier dans les opérations sur les data frames. On peut par exemple exclure les observations qui contiennent des données manquantes avec la méthode dropna().

4.7. Groupement par catégories

Le dataset iris contient des données pour trois catégories : Setosa, Versicolor et Virginica. Il est intéressant de disposer d’un mécanisme permettant de faire aisément quelques opérations statistiques sur des groupements de données.

On utilise pour cela la méthode groupby() en lui passant en argument la variable catégorielle utilisée pour le groupement. Si l’argument est une séquence de variables catégorielles, toutes les combinaisons de groupements sont générées.

>>> iris.groupby("variety").mean()
            sepal_length  sepal_width  petal_length  petal_width  sepal_rf
variety
Setosa             5.006        3.428         1.462        0.246  1.470188
Versicolor         5.936        2.770         4.260        1.326  2.160402
Virginica          6.588        2.974         5.552        2.026  2.230453

La méthode groupby() regroupe en fait trois opérations courantes dans le traitement des données (split-apply-combine):

  • Split sépare les données en sous groupes

  • Apply applique une fonction indépendamment à chaque groupe

  • Combine regroupe les résultats dans une data frame

On peut évidemment sélectionner un sous ensemble des données produites par groupby() avant application de la méthode statistique :

>>> iris.groupby("variety")[ ["petal_length", "petal_width"] ].mean()
            petal_length  petal_width
variety
Setosa             1.462        0.246
Versicolor         4.260        1.326
Virginica          5.552        2.026

4.8. Compter le nombre d’observations

Il est souvent nécessaires de dénombrer le nombre d’observations relatives à une catégorie.

La méthode value_counts() est utilisée.

>>> iris["variety"].value_counts()
Setosa        50
Virginica     50
Versicolor    50
Name: variety, dtype: int64

4.9. Restructurer la data frame

Pour faciliter le traitement, il est parfois utile de modifier l’ordonnancement des observations dans une data frame ou de changer sa structure.

La méthode sort_values() peut être utilisée pour modifier l’ordonnancement sans modifier la structure.

>>> iris.sort_values(by="sepal_width")
    sepal_length  sepal_width  petal_length  petal_width     variety  sepal_rf
60            5.0          2.0           3.5          1.0  Versicolor  2.500000
62            6.0          2.2           4.0          1.0  Versicolor  2.727273
119           6.0          2.2           5.0          1.5   Virginica  2.727273
68            6.2          2.2           4.5          1.5  Versicolor  2.818182
41            4.5          2.3           1.3          0.3      Setosa  1.956522
..            ...          ...           ...          ...         ...       ...
16            5.4          3.9           1.3          0.4      Setosa  1.384615
14            5.8          4.0           1.2          0.2      Setosa  1.450000
32            5.2          4.1           1.5          0.1      Setosa  1.268293
33            5.5          4.2           1.4          0.2      Setosa  1.309524
15            5.7          4.4           1.5          0.4      Setosa  1.295455

[150 rows x 6 columns]

On peut vouloir également modifier la structure de la data frame. Il existe deux formats de data frame.

4.9.1. Format long

Le format long présente les données avec une seule observation par ligne. Il est bien adapté pour le traitement automatisé mais n’est pas très pratique pour l’affichage car il contient un nombre de lignes plus important que le format wide utilisé jusqu’à présent dans ce cours.

Ce format est requis par certains packages de visualisation pour faciliter l’affichage.

Le passage du format wide au format long se fait avec la fonction melt() en passant dans l’argument id_vars le nom de la colonne à inclure. Les autres colonnes fournissent les paires (variable, value).

Cette opération crée un nouvel index et la relation entre les 4 variables et le specimen de fleur est perdu. La taille de la data frame créée valide la procédure.

>>> iris_long = pd.melt(iris, id_vars=['variety'])
>>> iris_long
    variety      variable     value
0       Setosa  sepal_length  5.100000
1       Setosa  sepal_length  4.900000
2       Setosa  sepal_length  4.700000
3       Setosa  sepal_length  4.600000
4       Setosa  sepal_length  5.000000
..         ...           ...       ...
745  Virginica      sepal.rf  2.233333
746  Virginica      sepal.rf  2.520000
747  Virginica      sepal.rf  2.166667
748  Virginica      sepal.rf  1.823529
749  Virginica      sepal.rf  1.966667

[750 rows x 3 columns]

On peut souhaiter conserver l’index initial comme une variable d’observation et conserver la relation entre les 4 variables de taille.

>>> iris_long = pd.melt(iris.reset_index(), id_vars=['index'])
>>> iris_long
    index      variable    value
0        0  sepal_length      5.1
1        1  sepal_length      4.9
2        2  sepal_length      4.7
3        3  sepal_length      4.6
4        4  sepal_length        5
..     ...           ...      ...
895    145      sepal_rf  2.23333
896    146      sepal_rf     2.52
897    147      sepal_rf  2.16667
898    148      sepal_rf  1.82353
899    149      sepal_rf  1.96667

[900 rows x 3 columns]

4.9.2. Format wide

Le format wide présente les données avec plusieurs observations par ligne. C’est le cas du dataset initial iris. C’est une présentation compacte adaptée à la présentation textuelle des données.

Le passage du format long au format wide se fait avec la méthode pivot() en passant dans l’argument index le nom de la colonne utilisée comme index, dans l’argument columns le nom des colonnes à créer et dans l’argument values le nom de la colonne contenant les valeurs.

>>> iris_long.pivot(index='index', columns="variable", values="value")
variable petal_length petal_width sepal_length sepal_rf sepal_width    variety
index
0                 1.4         0.2          5.1  1.45714         3.5     Setosa
1                 1.4         0.2          4.9  1.63333           3     Setosa
2                 1.3         0.2          4.7  1.46875         3.2     Setosa
3                 1.5         0.2          4.6  1.48387         3.1     Setosa
4                 1.4         0.2            5  1.38889         3.6     Setosa
...               ...         ...          ...      ...         ...        ...
145               5.2         2.3          6.7  2.23333           3  Virginica
146                 5         1.9          6.3     2.52         2.5  Virginica
147               5.2           2          6.5  2.16667           3  Virginica
148               5.4         2.3          6.2  1.82353         3.4  Virginica
149               5.1         1.8          5.9  1.96667           3  Virginica

[150 rows x 6 columns]