Licence CC BY-NC-ND Valérie Roy
_images/ensmp-25-alpha.png

regroupement de données pandas.DataFrame.groupby

Une table de données pandas est en 2 dimensions mais elle peut indiquer des sous-divisions de vos données. Par exemple, les passagers du Titanic sont divisés en femmes et hommes, en passagers de première, deuxième et troisiéme classe. On pourrait même les diviser en classe d’âge, enfants, jeunes, adultes…

Des analyses mettant en exergue ces différents groupes de personnes peuvent être intéressantes. Lors du naufrage du Titanic, valait-il mieux être une femme en première classe ou un enfant en troisième ?

On reprend nos données

import numpy as np
import pandas as pd
file = 'titanic.csv'
df = pd.read_csv(file, index_col=0)
df.head(3)
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
PassengerId
1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S

On va calculer des regroupements en utilisant la fonction pandas.DataFrame.groupby, à laquelle on indique un ou plusieurs critères.

groupement par critère unique

Par exemple, prenons le seul critère de genre des passagers (Sex). Cette colonne a deux valeurs: female et male, nous allons donc obtenir une partition de notre dataframe en deux dataframes : celle des hommes et celle des femmes, sur lesquelles nous allons pouvoir faire des analyses (moyenne…) différencées par genre.

Allons-y:

df_by_sex = df.groupby('Sex')

pandas calcule les différentes valeurs de la colonne en question (ici Sex), et partitionne la dataframe en autant de dataframes que de valeurs différentes.

pandas met les regroupements dans un objet de type DataFrameGroupBy (ici de nom df_by_sex) qui vous donne accès à de nombreuses fonctionnalités (regardez le help pour plus de détails), nous allons voir ici très peu de choses ici.

les tailles des groupes

Les objets de type DataFrameGroupBy contiennent une fonction très pratique pour récapituler les groupes : size.

df_by_sex.size()
Sex
female    314
male      577
dtype: int64

accéder aux sous-dataframes

On peut aussi itérer sur un objet de type DataFrameGroupBy afin de voir les différentes dataframes.

for name, subdf in df_by_sex:
    print(f"La dataframe de clé '{name}' a {subdf.shape[0]} éléments sur les {df.shape[0]}")
La dataframe de clé 'female' a 314 éléments sur les 891
La dataframe de clé 'male' a 577 éléments sur les 891

Voilà, la fonction a bien partitionné votre dataframe en autant de dataframes que de genre des personnes.

les groupes (dictionnaire d’index par clés)

Vous pouvez accéder, au travers du champ groups des objets de type DataFrameGroupBy, au dictionnaire vous donnant les groupes d’Index (ici deux parce male et female).

Chaque clé est une des valeurs possibles (donc à nouveau male et female), et sa valeur est la liste des index des lignes ayant cette valeur:

# df_by_sex.groups est un dictionnaire
# et voici ses clés et valeurs 
for k, v in df_by_sex.groups.items():
    print(f"{k}\t{v}")
female	→ Int64Index([  2,   3,   4,   9,  10,  11,  12,  15,  16,  19,
            ...
            867, 872, 875, 876, 880, 881, 883, 886, 888, 889],
           dtype='int64', name='PassengerId', length=314)
male	→ Int64Index([  1,   5,   6,   7,   8,  13,  14,  17,  18,  21,
            ...
            874, 877, 878, 879, 882, 884, 885, 887, 890, 891],
           dtype='int64', name='PassengerId', length=577)

groupement multi-critères

Si maintenant on s’intéresse à plusieurs colonnes ? Comment est-ce que ça pourrait se présenter à votre avis ?

La solution adoptée, c’est de passer à groupby, non plus une seule colonne mais .. une liste de colonnes.

Le fonctionnement de groupby dans ce cas consiste à

  • calculer pour chaque colonne les valeurs distinctes (comme dans le cas simple)

  • et en faire le produit cartésien pour obtenir les clés du groupement (incidemment, sous la forme de tuples)

Ainsi dans notre exemple si nous prenons les critères : Pclass etSex:

  • le premier critère donne trois valeurs 1, 2 et 3 pour les trois classes de navigation

  • le second donne 2 valeurs female et male

