modifications et slicing de dataframe

Où on apprend à découper et modifier des parties de dataframe

Nous allons nous intéresser dans ce notebook à la manière de découper (trancher) slicer les objets pandas comme des séries ou des dataframes, et à les manipuler. C’est souvent ce que vous allez faire sur vos tables: appliquer une fonction à une sous-partie de vos données.

Importons nos bibliothèques et nous allons lire une table des passagers du Titanic pour servir d’exemple.

import pandas as pd
import numpy as np

Lisons notre dataframe du Titanic et passons lui comme index des lignes, la colonne PassengerId.

file = 'titanic.csv'
df = pd.read_csv(file, index_col='PassengerId')

et aussi, comme dans le notebook précédent on va le trier par âge histoire de bien voir la différence entre les index et les indices

df.sort_values(by='Age', inplace=True)
df.head(3)
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
PassengerId
804 1 3 Thomas, Master. Assad Alexander male 0.42 0 1 2625 8.5167 NaN C
756 1 2 Hamalainen, Master. Viljo male 0.67 1 1 250649 14.5000 NaN S
645 1 3 Baclini, Miss. Eugenie female 0.75 2 1 2666 19.2583 NaN C

copier une dataframe

Une chose que nous pouvons apprendre est à copier une dataframe. Pour cela il faut utiliser la méthodes copy des pandas.DataFrame.

df_copy = df.copy()
df_copy.head(3)      # df_copy est une nouvelle dataframe jumelle de l'originale
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
PassengerId
804 1 3 Thomas, Master. Assad Alexander male 0.42 0 1 2625 8.5167 NaN C
756 1 2 Hamalainen, Master. Viljo male 0.67 1 1 250649 14.5000 NaN S
645 1 3 Baclini, Miss. Eugenie female 0.75 2 1 2666 19.2583 NaN C

voilà df_copy est une nouvelle dataframe avec les mêmes valeurs que l’originale mais totalement indépendante.

créer une nouvelle colonne

Il est souvent pratique de créer une nouvelle colonne, en faisant un calcul à partir des colonnes existantes.
Les opérations sur les colonnes sont, en pratique, les seules opérations qui utilisent la forme df[nom_de_colonne]

# pour créer une nouvelle colonne
# par exemple ici je vais ajouter une colonne 'Deceased'
# qui est simplement l'opposé de 'Survived'

df['Deceased'] = 1 - df['Survived']
df.head(3)
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Deceased
PassengerId
804 1 3 Thomas, Master. Assad Alexander male 0.42 0 1 2625 8.5167 NaN C 0
756 1 2 Hamalainen, Master. Viljo male 0.67 1 1 250649 14.5000 NaN S 0
645 1 3 Baclini, Miss. Eugenie female 0.75 2 1 2666 19.2583 NaN C 0

contextualisons l’accès et la modification de parties d’une dataframe

Pour accéder ou modifier des sous-parties de dataframe, vous pourriez être tenté d’utiliser les syntaxes classiques d’accès aux éléments d’un tableau par leur indice, comme vous le feriez en Python.

Comme par exemple en Python:

L = [-12, 56, 34]
L[0] = "Hello !"
L
['Hello !', 56, 34]
L[1:2] = [100, 200, 300]
L
['Hello !', 100, 200, 300, 34]

Ou encore, d’utiliser l’accès à un tableau par une paires d’indices, comme vous le feriez en numpy:

mat = np.arange(25).reshape((5, 5))   # je crée la matrice 5x5 d'éléments 0 à 24
mat[2, 2] = 100                       # je modifie l'élément au milieu
mat[::4, ::4] = 10000                 # je modifie les 4 coins (::4 = du début à la fin avec un pas 4)
print(mat)                            # j'affiche la matrice
mat[0]                                # j'accède à sa première ligne
[[10000     1     2     3 10000]
 [    5     6     7     8     9]
 [   10    11   100    13    14]
 [   15    16    17    18    19]
 [10000    21    22    23 10000]]
array([10000,     1,     2,     3, 10000])

Mais voilà en pandas, c’est très différent: comme on l’a vu déjà, ils ont mis leurs efforts sur la gestion d’une indexation des lignes et des colonnes.

