M55 - Data Science & Scientific Computing 5
  1.   Cours
  2. Data Frames
  • M55 - Data Science and Scientific Computing 5

  • Syllabus

  •   Cours
    • Introduction
    • Series
    • Data Frames
    • Une analyse complète : le Titanic

  •   Exercices
    • Premières manipulations
    • Notes L3 Math
    • Population France
    • Netflix User
    • Presidentielles USA 2016

  •   Fichiers sources

Contenu de la page

  • 1 Création d’un DataFrame
    • 1.1 Colonne par colonne à partir …
      • 1.1.1 … d’une seule Series
      • 1.1.2 … à partir d’un dictionnaire de Series {label_1: serie_1, label_2: serie_2, etc} ou de listes de valeurs {label_1: [val_1, val_2, etc], label_2: [val_1, val_2, etc], etc}
      • 1.1.3 … à partir d’un tableau NumPy à deux dimensions
    • 1.2 Ligne par ligne à partir …
      • 1.2.1 … d’une liste de Series [serie_1, serie_2, etc] ou d’une liste de dictionnaires [dico_1, dico_2, etc]
  • 2 Accéder aux éléments d’un DataFrame
    • 2.1 Selectionner une colonne : df['nom_colonne'] ou df.nom_colonne
    • 2.2 Selectionner une ligne : loc et iloc
    • 2.3 Résumé
  • 3 Ajouter de lignes ou colonnes à un DataFrame
    • 3.1 Ajout d’une colonne à un DataFrame
    • 3.2 Ajout d’une ligne d’un DataFrame
  • 4 Supprimer des lignes ou des colonnes
  • 5 Aperçu des données : synthèse et statistiques descriptives
    • 5.1 Afficher les premières/dernières lignes
    • 5.2 Nombre et type des données
    • 5.3 Somme etc.
    • 5.4 Statistiques descriptives
    • 5.5 Corrélation et covariance
    • 5.6 Valeurs uniques, dénombrement de valeurs et appartenance
  • 6 Aggregations dans des DataFrames
    • 6.1 groupby : analyse par groupes
    • 6.2 pivot_table
    • 6.3 Tableaux croisés crosstab
  • 7 Calculs sur les objets Series et DataFrames
    • 7.1 Filtres (masques) dans des DataFrames
      • 7.1.1 Avec la syntaxe NumPy
      • 7.1.2 Avec query()
    • 7.2 Arithmétique et alignement des données : différences avec Numpy
    • 7.3 L’indexation des DataFrames
    • 7.4 Copie VS vue
  1.   Cours
  2. Data Frames

Introduction aux structures de données pandas : les DataFrames (tableaux bidimensionnels)

Auteur·rice

Gloria FACCANONI

Date de publication

30 novembre 2024

Commençons par importer la bibliothèque Pandas (et la bibliotèque NumPy) avec les alias classiques pd et np :

import pandas as pd
import numpy as np

La deuxième structure fondamentale de Pandas est le DataFrame. Elle peut être considérée comme une généralisation d’une matrice NumPy, où les lignes et les colonnes sont identifiées par des étiquettes, ou encore comme un dictionnaire de Series partageant le même index.

1 Création d’un DataFrame

On peut créer un DataFrame

  • colonne par colonne à partir
    • d’une seule Series
    • d’un dictionnaire de Series ou d’un dictionnaire de listes (chaque élément du dictionnaire est une colonne)
    • d’une matrice NumPy, et il faudra éventuellement préciser les noms des colonnes et des lignes
  • ligne par ligne à partir
    • d’une liste de Series ou d’une liste de dictionnaires (chaque élément de la liste est une ligne)

1.1 Colonne par colonne à partir …

1.1.1 … d’une seule Series

population = pd.Series( data=[8.516, 67.06, 328.2, 1_386],           
                        index=["Suisse", "France", "USA", "Chine"] # les index seront les labels des lignes
                        )

# df0 = pd.DataFrame( data=population )
df0 = pd.DataFrame( data=population, 
                    columns=["Population"] 
                    )
df0
Population
Suisse 8.516
France 67.060
USA 328.200
Chine 1386.000
display(df0.values) # un array 2D numpy
display(df0.index) # un objet de type Index
display(df0.columns) # un objet de type Index
display(df0["Population"]) # une Series
display(df0["Population"]["Suisse"]) # DataFrame["colonne"]["ligne"]
array([[   8.516],
       [  67.06 ],
       [ 328.2  ],
       [1386.   ]])
Index(['Suisse', 'France', 'USA', 'Chine'], dtype='object')
Index(['Population'], dtype='object')
Suisse       8.516
France      67.060
USA        328.200
Chine     1386.000
Name: Population, dtype: float64
8.516

Si les éléments index et columns d’un DataFrame ont leurs attributs name définis, ceux-ci seront également affichés :

df0.index.name = "États"
df0
Population
États
Suisse 8.516
France 67.060
USA 328.200
Chine 1386.000
df0.columns.name = "Caractéristiques"
df0
Caractéristiques Population
États
Suisse 8.516
France 67.060
USA 328.200
Chine 1386.000
df0.T # transposée, comme en numpy
États Suisse France USA Chine
Caractéristiques
Population 8.516 67.06 328.2 1386.0

1.1.2 … à partir d’un dictionnaire de Series {label_1: serie_1, label_2: serie_2, etc} ou de listes de valeurs {label_1: [val_1, val_2, etc], label_2: [val_1, val_2, etc], etc}

Chaque Series devient une colonne du DataFrame.

Les clés du dictionnaire sont les noms des colonnes et les valeurs associées sont les Series ou listes de valeurs pour chaque colonne. 
Les index peuvent être passés ou générés automatiquement.
Le dictionnaire étant une structure non ordonnée, Pandas ??????.

