.. _chapter_pandas: ****** 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 Lecture / écriture de données ============================= :mod:`pandas` est capable de lire un grand nombre de données structurées avec la méthode :meth:`read_*()`. L'écriture de données dans un fichier utilise la méthode :meth:`to_*()`. .. image:: ../images/14_io_readwrite1.svg :width: 75 % :align: center 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 :download:`iris.csv <../data/iris.csv>`. La lecture se fait avec la méthode :meth:`~pandas.read_csv()` qui prend en argument un fichier ``csv`` et retourne une data frame:: >>> iris = pd.read_csv("iris.csv") >>> type(iris) La :class:`DataFrame` ===================== Pandas permet l'utilisation de structures de données bi dimensionnelles, de type tableur. Ces structures s'appellent des :class:`~pandas.DataFrame`. .. image:: ../images/14_table_dataframe1.svg :width: 75 % :align: center La méthode :meth:`~pandas.DataFrame.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 :meth:`~pandas.DataFrame.values` retourne l'ensemble structuré des valeurs de la data frame sous la forme d'un :class:`numpy.ndarray`. >>> v = iris.values >>> type(v) 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 :meth:`~pandas.DataFrame.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 :meth:`~pandas.DataFrame.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 :meth:`~pandas.DataFrame.ndim`, :meth:`~pandas.DataFrame.size` et :meth:`~pandas.DataFrame.shape` renseignent également sur la structure de la dataframe. >>> iris.ndim, iris.size, iris.shape (2, 750, (150, 5)) Création -------- Les données sont le plus souvent contenues dans des fichiers externes que l'on lit avec la méthode :meth:`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``. Les :class:`Series` =================== Dans la terminologie Pandas, chaque colonne est une :class:`~pandas.Series` et est accessible avec l'opérateur d'indexation. Contrairement à une simple liste Python, une :class:`~pandas.Series` est une structure de données unidimensionnelle pour laquelle tous les éléments ont le même type (:class:`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 :class:`~pandas.Series` de même dimension. .. image:: ../images/14_table_series.svg :width: 40 % :align: center Extrayons une colonne de la data frame :class:`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 :class:`~pandas.Series`. >>> type(pl) Une :class:`~pandas.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) Les attributs ------------- Les attributs renseignent sur la taille, le type, le contenu, etc... de la :class:`~pandas.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]) Les opérations vectorisées -------------------------- :mod:`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 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 :mod:`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 :class:`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 Indexation et itération ----------------------- L'indexation est l'opération qui permet de sélectionner un sous ensemble des données d'une :class:`~pandas.Series`. L'itération est l'opération qui permet de parcourir tout ou partie des éléments d'une :class:`~pandas.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 :meth:`~pandas.Series.at` ou :meth:`~pandas.Series.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 :meth:`~pandas.Series.items` retourne un objet :class:`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 :meth:`~pandas.Series.keys` retourne la séquence d'index. >>> pl.keys() RangeIndex(start=0, stop=150, step=1) Application d'une fonction -------------------------- :meth:`~pandas.Series.apply` permet d'appliquer la fonction passée en argument à chaque élément de la :class:`~pandas.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 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) 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 `__. Sélection de variables ---------------------- .. image:: ../images/14_subset_columns.svg :width: 75 % :align: center 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] Sélection d'observations ------------------------ .. image:: ../images/14_subset_rows.svg :width: 75 % :align: center 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 La méthode :meth:`~pandas.DataFrame.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 :meth:`~pandas.DataFrame.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 :class:`~pandas.DataFrame` lorsqu'il est ``False``. C'est la valeur par défaut. S'il est ``True`` la :class:`~pandas.DataFrame` sur laquelle la méthode :meth:`~pandas.DataFrame.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 :meth:`~pandas.DataFrame.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 ``@``. Création de colonne =================== Il est parfois nécessaire de construire des données additionnelles à celles lues avec la méthode :meth:`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 :meth:`~pandas.DataFrame.rename`. Statistiques descriptives ========================= Les méthodes statistiques évoquées pour les :class:`~pandas.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 :meth:`~pandas.DataFrame.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 :meth:`~pandas.DataFrame.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 :meth:`~pandas.DataFrame.dropna`. 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 :meth:`~pandas.DataFrame.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 :meth:`~pandas.DataFrame.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 :meth:`~pandas.DataFrame.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 Compter le nombre d'observations ================================ Il est souvent nécessaires de dénombrer le nombre d'observations relatives à une catégorie. La méthode :meth:`~pandas.Series.value_counts` est utilisée. >>> iris["variety"].value_counts() Setosa 50 Virginica 50 Versicolor 50 Name: variety, dtype: int64 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 :meth:`~pandas.DataFrame.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. 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 :func:`~pandas.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] 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 :meth:`~pandas.DataFrame.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]