Introduction aux structures de données pandas : les DataFrames (tableaux bidimensionnels)
Commençons par importer la bibliothèque Pandas (et la bibliotèque NumPy) avec les alias classiques pd
et 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 :
Caractéristiques | Population |
---|---|
États | |
Suisse | 8.516 |
France | 67.060 |
USA | 328.200 |
Chine | 1386.000 |
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 | 100 | 1000 | 10 | |
1 | 50 | 300 | 20 | |
2 | 20 | 400 | 5 | |
3 | 100 | 50 | 200 | |
4 | Snapchat | 50 | 200 | 25 |
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
Snapchat | |||||
---|---|---|---|---|---|
Budget | 100 | 50 | 20 | 100 | 50 |
Audience | 1000 | 300 | 400 | 50 | 200 |
CPM | 10 | 20 | 5 | 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
2 Accéder aux éléments d’un DataFrame
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 tableauA
- 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
.
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.
Population | Area | |
---|---|---|
Suisse | 8.516 | 41285 |
France | 67.060 | 551695 |
USA | 328.200 | 3796742 |
Chine | 1386.000 | 9596961 |
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
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
:
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)
oudf.drop(labels = nom_ligne)
pour supprimer une lignedf.drop( nom_colonne, axis=1)
oudf.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 |
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.
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.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.
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.
Pour connaitre le nombre de valeurs manquantes dans chaque colonne, on peut utiliser la méthode .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()
.
<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
5.4 Statistiques descriptives
Pour obtenir un résumé statistique des variables numériques, on peut utiliser la méthode .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 :
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 |
<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
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.
# 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 :
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
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 :
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x7baf7a12e2d0>
On peut extraire une seule colonne de cet objet, et on obtient un objet SeriesGroupBy
:
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 colonnefoo
)size
combien d’éléments sont dans chaque groupegroups
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) :
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
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 :
data.groupby(['foo', 'bar'])['baz'].sum()
: regroupe le DataFrame par les colonnesfoo
etbar
, puis calcule la somme debaz
pour chaque combinaison unique de valeurs dansfoo
etbar
..unstack()
: transforme le résultat en une table pivotée, en déplaçant les valeurs uniques debar
en colonnes. Ainsi, pour chaque valeur defoo
, vous obtenez les sommes debaz
réparties par les différentes valeurs debar
.
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 :
# 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
:
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.
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
.
pd.crosstab(data['foo'], data['bar'])
: compte le nombre de fois que chaque paire de valeurs (foo
,bar
) apparaît dans le DataFrame.- 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)
.
- Les valeurs uniques de
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.
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'
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
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
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)
:
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.
Seconde | Premiere | |
---|---|---|
E | 0.995046 | 0.673855 |
C | 1.334652 | 0.216228 |
d | -0.117584 | -0.309592 |
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]])