Manipulation de données avec pandas
Dans cet article, on va essayer de clarifier les différentes options qui s'offrent à vous pour manipuler vos données avec Python en utilisant la librairie pandas
. On se place dans le cas d'un tableau de données tidy, c'est à dire où chaque colonne représente une variable, ou caractéristique, et chaque ligne une observation. Dans ce cadre, on présente les options possibles pour :
- Filtrer les observations, c'est à dire conserver certaines lignes.
- Sélectionner des variables, c'est à dire conserver certaines colonnes.
En particulier, on mettra l'accent sur les différences entre les fonctions loc
et iloc
et le rôle particulier des index. Pour illustrer cet article, on s'appuie sur la base de données titanic
, disponible notamment sur Kaggle. On importe la version csv de cette base avec read_csv
et on affiche ses caractéristiques :
import pandas as pd
titanic = pd.read_csv('Data/titanic-survival.csv')
print(titanic.shape)
titanic.tail()
(1309, 14)
pclass | survived | name | sex | age | sibsp | parch | ticket | fare | cabin | embarked | boat | body | home.dest | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1304 | 3 | 0 | Zabour, Miss. Hileni | female | 14.5 | 1 | 0 | 2665 | 14.4542 | NaN | C | NaN | 328.0 | NaN |
1305 | 3 | 0 | Zabour, Miss. Thamine | female | NaN | 1 | 0 | 2665 | 14.4542 | NaN | C | NaN | NaN | NaN |
1306 | 3 | 0 | Zakarian, Mr. Mapriededer | male | 26.5 | 0 | 0 | 2656 | 7.2250 | NaN | C | NaN | 304.0 | NaN |
1307 | 3 | 0 | Zakarian, Mr. Ortin | male | 27.0 | 0 | 0 | 2670 | 7.2250 | NaN | C | NaN | NaN | NaN |
1308 | 3 | 0 | Zimmerman, Mr. Leo | male | 29.0 | 0 | 0 | 315082 | 7.8750 | NaN | S | NaN | NaN | NaN |
Cette base contient 1309 observations (ici des passagers) et 14 variables qui permet de les caractériser. Voyons comment sélectionner des sous-ensembles de ces données.
Filtrer des observations
On s'intéresse tout d'abord à la sélection sur les lignes. Différentes options sont possibles :
À partir de leur position avec iloc
Dans pandas
, il est possible de sélectionner certaines lignes directement en renseignant leur position avec la syntaxe data_frame.iloc[positions_lignes, positions_colonnes]
. En ayant bien en tête qu'en Python, la première observation correspond à la position 0, on peut par exemple sélectionner les lignes 1 à 3 :
titanic.iloc[0:3, :]
# "0:3" lignes de la position 0 à la position 3 exclue
# ":" pour sélectionner l'ensemble des colonnes
pclass | survived | name | sex | age | sibsp | parch | ticket | fare | cabin | embarked | boat | body | home.dest | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 1 | Allen, Miss. Elisabeth Walton | female | 29.0000 | 0 | 0 | 24160 | 211.3375 | B5 | S | 2 | NaN | St Louis, MO |
1 | 1 | 1 | Allison, Master. Hudson Trevor | male | 0.9167 | 1 | 2 | 113781 | 151.5500 | C22 C26 | S | 11 | NaN | Montreal, PQ / Chesterville, ON |
2 | 1 | 0 | Allison, Miss. Helen Loraine | female | 2.0000 | 1 | 2 | 113781 | 151.5500 | C22 C26 | S | NaN | NaN | Montreal, PQ / Chesterville, ON |
On note que l'écriture a:b
sélectionne les lignes de la position a
inclue à la position b
exclue. Les index négatifs partent de la fin de la base, ainsi l'index -1
correspond à la dernière observation. Il est possible aussi de renseigner les positions souhaitées dans une liste, ou de faire des combinaisons plus complexes en utilisant range
et la concaténation de listes avec +
, ci-dessous les lignes 1 à 2, 10 à 12 et l'avant-dernière à la dernière :
titanic.iloc[[0, 1] + list(range(9, 12)) + list(range(-2, 0)), :]
pclass | survived | name | sex | age | sibsp | parch | ticket | fare | cabin | embarked | boat | body | home.dest | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 1 | Allen, Miss. Elisabeth Walton | female | 29.0000 | 0 | 0 | 24160 | 211.3375 | B5 | S | 2 | NaN | St Louis, MO |
1 | 1 | 1 | Allison, Master. Hudson Trevor | male | 0.9167 | 1 | 2 | 113781 | 151.5500 | C22 C26 | S | 11 | NaN | Montreal, PQ / Chesterville, ON |
9 | 1 | 0 | Artagaveytia, Mr. Ramon | male | 71.0000 | 0 | 0 | PC 17609 | 49.5042 | NaN | C | NaN | 22.0 | Montevideo, Uruguay |
10 | 1 | 0 | Astor, Col. John Jacob | male | 47.0000 | 1 | 0 | PC 17757 | 227.5250 | C62 C64 | C | NaN | 124.0 | New York, NY |
11 | 1 | 1 | Astor, Mrs. John Jacob (Madeleine Talmadge Force) | female | 18.0000 | 1 | 0 | PC 17757 | 227.5250 | C62 C64 | C | 4 | NaN | New York, NY |
1307 | 3 | 0 | Zakarian, Mr. Ortin | male | 27.0000 | 0 | 0 | 2670 | 7.2250 | NaN | C | NaN | NaN | NaN |
1308 | 3 | 0 | Zimmerman, Mr. Leo | male | 29.0000 | 0 | 0 | 315082 | 7.8750 | NaN | S | NaN | NaN | NaN |
Vous aurez peut-être remarqué qu'il est aussi possible de filtrer sur les observations avec .loc
en renseignant leur position. C'est en fait dû au fait que par défaut, l'index des lignes correspond à leur position. Mais si vous voulez filtrer en fonction de la position des lignes, il est préférable d'utiliser iloc
. À noter aussi que la syntaxe a:b
dans le cas où vous utilisez .loc
inclue l'élément à la position b
:
titanic.loc[0:3, :]
pclass | survived | name | sex | age | sibsp | parch | ticket | fare | cabin | embarked | boat | body | home.dest | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 1 | Allen, Miss. Elisabeth Walton | female | 29.0000 | 0 | 0 | 24160 | 211.3375 | B5 | S | 2 | NaN | St Louis, MO |
1 | 1 | 1 | Allison, Master. Hudson Trevor | male | 0.9167 | 1 | 2 | 113781 | 151.5500 | C22 C26 | S | 11 | NaN | Montreal, PQ / Chesterville, ON |
2 | 1 | 0 | Allison, Miss. Helen Loraine | female | 2.0000 | 1 | 2 | 113781 | 151.5500 | C22 C26 | S | NaN | NaN | Montreal, PQ / Chesterville, ON |
3 | 1 | 0 | Allison, Mr. Hudson Joshua Creighton | male | 30.0000 | 1 | 2 | 113781 | 151.5500 | C22 C26 | S | NaN | 135.0 | Montreal, PQ / Chesterville, ON |
Si le dataframe avait eu un autre index que celui de la position par défaut, cette syntaxe n'aurait fonctionné qu'avec iloc
:
# on crée un index correspondant au nom des passagers
titanic_index_name = titanic.set_index("name")
# titanic_index_name.loc[0:3, :] renvoie une erreur
titanic_index_name.iloc[0:3, ]
pclass | survived | sex | age | sibsp | parch | ticket | fare | cabin | embarked | boat | body | home.dest | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
name | |||||||||||||
Allen, Miss. Elisabeth Walton | 1 | 1 | female | 29.0000 | 0 | 0 | 24160 | 211.3375 | B5 | S | 2 | NaN | St Louis, MO |
Allison, Master. Hudson Trevor | 1 | 1 | male | 0.9167 | 1 | 2 | 113781 | 151.5500 | C22 C26 | S | 11 | NaN | Montreal, PQ / Chesterville, ON |
Allison, Miss. Helen Loraine | 1 | 0 | female | 2.0000 | 1 | 2 | 113781 | 151.5500 | C22 C26 | S | NaN | NaN | Montreal, PQ / Chesterville, ON |
À partir de leur index avec loc
Cette dernière remarque nous amène tout naturellement vers la seconde manière de filtrer les observations : à partir de leur index. En effet, il est possible avec pandas
de créer un index qui permet de sélectionner des lignes en fonction de leur label. Ici nous avons créé le dataframe titanic_index_name
qui identifie chaque ligne avec le nom du passager à laquelle elle correspond. On peut alors sélectionner une ou plusieurs lignes en utilisant ces labels, que l'on renseigne dans une liste :
titanic_index_name.index
Index(['Allen, Miss. Elisabeth Walton', 'Allison, Master. Hudson Trevor',
'Allison, Miss. Helen Loraine', 'Allison, Mr. Hudson Joshua Creighton',
'Allison, Mrs. Hudson J C (Bessie Waldo Daniels)',
'Anderson, Mr. Harry', 'Andrews, Miss. Kornelia Theodosia',
'Andrews, Mr. Thomas Jr',
'Appleton, Mrs. Edward Dale (Charlotte Lamson)',
'Artagaveytia, Mr. Ramon',
...
'Yasbeck, Mr. Antoni', 'Yasbeck, Mrs. Antoni (Selini Alexander)',
'Youseff, Mr. Gerious', 'Yousif, Mr. Wazli', 'Yousseff, Mr. Gerious',
'Zabour, Miss. Hileni', 'Zabour, Miss. Thamine',
'Zakarian, Mr. Mapriededer', 'Zakarian, Mr. Ortin',
'Zimmerman, Mr. Leo'],
dtype='object', name='name', length=1309)
# Une ligne :
titanic_index_name.loc["Allen, Miss. Elisabeth Walton", :]
# Plusieurs lignes : les index de la famille Allen
liste_allen = [i for i in titanic_index_name.index if i.startswith("Allen")]
titanic_index_name.loc[liste_allen, :]
pclass | survived | sex | age | sibsp | parch | ticket | fare | cabin | embarked | boat | body | home.dest | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
name | |||||||||||||
Allen, Miss. Elisabeth Walton | 1 | 1 | female | 29.0 | 0 | 0 | 24160 | 211.3375 | B5 | S | 2 | NaN | St Louis, MO |
Allen, Mr. William Henry | 3 | 0 | male | 35.0 | 0 | 0 | 373450 | 8.0500 | NaN | S | NaN | NaN | Lower Clapton, Middlesex or Erdington, Birmingham |
À noter que si vous sélectionnez une seule ligne, renseigner le label correspondant directement dans une chaîne de caractères vous renverra une Serie
, soit un vecteur de valeurs pour pandas
alors que le renseigner dans une liste vous renverra un DataFrame
avec une seule ligne :
print(type(titanic_index_name.loc["Allen, Miss. Elisabeth Walton", :]),
type(titanic_index_name.loc[["Allen, Miss. Elisabeth Walton"], :]))
<class 'pandas.core.series.Series'> <class 'pandas.core.frame.DataFrame'>
À partir de conditions avec loc
et []
Enfin, et c'est sans doute le cas qui est le plus utilisé dans la pratique, on peut filtrer sur les observations à partir d'une condition. L'idée est de garder seulement les lignes correspondant à la condition.
Imaginons par exemple qu'on veuille le tableau contenant uniquement les passagères. La condition titanic.sex == "female"
va renvoyer un vecteur de booléens de la taille du nombre de lignes de titanic
renseignant True
ou False
en fonction de si la ligne correspond à un passager ou une passagère :
titanic.sex == "female"
0 True
1 False
2 True
3 False
4 True
...
1304 True
1305 True
1306 False
1307 False
1308 False
Name: sex, Length: 1309, dtype: bool
On peut utiliser cette condition pour sélectionner un sous-ensemble du dataframe original avec .loc
ou directement avec la syntaxe data[condition]
.
Attention : Si vous souhaitez créer un nouveau dataframe correspondant à ce sous-ensemble, pensez à utiliser la méthode .copy()
, sinon vous aurez le warning SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame lorsque vous modifierez ce nouveau dataframe. C'est dû au fait que ces modifications affectent également le dataframe original si une copie n'a pas été créée.
# avec .loc
titanic_femmes_loc = titanic.loc[titanic.sex == "female", :].copy()
# avec []
titanic_femmes_croch = titanic[titanic.sex == "female"].copy()
# On teste qu'on a bien le même dataframe et on affiche les 1eres lignes
print(titanic_femmes_loc.equals(titanic_femmes_croch))
titanic_femmes_loc.head(4)
True
pclass | survived | name | sex | age | sibsp | parch | ticket | fare | cabin | embarked | boat | body | home.dest | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 1 | Allen, Miss. Elisabeth Walton | female | 29.0 | 0 | 0 | 24160 | 211.3375 | B5 | S | 2 | NaN | St Louis, MO |
2 | 1 | 0 | Allison, Miss. Helen Loraine | female | 2.0 | 1 | 2 | 113781 | 151.5500 | C22 C26 | S | NaN | NaN | Montreal, PQ / Chesterville, ON |
4 | 1 | 0 | Allison, Mrs. Hudson J C (Bessie Waldo Daniels) | female | 25.0 | 1 | 2 | 113781 | 151.5500 | C22 C26 | S | NaN | NaN | Montreal, PQ / Chesterville, ON |
6 | 1 | 1 | Andrews, Miss. Kornelia Theodosia | female | 63.0 | 1 | 0 | 13502 | 77.9583 | D7 | S | 10 | NaN | Hudson, NY |
Il est bien sûr possible de faire des conditions plus complexes avec les opérateurs &
(opérateur logique "ET") et |
(opérateur logique "OU"), par exemple les femmes de plus de 60 ans :
my_cond = (titanic.sex == "female") & (titanic.age >= 60)
titanic.loc[my_cond, :].head(4)
pclass | survived | name | sex | age | sibsp | parch | ticket | fare | cabin | embarked | boat | body | home.dest | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
6 | 1 | 1 | Andrews, Miss. Kornelia Theodosia | female | 63.0 | 1 | 0 | 13502 | 77.9583 | D7 | S | 10 | NaN | Hudson, NY |
43 | 1 | 1 | Bucknell, Mrs. William Robert (Emma Eliza Ward) | female | 60.0 | 0 | 0 | 11813 | 76.2917 | D15 | C | 8 | NaN | Philadelphia, PA |
61 | 1 | 1 | Cavendish, Mrs. Tyrell William (Julia Florence... | female | 76.0 | 1 | 0 | 19877 | 78.8500 | C46 | S | 6 | NaN | Little Onn Hall, Staffs |
78 | 1 | 1 | Compton, Mrs. Alexander Taylor (Mary Eliza Ing... | female | 64.0 | 0 | 2 | PC 17756 | 83.1583 | E45 | C | 14 | NaN | Lakewood, NJ |
Sélectionner des variables
À partir de leur position avec iloc
De manière complètement analogue à ce que l'on faisait avec les lignes, il est possible de sélectionner certaines variables en indiquant dans iloc
une liste de positions, par exemple pour sélectionner la première et la dernière colonne :
titanic.iloc[:, [0, -1]].head()
pclass | home.dest | |
---|---|---|
0 | 1 | St Louis, MO |
1 | 1 | Montreal, PQ / Chesterville, ON |
2 | 1 | Montreal, PQ / Chesterville, ON |
3 | 1 | Montreal, PQ / Chesterville, ON |
4 | 1 | Montreal, PQ / Chesterville, ON |
À noter que cette notation ne fonctionnera pas ni avec .loc
, ni avec []
, qui ont besoin des labels des variables.
À partir de leur nom avec loc
et []
Tout comme on pouvait filtrer les lignes à partir de leur label quand on définissait un index, il est possible de sélectionner certaines variables en les renseignant dans une liste avec .loc
et []
:
# avec .loc
titanic_select_loc = titanic.loc[:, ["pclass", "sex", "age"]].copy()
# avec []
titanic_select_croch = titanic[["pclass", "sex", "age"]].copy()
print(titanic_select_loc.equals(titanic_select_croch))
titanic_select_loc.head(4)
True
pclass | sex | age | |
---|---|---|---|
0 | 1 | female | 29.0000 |
1 | 1 | male | 0.9167 |
2 | 1 | female | 2.0000 |
3 | 1 | male | 30.0000 |
Notons qu'avec loc
on peut également utiliser la syntaxe "label1" : "labeln"
pour sélectionner toutes les colonnes se situant entre deux colonnes inclues :
titanic.loc[:, "pclass":"sex"].head()
pclass | survived | name | sex | |
---|---|---|---|---|
0 | 1 | 1 | Allen, Miss. Elisabeth Walton | female |
1 | 1 | 1 | Allison, Master. Hudson Trevor | male |
2 | 1 | 0 | Allison, Miss. Helen Loraine | female |
3 | 1 | 0 | Allison, Mr. Hudson Joshua Creighton | male |
4 | 1 | 0 | Allison, Mrs. Hudson J C (Bessie Waldo Daniels) | female |
Enfin, de la même manière qu'avec les lignes, sélectionner une seule colonne en renseignant son label directement ou dans une liste renverra une Serie
ou un DataFrame
:
print(type(titanic.loc[:, "pclass"]), type(titanic.loc[:, ["pclass"]]))
<class 'pandas.core.series.Series'> <class 'pandas.core.frame.DataFrame'>
À partir de conditions avec loc
Enfin, il est possible de sélectionner certaines colonnes en fonction de conditions, représentées par un vecteur de booléens. En général, on va créer ce vecteur en s'appuyant sur les méthodes pandas
permettant de caractériser les colonnes de notre dataframe, comme .columns
et .dtypes
:
titanic.columns, titanic.dtypes
(Index(['pclass', 'survived', 'name', 'sex', 'age', 'sibsp', 'parch', 'ticket',
'fare', 'cabin', 'embarked', 'boat', 'body', 'home.dest'],
dtype='object'),
pclass int64
survived int64
name object
sex object
age float64
sibsp int64
parch int64
ticket object
fare float64
cabin object
embarked object
boat object
body float64
home.dest object
dtype: object)
On peut par exemple sélectionner les variables renseignant des entiers (cond1
), ou celles commençant par la lettre b :
cond1 = titanic.dtypes == "int64"
titanic.loc[:, cond1].head()
pclass | survived | sibsp | parch | |
---|---|---|---|---|
0 | 1 | 1 | 0 | 0 |
1 | 1 | 1 | 1 | 2 |
2 | 1 | 0 | 1 | 2 |
3 | 1 | 0 | 1 | 2 |
4 | 1 | 0 | 1 | 2 |
cond2 = [i for i in titanic.columns if i.startswith("s")]
titanic.loc[:, cond2].head()
survived | sex | sibsp | |
---|---|---|---|
0 | 1 | female | 0 |
1 | 1 | male | 1 |
2 | 0 | female | 1 |
3 | 0 | male | 1 |
4 | 0 | female | 1 |
Synthèse : j'utilise quoi du coup?
iloc
ou loc
?
En résumé, les syntaxes data.iloc[i, j]
et data.loc[i, j]
suivent la même logique en permettant dans le même appel de filtrer sur les lignes en i et sur les colonnes en j. Le choix entre iloc
et loc
est seulement guidé par le fait que votre sélection porte sur :
- les positions des lignes ou des colonnes. Dans ce cas vous devez utiliser
iloc
. - les noms des colonnes, les index des lignes, ou un vecteur de booléens. Dans ce cas utilisez
loc
.
Rappelez vous que sélectionner par les positions avec .loc
n'est possible que quand les index par défaut sont les positions des lignes. Mais cela porte à confusion. Prenons par exemple notre table triée par âge pour laquelle on n'a pas réinitialisé les index :
titanic_sorted = titanic.sort_values("age").copy()
titanic_sorted.head(5)
pclass | survived | name | sex | age | sibsp | parch | ticket | fare | cabin | embarked | boat | body | home.dest | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
763 | 3 | 1 | Dean, Miss. Elizabeth Gladys "Millvina" | female | 0.1667 | 1 | 2 | C.A. 2315 | 20.5750 | NaN | S | 10 | NaN | Devon, England Wichita, KS |
747 | 3 | 0 | Danbom, Master. Gilbert Sigvard Emanuel | male | 0.3333 | 0 | 2 | 347080 | 14.4000 | NaN | S | NaN | NaN | Stanton, IA |
1240 | 3 | 1 | Thomas, Master. Assad Alexander | male | 0.4167 | 0 | 1 | 2625 | 8.5167 | NaN | C | 16 | NaN | NaN |
427 | 2 | 1 | Hamalainen, Master. Viljo | male | 0.6667 | 1 | 1 | 250649 | 14.5000 | NaN | S | 4 | NaN | Detroit, MI |
1111 | 3 | 0 | Peacock, Master. Alfred Edward | male | 0.7500 | 1 | 1 | SOTON/O.Q. 3101315 | 13.7750 | NaN | S | NaN | NaN | NaN |
Si l'on veut la première et la 5e ligne de ce nouveau dataframe, il faut bien utiliser iloc
et non loc
qui renverra les index 0 et 5 de l'ancien dataframe titanic
:
titanic_sorted.iloc[[0, 4], :]
pclass | survived | name | sex | age | sibsp | parch | ticket | fare | cabin | embarked | boat | body | home.dest | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
763 | 3 | 1 | Dean, Miss. Elizabeth Gladys "Millvina" | female | 0.1667 | 1 | 2 | C.A. 2315 | 20.575 | NaN | S | 10 | NaN | Devon, England Wichita, KS |
1111 | 3 | 0 | Peacock, Master. Alfred Edward | male | 0.7500 | 1 | 1 | SOTON/O.Q. 3101315 | 13.775 | NaN | S | NaN | NaN | NaN |
titanic_sorted.loc[[0, 4], :]
pclass | survived | name | sex | age | sibsp | parch | ticket | fare | cabin | embarked | boat | body | home.dest | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 1 | Allen, Miss. Elisabeth Walton | female | 29.0 | 0 | 0 | 24160 | 211.3375 | B5 | S | 2 | NaN | St Louis, MO |
4 | 1 | 0 | Allison, Mrs. Hudson J C (Bessie Waldo Daniels) | female | 25.0 | 1 | 2 | 113781 | 151.5500 | C22 C26 | S | NaN | NaN | Montreal, PQ / Chesterville, ON |
On aurait aussi pu utiliser le paramètre ignore_index
dans sort_values()
pour réinitialiser directement les index.
[]
ou loc
?
Les crochets n'explicitent pas autant s'ils sélectionnent sur des lignes ou des colonnes. Si vous renseignez une liste de labels ils vont sélectionner des colonnes, et si vous renseignez une liste de booléens ils vont filtrer sur les lignes. Il est possible de chaîner les opérations sur les lignes ou les colonnes. Ainsi : titanic[titanic.sex == "female"]["name"]
donnera le même résultat que titanic.loc[titanic.sex == "female", "name"]
, c'est à dire les noms (colonne "name") des passagères (lignes correspondant aux femmes).
Il vous revient donc de choisir ce qui vous convient le mieux. Dans le cas où on sélectionne à la fois sur les lignes et les colonnes, la syntaxe de loc
a l'avantage d'être plus lisible. Dans le cas où on sélectionne seulement des colonnes ou seulement des lignes, l'usage des crochets permettra de taper les instructions légèrement plus rapidement. Enfin quand on veut extraire une série d'un dataframe, on utilisera avantageusement la syntaxe data.nom_col
, équivalente à data["nom_col"]
.
C'est la fin de cet article! N'hésitez pas à visiter notre site et à nous suivre sur Twitter et Linkedin. Pour retrouver l'ensemble du code ayant servi à générer cette note, vous pouvez vous rendre sur le github de Statoscop.