Leçon 5 sur 8
Unité · Travailler le tableau
Sélectionner et filtrer, et l'avertissement devenu erreur
loc, iloc et masques booléens utilisés délibérément — plus ce que signalait le SettingWithCopyWarning, pourquoi pandas 3 l'a remplacé par ChainedAssignmentError, et ce que cela change pour le code que vous avez déjà.
Trois façons de sélectionner, et quand chacune convient
import pandas as pd
muac = read_register(RAW / "muac-screening-artibonite-2024.v1.csv")
Par étiquette, avec loc. Les lignes par valeur d’index, les colonnes par
nom. C’est celle vers laquelle aller par défaut, car elle dit ce qu’elle fait.
muac.loc[muac["commune"] == "Gonaives", ["child_id", "muac_mm", "outcome"]]
Par position, avec iloc. Lignes et colonnes par position entière.
muac.iloc[0] # premiere ligne
muac.iloc[:5, :3] # cinq premieres lignes, trois premieres colonnes
iloc sert à regarder, non à raisonner. La position dépend de l’ordre du
fichier, et un script qui dit iloc[:, 3] casse silencieusement le jour où
l’export gagne une colonne. Réservez-le à l’inspection et au travail réellement
positionnel.
Une colonne seule est une Series ; une liste de colonnes est un DataFrame :
muac["muac_mm"] # Series
muac[["muac_mm"]] # DataFrame a une colonne
muac[["commune", "muac_mm"]] # DataFrame a deux colonnes
Le double crochet n’est pas un choix esthétique. Certaines fonctions exigent un DataFrame, et une Series échouera loin de l’endroit où l’erreur a été commise.
Masques booléens
Un masque est une Series de True/False de même longueur que le tableau.
severe = muac["muac_mm"] < 115
print(severe.sum()) # combien
print(severe.mean()) # quelle part
muac.loc[severe]
.sum() et .mean() sur un masque booléen donnent l’effectif et la proportion.
C’est la façon la plus rapide de répondre à « combien » et « quelle part » sans
groupby.
Combiner des conditions
# Les parentheses sont obligatoires : & lie plus fort que <
under_five = (muac["age_months"] < 60) & (muac["muac_mm"] < 125)
# Utilisez & | ~ — non and / or / not, qui ne savent pas operer element par element
either = (muac["muac_mm"] < 115) | (muac["oedema"] == True)
Écrire and ici lève ValueError: The truth value of a Series is ambiguous. Le
message est opaque la première fois et signifie toujours la même chose : vous
avez utilisé un opérateur scalaire sur un vecteur.
Les valeurs manquantes ne sont pas False
C’est le piège qui change une charge de cas.
print(len(muac)) # 4218
print((muac["muac_mm"] < 125).sum()) # ne compte que les enfants mesures
print(muac["muac_mm"].isna().sum()) # ceux exclus silencieusement
Une comparaison avec NA vaut NA, et NA n’est pas sélectionné. Un filtre
écarte donc discrètement tout enfant non mesuré — ce qui est juste si vous
vouliez dire « enfants mesurés en dessous de 125 mm » et faux si vous vouliez
dire « enfants dont on ne sait pas qu’ils sont au-dessus de 125 mm ».
Soyez explicite sur ce que vous vouliez dire :
measured = muac["muac_mm"].notna()
gam = measured & (muac["muac_mm"] < 125)
print(f"evalues : {measured.sum()}, cas de MAG : {gam.sum()}")
print(f"taux de MAG : {gam.sum() / measured.sum():.3f}")
Le dénominateur est tout l’enjeu. Diviser par len(muac) au lieu de
measured.sum() rapporte une prévalence plus faible pour une raison sans rapport
avec la nutrition.
isin, between, query
north = muac["commune"].isin(["Gonaives", "Terre-Neuve", "Anse-Rouge"])
plausible = muac["muac_mm"].between(80, 220)
between est inclusif aux deux bornes par défaut et — c’est l’essentiel —
renvoie False pour NA. Donc ~muac["muac_mm"].between(80, 220) compte une
mesure manquante comme une mesure impossible. Ce sont deux défauts différents et
un seul est une erreur de saisie :
impossible = muac["muac_mm"].notna() & ~muac["muac_mm"].between(80, 220)
query se lit bien pour les conditions longues et mérite d’être connu :
muac.query("age_months < 60 and muac_mm < 125")
Il est plus lent et masque les fautes de frappe dans une chaîne : préférez les masques dans un script qui doit échouer bruyamment.
L’avertissement devenu erreur
Les anciennes versions de pandas produisaient un avertissement que des générations d’analystes ont appris à masquer :
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
Il signalait une affectation chaînée — sélectionner, puis affecter à la sélection :
muac[muac["commune"] == "Gonaives"]["muac_mm"] = 0
Le premier crochet produit un nouvel objet. Que l’affectation atteigne ou non le tableau d’origine dépendait de l’agencement mémoire, ce qui faisait que la même ligne pouvait fonctionner lundi et pas mardi. L’avertissement existait parce que pandas ne pouvait pas vous dire lequel des deux s’était produit.
Ce que fait pandas 3 à la place
Le Copy-on-Write est toujours actif et ne peut plus être désactivé. Deux conséquences.
Le comportement est désormais défini. Une sélection n’écrit jamais dans son
parent — non pas parfois, jamais. SettingWithCopyWarning n’existe plus ;
pd.errors.SettingWithCopyWarning lève AttributeError.
L’affectation chaînée vous dit qu’elle n’a rien fait.
import pandas as pd
df = pd.DataFrame({"a": [1, 2, 3], "b": [10, 20, 30]})
df[df["a"] > 1]["b"] = 0
ChainedAssignmentError: A value is being set on a copy of a
DataFrame or Series through chained assignment. Such chained
assignment never works ...
ChainedAssignmentError dérive de Warning : par défaut la ligne s’affiche et
l’exécution continue. Dans un script dont les chiffres comptent, promouvez-la :
import warnings
warnings.simplefilter("error", pd.errors.ChainedAssignmentError)
Le script s’arrête alors au lieu de rapporter un chiffre calculé à partir d’une modification qui n’a jamais eu lieu.
La forme correcte, et ce qui change pour le code existant
# Un seul .loc, les deux axes, une seule operation.
muac.loc[muac["commune"] == "Gonaives", "muac_mm"] = pd.NA
Si vous maintenez du code écrit pour pandas 1 ou 2, le risque de migration va
dans un seul sens : une affectation chaînée qui modifiait le tableau ne le
modifie plus. Le chiffre change et rien ne plante. Cherchez ][" et les appels
à .copy() ajoutés pour faire taire l’ancien avertissement — ces copies sont
désormais inutiles, et chacune retirée est une économie réelle sur un gros
tableau.
.copy() et les cas où vous le voulez encore
Sous Copy-on-Write, df[mask] est déjà indépendant : un .copy() défensif
n’apporte rien. Il reste utile quand il documente une intention :
# Ce sous-ensemble va etre modifie et n'est la vue de rien.
gonaives = muac.loc[muac["commune"] == "Gonaives"].copy()
gonaives["flag"] = gonaives["muac_mm"] < 115
Ajouter une colonne à une sélection non copiée fonctionne et c’est le cas
courant ; le .copy() ci-dessus est une note au lecteur, non une nécessité.
Ajouter et modifier des colonnes
assign renvoie un nouveau tableau et se chaîne, ce qui garde une chaîne de
traitement lisible :
summary = (
muac
.loc[muac["muac_mm"].notna()]
.assign(
gam=lambda d: d["muac_mm"] < 125,
sam=lambda d: d["muac_mm"] < 115,
)
)
Le lambda d: compte : il désigne le tableau à ce point de la chaîne, si bien
que sam peut se définir à partir de colonnes créées par assign une ligne plus
tôt. Référencer muac directement dans la chaîne utiliserait le tableau
d’avant-filtre et désalignerait tout.
Pour des conditions à plus de deux branches, np.select vaut mieux que des
where imbriqués :
import numpy as np
muac["band"] = np.select(
[muac["muac_mm"] < 115, muac["muac_mm"] < 125],
["severe", "moderate"],
default="normal",
)
Les conditions sont évaluées dans l’ordre et la première qui correspond
l’emporte : < 115 doit donc venir en premier. Noter que default attrape aussi
les valeurs manquantes, ce qui n’est presque jamais souhaité — remettez-les
explicitement :
muac.loc[muac["muac_mm"].isna(), "band"] = pd.NA
Trier n’est pas classer
muac.sort_values(["commune", "screening_date"], ascending=[True, False])
Trier range des lignes. Cela ne dit rien de la réalité des écarts entre lignes voisines — un point que développe le projet Tableau de bord de programme nutritionnel, où neuf communes sur douze ont des intervalles de confiance qui se recouvrent et où l’ordre de tri n’est pas une information.
nlargest est la forme lisible du « top N » et est plus rapide qu’un tri du
tableau entier :
muac.groupby("commune")["muac_mm"].mean().nsmallest(5)
Ce qui vient ensuite
Vous savez sélectionner un sous-ensemble et le modifier sans vous demander si la
modification a eu lieu. La leçon suivante agrège : groupby, agrégation nommée,
et l’obtention d’un numérateur et d’un dénominateur issus de la même opération,
pour qu’ils ne puissent pas diverger.