import pandas as pd
import numpy as np # pandas reposant sur numpy on a souvent besoin des deux librairies
introduction¶
copier une dataframe ou une série¶
pour dupliquer une dataframe ou une série (ligne ou colonne)
toujours la méthode classique copy
des objets Python
vous allez utiliser la méthode pandas.DataFrame.copy
ou pandas.Series.copy
construisons une dataframe
df = pd.read_csv('data/titanic.csv', index_col='PassengerId')
copions la
df2 = df.copy()
modifions la copie
df2.loc[552, 'Age'] = 100
# vérifions
df2.head(1)
Survived Pclass Name Sex Age ...
PassengerId
552 0 2 Sharp, Mr. Percival James R male 100.0 ...
l’original n’est pas modifiée
df.head(1)
Survived Pclass Name Sex Age ...
PassengerId
552 0 2 Sharp, Mr. Percival James R male 27.0 ...
df2
est une nouvelle dataframe
avec les mêmes valeurs que l’originale df
mais totalement indépendante
# le code
df = pd.read_csv('data/titanic.csv', index_col='PassengerId')
df2 = df.copy()
df2.loc[552, 'Age'] = 100
df2.head(1)
df.head(1)
créer une nouvelle colonne¶
pour créer une nouvelle colonne
on la rajoute dans le dictionnaire des colonnes
souvent on crée une nouvelle colonne
en faisant un calcul sur des colonnes existantes
les opérations sur les colonnes peuvent utiliser la forme df[nom_de_colonne]
dans la dataframe du titanic, créons une colonne des décédés (donc 1 - les survivants)
df['Deceased'] = 1 - df['Survived']
nous avons rajouté la clé 'Deceased'
dans l’index des colonnespandas
voit sa dataframe comme un dictionnaire des colonnes
# le code
df['Deceased'] = 1 - df['Survived']
df.head(3)
rappels python
, numpy
¶
pour accéder ou modifier des sous-parties de dataframe nous pourrions être tentés:
- d’utiliser les syntaxes classiques d’accès aux éléments d’un tableau par leur indice
comme vous le feriez en Python
L = [10, 20, 30, 40, 60]
L[0] = "Hello !"
print(L) # ['Hello !', 20, 30, 40, 60]
L[1:3] = [200, 300, 500]
L
-> L[1:3] = [200, 300, 500]
ou d’utiliser l’accès à un tableau par une paires d’indices
comme vous le feriez ennumpy
créons une matrice
numpy
(4, 4), et modifions une sous-matrice
mat = np.arange(12).reshape((4, 3))
mat[0:2, 0:2] = 999
mat
-> [[999, 999, 2],
[999, 999, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
- ou encore enfin, en passant par la colonne puis la ligne
il se peut que ça fonctionne, mais ATTENTION il ne FAUT PAS faire comme ça !
df['Age'][552]
27.0
bref, ATTENTION: ce n’est pas comme ça que ça fonctionne en pandas!!!
# le code - rappels sur Python
L = [10, 20, 30, 40, 60]
L[0] = "Hello !"
print(L)
L[1:3] = [200, 300, 500]
L
['Hello !', 20, 30, 40, 60]
['Hello !', 200, 300, 500, 40, 60]
# le code - rappels sur numpy
mat = np.arange(12).reshape((4, 3))
mat[0:2, 0:2] = 999
mat
array([[999, 999, 2],
[999, 999, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
# le code - accéder à un élément de la df
# ATTENTION: ça marche mais IL NE FAUT PAS FAIRE COMME CA !
df['Age'][552]
np.float64(27.0)
localiser en pandas
¶
ligne,colonne vs colonne, ligne¶
la première grosse différence entre numpy
et pandas
est que
un tableau
numpy
de dimension 2
est organisé en ligne, colonne
c’est-à-dire quetab[i]
renvoie une lignemais on a vu précédemment que sur une dataframe
df[truc]
renvoie une colonne
donc déjà on sait qu’on ne pourra pas écrire quelque chose commedf[ligne, colonne]
NON
localisation avec loc
et iloc
¶
première chose à retenir donc, les accès dans la dataframe
se font au travers de 2 accessoires loc
et iloc
qui prennent cette fois-ci leurs arguments dans le bon sens (ligne, colonne)
df.loc[index_ligne, index_colonne]
OUIdf.iloc[indice_ligne, indice_colonne]
OUI
la différence entre les deux est que loc
se base sur les index
alors que iloc
(retenir: i pour integer) se base sur les indices
df = pd.read_csv('data/titanic.csv', index_col='PassengerId')
# NB: PassengerId comme index ^^^^^^^^^^^^^^^^^^^^^^^
df.head(2)
-> Survived Pclass Name ... Fare Cabin Embarked
PassengerId ...
552 0 2 Sharp, Mr. Percival James R ... 26.00 NaN S
638 0 2 Collyer, Mr. Harvey ... 26.25 NaN S
df.tail(1)
-> Survived Pclass Name ... Fare Cabin Embarked
PassengerId ...
832 1 2 Richards, Master. George Sibley ... 18.75 NaN S
# accès par l'index
# pour les lignes: la valeur de 'PassengerId'
# pour les colonnes: les noms des colonnes
df.loc[552, 'Name']
-> 'Sharp, Mr. Percival James R'
# accès par indice (plus rare en pratique)
# attention la colonne d'index ne compte pas
# i.e. la colonne d'indice 0 est 'Survived'
df.iloc[0, 2]
-> 'Sharp, Mr. Percival James R'
# pareil avec un indice négatif
df.iloc[-1, 2]
-> 'Richards, Master. George Sibley'
df = pd.read_csv('data/titanic.csv', index_col='PassengerId')
df.head(2)
df.tail(1)
df.loc[552, 'Name']
'Sharp, Mr. Percival James R'
df.iloc[0, 2]
'Sharp, Mr. Percival James R'
df.iloc[-1, 2]
'Richards, Master. George Sibley'
sélection multiple¶
une fois ceci assimilé, pandas
va offrir des techniques usuelles
pour sélectionner plusieurs lignes (ou colonnes)
- sélection multiple explicite
- slicing
commençons par la sélection multiple:
- si on ne précise pas les colonnes, on les obtient toutes
- on peut mentionner simplement plusieurs index (ou indices)
que l’on passe dans une liste
quelques exemples
# comme avec un tableau numpy,
# si on ne précise pas les colonnes
# on les obtient toutes
df.loc[552]
-> une série qui matérialise la première ligne
# on peut passer des listes à loc/iloc
# pour sélectionner explicitement
# plusieurs lignes / colonnesa
df.loc[[552, 832]]
-> une dataframe avec deux lignes correspondant
aux deux passagers 552 et 832
df.loc[[552, 832], ['Name', 'Pclass']]
-> la même dataframe mais réduite à deux colonnes
# à nouveau pour les indices de colonnes
# la colonne d'index ne compte pas
df.iloc[[0, -1], [2, 1]]
-> la même
# pour sélectionner plusieurs colonnes
# le plus simple c'est quand même cette forme
df[['Name', 'Pclass']]
-> 2 colonnes, toutes les lignes
# mais bien sûr on peut aussi faire
df.loc[:, ['Name', 'Pclass']]
df.loc[552]
Survived 0
Pclass 2
Name Sharp, Mr. Percival James R
Sex male
Age 27.0
SibSp 0
Parch 0
Ticket 244358
Fare 26.0
Cabin NaN
Embarked S
Name: 552, dtype: object
# bien sûr les index choisis
# ne pas forcément contigus
df.loc[[552, 832]]
# choisir plusieurs lignes et plusieurs colonnes
df.loc[[552, 832], ['Name', 'Pclass']]
# la même avec iloc
df.iloc[[0, -1], [2, 1]]
# plusieurs colonnes : forme #1 (le plus simple)
df[['Name', 'Pclass']]
# plusieurs colonnes : forme #2 (le plus explicite)
df.loc[:, ['Name', 'Pclass']]
slicing pandas
et bornes¶
on va accéder à des sous-dataframe
en étendant l’opération d’indexation [i]
à des slices [start:stop:step]
comme en python
et numpy
ATTENTION pour le slicing
il y a une grande différence entre loc
et iloc
- avec
loc
: la slice contient les bornes - alors que avec
iloc
la borne supérieure est exclue, comme c’est l’habitude en Python
slicing avec loc
par index¶
on peut slicer sur les index
MAIS ATTENTION pour les index stop
est compris
exemple
regardons les index (lignes et colonnes)
# les 5 premiéres lignes
df.index[:5]
-> Int64Index([552, 638, 499, 261, 395], dtype='int64', name='PassengerId')
# les 5 premières colonnes
df.columns[:5]
-> Index(['Survived', 'Pclass', 'Name', 'Sex', 'Age'], dtype='object')
# le slicing avec .loc est inclusif
df.loc[ 638:261, 'Pclass': 'Age']
-> retourne une dataframe avec
3 lignes (638 et 261 inclus)
4 colonnes ('Pclass' et 'Age' inclus)
# les ids des 5 premières lignes
df.index[:5]
Index([552, 638, 499, 261, 395], dtype='int64', name='PassengerId')
# les noms des 5 premières colonnes
df.columns[:5]
Index(['Survived', 'Pclass', 'Name', 'Sex', 'Age'], dtype='object')
# slice avec loc -> inclusif
df.loc[ 638:261, 'Pclass': 'Age'].shape # (3, 4)
(3, 4)
# le code
df.loc[ 638:261, 'Pclass': 'Age']
avec la méthode get_loc()
sur un objet Index, on peut facilement obtenir l’indice d’un index
# remarquons une méthode des Index
# pour obtenir l'indice d'un index
df.columns.get_loc('Pclass'), df.index.get_loc(261)
(1, 3)
slicing avec iloc
par indices¶
on peut slicer
sur les indicesdf.iloc[start:stop:step, start:stop:step]
ce cas est simple car il est conforme aux habitude Python/numpy
ici la borne supérieure stop
est exclue
et donc en particulier le nombre d’items sélectionnés coincide avec stop-start
exemple
si on prend les lignes d’indice 1
à 7
et les colonnes d’indice 1
à 4
on obtient 6 lignes et 3 colonnes
df.iloc[1:7, 1:4].shape
-> (6, 3)
# le code
df.iloc[1:7, 1:4].shape
(6, 3)
localiser des lignes et des colonnes¶
ou sous-lignes et sous-colonnes
avec le slicing, par indice et index, on peut obtenir des lignes et des colonnes
ou des sous-lignes et des sous-colonnes
on obtient des objets de type pandas.Series
on peut slicer, par index, pour obtenir une ligne
df.loc[552, :] # première ligne (toutes les colonnes)
df.loc[552, :].shape
-> (11,)
on peut slicer, par index, pour obtenir une colonne
df.loc[:, 'Survived'] # première colonne (toutes les lignes)
df.loc[:, 'Survived'].shape
-> (891,)
on peut slicer, par indice, pour obtenir une ligne
df.iloc[0, :] # première ligne (toutes les colonnes)
df.iloc[0, :].shape
-> (11,)
notez qu’on peut alors omettre les colonnes puisqu’on les prend toutes
df.iloc[0] # première ligne (toutes les colonnes)
df.iloc[0].shape
-> (11,)
on peut slicer, par indice, pour obtenir une colonne
df.iloc[:, 0] # première colonne (toutes les lignes)
df.iloc[:, 0].shape
-> (891,)
# le code
df.loc[552, :].shape
df.loc[552].shape
(11,)
# le code
df.loc[:, 'Survived'].shape
(891,)
# le code
df.iloc[0, :].shape
df.iloc[0].shape
(11,)
# le code
df.iloc[:, 0].shape
(891,)
exercice sélections multiples et slicing¶
- lisez le titanic et mettez les
PassengerId
comme index des lignes
# votre code
- localisez l’élément d’index
40
a. Quel est le type de l’élément ?
b. localisez le nom du passager d’index40
?
# votre code
- quel est le nom de la personne qui apparaît en avant-dernier dans le fichier
# votre code
- localisez les 3 derniers éléments de la ligne d’index
40
# votre code
- localisez les 4 derniers éléments de la colonne
Cabin
# votre code
df['Cabin'].iloc[-4:]
PassengerId
287 NaN
326 C32
396 NaN
832 NaN
Name: Cabin, dtype: object
- fabriquez une dataframe contenant
- les infos des 10 dernières lignes du fichier
- pour les colonnes
Name
,Pclass
etSurvived
# votre code
indexation par un masque¶
rappel sur les conditions¶
nous avons vu les masques, qui permettent
d’appliquer des conditions à une colonne ou à une data-frame
et comment utiliser ce tableau de booléens pour des décomptes
df = pd.read_csv('data/titanic.csv', index_col='PassengerId')
df_survived = (df['Survived'] == 1)
df_survived.sum()/len(df)
-> 0.3838383838383838
on a vu comment combiner ces conditions
vous ne pouvez pas utiliser and
, or
et not
python (pas vectorisés)
et devez utiliser &
, |
et ~
ou np.logical_and
, np.logical_or
et np.logical_not
taux de survie des passagers femmes de première classe
( ((df['Sex'] == 'female') & (df['Survived'] == 1) & (df['Pclass'] == 1)).sum()
/((df['Sex'] == 'female') & (df['Pclass'] == 1)).sum() )
# le code
df = pd.read_csv('data/titanic.csv', index_col='PassengerId')
df_survived = (df['Survived'] == 1)
print( df_survived.sum()/len(df) )
( ((df['Sex'] == 'female') & (df['Survived'] == 1) & (df['Pclass'] == 1)).sum()
/((df['Sex'] == 'female') & (df['Pclass'] == 1)).sum() )
0.3838383838383838
np.float64(0.9680851063829787)
sélection par masque booléen¶
les objets comme nous venons d’en construire
e.g. df['Sex'] == 'female'
sont des séries à valeur booléennes
une série à valeur booléennes s’appelle un masque (comme en numpy
)
pour accéder à des sous-parties d’une dataframe
on va simplement indexer une dataframe par un masque
i.e. on va isoler les lignes de la dataframe où la valeur du booléen est vraie
et pour ça on écrit simplement
df [ df['Sex'] == 'female' ]
# ou encore
df.loc[ df['Sex'] == 'female' ]
ici le masque est une série qui a le même index que la dataframe
et une valeur booléenne, qui va indiquer si la ligne en question
doit être sélectionnée ou non
# le code
# on fabrique une dataframe qui contient seulement les femmes
df [ df['Sex'] == 'female' ]
df[mask]
décortiqué¶
faisons le masque des passagers de sexe féminin
# le code
mask = df['Sex'] == 'female'
mask
-> PassengerId
552 False
638 False
499 True
261 False
395 True
...
463 False
287 False
326 True
396 False
832 False
Name: Sex, Length: 891, dtype: bool
vous obtenez une pandas.Series
de bool
sa taille est le nombre de lignes de votre dataframe
indiquant le résultat de la condition pour chaque les passagers
le passager d’Id
499
est une femme
pour extraire la sous-dataframe des femmes
on indexe notre dataframe, par cet objet de type Series
de booléens
seules sont conservées les lignes, dont les booléens sont vrais
dans l’expression df[mask]
dans les crochets on n’a plus ni une slice, ni une liste
mais un objet de type Series
, qui s’apparente à une colonne,
de booléens, que l’on appelle un masque
pour un code concis et lisible
il est recommandé d’écrire directement la version abrégée
df[df['Sex'] == 'female']
# ou encore, moins lourd amha
df[df.Sex == 'female']
# le code
mask = df.Sex == 'female'
print(type(mask)) # pandas.core.series.Series
print(mask.dtype) # dtype('bool')
print(mask.shape)
mask.head() # un masque de booléens sur la colonne des index donc la colonne PassengerId
<class 'pandas.core.series.Series'>
bool
(891,)
PassengerId
552 False
638 False
499 True
261 False
395 True
Name: Sex, dtype: bool
# on indexe directement la dataframe par un masque
df[mask].head()
# tout sur une ligne
df[df.Sex == 'female'].head()
exercice combinaison d’expressions booléennes¶
- en une seule ligne sélectionner la sous-dataframe des passagers
qui ne sont pas en première classe
et dont l’age est supérieur ou égal à 70 ans
# votre code
- Combien trouvez-vous de passagers ?
# votre code
- Accédez à la valeur
Name
du premier de ces passagers
# votre code
- Faites la même expression que la question 1
en utilisant les fonctionsnumpy.logical_and
,numpy.logical_not
# votre code
résumé des méthodes d’indexation¶
- indexation directe par un masque
df[mask]
- on peut aussi utiliser un masque avec
.loc[mask, columns]
- on peut aussi utiliser un masque avec
- indexation au travers de
.loc[]
/.iloc[]
- par un index/indice resp.
- par liste explicite
- par slicing:
- borne sup incluse avec
.loc[]
- et exclue avec
.iloc[]
(comme d’hab en Python)
- borne sup incluse avec
on peut mélanger les méthodes d’indexation
- ex1: une liste pour les lignes et une slice pour les colonnes
df.loc[
# dans la dimension des lignes: une liste
[450, 3, 67],
# 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
- ex2: un masque booléen pour les lignes et une liste pour les colonnes
les colonnesSex
etSurvived
des passagers de plus de 71 ans
df.loc[df['Age'] >= 71, ['Sex', 'Survived']]
-> Sex Survived
PassengerId
97 male 0
494 male 0
631 male 1
852 male 0
le type du résultat dépend bien entendu de la dimension de la sélection
- dimension 2: DataFrame
- dimension 1: Series
- dimension 0: le type de la cellule sélectionnée
# le code
df.loc[
# dans la dimension des lignes: une liste
[450, 3, 67],
# dans la dimension des colonnes: une slice
'Sex':'Cabin':2]
# le code
df.loc[df['Age'] >= 71, ['Sex', 'Survived']]
règles des modifications¶
sélections de parties de dataframe¶
une opération sur une dataframe pandas
renvoie une sous-partie de la dataframe
le problème
- savoir si cette sous-partie réfère la dataframe initiale ou est une copie de la data-frame initiale
- ...ça dépend du contexte
vous devez vous en soucier ?
- oui, dès que vous voulez modifier des sous-parties de dataframe
- tant que vous ne faites que lire, tout va bien
en effet
si c’est une copie
votre modification ne sera pas prise en compte sur la dataframe d’origine
(voire pire elle sera prise en compte un peu par hasard mais vous ne pouvez pas compter sur le résultat)si c’est une référence partagée (une vue)
vos modifications dans la sélection, seront bien répercutées dans les données d’origine
donc
savoir si une opération retourne une copie ou une référence, c’est important !
et dépend toujours du contexte
à retenir
en utilisant les méthodes
pandas.DataFrame.loc[line, column]
etpandas.DataFrame.iloc[line, column]
on ne crée pas de copie mais des références partagées
c’est la bonne façon de fairedès que vous utiliser un chaînage d’indexation pour modifier
que ce soitdf[l][c]
oudf.loc[l][c]
oudf.iloc[l][c]
vous ne pouvez pas compter sur le résultat
ça fonctionne par hasard
à éviter absolument
(pour les avancés) ce problème s’appelle le chained indexing
https://
modification d’une copie¶
cette section est un peu avancée; pour les groupes de débutants, retenez simplement de toujours utiliser .loc()
(ou .iloc()
selon le contexte) pour créer des sélections de vos dataframes, si l’objectif est d’en modifir le contenue
par chainage d’indexations
prenons une dataframe et accèdons à une colonne
en utilisant la syntaxe classique d’accès à une colonne comme à une clé d’un dictionnaire
la colonne des survivants 'Survived'
df = pd.read_csv('data/titanic.csv', index_col='PassengerId')
df['Survived']
on obtient une colonne de type pandas.Series
accédons à l’élément d’index 1
de la colonne
df = pd.read_csv('data/titanic.csv', index_col='PassengerId')
df['Survived'][1]
-> 0
Pouvons-nous utiliser cette manière d’accéder pour modifier l’élément ?
et ressusciter le passager d’index 1 en changeant son état de survie
essayons, on obtient un message d’erreur:
df['Survived'][1] = 1
A value is trying to be set on a copy of a slice from a DataFrame
See the caveats in the documentation: <https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy>
df['Survived'][1] = 1
non
df['Survived'][1]
est clairement une indexation par chaînage, on voit les[][]
- ce n’est pas une référence
- toutes les indexations par chaînage sont des copies
- elle ne doivent pas être utilisées pour des modifications
si ça fonctionne c’est par hasard, vous devez utiliser loc
ou iloc
!
df.loc[1, 'Survived'] = 1
# le code
df = pd.read_csv('data/titanic.csv', index_col='PassengerId')
df.loc[552, 'Survived']
np.int64(0)
df['Survived'][552] = 1
# possible que df['Survived'][1] soit passé à 1, par hasard
# mais votre code est faux
# et dans tous les cas vous recevez un gros warning !
df.loc[552, 'Survived']
/tmp/ipykernel_2537/3582046640.py:1: FutureWarning: ChainedAssignmentError: behaviour will change in pandas 3.0!
You are setting values through chained assignment. Currently this works in certain cases, but when using Copy-on-Write (which will become the default behaviour in pandas 3.0) this will never work to update the original DataFrame or Series, because the intermediate object on which we are setting values will behave as a copy.
A typical example is when you are setting values in a column of a DataFrame, like:
df["col"][row_indexer] = value
Use `df.loc[row_indexer, "col"] = values` instead, to perform the assignment in a single step and ensure this keeps updating the original `df`.
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
df['Survived'][552] = 1
/tmp/ipykernel_2537/3582046640.py:1: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
df['Survived'][552] = 1
np.int64(1)
# ça c'est la façon propre de faire
df.loc[552, 'Survived'] = 1
df.loc[552, 'Survived']
np.int64(1)
# la preuve
df.loc[552, 'Survived'] = 0
df.loc[552, 'Survived']
np.int64(0)
# le code
print(df['Age'][889])
# le code
df.loc[889, 'Age'] = 27.5
# le code
df['Age'][889] = 27.5
nan
/tmp/ipykernel_2537/2655944779.py:8: FutureWarning: ChainedAssignmentError: behaviour will change in pandas 3.0!
You are setting values through chained assignment. Currently this works in certain cases, but when using Copy-on-Write (which will become the default behaviour in pandas 3.0) this will never work to update the original DataFrame or Series, because the intermediate object on which we are setting values will behave as a copy.
A typical example is when you are setting values in a column of a DataFrame, like:
df["col"][row_indexer] = value
Use `df.loc[row_indexer, "col"] = values` instead, to perform the assignment in a single step and ensure this keeps updating the original `df`.
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
df['Age'][889] = 27.5
/tmp/ipykernel_2537/2655944779.py:8: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
df['Age'][889] = 27.5
faire des copies explicites¶
vous ne voulez pas modifier la dataframe d’origine ?
faites une copie explicite de la sous-dataframe
df2 = df[ ['Survived', 'Pclass', 'Sex'] ].copy() # copie explicite
df2.loc[1, 'Survived'] # 1
df2.loc[1, 'Survived'] = 0 # on le passe à 0
df2.loc[1, 'Survived'] # 0 maintenant
df.loc[1, 'Survived'] # toujours 1 dans la dataframe d'origine df
si l’idée est de ne modifier qu’une copie d’une dataframe
utilisez copy
pour maîtriser ce que vous faites
et coder ainsi explicitement et proprement
# le code
df1 = df.loc[ :, ['Survived', 'Pclass', 'Sex'] ]
df1.loc[1, 'Survived'] = 1
# le code
df2 = df[ ['Survived', 'Pclass', 'Sex'] ].copy()
print(df2.loc[1, 'Survived'])
df2.loc[1, 'Survived'] = 0
print(df2.loc[1, 'Survived'])
print(df.loc[1, 'Survived'])
0
0
0