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’utiliserloc
par contre les formes qui utilisent un chained indexing - que ce soit
df[l][c]
oudf.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
etSurvived
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
etnp.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 deloc
, bien entenduqui 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 !