Ils ont priviligié le repérage des éléments d’une dataframe par des index (les noms de colonnes et les labels de lignes), et pas les indices comme en Python et en numpy

Pourquoi ? parce que si vous utilisez pandas c’est que vous avez besoin de voir vos données sous la forme d’une table avec des labels pour indexer les lignes et les colonnes. Si vous n’avez pas besoin d’index particuliers, ça veut dire que vous êtes à l’aise pour manipuler vos données uniquement à base d’indices - des entiers - et dans ce cas-là autant utiliser un simple tableau numpy : vous n’allez pas stocker une matrice dans une dataframe ! numpy et ses indices ligne, colonne vous suffisent !

Néanmoins, pandas offre des techniques assez similaires, et assez puissantes aussi, que nous allons étudier dans ce notebook.

rappels : loc pour les accès atomiques

on l’a vu dans le notebook précédent, les accès à un dataframe pandas se font

  • le plus souvent à base d’index et non pas d’indices

  • et dans ce cas on utilise df.loc pour accéder aux lignes et cellules

df.head(3)
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Deceased
PassengerId
804 1 3 Thomas, Master. Assad Alexander male 0.42 0 1 2625 8.5167 NaN C 0
756 1 2 Hamalainen, Master. Viljo male 0.67 1 1 250649 14.5000 NaN S 0
645 1 3 Baclini, Miss. Eugenie female 0.75 2 1 2666 19.2583 NaN C 0
# avec loc, c'est ligne, colonne
# et avec des index (pas des indices)
df.loc[756, 'Name']
'Hamalainen, Master. Viljo'
# pour upgrader un passager
df.loc[645, 'Pclass'] -= 1
df.head(3)
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Deceased
PassengerId
804 1 3 Thomas, Master. Assad Alexander male 0.42 0 1 2625 8.5167 NaN C 0
756 1 2 Hamalainen, Master. Viljo male 0.67 1 1 250649 14.5000 NaN S 0
645 1 2 Baclini, Miss. Eugenie female 0.75 2 1 2666 19.2583 NaN C 0

slicing

df.loc et bornes inclusives

Du coup, la première chose qu’on peut avoir envie de faire, c’est d’accéder à la dataframe par des slices; ça doit commencer à être banal maintenant, puisqu’à chaque fois qu’on voit une structure de données qui s’utilise avec [] on finit par étendre le sens de l’opération pour des slices.

Rappelez-vous qu’en Python une slice c’est de la forme start:stop:step, et qu’on peut éluder les morceaux qu’on veut, c’est-à-dire que par exemple : désigne une slice qui couvre tout l’espace, ::-1 permet de renverser l’ordre, je vous renvoie aux chapitres idoines si ce n’est plus clair pour vous.

Par contre, il faut tout de suite souligner une différence, qui est que dans le cas des index les slices de dataframes contiennent les bornes, ce qui, vous vous souvenez, n’a jamais été le cas jusqu’ici avec les slices en Python ou numpy, où la borne supérieure est toujours exclue; voyons cela

df.head(5)
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Deceased
PassengerId
804 1 3 Thomas, Master. Assad Alexander male 0.42 0 1 2625 8.5167 NaN C 0
756 1 2 Hamalainen, Master. Viljo male 0.67 1 1 250649 14.5000 NaN S 0
645 1 2 Baclini, Miss. Eugenie female 0.75 2 1 2666 19.2583 NaN C 0
470 1 3 Baclini, Miss. Helene Barbara female 0.75 2 1 2666 19.2583 NaN C 0
79 1 2 Caldwell, Master. Alden Gates male 0.83 0 2 248738 29.0000 NaN S 0
# je sélectionne les lignes entre 
# l'index 756 et l'index 470 INCLUSIVEMENT

df.loc[756:470]
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Deceased
PassengerId
756 1 2 Hamalainen, Master. Viljo male 0.67 1 1 250649 14.5000 NaN S 0
645 1 2 Baclini, Miss. Eugenie female 0.75 2 1 2666 19.2583 NaN C 0
470 1 3 Baclini, Miss. Helene Barbara female 0.75 2 1 2666 19.2583 NaN C 0

