import pandas as pd
import numpy as np # pandas reposant sur numpy on a souvent besoin des deux librairiesintroduction¶
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 ennumpycré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.0bref, 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
matarray([[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
numpyde 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 listeon peut aussi passer un masque
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 ou colonnes
# ici 2 lignes
df.loc[[552, 832]]
-> une dataframe avec deux lignes correspondant
aux deux passagers 552 et 832
# ici 2 lignes et 2 colonnes
df.loc[[552, 832], ['Name', 'Pclass']]
-> la même dataframe mais réduite à deux colonnes
# avec iloc
# ATTENTION: pour les indices de colonnes
# la colonne d'index ne compte pas, évidemment
df.iloc[[0, -1], [2, 1]]
-> la même
# plusieurs colonnes
# une forme équivalente à df[['Name', 'Pclass']]
df.loc[:, ['Name', 'Pclass']]
# .loc avec un masque
df.loc[df.Pclass == 1, ['Name', 'Sex']]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# indexation par une liste
# 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 avec .loc
df.loc[:, ['Name', 'Pclass']]# .loc avec un masque (généralement sur les lignes)
df.loc[df.Pclass == 1, ['Name', 'Sex']]slicing pandas et bornes¶
comme en python et numpy
on peut étendre l’opération d’indexation [i] à des slices [start:stop:step]
ATTENTION pour le slicing
il y a une grande différence entre loc et iloc
avec
loc: la slice contient les bornesalors que avec
ilocla 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 avec .loc[] (pour les index donc), stop est inclus
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 une ligne ou une colonne¶
→
ou un extrait
par exemple on peut slicer, par index, pour obtenir une ligne
(dans ce cas on obtient un objet de type pandas.Series)
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,)exercice sélections multiples et slicing¶
lisez le titanic et mettez les
PassengerIdcomme index des lignes
# votre codelocalisez l’élément d’index
40
a. Quel est le type de l’élément ?
b. localisez le nom du passager d’index40?
# votre codequel est le nom de la personne qui apparaît en avant-dernier dans le fichier
# votre codelocalisez les 3 derniers éléments (colonnes) de la ligne d’index
40
# votre codelocalisez les 4 derniers éléments de la colonne
Cabin
# votre codefabriquez une dataframe contenant
les infos des 10 dernières lignes du fichier
pour les colonnes
Name,PclassetSurvived
# votre codeindexation par un masque¶
rappel sur les masques¶
nous avons vu les masques, qui permettent
d’appliquer des conditions à une colonne ou à une dataframe
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.3838383838383838on 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
# le code pour calculer les taux de survie
# - global
# - des passagers femmes de première classe
# le code
df = pd.read_csv('data/titanic.csv', index_col='PassengerId')
mask_survived = (df['Survived'] == 1)
rate_global = mask_survived.sum()/len(df)
print(f"taux survie global = {rate_global:.2f}")
# the group
mask_female_1 = (df['Sex'] == 'female') & (df['Pclass'] == 1)
# survived in the group
mask_female_1_survived = mask_female_1 & mask_survived
rate_group = mask_female_1_survived.sum() / mask_female_1.sum()
print(f"rate in the group {rate_group:.2f}")taux survie global = 0.38
rate in the group 0.97
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.loc [ 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: boolvous 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 codeCombien trouvez-vous de passagers ?
# votre codeAccédez à la valeur
Namedu premier de ces passagers
# votre codeFaites la même expression que la question 1
en utilisant les fonctionsnumpy.logical_and,numpy.logical_not
# votre coderésumé des méthodes d’indexation¶
indexation directe par un masque
df[mask]on peut aussi utiliser un masque avec
.loc[mask, columns]
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)
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 F33ex2: un masque booléen pour les lignes et une liste pour les colonnes
les colonnesSexetSurviveddes 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 0le 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]
-> 0Pouvons-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] = 1A 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_2708/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_2708/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.5nan
/tmp/ipykernel_2708/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_2708/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 dfsi 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