Post

Résumé du cours « Pandas » de Kaggle (2) – Leçons 4‑6

Guide pratique de pandas pour nettoyer et transformer les données: résumé enrichi du cours « Pandas » de Kaggle. Cette seconde partie couvre les Leçons 4–6 (groupby, tri, dtypes, NaN, renommage, concat/join).

Résumé du cours « Pandas » de Kaggle (2) – Leçons 4‑6

Je regroupe ici mes notes issues du cours Pandas de Kaggle.
Le volume étant conséquent, j’ai scindé l’article en deux parties.

Certificat d’achèvement

Lesson 4. Grouping and Sorting

Il arrive souvent qu’on doive regrouper les données et appliquer des opérations par groupe, ou encore trier selon certains critères.

Analyse par groupe

La méthode groupby() regroupe les lignes partageant la même valeur dans une colonne donnée, puis permet d’inspecter ou de transformer chaque groupe.

Nous avons déjà vu la méthode value_counts(). On peut reproduire un comportement équivalent avec groupby() comme suit:

1
reviews.groupby('taster_name').size()
  1. regrouper le DataFrame reviews par valeurs égales de la colonne taster_name
  2. renvoyer une Series avec la taille (nombre de lignes) de chaque groupe

Ou bien:

1
reviews.groupby('taster_name').taster_name.count()
  1. regrouper le DataFrame reviews par valeurs égales de la colonne taster_name
  2. dans chaque groupe, sélectionner la colonne taster_name
  3. renvoyer le nombre de valeurs non manquantes sous forme de Series

Autrement dit, value_counts() est un raccourci pour ce type d’opération. Outre count(), on peut utiliser toute fonction de synthèse de la même manière. Par exemple, pour obtenir le prix minimal par note:

1
reviews.groupby('points').price.min()
1
2
3
4
5
6
7
points
80      5.0
81      5.0
       ... 
99     44.0
100    80.0
Name: price, Length: 21, dtype: float64
  1. regrouper le DataFrame reviews par valeurs égales de la colonne points
  2. dans chaque groupe, sélectionner la colonne price
  3. renvoyer la valeur minimale sous forme de Series

On peut aussi regrouper selon plusieurs colonnes. Pour sélectionner, par pays et par province, le vin ayant la note la plus élevée:

1
reviews.groupby(['country', 'province']).apply(lambda df: df.loc[df.points.idxmax()])

Une autre méthode utile des objets DataFrameGroupBy est agg(). Elle permet d’exécuter plusieurs fonctions à la fois sur chaque groupe.

En argument, on peut passer:

  • une fonction
  • une chaîne avec le nom de la fonction
  • une liste de fonctions et/ou de noms de fonctions
  • un dictionnaire dont les clés sont des étiquettes d’axe et les valeurs des fonctions (ou listes de fonctions) à appliquer à cet axe

Les fonctions doivent pouvoir:

Cet éclaircissement n’apparaît pas dans le cours Kaggle original; il est ajouté d’après la documentation officielle de pandas.

Par exemple, pour calculer des statistiques de prix par pays:

1
reviews.groupby(['country']).price.agg([len, min, max])

Ici, len désigne la fonction Python intégrée len(). Dans cet exemple, on l’utilise pour afficher le nombre de valeurs de prix (price) par groupe (country), en incluant les valeurs manquantes. Comme la fonction accepte DataFrame/Series en entrée, elle est applicable ici.

La méthode count() de pandas, elle, compte uniquement les valeurs valides (non manquantes), d’où une différence de comportement.

Cette précision, absente du cours Kaggle, s’appuie sur la documentation officielle de Python et de pandas.

Index multi-niveaux

Avec groupby(), on obtient parfois en sortie des DataFrame indexés par un index à plusieurs niveaux (MultiIndex) plutôt qu’un simple index plat.

1
2
countries_reviewed = reviews.groupby(['country', 'province']).description.agg([len])
countries_reviewed
len
Countryprovince
ArgentinaMendoza Province3264
Other536
.........
UruguaySan Jose3
Uruguay24
1
2
mi = countries_reviewed.index
type(mi)
1
pandas.core.indexes.multi.MultiIndex

Le MultiIndex offre des méthodes spécifiques aux structures hiérarchiques. Le guide utilisateur de pandas détaille les cas d’usage et bonnes pratiques dans la section MultiIndex / advanced indexing.

Cela dit, la méthode la plus fréquemment utilisée sera sans doute reset_index() pour aplatir l’index en index simple.

1
countries_reviewed.reset_index()
 countryprovincelen
0ArgentinaMendoza Province3264
1ArgentinaOther536
423UruguaySan Jose3
424UruguayUruguay24

Tri

Si l’on observe countries_reviewed, on constate que le résultat d’un groupby est renvoyé dans l’ordre des valeurs d’index. Autrement dit, l’ordre des lignes provient des étiquettes d’index, pas du contenu.