Il y a tout de même une certaine logique, c’est que les index sont a priori mélangés (et peuvent être des noms et pas des entiers), mais bon ca reste troublant au début. Et ce ne sera pas le cas pour iloc qui travaille sur les indices.

df.loc avec slicing sur les colonnes

Voyons comment faire du slicing dans l’autre direction

# si j'écris ceci, je désigne 
# toutes les lignes de la colonne 
# donc toute la colonne Pclass

df.loc[:, 'Pclass']
PassengerId
804    3
756    2
645    2
470    3
79     2
      ..
860    3
864    3
869    3
879    3
889    3
Name: Pclass, Length: 891, dtype: int64
# d'ailleurs effectivement, c'est optimisé
# au point que c'est le même objet en mémoire !

df.loc[:, 'Pclass'] is df['Pclass']
True

Et donc logiquement ici, si je veux sélectionner une plage de colonnes, je vais utiliser deux slices:

  • dans la direction des lignes, on prend tout avec une simple slice :

  • dans la direction des colonnes, le slicing marche aussi en mode inclusif

# ici comme pour les lignes, comme on est dans l'espace des index
# et pas celui des indices, les bornes de la slice sont INCLUSIVES

df.loc[:, 'Sex':'Parch'].head(3)
Sex Age SibSp Parch
PassengerId
804 male 0.42 0 1
756 male 0.67 1 1
645 female 0.75 2 1

df.loc pour écrire : bornes inclusives

On peut parfaitement modifier une dataframe au travers de slices, toujours en utilisant df.loc, et toujours avec bornes inclusives bien entendu :

df.head(5)
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Deceased
PassengerId
804 1 3 Thomas, Master. Assad Alexander male 0.42 0 1 2625 8.5167 NaN C 0
756 1 2 Hamalainen, Master. Viljo male 0.67 1 1 250649 14.5000 NaN S 0
645 1 2 Baclini, Miss. Eugenie female 0.75 2 1 2666 19.2583 NaN C 0
470 1 3 Baclini, Miss. Helene Barbara female 0.75 2 1 2666 19.2583 NaN C 0
79 1 2 Caldwell, Master. Alden Gates male 0.83 0 2 248738 29.0000 NaN S 0
# sans vouloir chercher un "use case" très utile
# multiplions par 1000 une portion de la dataframe

# les lignes entre 756 et 470 inclusivement
# les colonnes entre SibSp et Parch inclusivement

# quand on écrit x *= 1000,
# cela signifie x = x * 1000

df.loc[756:470, 'SibSp':'Parch'] *= 1000

# vérifions
df.head(5)
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Deceased
PassengerId
804 1 3 Thomas, Master. Assad Alexander male 0.42 0 1 2625 8.5167 NaN C 0
756 1 2 Hamalainen, Master. Viljo male 0.67 1000 1000 250649 14.5000 NaN S 0
645 1 2 Baclini, Miss. Eugenie female 0.75 2000 1000 2666 19.2583 NaN C 0
470 1 3 Baclini, Miss. Helene Barbara female 0.75 2000 1000 2666 19.2583 NaN C 0
79 1 2 Caldwell, Master. Alden Gates male 0.83 0 2 248738 29.0000 NaN S 0

slicing généralisé

Bon bien sûr on peut mélanger toutes les features que nous connaissons déjà, et écrire des sélections arbitrairement compliquées - pas souvent utiles, mais simplement pour montrer que toute la logique est préservée

df.head(8)
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Deceased
PassengerId
804 1 3 Thomas, Master. Assad Alexander male 0.42 0 1 2625 8.5167 NaN C 0
756 1 2 Hamalainen, Master. Viljo male 0.67 1000 1000 250649 14.5000 NaN S 0
645 1 2 Baclini, Miss. Eugenie female 0.75 2000 1000 2666 19.2583 NaN C 0
470 1 3 Baclini, Miss. Helene Barbara female 0.75 2000 1000 2666 19.2583 NaN C 0
79 1 2 Caldwell, Master. Alden Gates male 0.83 0 2 248738 29.0000 NaN S 0
832 1 2 Richards, Master. George Sibley male 0.83 1 1 29106 18.7500 NaN S 0
306 1 1 Allison, Master. Hudson Trevor male 0.92 1 2 113781 151.5500 C22 C26 S 0
828 1 2 Mallet, Master. Andre male 1.00 0 2 S.C./PARIS 2079 37.0042 NaN C 0
# tous ce qu'on a appris jusqu'ici à propos des slices
# fonctionne comme attendu, à part cette histoire de 
# borne supérieure qui est inclusive avec les index