population = pd.Series([8.516, 67.06, 328.2, 1_386],            index=["Suisse", "France", "USA", "Chine"])
area       = pd.Series([41_285, 551_695, 3_796_742, 9_596_961], index=["Suisse", "France", "USA", "Chine"])

df1 = pd.DataFrame( { "Population" : population, 
                      "Area" : area } )
df1
Population Area
Suisse 8.516 41285
France 67.060 551695
USA 328.200 3796742
Chine 1386.000 9596961
dico = { "RS"      : ["Facebook", "Twitter", "Instagram", "Linkedin", "Snapchat"],
         "Budget"  : [100, 50,20, 100, 50],
         "Audience": [1000, 300, 400,50, 200],
         "CPM"     : [10, 20, 5, 200, 25] }
df1bis = pd.DataFrame(dico)
df1bis
RS Budget Audience CPM
0 Facebook 100 1000 10
1 Twitter 50 300 20
2 Instagram 20 400 5
3 Linkedin 100 50 200
4 Snapchat 50 200 25
df1bis["RS"][2]
'Instagram'

1.1.3 … à partir d’un tableau NumPy à deux dimensions

On peut aussi créer un DataFrame à partir d’un tableau NumPy à deux dimensions. On peut spécifier les noms des colonnes et des lignes :

ARRAY = np.array([ [100,   50,  20, 100,  50], 
                   [1000, 300, 400,  50, 200], 
                   [10,    20,   5, 200,  25] ])
columns = ["Facebook", "Twitter", "Instagram", "Linkedin", "Snapchat"]
indices = ["Budget", "Audience", "CPM"]

df3 = pd.DataFrame( data=ARRAY, 
                    columns=columns, 
                    index=indices
                    )
df3
Facebook Twitter Instagram Linkedin Snapchat
Budget 100 50 20 100 50
Audience 1000 300 400 50 200
CPM 10 20 5 200 25
df4 = df3.T
df4 
Budget Audience CPM
Facebook 100 1000 10
Twitter 50 300 20
Instagram 20 400 5
Linkedin 100 50 200
Snapchat 50 200 25

1.2 Ligne par ligne à partir …

1.2.1 … d’une liste de Series [serie_1, serie_2, etc] ou d’une liste de dictionnaires [dico_1, dico_2, etc]

Chaque Series/Dictionnaire devient une ligne du DataFrame.

population = pd.Series(data=[8.516, 67.06, 328.2, 1_386],            index=["Suisse", "France", "USA", "Chine"])
area       = pd.Series(data=[41_285, 551_695, 3_796_742, 9_596_961], index=["Suisse", "France", "USA", "Chine"])
df1 = pd.DataFrame( data=[population, area], 
                    index=["Population", "Area"])
df1
Suisse France USA Chine
Population 8.516 67.06 328.2 1386.0
Area 41285.000 551695.00 3796742.0 9596961.0
population = {"Suisse":8.516, "France":67.06, "USA":328.2, "Chine":1_386}
area       = {"Suisse":41_285, "France":551_695, "USA":3_796_742, "Chine":9_596_961}
df2 = pd.DataFrame( data=[population, area], 
                    index=["Population", "Area"])
df2
Suisse France USA Chine
Population 8.516 67.06 328.2 1386
Area 41285.000 551695.00 3796742.0 9596961

Si les deux dictionnaires ne contiennent pas les mêmes clés, les valeurs manquantes sont remplacées par NaN :

population = {"Suisse":8.516, "France":67.06, "USA":328.2}
area       = {"Suisse":41_285, "France":551_695, "Chine":9_596_961}
df2bis = pd.DataFrame( data=[population, area], 
                       index=["Population", "Area"])
df2bis
Suisse France USA Chine
Population 8.516 67.06 328.2 NaN
Area 41285.000 551695.00 NaN 9596961.0
display(df2bis.values) # tableau numpy à 2 dimensions
display(df2bis.index) # objet de type Index
display(df2bis.columns) # objet de type Index
display(df2bis["France"]) # DataFrame["colonne"] renvoie une Series !!!! NB
display(df2bis["France"]["Population"]) # DataFrame["colonne"]["ligne"] !!! NB l'ordre
array([[8.516000e+00, 6.706000e+01, 3.282000e+02,          nan],
       [4.128500e+04, 5.516950e+05,          nan, 9.596961e+06]])
Index(['Population', 'Area'], dtype='object')
Index(['Suisse', 'France', 'USA', 'Chine'], dtype='object')
Population        67.06
Area          551695.00
Name: France, dtype: float64
67.06
df2bis.T # transposée, comme en numpy
Population Area
Suisse 8.516 41285.0
France 67.060 551695.0
USA 328.200 NaN
Chine NaN 9596961.0

2 Accéder aux éléments d’un DataFrame

population = pd.Series([8.516, 67.06, 328.2, 1_386],            index=["Suisse", "France", "USA", "Chine"])
area       = pd.Series([41_285, 551_695, 3_796_742, 9_596_961], index=["Suisse", "France", "USA", "Chine"])

df1 = pd.DataFrame( { "Population" : population, 
                      "Area" : area } )
display(df1) 
Population Area
Suisse 8.516 41285
France 67.060 551695
USA 328.200 3796742
Chine 1386.000 9596961
display(df1.values)   # tableau numpy à 2 dimensions
display(df1.index)    # objet de type Index
display(df1.columns)  # objet de type Index
array([[8.516000e+00, 4.128500e+04],
       [6.706000e+01, 5.516950e+05],
       [3.282000e+02, 3.796742e+06],
       [1.386000e+03, 9.596961e+06]])
Index(['Suisse', 'France', 'USA', 'Chine'], dtype='object')
Index(['Population', 'Area'], dtype='object')