On peut trier explicitement autrement avec sort_values(). Par exemple, trier pays et provinces par le nombre d’entrées (‘len’) croissant:

1
2
countries_reviewed = countries_reviewed.reset_index()
countries_reviewed.sort_values(by='len')
 countryprovincelen
179GreeceMuscat of Kefallonian1
192GreeceSterea Ellada1
415USWashington8639
392USCalifornia36247

Par défaut, sort_values() trie par ordre croissant; on peut obtenir l’ordre décroissant via:

1
countries_reviewed.sort_values(by='len', ascending=False)
 countryprovincelen
392USCalifornia36247
415USWashington8639
63ChileCoelemu1
149GreeceBeotia1

Pour trier par index, utilisez sort_index(), avec la même interface et le même ordre par défaut (décroissant).

1
countries_reviewed.sort_index()
 countryprovincelen
0ArgentinaMendoza Province3264
1ArgentinaOther536
423UruguaySan Jose3
424UruguayUruguay24

Enfin, on peut trier selon plusieurs colonnes en une fois:

1
countries_reviewed.sort_values(by=['country', 'len'])

Lesson 5. Data Types and Missing Values

Dans la pratique, les données ne sont pas toujours bien nettoyées; il faut souvent convertir les types vers ceux attendus, et gérer des valeurs manquantes. Lors d’un traitement ou d’une analyse, c’est très souvent l’étape la plus chronophage.

Types de données

Le type d’une colonne de DataFrame, ou d’une Series, est son dtype. On le consulte via l’attribut dtype. Exemple sur la colonne price de reviews:

1
reviews.price.dtype
1
dtype('float64')

On peut aussi consulter d’un coup le dtype de toutes les colonnes via dtypes:

1
reviews.dtypes
1
2
3
4
5
6
country        object
description    object
                ...  
variety        object
winery         object
Length: 13, dtype: object

Le dtype indique la représentation interne utilisée par pandas. Par exemple, float64 pour un flottant 64 bits, int64 pour un entier 64 bits.

Point notable: une colonne purement textuelle n’a pas un type « chaîne » natif; elle est typée génériquement object.

Avec astype(), on convertit une colonne d’un type à un autre. Par exemple, convertir points (en int64) en float64:

1
reviews.points.astype('float64')
1
2
3
4
5
6
0         87.0
1         87.0
          ... 
129969    90.0
129970    90.0
Name: points, Length: 129971, dtype: float64

L’index d’un DataFrame ou d’une Series possède lui aussi un type:

1
reviews.index.dtype
1
dtype('int64')

Pandas gère également des types externes, comme les catégories (categorical) et les séries temporelles (datetime).

Valeurs manquantes

Les entrées vides reçoivent la valeur NaN (abréviation de « Not a Number »). Pour des raisons techniques, NaN est toujours de type float64.

Pandas fournit des fonctions pour détecter les manquants. Nous avons effleuré cela précédemment: en plus des méthodes, il existe les fonctions autonomes pd.isna et pd.notna. Elles renvoient un booléen (ou un tableau de booléens) indiquant si l’entrée est manquante ou non, et s’emploient ainsi:

1
reviews[pd.isna(reviews.country)]

Souvent, il faut détecter les manquants et les imputer. Une stratégie consiste à remplacer par une valeur fixe avec fillna(). Exemple: remplacer tous les NaN de reviews.region_2 par "Unknown":

1
reviews.region_2.fillna("Unknown")

On peut aussi propager vers l’avant (forward fill) ou l’arrière (backward fill) la valeur valide la plus proche, respectivement via ffill() et bfill().

Autrefois, on pouvait utiliser fillna() avec le paramètre method='ffill'/'bfill'. Depuis pandas 2.1.0, cette approche est dépréciée; privilégiez ffill()/bfill() selon le besoin.

Parfois, même sans manquants, il faut remplacer en masse certaines valeurs par d’autres. Le cours Kaggle illustre cela avec le changement de handle Twitter d’un critique; pour un exemple plus parlant côté coréen, imaginons ceci.

En République de Corée, on scinde le nord de la province du Gyeonggi pour créer une nouvelle entité administrative, Gyeonggibuk-do, et un jeu de données adopte ce nom. Quelqu’un propose ensuite, de manière farfelue, de renommer Gyeonggibuk-do en Pyeonghwanuri Special Self-Governing Province et parvient à l’imposer dans notre scénario fictif. Fictif, certes, mais la ressemblance avec une situation réelle potentielle fait froid dans le dos. Il faut alors remplacer dans le jeu de données la chaîne "Gyeonggibuk-do" par "Pyeonghwanuri State" ou "Pyeonghwanuri Special Self-Governing Province". En pandas, une manière simple est d’utiliser replace().