df.loc[804:828:2, 'Sex':'Ticket':2]
Sex SibSp Ticket
PassengerId
804 male 0 2625
645 female 2000 2666
79 male 0 248738
306 male 1 113781

copied or not copied, that is the question

Pour terminer cette section, pour les curieux, il y a une question parfois épineuse qui se pose lorsqu’on fait des sélections de parties de dataframe.

Quand une opération sur une dataframe pandas renvoie une sous-partie de la dataframe, savoir si cette sélection est en fait une référence partagée vers, ou si c’est une copie de la dataframe d’origine, … dépend du contexte !!

Bon très bien, vous dites-vous mais en quoi cela me concerne-t-il ! il gère bien comme il veut ses sous-tableaux, je ne vais pas m’en soucier …

alors oui cela est vrai … jusqu’à ce que vous vous mettiez à modifier des sous-parties de dataframe …

  • si la sous-partie est une copie de la sous-partie de dataframe, votre modification ne sera pas prise en compte sur la dataframe d’origine ! évidemment…

  • et si c’est une référence partagée vers une partie de la dataframe d’origine, alors vos modifications dans la sélection vont bien se répercuter dans les données d’origine.

ahhh … vous commencez à comprendre: savoir si une opération retourne une copie ou une référence devient important mais dépend du contexte.

Ce qu’il faut retenir c’est que

  • en utilisant la forme df.loc[line, column] on ne crée pas de copie, c’est la bonne façon d’utiliser loc

  • par contre les formes qui utilisent un chained indexing - que ce soit df[l][c] ou df.loc[l][c], on n’est plus du tout sûr du résultat : il ne faut pas les utiliser pour modifier quoi que ce soit !!

autres mécanismes d’indexation

accès à une liste explicite de lignes ou colonnes

Nous voulons maintenant prendre une référence sur une sous-partie d’une dataframe qui ne s’exprime pas sous la forme d’une slice (tranche), mais par contre nous possédons la liste des (index des) lignes et des colonnes que nous souhaitons conserver dans ma sous-partie de dataframe.

pandas sait parfaitement le faire :

  • on utilise df.loc[] puisqu’on va désigner des index,

  • et on va passer dans les [], non plus des slices, mais tout simplement des listes (et de plus, vous donnez les index dans l’ordre qui vous intéresse) :

Prenons ainsi par exemple

  • les lignes d’index 450, 3, 67, 800 et 678

  • et les colonnes Age, Pclass et Survived

Et comme ce sont des index, nous utilisons loc.

# c'est facile de créer une sélection de lignes et de colonnes 
df.loc[[450, 3, 67, 800, 678], ['Age', 'Pclass', 'Survived']]
Age Pclass Survived
PassengerId
450 52.0 1 1
3 26.0 3 1
67 29.0 2 1
800 30.0 3 0
678 18.0 3 1

recherche selon une formule booléenne

Nous avons vu dans le notebook précédent que nous pouvions faire des tests sur toutes les valeurs d’une colonne et que cela nous rendait un tableau de booléens.

# cette expression retourne une Series
mask = df['Pclass'] >= 3
type(mask)
pandas.core.series.Series
# voyons ce qu'elle contient
mask.head() # un masque de booléens sur la colonne des index donc la colonne PassengerId !
PassengerId
804     True
756    False
645    False
470     True
79     False
Name: Pclass, dtype: bool

La dernière manière d’accéder à des sous-parties de dataframe, va être d’indexer une dataframe par un masque de booléens sur la colonne des index i.e. on va isoler de la dataframe les lignes où la valeur du booléen est vraie.

Par exemple, pour extraire de la dataframe les lignes correspondant aux voyageurs en 3-ième classe, on va utiliser mask - un objet de type Series donc, qui contient des booléens - comme moyen pour indexer la dataframe.