et donc on va avoir 6 tuples qui serviront de clés pour le groupement : (1, ‘female’), (1, ‘male’), (2, ‘female’)…

df_by_sex_class = df.groupby(['Pclass', 'Sex'])

les tailles des groupes

Pour faire une synthèse on peut utiliser size() pour récapituler les groupes; la présentation nous montre bien le produit cartésien qui a été fait

df_by_sex_class.size()
Pclass  Sex   
1       female     94
        male      122
2       female     76
        male      108
3       female    144
        male      347
dtype: int64

En une seule ligne:

df.groupby(['Pclass', 'Sex']).size()
Pclass  Sex   
1       female     94
        male      122
2       female     76
        male      108
3       female    144
        male      347
dtype: int64

accéder aux sous-dataframes

De même nous pouvons itérer sur les sous-dataframes.

# pour itérer sur les 6 catégories
for name, dataframe in df_by_sex_class:
    print(f"{len(dataframe)} passagers en classe '{name[0]}' de genre '{name[1]}'")
94 passagers en classe '1' de genre 'female'
122 passagers en classe '1' de genre 'male'
76 passagers en classe '2' de genre 'female'
108 passagers en classe '2' de genre 'male'
144 passagers en classe '3' de genre 'female'
347 passagers en classe '3' de genre 'male'

Pour les curieux, une petite astuce, utile à ce stade; on pourrait avoir envie d’utiliser la méthode .head() pour afficher chacune des sous-dataframes, en écrivant ceci

# malheureusement ceci ne marche pas !!
for name, dataframe in df_by_sex_class:
    dataframe.head(1)

En fait ce qui se passe ici, c’est que la méthode .head() renvoie un objet que le notebook sait afficher; donc quand on écrit une cellule qui ne contient que (ou dont la dernière instruction est) df.head(), cet objet se fait afficher parce que c’est le résultat de la cellule (comme quand 40 se fait affichez quand vous évaluez une cellule avec juste 10+30)

Mais dans le cas du for qu’on est en train de vouloir écrire, le résultat du for est None, et rien ne se fait afficher; il nous faut donc provoquer l’affichage (un peu comme quand on est obligé d’insérer un print() au milieu d’une cellule); voici l’astuce pour arriver à provoquer l’affichage souhaité :

# pour que ça fonctionne il faut forcer l'affichage 
# en utilisant display() qui se trouve dans le module IPython
import IPython 
for name, dataframe in df_by_sex_class:
    IPython.display.display(dataframe.head(1))
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
PassengerId
2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
PassengerId
7 0 1 McCarthy, Mr. Timothy J male 54.0 0 0 17463 51.8625 E46 S
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
PassengerId
10 1 2 Nasser, Mrs. Nicholas (Adele Achem) female 14.0 1 0 237736 30.0708 NaN C
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
PassengerId
18 1 2 Williams, Mr. Charles Eugene male NaN 0 0 244373 13.0 NaN S
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
PassengerId
3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.925 NaN S
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
PassengerId
1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.25 NaN S

À vous de jouer : calculer le groupby avec le genre, la classe et si la personne a survécu ou non. Dans quel groupe de personnes reste-il le plus de survivants ? Et le moins ?

# votre code ici (une petite idée de correction ci-dessous)
df_by_sex_class_survived = df.groupby(['Pclass', 'Sex', 'Survived'])
df_by_sex_class_survived.size()
Pclass  Sex     Survived
1       female  0             3
                1            91
        male    0            77
                1            45
2       female  0             6
                1            70
        male    0            91
                1            17
3       female  0            72
                1            72
        male    0           300
                1            47
dtype: int64

les groupes (dictionnaire d’index par clés)

Lister les clés

Les clés sont des tuples de valeurs.

df_by_sex_class_survived.groups.keys()
dict_keys([(1, 'female', 0), (1, 'female', 1), (1, 'male', 0), (1, 'male', 1), (2, 'female', 0), (2, 'female', 1), (2, 'male', 0), (2, 'male', 1), (3, 'female', 0), (3, 'female', 1), (3, 'male', 0), (3, 'male', 1)])

Les valeurs sont des listes d’index, ça me permet de retrouver les entrées dans la dataframe d’origine.