1
rok_2030_census.province.replace("Gyeonggibuk-do", "Pyeonghwanuri Special Self-Governing Province")

Ce code remplace efficacement toutes les occurrences de "Gyeonggibuk-do" par « le truc interminable » dans la colonne province du DataFrame rok_2030_census. On se contente de soupirer de soulagement à l’idée que personne n’ait eu à exécuter ça dans la vraie vie.

Ce type de remplacement textuel sert aussi au nettoyage des manquants quand ceux-ci sont encodés par des chaînes comme "Unknown", "Undisclosed", "Invalid", ce qui est courant dans des jeux construits par OCR d’anciens documents administratifs.

Lesson 6. Renaming and Combining

Il arrive de devoir renommer une colonne ou un index, et de devoir combiner des DataFrame ou des Series.

Renommer

La méthode rename() renomme des colonnes ou des index. Plusieurs formats d’arguments sont possibles, mais un dictionnaire Python est souvent le plus simple. Exemple: renommer la colonne points en score, et les index 0, 1 en firstEntry, secondEntry dans reviews:

1
reviews.rename(columns={'points': 'score'})
1
reviews.rename(index={0: 'firstEntry', 1: 'secondEntry'})

Renommer des colonnes est courant; renommer des index l’est beaucoup moins. Pour ce dernier cas, on préfère en général set_index(), comme vu précédemment.

Les axes de lignes et de colonnes possèdent un attribut name. Avec rename_axis(), on peut aussi renommer ces axes. Par exemple, appeler l’axe des lignes wines et celui des colonnes fields:

1
reviews.rename_axis("wines", axis='index').rename_axis("fields", axis='columns')

Combiner des jeux de données

Pandas propose trois fonctions clés pour combiner des DataFrame/Series, de la plus simple à la plus riche: concat(), join() et merge(). Le cours Kaggle se concentre sur concat() et join(), la plupart des usages de merge() pouvant être couverts plus simplement par join().

concat() est la plus simple: elle « colle » plusieurs DataFrame/Series le long d’un axe. Utile quand les objets à concaténer partagent les mêmes colonnes. Par défaut, la concaténation se fait sur l’axe des lignes; avec axis=1 ou axis='columns', on concatène sur l’axe des colonnes.

1
2
3
4
5
6
7
8
>>> s1 = pd.Series(['a', 'b'])
>>> s2 = pd.Series(['c', 'd'])
>>> pd.concat([s1, s2])
0    a
1    b
0    c
1    d
dtype: object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
>>> df1 = pd.DataFrame([['a', 1], ['b', 2]],
...                    columns=['letter', 'number'])
>>> df1
  letter  number
0      a       1
1      b       2
>>> df2 = pd.DataFrame([['c', 3], ['d', 4]],
...                    columns=['letter', 'number'])
>>> df2
  letter  number
0      c       3
1      d       4
>>> pd.concat([df1, df2])
  letter  number
0      a       1
1      b       2
0      c       3
1      d       4
>>> df4 = pd.DataFrame([['bird', 'polly'], ['monkey', 'george']],
...                    columns=['animal', 'name'])
>>> df4
   animal    name
0    bird   polly
1  monkey  george
>>> pd.concat([df1, df4], axis=1)
  letter  number  animal    name
0      a       1    bird   polly
1      b       2  monkey  george

D’après la documentation officielle de pandas, si vous devez assembler plusieurs lignes en un seul DataFrame, évitez d’ajouter ligne par ligne dans une boucle; stockez les lignes dans une liste puis concaténez-les en une fois avec concat().

join() est plus riche: il joint un autre DataFrame sur l’index. Si des noms de colonnes se chevauchent, il faut fournir lsuffix et rsuffix pour distinguer les colonnes communes des deux DataFrame.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> df = pd.DataFrame({'key': ['K0', 'K1', 'K2', 'K3', 'K4', 'K5'],
...                    'A': ['A0', 'A1', 'A2', 'A3', 'A4', 'A5']})
>>> df
  key   A
0  K0  A0
1  K1  A1
2  K2  A2
3  K3  A3
4  K4  A4
5  K5  A5
>>> other = pd.DataFrame({'key': ['K0', 'K1', 'K2'],
...                       'B': ['B0', 'B1', 'B2']})
>>> other
  key   B
0  K0  B0
1  K1  B1
2  K2  B2
>>> df.join(other, lsuffix='_caller', rsuffix='_other')
  key_caller   A key_other    B
0         K0  A0        K0   B0
1         K1  A1        K1   B1
2         K2  A2        K2   B2
3         K3  A3       NaN  NaN
4         K4  A4       NaN  NaN
5         K5  A5       NaN  NaN
Cet article est sous licence CC BY-NC 4.0 par l'auteur.