# voyez qu'ici dans les crochets on n'a plus 
# une slice, ni une liste, 
# mais une colonne (une Series) de booléens
# qu'on appelle un masque

df.loc[ mask ]
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Deceased
PassengerId
804 1 3 Thomas, Master. Assad Alexander male 0.42 0 1 2625 8.5167 NaN C 0
470 1 3 Baclini, Miss. Helene Barbara female 0.75 2000 1000 2666 19.2583 NaN C 0
382 1 3 Nakid, Miss. Maria ("Mary") female 1.00 0 2 2653 15.7417 NaN C 0
165 0 3 Panula, Master. Eino Viljami male 1.00 4 1 3101295 39.6875 NaN S 1
387 0 3 Goodwin, Master. Sidney Leonard male 1.00 5 2 CA 2144 46.9000 NaN S 1
... ... ... ... ... ... ... ... ... ... ... ... ...
860 0 3 Razi, Mr. Raihed male NaN 0 0 2629 7.2292 NaN C 1
864 0 3 Sage, Miss. Dorothy Edith "Dolly" female NaN 8 2 CA. 2343 69.5500 NaN S 1
869 0 3 van Melkebeke, Mr. Philemon male NaN 0 0 345777 9.5000 NaN S 1
879 0 3 Laleff, Mr. Kristo male NaN 0 0 349217 7.8958 NaN S 1
889 0 3 Johnston, Miss. Catherine Helen "Carrie" female NaN 1 2 W./C. 6607 23.4500 NaN S 1

490 rows × 12 columns

Notez que bien souvent on ne prendra pas la peine de décortiquer comme ça, et on écrira directement

# en une seule ligne, c'est un peu moins lisible 
# mais c'est un idiome fréquent

# je rajoute .head(3) pour abrèger un peu

df[df['Pclass'] >= 3].head(3)
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Deceased
PassengerId
804 1 3 Thomas, Master. Assad Alexander male 0.42 0 1 2625 8.5167 NaN C 0
470 1 3 Baclini, Miss. Helene Barbara female 0.75 2000 1000 2666 19.2583 NaN C 0
382 1 3 Nakid, Miss. Maria ("Mary") female 1.00 0 2 2653 15.7417 NaN C 0

combinaison d’expressions booléennes

Un peu plus sophistiqué, nous pouvons mettre plusieurs conditions, par exemple des passagers qui ne sont pas en première classe et dont l’age est supérieur à 70 ans.

Mais comment écrire ces conditions …

# on pourrait être tenté d'écrire quelque chose comme ceci

try:
    df['Age'] >= 70 and not(df['Pclass'] == 1)
except ValueError as e:
    print(f"Ce n'est pas bon, il me dit '{e}'")
Ce n'est pas bon, il me dit 'The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().'

Est-ce que cela ne vous rappelle pas quelque chose ?
Nous avons déjà vu le même comportement lorsqu’il s’était agi d’écrire des conditions sur les tableaux numpy; alors oui parmi les petites choses que l’on peut trouver parfois contre-intuitives avec numpy et pandas, il y a les expressions logiques sur les tableaux de booléens.

Vous ne pouvez pas utiliser and, or et not !

  • soit vous utilisez les np.logical_and, np.logical_or et np.logical_not mais ce n’est pas super lisible …

  • soit vous utilisez les &, | et ~ (les opérateurs logiques qu’on appelle bitwise i.e. qui travaillent bit à bit) et vous parenthésez bien !

mask_age = (df['Age'] >= 70) & (~ (df['Pclass'] == 1)) # une pandas.Series sur les index
mask_age.head(3)
PassengerId
804    False
756    False
645    False
dtype: bool
df[mask_age]
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Deceased
PassengerId
673 0 2 Mitchell, Mr. Henry Michael male 70.0 0 0 C.A. 24580 10.500 NaN S 1
117 0 3 Connors, Mr. Patrick male 70.5 0 0 370369 7.750 NaN Q 1
852 0 3 Svensson, Mr. Johan male 74.0 0 0 347060 7.775 NaN S 1

Ou de la manière concise habituellement utilisée:

