L'analyse de données s'intéresse souvent à grouper des données suivant un critère, le poids suivant l'âge, le salaire suivant métier, les dividendes des entreprises suivant le pays etc.
Pour faire cela Pandas offre la méthode groupby
. Cette méthode
import pandas as pd
import numpy as np
np.random.seed(2)
pd.set_option('display.precision', 3)
size = 20
df = pd.DataFrame({'A': np.random.randn(size),
'B': np.random.randint(5,size=size),
'C': np.random.randint(5,size=size)})
df.B += 3
df
A | B | C | |
---|---|---|---|
0 | -0.417 | 6 | 1 |
1 | -0.056 | 4 | 4 |
2 | -2.136 | 5 | 2 |
3 | 1.640 | 3 | 3 |
4 | -1.793 | 7 | 0 |
5 | -0.842 | 7 | 3 |
6 | 0.503 | 5 | 0 |
7 | -1.245 | 7 | 2 |
8 | -1.058 | 5 | 2 |
9 | -0.909 | 4 | 0 |
10 | 0.551 | 3 | 4 |
11 | 2.292 | 5 | 2 |
12 | 0.042 | 5 | 0 |
13 | -1.118 | 4 | 2 |
14 | 0.539 | 3 | 4 |
15 | -0.596 | 4 | 1 |
16 | -0.019 | 3 | 3 |
17 | 1.175 | 5 | 0 |
18 | -0.748 | 4 | 2 |
19 | 0.009 | 4 | 1 |
Regroupons nos données suivant B et calculons pour toutes les valeurs qui ont la même valeur de B leur moyenne en A et en C (cela pourrait être le poids moyen A et la taille moyenne C de toutes les personnes suivant leur âge B, enfin avec d'autres chiffres).
Ensuite on ajoute une nouvelle colonne qui calcule la taille de chaque groupe.
df2 = df.groupby('B').mean()
df2['countB'] = df.groupby('B').size()
df2
A | C | countB | |
---|---|---|---|
B | |||
3 | 0.678 | 3.500 | 4 |
4 | -0.570 | 1.667 | 6 |
5 | 0.136 | 1.000 | 6 |
6 | -0.417 | 1.000 | 1 |
7 | -1.293 | 1.667 | 3 |
Le contenu de chaque groupe est stocké dans groups
. Pour voir les données d'un groupe dans un tableau on utilisera get_group
.
df.groupby('B').groups
{3: [3, 10, 14, 16], 4: [1, 9, 13, 15, 18, 19], 5: [2, 6, 8, 11, 12, 17], 6: [0], 7: [4, 5, 7]}
df.groupby('B').get_group(7)
A | B | C | |
---|---|---|---|
4 | -1.793 | 7 | 0 |
5 | -0.842 | 7 | 3 |
7 | -1.245 | 7 | 2 |
On peut aussi indiquer plusieurs champs pour regrouper les données ce qui donne des index et sous-index.
df.groupby(['B','C']).first() # get first value of A for each group
A | ||
---|---|---|
B | C | |
3 | 3 | 1.640 |
4 | 0.551 | |
4 | 0 | -0.909 |
1 | -0.596 | |
2 | -1.118 | |
4 | -0.056 | |
5 | 0 | 0.503 |
2 | -2.136 | |
6 | 1 | -0.417 |
7 | 0 | -1.793 |
2 | -1.245 | |
3 | -0.842 |
df.groupby(['B','C']).get_group((3,3))
A | B | C | |
---|---|---|---|
3 | 1.640 | 3 | 3 |
16 | -0.019 | 3 | 3 |
Il est aussi possible de grouper suivant les valeurs de l'index ou d'un sous index. Pour cela il faut indiquer le niveau de l'index à la place du nom de la colonne.
dfm = df.groupby(['B','C']).first()
dfm
A | ||
---|---|---|
B | C | |
3 | 3 | 1.640 |
4 | 0.551 | |
4 | 0 | -0.909 |
1 | -0.596 | |
2 | -1.118 | |
4 | -0.056 | |
5 | 0 | 0.503 |
2 | -2.136 | |
6 | 1 | -0.417 |
7 | 0 | -1.793 |
2 | -1.245 | |
3 | -0.842 |
dfm.groupby(level=1).sum()
A | |
---|---|
C | |
0 | -2.200 |
1 | -1.013 |
2 | -4.499 |
3 | 0.799 |
4 | 0.495 |
Il est possible d'appliquer différentes opérations (fonctions) d'un coup :
df.groupby('B').agg([np.mean, 'last']) # some function are predefined and therefore can be named
/tmp/ipykernel_92450/2617865426.py:1: FutureWarning: The provided callable <function mean at 0x7fdaa8046a70> is currently using SeriesGroupBy.mean. In a future version of pandas, the provided callable will be used directly. To keep current behavior pass the string "mean" instead. df.groupby('B').agg([np.mean, 'last']) # some function are predefined and therefore can be named
A | C | |||
---|---|---|---|---|
mean | last | mean | last | |
B | ||||
3 | 0.678 | -0.019 | 3.500 | 3 |
4 | -0.570 | 0.009 | 1.667 | 1 |
5 | 0.136 | 1.175 | 1.000 | 0 |
6 | -0.417 | -0.417 | 1.000 | 1 |
7 | -1.293 | -1.245 | 1.667 | 2 |
df.groupby('B').agg({'A': "sum", 'C': lambda x : x[x%2 == 0].mean() })
A | C | |
---|---|---|
B | ||
3 | 2.712 | 4.0 |
4 | -3.418 | 2.0 |
5 | 0.817 | 1.0 |
6 | -0.417 | NaN |
7 | -3.880 | 1.0 |
Un problème avec groupby
est qu'on perd le nombre de lignes initiales ce qui complique les choses
si on désire reporter le résultat dans le tableau initial.
Imaginons que je désire ajouter à mon tableau une colonne qui soit la moyenne mensuelle pour chaque valeur afin de relativiser mes valeurs (par rapport à la moyenne).
Avec groupby
j'ai bien la moyenne mais je n'ai plus mes valeurs initiales. Je pourrais faire une opération
compliquée pour ajouter une colonne au tableau initial dans laquelle je recopie la bonne valeur du groupby
.
Je peux aussi utiliser transform
qui fait ce travail.
Remplacer la fonction de réduction f
sur les groupes par transform(f)
permet de conserver le nombre de
lignes du tableau donné.
df = pd.DataFrame({'month': np.random.randint(1,4,size=10),
'day sales': np.random.randint(50,size=10)}).sort_values('month')
df
month | day sales | |
---|---|---|
0 | 1 | 20 |
1 | 1 | 26 |
2 | 2 | 23 |
3 | 2 | 22 |
5 | 2 | 37 |
6 | 2 | 10 |
7 | 2 | 8 |
8 | 2 | 26 |
4 | 3 | 43 |
9 | 3 | 35 |
df.groupby('month').mean()
day sales | |
---|---|
month | |
1 | 23.0 |
2 | 21.0 |
3 | 39.0 |
df.groupby('month').transform("mean")
day sales | |
---|---|
0 | 23.0 |
1 | 23.0 |
2 | 21.0 |
3 | 21.0 |
5 | 21.0 |
6 | 21.0 |
7 | 21.0 |
8 | 21.0 |
4 | 39.0 |
9 | 39.0 |
df['mean day sales'] = df.groupby('month').transform("mean")['day sales']
df
month | day sales | mean day sales | |
---|---|---|---|
0 | 1 | 20 | 23.0 |
1 | 1 | 26 | 23.0 |
2 | 2 | 23 | 21.0 |
3 | 2 | 22 | 21.0 |
5 | 2 | 37 | 21.0 |
6 | 2 | 10 | 21.0 |
7 | 2 | 8 | 21.0 |
8 | 2 | 26 | 21.0 |
4 | 3 | 43 | 39.0 |
9 | 3 | 35 | 39.0 |
In the example above, the months are integers, whereas we could use the datetime
type. But in that case, we would also have the time, and thus grouping by day requires defining what a day is (or a week or a month).
We will see how to do this in pd07 on temporal DataFrames.