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.