# plus de 70 ans, et pas en première classe
# remarquez que ça se bouscule pas dans cette catégorie...

df.loc [ (df['Age'] >= 70) & (~ (df['Pclass'] == 1)) ] 
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Deceased
PassengerId
673 0 2 Mitchell, Mr. Henry Michael male 70.0 0 0 C.A. 24580 10.500 NaN S 1
117 0 3 Connors, Mr. Patrick male 70.5 0 0 370369 7.750 NaN Q 1
852 0 3 Svensson, Mr. Johan male 74.0 0 0 347060 7.775 NaN S 1
# pareil avec les opérateurs numpy
# personnellement je préfère la version précédente mais bon

df.loc [ np.logical_and(df['Age'] >= 70, np.logical_not(df['Pclass'] == 1)) ] # bof ...
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Deceased
PassengerId
673 0 2 Mitchell, Mr. Henry Michael male 70.0 0 0 C.A. 24580 10.500 NaN S 1
117 0 3 Connors, Mr. Patrick male 70.5 0 0 370369 7.750 NaN Q 1
852 0 3 Svensson, Mr. Johan male 74.0 0 0 347060 7.775 NaN S 1

résumé des méthodes d’indexation

Pour résumer cette partie, nous avons vu trois méthodes d’indexation utilisables avec loc :

  • on peut utiliser une slice, et parce qu’on manipule des index et pas des indices dans ce cas les bornes sont inclusives (on va voir tout de suite qu’avec les indices par contre les bornes sont les bornes habituelles, avec la fin exclue)

  • on peut utiliser une liste explicite, pour choisir exactement et dans le bon ordre les index qui nous intéressent

  • on peut utiliser un masque, c’est-à-dire une colonne obtenue en appliquant une expression booléenne à la dataframe de départ - cette méthode s’applique sans doute plus volontiers à la sélection de lignes

Remarquez d’ailleurs, pour les geeks, que si on veut on peut même mélanger ces trois méthodes d’indexation; c’est-à-dire par exemple utiliser une liste pour les lignes et une slice pour les colonnes :

# on peut indexer par exemple
# les lignes avec une liste
# les colonnes avec une slice
df.loc[
    # dans la dimension des lignes: une liste
    [450, 3, 67, 800, 678], 
    # dans la dimension des colonnes: une slice
    'Sex':'Cabin':2]
Sex SibSp Ticket Cabin
PassengerId
450 male 0 113786 C104
3 female 0 STON/O2. 3101282 NaN
67 female 0 C.A. 29395 F33
800 female 1 345773 NaN
678 female 0 4138 NaN

travailler avec les indices : bornes habituelles

Dans les - rares - cas où on veut travailler avec les indices plutôt qu’avec les index, tout fonctionne presque exactement pareil qu’avec les index, sauf que

  • on doit utiliser iloc au lieu de loc, bien entendu

  • qui supportent les mêmes mécanismes de slicing et d’indexation que l’on vient de voir,

  • et dans ce cas comme on est dans l’espace des indices, les bornes des slices se comportent comme les bornes habituelles (début inclus, fin exclue)

Je vous invite à vérifier ce point par vous même, en remettant à leur valeur originelle la portion de la dataframe que l’on avait un peu arbitrairement multipliée par 1000 tout à l’heure, tout ça en utilisant iloc

# je vous rappelle où on en est
df.head(5)
Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked Deceased
PassengerId
804 1 3 Thomas, Master. Assad Alexander male 0.42 0 1 2625 8.5167 NaN C 0
756 1 2 Hamalainen, Master. Viljo male 0.67 1000 1000 250649 14.5000 NaN S 0
645 1 2 Baclini, Miss. Eugenie female 0.75 2000 1000 2666 19.2583 NaN C 0
470 1 3 Baclini, Miss. Helene Barbara female 0.75 2000 1000 2666 19.2583 NaN C 0
79 1 2 Caldwell, Master. Alden Gates male 0.83 0 2 248738 29.0000 NaN S 0
# votre mission si vous l'acceptez
# rediviser par 1000 les 6 cases, mais à bases d'indices cette fois-ci
# donc en utilisant iloc
...
Ellipsis

problème de modification de copies (pour les avancés)