2.1 Selectionner une colonne : df['nom_colonne'] ou df.nom_colonne

Pour séléctionner la colonne col1 d’un DataFrame on peut utiliser df.col1, mais on préfèrera généralement df["col1"]. Le résultat est une Series.

# La colonne "Population" : c'est une Series
display(df1["Population"])

# Les colonnes "Population" et "Area" : c'est un DataFrame
display(df1[["Population", "Area"]])
Suisse       8.516
France      67.060
USA        328.200
Chine     1386.000
Name: Population, dtype: float64
Population Area
Suisse 8.516 41285
France 67.060 551695
USA 328.200 3796742
Chine 1386.000 9596961

Attention :

  • dans un tableau numpy à 2 dimensions, A[0] renvoi la première ligne du tableau A
  • pour un objet DataFrame, l’écriture A[0] renvoi la première colonne.

C’est pourquoi il vaut mieux considérer un objet Dataframe comme un dictionnaire dont les clés sont les labels des colonnes plutôt que comme un tableau NumPy.

Une fois une colonne selectionnée (c’est une série), on peut selecionner un élément :

# La valeur de la cellule "Population" de la ligne "France"
col1 = "Population"
row1 = "France"
num_row1 = 1

display( df1[col1][row1] )
# display( df1[col1][num_row1] ) # deprecated
display( df1[col1].loc[row1] )
display( df1[col1].iloc[num_row1] )
67.06
67.06
67.06

Puisque les colonnes sont des Series, on peut utiliser les méthodes de Series sur les colonnes d’un DataFrame. En particulier, tout ce que l’on a dit sur le slicing des Series est valable pour les colonnes d’un DataFrame.

#  SLICING
# =========

# Slicing sur les lignes: les valeurs de la colonne "Population" des lignes de "France" à "USA" inclus
display(df1["Population"]["France":"USA"])

# La colonne "Population", toutes les lignes
display(df1["Population"][:]) 

# Les valeurs de la ligne France
# ERROR : on ne peut pas extraire une ligne avec [:] !!!
# display(df1[:]["France"]) 
France     67.06
USA       328.20
Name: Population, dtype: float64
Suisse       8.516
France      67.060
USA        328.200
Chine     1386.000
Name: Population, dtype: float64

2.2 Selectionner une ligne : loc et iloc

Si on veut selectionner une ligne, on utilise

  • soit la méthode .loc[label]
  • soit la méthode .iloc[indice]

Dans les deux cas, on obtient une Series dont les index sont les noms des colonnes. Ensuite, on pourra utiliser les méthodes de Series pour accéder aux éléments de la ligne.

NB De cette manière, on peut accéder à un élément d’un DataFrame avec la syntaxe df.loc[label_ligne, nom_colonne] ou df.iloc[indice_ligne, indice_colonne] et retrouver ainsi l’ordre d’indexation des lignes et des colonnes de numpy : .iloc[i,j] renvoi l’élément de la ligne i et de la colonne j.

df1
Population Area
Suisse 8.516 41285
France 67.060 551695
USA 328.200 3796742
Chine 1386.000 9596961
display(df1.loc["France"]) # la ligne "France"

display(df1.loc["France", "Population"]) # la valeur de la cellule "Population" de la ligne "France"

display(df1.loc["France":"USA", "Population"]) # les valeurs de la colonne "Population" des lignes de "France" à "USA" inclus
display(df1.loc["France":"USA", ["Population", "Area"]]) # les valeurs des colonnes "Population" et "Area" des lignes de "France" à "USA" inclus

display(df1.loc[:, "Population"]) # toutes les valeurs de la colonne "Population"
Population        67.06
Area          551695.00
Name: France, dtype: float64
67.06
France     67.06
USA       328.20
Name: Population, dtype: float64
Population Area
France 67.06 551695
USA 328.20 3796742
Suisse       8.516
France      67.060
USA        328.200
Chine     1386.000
Name: Population, dtype: float64
display(df1.iloc[1]) # la ligne d'indice 1

display(df1.iloc[1, 0]) # la valeur de la cellule d'indice 1, 0

display(df1.iloc[1:3, 0]) # les valeurs de la colonne "Population" des lignes d'indice 1 à 3 exclus
display(df1.iloc[1:3, [0, 1]]) # les valeurs des colonnes "Population" et "Area" des lignes d'indice 1 à 3 exclus

display(df1.iloc[:, 0]) # toutes les valeurs de la colonne "Population"  idem que df1["Population"]
Population        67.06
Area          551695.00
Name: France, dtype: float64
67.06
France     67.06
USA       328.20
Name: Population, dtype: float64
Population Area
France 67.06 551695
USA 328.20 3796742
Suisse       8.516
France      67.060
USA        328.200
Chine     1386.000
Name: Population, dtype: float64

On peut alors utiliser les masques de numpy etc.

display(df1)

# les lignes dont la Population est supérieure à 100, les colonnes "Population" et "Area"
# df1.loc[ df1["Population"] > 100, ["Population", "Area"] ] 
df1.loc[ df1["Population"] > 100, : ] 
Population Area
Suisse 8.516 41285
France 67.060 551695
USA 328.200 3796742
Chine 1386.000 9596961
Population Area
USA 328.2 3796742
Chine 1386.0 9596961

2.3 Résumé