Par exemple, si nous voulons accéder aux trois seules femmes de première classe qui n’ont pas survécu. La clé est (1, 'female', 0).

Nous allons, cette fois, les rechercher dans la grande dataframe. Remarquez que bien sûr ici on utilise loc puisque nous sommes uniquement dans l’espace des index.

df.loc[df_by_sex_class_survived.groups[(1, 'female', 0)]]
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
PassengerId
178 0 1 Isham, Miss. Ann Elizabeth female 50.0 0 0 PC 17595 28.7125 C49 C
298 0 1 Allison, Miss. Helen Loraine female 2.0 1 2 113781 151.5500 C22 C26 S
499 0 1 Allison, Mrs. Hudson J C (Bessie Waldo Daniels) female 25.0 1 2 113781 151.5500 C22 C26 S

découper des intervalles de valeurs dans une colonne

Parfois, nous sommes intéressés, non pas par les différentes valeurs d’une colonne (qui seraient trop nombreuses) mais par des intervalles de ces valeurs.

Prenons par exemple la colonne des âges. Si nous faisons un groupement brutalement sur la colonne Age, nous obtenons 88 âges différents, ce qui ne nous apporte pas une information intéressante.

Par contre ça devient plus intéressant si on raisonne par classes d’âges, par exemple les enfants, jeunes, adultes et les plus de 55 ans.

  • ‘enfant’ disons entre 0 et 12 ans

  • ‘jeune’ disons entre 12 et 19 ans

  • ‘adulte’ disons entre 19 et 55 ans

  • ‘+55’ et les personnes de plus de 55 ans

Nous aimerions donc avoir une colonne dans notre dataframe avec ces labels pour compléter les âges.

La fonction pandas.cut, appliquée à une colonne de votre dataframe, va vous générer une telle colonne, et vous pouvez donner des labels aux intervalles:

Nous allons rajouter la colonne à la dataframe. Sachant que les colonnes d’une dataframe sont les clés d’un dictionnaire, pour ajouter une colonne à votre dataframe, il faut faire comme pour les dict en Python.

# 'bin' en anglais signifie corbeille ou panier
# c'est comme si on mettait les gens dans 4 paniers

df['age-periode'] = pd.cut(df['Age'], bins=[0, 12, 19, 55, 100])

Je montre les 6 premières lignes des 3 dernières colonnes de la dataframe:

# on a rajouté une colonne 
df[df.columns[-3:]].head(6)
Cabin Embarked age-periode
PassengerId
1 NaN S (19.0, 55.0]
2 C85 C (19.0, 55.0]
3 NaN S (19.0, 55.0]
4 C123 S (19.0, 55.0]
5 NaN S (19.0, 55.0]
6 NaN Q NaN

Je donne des noms aux périodes d’âge (ici on va rajouter encore une colonne)

df['name-age-periode'] = pd.cut(df['Age'], bins=[0, 12, 19, 55, 100], 
                                labels=['children', ' young', 'adult', 'old'])
df[df.columns[-3:]].head(6)
Embarked age-periode name-age-periode
PassengerId
1 S (19.0, 55.0] adult
2 C (19.0, 55.0] adult
3 S (19.0, 55.0] adult
4 S (19.0, 55.0] adult
5 S (19.0, 55.0] adult
6 Q NaN NaN

Et maintenant nous pouvons utiliser cette colonne dans des groupby

df.groupby(['name-age-periode']).size()
name-age-periode
children     69
 young       95
adult       510
old          40
dtype: int64
# etc...
df.groupby(['name-age-periode', 'Survived']).size()
name-age-periode  Survived
children          0            29
                  1            40
 young            0            56
                  1            39
adult             0           311
                  1           199
old               0            28
                  1            12
dtype: int64
# etc..
df.groupby(['Pclass', 'Sex', 'Survived', ]).size()
Pclass  Sex     Survived
1       female  0             3
                1            91
        male    0            77
                1            45
2       female  0             6
                1            70
        male    0            91
                1            17
3       female  0            72
                1            72
        male    0           300
                1            47
dtype: int64

Vous avez désormais une petite idée de l’utilisation de la fonction groupby pour des recherches multi-critères sur une table de données.