En première lecture de ce notebook, cette section ne sera compréhensible que par des élèves avancés, les autres pourront y revenir plus tard.

On va voir rapidement le problème de tentative de modification d’une copie d’une dataframe.

modification par chaînage d’indexations

Supposez qu’on accède à une colonne, par exemple celle de la survie qui s’appelle Survived, en utilisant la syntaxe classique d’accès à une clé d’un dictionnaire.

df['Survived']
PassengerId
804    1
756    1
645    1
470    1
79     1
      ..
860    0
864    0
869    0
879    0
889    0
Name: Survived, Length: 891, dtype: int64

On obtient une seule colonne, elle est de type pandas.Series, on le savait déjà.

Maintenant que j’ai une colonne, rien ne m’empêche d’accéder à un élément de la colonne, avec la simple notation d’accès à un élément d’un tableau comme dans Python, prenons l’élément d’index 1.

# so far, so good
df['Survived'][1]
0

Maintenant LA question. Je viens d’accéder à un élément de la colonne Survived, puis-je utiliser cette manière d’accéder pour modifier l’élément ?

Dit autrement, puis-je ressusciter le pauvre passager d’index 1 en faisant passer son état de survie à 1 par l’affectation df['Survived'][1] = 1

La réponse est non ! Pourquoi ? parce que df['Survived'][1] est une copie ! pas une référence vers une partie de la dataframe df !

On appelle cela une indexation par chaînage (on chaîne ['Survived']et [1]) et bien: toutes les indexations par chaînage sont des copies et ne peuvent pas donner lieu à des modifications …

Vous avez l’obligation d’utiliser loc ou iloc !

Pour les avancés ce problème s’appelle le chained indexing et pour plus d’explications regardez là https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy (quand vous en aurez le temps …)

indexation par une liste et modification

On va indexer une dataframe par une liste d’index de colonnes sans utiliser loc ni iloc. Dans cet exemple on isole les trois colonnes Survived, Pclass et Sex

df1 = df[ ['Survived', 'Pclass', 'Sex'] ]
df1.head()
Survived Pclass Sex
PassengerId
804 1 3 male
756 1 2 male
645 1 2 female
470 1 3 female
79 1 2 male

On obtient une dataframe que nous appelons df1. Donc vous vous rappelez que nous avons deux possibilité pour la sous-partie d’une dataframe, obtenue par découpage de la dataframe d’origine:

  • c’est une copie de la dataframe (vous ne devez pas la modifier)

  • c’est une référence sur la dataframe (vous pouvez la modifier).

LA question est donc de savoir si df1 est une copie ou une référence sur votre dataframe ?

C’est une copie donc vous ne devez pas tenter de la modifier mais on va le faire.

On tente de ressusciter notre pauvre passager d’index 1 en utilisant loc sur la sous-dataframe df1 (on a oublié que df1 était une copie).

On regarde ce que vaut l’élément qu’on veut modifier:

df1.loc[1, 'Survived']
0

ok 0. On tente de le modifier:

df1.loc[1, 'Survived'] = 1
/usr/share/miniconda/envs/python-numérique/lib/python3.9/site-packages/pandas/core/indexing.py:1817: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  self._setitem_single_column(loc, value, pi)

Je recois un warning de pandas me disant que j’ai potentiellement un problème. Comme il n’est pas sûr que pour moi ca en soit un, il me donne un simple avertissement et non une erreur.

En fait, là il m’indique que: si je pensais modifier df en passant par df1 alors je me trompe puisque df1 est une copie de ma dataframe df, donc df ne sera pas modifié.

Il se peut que ce soit ce que vous voulez (que df1 soit une copie) ! mais alors pourquoi ne l’avez vous pas clairement indiqué en faisant une copie explicite !

Si mon idée était bien de ne modifier que df1 parce que je veux une copie de df: alors je le code proprement:

df2 = df[ ['Survived', 'Pclass', 'Sex'] ].copy()
df2.head()
Survived Pclass Sex
PassengerId
804 1 3 male
756 1 2 male
645 1 2 female
470 1 3 female
79 1 2 male
df2.loc[1, 'Survived'] = 1

Ah voilà qui est mieux !