Syntaxe Description
df[eti_col] Sélectionne une colonne ou une séquence de colonnes. Peut aussi servir pour filtres booléens ou slices.
df.loc[eti_ligne] Sélectionne une ou plusieurs lignes par leur étiquette de ligne (eti_ligne).
df.loc[:, eti_col] Sélectionne une ou plusieurs colonnes par leurs étiquettes de colonnes (eti_col).
df.loc[eti_ligne, eti_col] Sélectionne lignes (eti_ligne) et colonnes (eti_col) par leurs étiquettes.
df.iloc[pos] Sélectionne une ou plusieurs lignes par leur position entière (pos).
df.iloc[:, pos] Sélectionne une ou plusieurs colonnes par leur position entière (pos).
df.iloc[pos_i, pos_j] Sélectionne lignes (pos_i) et colonnes (pos_j) par leurs positions entières.
df.at[eti_ligne, eti_col] Accède à une valeur scalaire unique par étiquette de ligne (eti_ligne) et de colonne (eti_col).
df.iat[i, j] Accède à une valeur scalaire unique par position entière de ligne (i) et de colonne (j).
Méthode reindex Réorganise ou sélectionne lignes/colonnes par leurs étiquettes (eti_ligne et/ou eti_col).
Méthodes get_value / set_value Accède ou modifie une valeur unique par étiquette (obsolètes, utilisez at/iat).

3 Ajouter de lignes ou colonnes à un DataFrame

3.1 Ajout d’une colonne à un DataFrame

On ajoute une colonne à un DataFrame en utilisant la notation df['nom_colonne'] = serie, eventuellement en utilisant une colonne existante pour calculer la nouvelle colonne.

df1 = pd.DataFrame( { "Population" : population, 
                      "Area" : area } )
df1
Population Area
Suisse 8.516 41285
France 67.060 551695
USA 328.200 3796742
Chine 1386.000 9596961
df1["Density"] = df1["Population"] / df1["Area"]
df1
Population Area Density
Suisse 8.516 41285 0.000206
France 67.060 551695 0.000122
USA 328.200 3796742 0.000086
Chine 1386.000 9596961 0.000144

Les colonnes sont toujours ajoutées à la fin du DataFrame. Si on veut spécifier une position (et décaler les autres colonnes) on utilise la méthode .insert() :

# ajout d'une colonne à une position donnée
df1.insert( 0, "DensityBIS", df1["Population"] / df1["Area"] )
df1
DensityBIS Population Area Density
Suisse 0.000206 8.516 41285 0.000206
France 0.000122 67.060 551695 0.000122
USA 0.000086 328.200 3796742 0.000086
Chine 0.000144 1386.000 9596961 0.000144

3.2 Ajout d’une ligne d’un DataFrame

df1 = pd.DataFrame( { "Population" : population, 
                      "Area" : area } )
df1
Population Area
Suisse 8.516 41285
France 67.060 551695
USA 328.200 3796742
Chine 1386.000 9596961

Pour ajouter une ligne à un DataFrame, on utilise la méthode .loc[label] = serie :

df1.loc["Italie"] = [60.36, 301338]
df1
Population Area
Suisse 8.516 41285.0
France 67.060 551695.0
USA 328.200 3796742.0
Chine 1386.000 9596961.0
Italie 60.360 301338.0

4 Supprimer des lignes ou des colonnes

Pour supprimer une ligne ou une colonne on utilise la méthode .drop() :

  • df.drop( nom_ligne, axis=0) ou df.drop(labels = nom_ligne) pour supprimer une ligne
  • df.drop( nom_colonne, axis=1) ou df.drop( columns = nom_colonne) pour supprimer une colonne

On ajoute l’option inplace=True pour modifier le DataFrame, inplace=False pour renvoyer un nouveau DataFrame sans modifier l’original.

Attention, si la la ligne ou la colonne n’existe pas, une erreur sera levée. Pour éviter cela, on peut utiliser l’option errors='ignore'.

# Création du DataFrame

df1 = pd.DataFrame( { "Population" : population, 
                      "Area" : area } )
df1.loc["Italie"] = [ 60.36, 301338]

df1["Density"] = df1["Population"] / df1["Area"]
df1.insert( 0, "DensityBIS", df1["Population"] / df1["Area"] )

df1
DensityBIS Population Area Density
Suisse 0.000206 8.516 41285.0 0.000206
France 0.000122 67.060 551695.0 0.000122
USA 0.000086 328.200 3796742.0 0.000086
Chine 0.000144 1386.000 9596961.0 0.000144
Italie 0.000200 60.360 301338.0 0.000200
# Supprimoons une colonne

df1.drop( columns="DensityBIS", inplace=True ) # suppression d'une colonne
# del df1["Density"]  # autre méthode pour supprimer une colonne

df1
Population Area Density
Suisse 8.516 41285.0 0.000206
France 67.060 551695.0 0.000122
USA 328.200 3796742.0 0.000086
Chine 1386.000 9596961.0 0.000144
Italie 60.360 301338.0 0.000200
# Supprimons une ligne

df1.drop(labels=["Chine"], inplace=True) # suppression d'une ligne
display(df1)
Population Area Density
Suisse 8.516 41285.0 0.000206
France 67.060 551695.0 0.000122
USA 328.200 3796742.0 0.000086
Italie 60.360 301338.0 0.000200

5 Aperçu des données : synthèse et statistiques descriptives

Créons d’abord un DataFrame un peu plus richement rempli (on verra plus tard comment importer des données depuis un fichier CSV) :

data = pd.DataFrame( { "foo" : ["one", "one", "one", "two", "two", "two"],
                        "bar" : ["A", "A", "B", "A", "B", "B"],
                        "baz" : [1, 2, 3, 4, 5, 6], 
                        "zoo" : ["x", "y", "z", "q", "w", 't'] } )

data
foo bar baz zoo
0 one A 1 x
1 one A 2 y
2 one B 3 z
3 two A 4 q
4 two B 5 w
5 two B 6 t

5.1 Afficher les premières/dernières lignes

Un bon réflexe à adopter après la création/l’importation d’un DataFrale, et après toute transformation importante, est de visualiser le jeu de données, ou du moins quelques lignes, afin de vérifier que tout s’est correctement déroulé. Pour cela, il existe deux méthodes principales :

  • la méthode .head() permettant de sélectionner par défaut les 5 premières lignes du data frame. Il est possible de préciser entre parenthèses le nombre de lignes à afficher ;
  • la méthode .tail() permettant de sélectionner par défaut les 5 dernières lignes du data frame. Il est également possible de préciser entre parenthèses le nombre de lignes à afficher.

Il n’est pas possible (par défaut) d’afficher plus de 60 lignes d’un data frame, afin de ne pas surcharger inutilement le notebook. De façon plus globale, chercher à visualiser l’ensemble d’un data frame n’est généralement pas une bonne pratique. Si cela est tout à fait envisageable avec quelques dizaines de lignes, ça devient vite impossible avec plusieurs millions de lignes ! Si vous cherchez à afficher plus de 60 lignes, vous aurez finalement comme résultat les 5 premières et dernières lignes du data frame.

# afficher les 5 premières lignes
display(data.head())
foo bar baz zoo
0 one A 1 x
1 one A 2 y
2 one B 3 z
3 two A 4 q
4 two B 5 w
# afficher les 2 dernières lignes
display(data.tail(2))
foo bar baz zoo
4 two B 5 w
5 two B 6 t

5.2 Nombre et type des données

Combien de lignes comporte le DataFrame ? Et combien de colonnes ? Tout comme avec les arrays NumPy, il est possible de répondre à ces questions via l’attribut .shape. Le résultat est un tuple : le premier élément correspond au nombre de lignes, et le second au nombre de colonnes. On peut naturellement stocker le résultat de cet attribut dans une variable pour réutiliser ces éléments ultérieurement.

dim = data.shape
dim
(6, 4)

Au-delà des dimensions, on peut avoir envie de connaître les types de chacune de nos variables. On peut accéder à cela très simplement à partir de l’attribut .dtypes. On obtient un objet Series :les index de cette série sont les étiquettes des colonnes, et les valeurs de cette série sont les types de données. Vous noterez que le type de foo est objet, alors que nous avons pourtant des chaînes de caractères. C’est une chose à connaître, mais le type objet de Pandas correspond en fait à une colonne de type chaîne de caractères.

data.dtypes
foo    object
bar    object
baz     int64
zoo    object
dtype: object

Pour connaitre le nombre de valeurs manquantes dans chaque colonne, on peut utiliser la méthode .isnull().

data.isnull()
foo bar baz zoo
0 False False False False
1 False False False False
2 False False False False
3 False False False False
4 False False False False
5 False False False False

Ces deux informations peuvent être combinées pour obtenir un résumé complet des données avec la méthode .info().

data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6 entries, 0 to 5
Data columns (total 4 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   foo     6 non-null      object
 1   bar     6 non-null      object
 2   baz     6 non-null      int64 
 3   zoo     6 non-null      object
dtypes: int64(1), object(3)
memory usage: 324.0+ bytes

5.3 Somme etc.

On calcule la somme des élements pour chaque colonne avec la méthode .sum(). Rappel : la somme entre chaînes de caractères est une concaténation.

# Elles renvoient des Series dont les Index sont les colonnes du DataFrame
display(data.sum()) # idem que data.sum(axis=0) ou data.sum(axis='rows') 
display(data.min())
display(data.max())
# display(data.mean())
# display(data.median())
# display(data.std())
display(data.count())
foo    oneoneonetwotwotwo
bar                AABABB
baz                    21
zoo                xyzqwt
dtype: object
foo    one
bar      A
baz      1
zoo      q
dtype: object
foo    two
bar      B
baz      6
zoo      z
dtype: object
foo    6
bar    6
baz    6
zoo    6
dtype: int64
data['baz'].mean()
3.5

5.4 Statistiques descriptives

Pour obtenir un résumé statistique des variables numériques, on peut utiliser la méthode .describe() :

data.describe()
baz
count 6.000000
mean 3.500000
std 1.870829
min 1.000000
25% 2.250000
50% 3.500000
75% 4.750000
max 6.000000

Cette méthode renvoie un objet de type DataFrame, où les statistiques descriptives sont calculées pour chaque colonne numérique. Pour les variables catégorielles, on peut obtenir le nombre de valeurs uniques, la valeur la plus fréquente et sa fréquence :

data[['foo', 'bar', 'zoo']].describe()
foo bar zoo
count 6 6 6
unique 2 2 6
top one A x
freq 3 3 1
data.describe(include="all")
foo bar baz zoo
count 6 6 6.000000 6
unique 2 2 NaN 6
top one A NaN x
freq 3 3 NaN 1
mean NaN NaN 3.500000 NaN
std NaN NaN 1.870829 NaN
min NaN NaN 1.000000 NaN
25% NaN NaN 2.250000 NaN
50% NaN NaN 3.500000 NaN
75% NaN NaN 4.750000 NaN
max NaN NaN 6.000000 NaN

On peut modifier le type de données d’une colonne avec la méthode .astype() et demander à Pandas d’y associer des codes numériques. Si on ne veut pas ecraser le DataFrame initial, on peut utiliser la méthode .copy().

data_bis = data.copy()
for col in data_bis.select_dtypes(include='object').columns:
    data_bis[col] = data_bis[col].astype('category').cat.codes

data_bis
foo bar baz zoo
0 0 0 1 3
1 0 0 2 4
2 0 1 3 5
3 1 0 4 0
4 1 1 5 2
5 1 1 6 1
data_bis.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6 entries, 0 to 5
Data columns (total 4 columns):
 #   Column  Non-Null Count  Dtype
---  ------  --------------  -----
 0   foo     6 non-null      int8 
 1   bar     6 non-null      int8 
 2   baz     6 non-null      int64
 3   zoo     6 non-null      int8 
dtypes: int64(1), int8(3)
memory usage: 198.0 bytes
data_bis.describe()
foo bar baz zoo
count 6.000000 6.000000 6.000000 6.000000
mean 0.500000 0.500000 3.500000 2.500000
std 0.547723 0.547723 1.870829 1.870829
min 0.000000 0.000000 1.000000 0.000000
25% 0.000000 0.000000 2.250000 1.250000
50% 0.500000 0.500000 3.500000 2.500000
75% 1.000000 1.000000 4.750000 3.750000
max 1.000000 1.000000 6.000000 5.000000

5.5 Corrélation et covariance

Certaines statistiques sommaires, comme la corrélation et la covariance, sont calculées à partir de paires d’arguments.

TO DO !!!!!!!!!!!!!!!!!!!!

5.6 Valeurs uniques, dénombrement de valeurs et appartenance

Une autre catégorie de méthodes connexes permet d’extraire des informations sur les valeurs contenues dans une série unidimensionnelle.

data
foo bar baz zoo
0 one A 1 x
1 one A 2 y
2 one B 3 z
3 two A 4 q
4 two B 5 w
5 two B 6 t
data.nunique()
foo    2
bar    2
baz    6
zoo    6
dtype: int64
# chaque colonne étant une Series, on peut utiliser la méthode nunique() sur chaque colonne
for c in data.columns:
    print(c, data[c].nunique())
foo 2
bar 2
baz 6
zoo 6

Pour chaque colonne=série, on affiche les valeurs uniques :

for col in data.columns:
    print(col, data[col].unique())
foo ['one' 'two']
bar ['A' 'B']
baz [1 2 3 4 5 6]
zoo ['x' 'y' 'z' 'q' 'w' 't']

6 Aggregations dans des DataFrames

6.1 groupby : analyse par groupes

La méthode groupby permet de grouper les données suivant certains critères. Elle est basée sur trois étapes : séparation, application et combinaison.

  • On sépare notre DataFrame en fonction d’un critère (généralement une ou plusieurs colonnes).
  • On applique des fonctions sur les groupes obtenus.
  • On combine les résultats obtenus pour chaque groupe
data
foo bar baz zoo
0 one A 1 x
1 one A 2 y
2 one B 3 z
3 two A 4 q
4 two B 5 w
5 two B 6 t

On va regrouper les données en fonction de la colonne foo. On obtient un objet DataFrameGroupBy, c’est-à-dire une sorte de dictionnaire de DatFrame dont les clés sont les valeurs uniques de la colonne foo et les valeurs sont les DataFrame correspondant à chaque groupe :

data_gbfoo = data.groupby("foo")
data_gbfoo
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x7baf7a12e2d0>

On peut extraire une seule colonne de cet objet, et on obtient un objet SeriesGroupBy :

data_gbfoo["baz"]
<pandas.core.groupby.generic.SeriesGroupBy object at 0x7baf7a12e5a0>

C’est un peu déroutant, car on ne voit pas explicitement les groupes. Voici trois façons de voir les groupes :

  • ngroups permet de connaître le nombre de groupes obtenus (ici 2 car on a deux valeurs uniques dans la colonne foo)
  • size combien d’éléments sont dans chaque groupe
  • groups les indices des lignes de chaque groupe avec la méthode.
# Combien de groupes ?
nb_groupe = data_gbfoo.ngroups
display(nb_groupe)

# Combien d'éléments par groupe ?
nb_element_par_groupe = data_gbfoo.size()
display(nb_element_par_groupe)

# Quelles sont les indices des lignes de chaque groupe ?
group_indices = data_gbfoo.groups
display(group_indices)
2
foo
one    3
two    3
dtype: int64
{'one': [0, 1, 2], 'two': [3, 4, 5]}

On peut extraire autant de dataframes que de groupes avec la méthode get_group(), mais ce n’est pas l’utilisation principale de groupby.

data_one = data_gbfoo.get_group("one") # les lignes du groupe "one" constitue un dataframe
data_two = data_gbfoo.get_group("two") # les lignes du groupe "two" constitue un dataframe
display(data_one, data_two)
foo bar baz zoo
0 one A 1 x
1 one A 2 y
2 one B 3 z
foo bar baz zoo
3 two A 4 q
4 two B 5 w
5 two B 6 t

Généralement on applique des fonctions d’agrégation sur les groupes obtenus.

Par exemple, on peut calculer la somme des éléments de chaque groupe (on se rappelle que l’operation “somme” entre chaînes de caractères correspond à la concaténation de ces chaînes) :

data_gbfoo.sum()
bar baz zoo
foo
one AAB 6 xyz
two ABB 15 qwt
data.groupby('foo')['baz'].sum()
foo
one     6
two    15
Name: baz, dtype: int64

On peut aussi regrouper par plusieurs colonnes, puis appliquer des fonctions d’agrégation :

# on regroupe les couples (foo, bar) et on somme les valeurs de la colonne "baz"
data.groupby(['foo', 'bar'])['baz'].sum() #.unstack()
foo  bar
one  A       3
     B       3
two  A       4
     B      11
Name: baz, dtype: int64
# ???? MULTIINDEX
# data.groupby(['foo', 'bar'])[['baz','zoo']].sum() #.unstack()
data.groupby(['foo', 'bar'])['baz'].sum().unstack()
bar A B
foo
one 3 3
two 4 11

Cette commande permet de regrouper les données de data en utilisant les colonnes foo et bar, puis de calculer la somme de la colonne baz pour chaque groupe unique de foo et bar.

Voici les étapes décomposées :

  1. data.groupby(['foo', 'bar'])['baz'].sum() : regroupe le DataFrame par les colonnes foo et bar, puis calcule la somme de baz pour chaque combinaison unique de valeurs dans foo et bar.
  2. .unstack() : transforme le résultat en une table pivotée, en déplaçant les valeurs uniques de bar en colonnes. Ainsi, pour chaque valeur de foo, vous obtenez les sommes de baz réparties par les différentes valeurs de bar.

Le résultat est un DataFrame avec les valeurs de foo en index et celles de bar en colonnes, où chaque cellule contient la somme correspondante de baz.

data_grfoobar = data.groupby(['foo', 'bar'])

display(data_grfoobar.get_group(('one', 'A'))) # df pour qui foo == 'one' et bar == 'A'
display(data_grfoobar.get_group(('one', 'B'))) # df pour qui foo == 'one' et bar == 'B'
display(data_grfoobar.get_group(('two', 'A'))) # df pour qui foo == 'two' et bar == 'A'
display(data_grfoobar.get_group(('two', 'B'))) # df pour qui foo == 'two' et bar == 'B'
foo bar baz zoo
0 one A 1 x
1 one A 2 y
foo bar baz zoo
2 one B 3 z
foo bar baz zoo
3 two A 4 q
foo bar baz zoo
4 two B 5 w
5 two B 6 t

6.2 pivot_table

Pour obtenir les mêmes résultats que précédemment, on peut utiliser la méthode pivot_table qui permet de créer un tableau croisé dynamique :

data
foo bar baz zoo
0 one A 1 x
1 one A 2 y
2 one B 3 z
3 two A 4 q
4 two B 5 w
5 two B 6 t
# Analogie avec une fonction de 2 variables
# z = f(x,y)  ⤳  z = sum(baz(foo,bar))
# Comme au couple (foo_i,bar_i) on associe plusieurs valeurs de baz, 
# il faut appliquer une opération à ces valeurs, par exemple la somme 

# Notation : pivot_table(values, index, columns, aggfunc)
# index = x (ici foo), columns = y (ici bar), values = baz, aggfunc = sum

data.pivot_table(index='foo', columns='bar', values='baz', aggfunc='sum')
bar A B
foo
one 3 3
two 4 11

Lorsqu’on a besoin de calculer des sous-totau, on peut utiliser l’option margins=True :

data.pivot_table(index='foo', columns='bar', values='baz', aggfunc='sum', margins=True)
bar A B All
foo
one 3 3 6
two 4 11 15
All 7 14 21

6.3 Tableaux croisés crosstab

Un tableau croisé crosstab est un cas particulier de tableau croisé dynamique pivot_table qui calcule la fréquence des groupes.

data
foo bar baz zoo
0 one A 1 x
1 one A 2 y
2 one B 3 z
3 two A 4 q
4 two B 5 w
5 two B 6 t

La commande pd.crosstab(data['foo'], data['bar']) crée un tableau croisé (ou “contingence”) en comptant les occurrences de chaque combinaison unique de valeurs dans les colonnes foo et bar du DataFrame data.

  1. pd.crosstab(data['foo'], data['bar']) : compte le nombre de fois que chaque paire de valeurs (foo,bar) apparaît dans le DataFrame.
  2. Le résultat est un nouveau DataFrame avec :
    • Les valeurs uniques de foo comme index (lignes).
    • Les valeurs uniques de bar comme colonnes.
    • Les cellules contiennent le nombre d’occurrences de chaque combinaison (foo, bar).
pd.crosstab(data['foo'], data['bar'])
bar A B
foo
one 2 1
two 1 2
# idem que
data.pivot_table(values='baz', index='foo', columns='bar', aggfunc='count')
bar A B
foo
one 2 1
two 1 2

C’est utile pour obtenir une vue d’ensemble des fréquences d’association entre les deux variables. Pour obtenir les fréquences au lieu des comptes bruts dans un tableau croisé de pandas, on ajoute l’argument normalize=True, chaque valeur est alors divisée par le total de toutes les combinaisons. Cela affichera chaque valeur comme une proportion du total, ce qui correspond aux fréquences relatives.

pd.crosstab(data['foo'], data['bar'], normalize=True)
bar A B
foo
one 0.333333 0.166667
two 0.166667 0.333333

On peut aussi normaliser par ligne ou par colonne :

  • Par ligne (chaque ligne totalisera 1): normalize='index'
  • Par colonne (chaque colonne totalisera 1): normalize='columns'
pd.crosstab(data['foo'], data['bar'], normalize='index')
bar A B
foo
one 0.666667 0.333333
two 0.333333 0.666667

7 Calculs sur les objets Series et DataFrames

La plupart de ce qu’on a pu voir avec les arrays de NumPy est applicable aux objets Series et DataFrames de Pandas. On peut par exemple effectuer des opérations arithmétiques sur les objets, ou encore appliquer des fonctions mathématiques.

7.1 Filtres (masques) dans des DataFrames

data
foo bar baz zoo
0 one A 1 x
1 one A 2 y
2 one B 3 z
3 two A 4 q
4 two B 5 w
5 two B 6 t

7.1.1 Avec la syntaxe NumPy

data["bar"]=="A"
0     True
1     True
2    False
3     True
4    False
5    False
Name: bar, dtype: bool
data[data["bar"]=="A"]
foo bar baz zoo
0 one A 1 x
1 one A 2 y
3 two A 4 q
data[(data["bar"]=="A") & (data["baz"]>=2)] # masque : `&` pour le `and`, `|` pour le `or` et `!` pour le `not` ;  parenthèses obligatoires
foo bar baz zoo
1 one A 2 y
3 two A 4 q

7.1.2 Avec query()

data.query("bar=='A'")
foo bar baz zoo
0 one A 1 x
1 one A 2 y
3 two A 4 q
data.query("bar=='A' & baz>=2") # idem que data[(data["bar"]=="A") & (data["baz"]>=2)]
foo bar baz zoo
1 one A 2 y
3 two A 4 q

7.2 Arithmétique et alignement des données : différences avec Numpy

Il existe une différence très importante : lorsque vous faites une opération entre deux objets Series, l’opération se fait terme à terme, mais les termes sont identifiés par leur index. Si les index ne correspondent pas, le résultat sera un objet Series avec un index qui est l’union des deux index initiaux. Les valeurs correspondant à des index qui n’étaient pas présents dans les deux objets initiaux seront des valeurs manquantes, notées NaN (pour Not a Number).

ma_serie4 = pd.Series( np.random.randn(5), index=["A","B","C","D","E"] )
display(ma_serie4)
ma_serie5 = pd.Series( np.random.randn(4), index=["A","B","C","F"] )
display(ma_serie5)
ma_serie_somme = ma_serie4 + ma_serie5
display(ma_serie_somme)
A   -0.481857
B   -2.095946
C   -2.000258
D   -0.398062
E   -0.712240
dtype: float64
A    0.122891
B    0.309032
C    0.169706
F   -0.732845
dtype: float64
A   -0.358966
B   -1.786914
C   -1.830552
D         NaN
E         NaN
F         NaN
dtype: float64

On voit ici que les deux Series ont en commun A, B et C. La somme donne bien une valeur qui est la somme des deux objets Series. Pour D, E et F, on obtient une valeur manquante car l’un des deux objets Series ne comprend pas d’index D, E ou F. On a donc la somme d’une valeur manquante et d’une valeur présente qui logiquement donne une valeur manquante. Si vous voulez faire une somme en supposant que les données manquantes sont équivalentes à 0, il faut utiliser : s1.add(s2, fill_value=0):

ma_serie_somme_BIS = ma_serie4.add(ma_serie5, fill_value=0)
display(ma_serie_somme_BIS)
A   -0.358966
B   -1.786914
C   -1.830552
D   -0.398062
E   -0.712240
F   -0.732845
dtype: float64

7.3 L’indexation des DataFrames

Vous pouvez avoir besoin de modifier les index de vos données. Pour cela, différentes options s’offrent à vous :

  • .reindex() permet de sélectionner des colonnes et de réordonner les colonnes et les lignes. Si une colonne ou une ligne n’est pas présente dans le DataFrame initial, elle sera ajoutée avec des valeurs manquantes.
df5 = pd.DataFrame( np.random.randn(5,2), index=["a","b","c","d","e"], columns=["Col 1","Col 2"])
df5
Col 1 Col 2
a -2.411666 -1.114327
b -0.359489 0.276882
c 0.216228 1.334652
d -0.309592 -0.117584
e 0.673855 0.995046
# on ne garde que les lignes "c","d","e" mais on les réordonne
# on garde toutes les colonnes mais on les réordonne aussi
df6 = df5.reindex(index=["e","c","d"], columns=["Col 2","Col 1"])
df6
Col 2 Col 1
e 0.995046 0.673855
c 1.334652 0.216228
d -0.117584 -0.309592
  • .rename() permet de renommer les colonnes ou les lignes.
df7 = df6.rename(index={"e":"E", "c":"C"}, columns={"Col 1":"Premiere", "Col 2":"Seconde"})
df7
Seconde Premiere
E 0.995046 0.673855
C 1.334652 0.216228
d -0.117584 -0.309592
df8 = df7.rename(index=str.upper, columns=str.title)
df8
Seconde Premiere
E 0.995046 0.673855
C 1.334652 0.216228
D -0.117584 -0.309592
df9 = df8.rename(mapper=lambda x: x.upper(), axis=1)
df9
SECONDE PREMIERE
E 0.995046 0.673855
C 1.334652 0.216228
D -0.117584 -0.309592

7.4 Copie VS vue

Au même titre que les objets de NumPy (ou les listes tout simplement), il est important de comprendre comment les objets Series et DataFrame sont alloués. Lorsqu’on crée un objet DataFrame ou Series à partir d’un autre objet, le fait de savoir si on a affaire à une copie ou à une référence dépend de l’objet d’origine. Lorsqu’on travaille sur un array, il s’agit juste d’une référence aux valeurs.

Dans l’exemple suivant, on voit que l’array initial est impacté par la modification de l’objet DataFrame. Si vous faites la même chose avec une liste, Pandas crée une copie.

Une fois que vous avez créé votre DataFrame, si vous allouez le même DataFrame à un objet, il va faire référence au premier DataFrame.

Si vous créez un DataFrame à partir d’une partie de votre DataFrame, vous obtiendrez une vue de votre DataFrame.

Conclusion, si vous voulez réellement une copie, il faudra utiliser la méthode .copy() mais soyez attentifs à l’espace nécessaire. Vous pouvez créer des vues comme avec NumPy en utilisant .view().

# on crée un array 
arr1 = np.arange(6).reshape(3,2)
# on crée un DataFrame à partir de l’array
df1 = pd.DataFrame(arr1)

print("Avant modification")
display(arr1)
display(df1)

print("Après modification d'une valeur du DataFrame")
df1.iloc[1,1] = 22
display(df1)
display(arr1) # <--- l'array aussi A ÉTÉ MODIFIÉ
Avant modification
Après modification d'une valeur du DataFrame
array([[0, 1],
       [2, 3],
       [4, 5]])
0 1
0 0 1
1 2 3
2 4 5
0 1
0 0 1
1 2 22
2 4 5
array([[ 0,  1],
       [ 2, 22],
       [ 4,  5]])
Retour au sommet
Series
Une analyse complète : le Titanic

© Copyright 2024, Gloria Faccanoni

 

This page is built with ❤️ and Quarto.