Filtrer des lignes — poser la même question à chaque ligne
Conserver uniquement les lignes qui répondent oui : masques booléens, combinaison avec &, | et ~, isin, between et les méthodes .str, lignes filtrées avec .loc — et modification de ces seules lignes sans l'affectation chaînée qui ne fait rien en silence.
Comment voulez-vous apprendre ?
Le problème que nous résolvons
Le responsable du magasin envoie un message : « Quelles commandes de la succursale nord portaient sur plus de trois articles ? Je veux les voir. »
Avec les huit commandes du chapitre précédent, vous pourriez répondre à l'œil. Faites glisser un doigt dans la colonne branch, arrêtez-vous à chaque north, regardez la colonne quantity. Trois lignes. C'est fait.
Imaginez maintenant le vrai fichier — douze mille commandes, une année entière. L'œil ne suffit plus, et la première idée qui vient à la plupart des programmeurs Python est une boucle :
import pandas as pd
orders = pd.read_csv("orders.csv")
keep = []
for i in range(len(orders)):
row = orders.iloc[i]
if row["branch"] == "north" and row["quantity"] > 3:
keep.append(int(row["order_id"]))
print(keep)[1001, 1005, 1008]Le résultat est correct. Mais voyez ce que ça a coûté : un compteur, une liste vide, un if, un append, et le tableau découpé ligne par ligne. Le résultat n'est plus un tableau — c'est une simple liste de numéros de commande. Pour montrer au responsable les produits et les prix en plus, il faudrait reboucler. Sur douze mille lignes, c'est aussi lent, car chaque orders.iloc[i] construit une nouvelle Series juste pour lire deux valeurs.
Pandas propose une autre façon de poser la question, et c'est l'opération que vous ferez le plus souvent sur un DataFrame : poser à chaque ligne la même question oui/non en une seule fois, puis conserver les lignes qui ont répondu oui. C'est le filtrage, et c'est tout le sujet de ce chapitre. La boucle ci-dessus devient une ligne, le résultat reste un tableau, et le code ressemble presque à la phrase du responsable.
À la fin de ce chapitre, vous saurez
- Transformer une question en masque booléen — une colonne de
True/False, une valeur par ligne — et l'utiliser pour conserver des lignes, ou pour les compter avecmask.sum()etmask.mean() - Combiner des conditions avec
&,|et~, et expliquer pourquoi les parenthèses sont obligatoires - Utiliser
isin,betweenet les méthodes.strpour des listes, des intervalles et des motifs textuels - Filtrer les lignes et sélectionner des colonnes en une seule étape avec
.loc[mask, columns] - Modifier les valeurs des lignes filtrées en toute sécurité avec
.loc[mask, column] = value, et reconnaître laChainedAssignmentErrorque produit la méthode non sécurisée dans pandas 3
Prérequis : Sélectionner des colonnes et des lignes.
Créer le fichier d'abord
Ce chapitre utilise le même fichier orders.csv que le précédent. Si vous ne l'avez pas, créez-le dans votre dossier de projet avec exactement ces neuf lignes :
order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0Avant d'écrire le code
Un filtre est une question écrite en code. La plupart des bugs de filtrage ne viennent pas de pandas — ils viennent d'une question qui n'a jamais été clairement posée, ou d'une colonne qui ne contient pas ce qu'on croyait. Avant la première ligne de filtrage, faites donc ces cinq choses.
1. Décomposez la question en conditions, une par colonne. Disséquez la phrase du responsable :
- « commandes de la succursale nord » → colonne
branch, test égal à, valeur"north" - « plus de trois articles » → colonne
quantity, test supérieur à, valeur3 - « Je veux les voir » → pas de test ; conserver toute la ligne
Cette liste vous force à prendre deux décisions. « Plus de trois » correspond à > 3, pas >= 3 — une commande d'exactement trois articles ne compte pas. Et les deux conditions sont liées par et : une commande doit passer les deux. Ces deux décisions constituent le filtre entier ; le code n'en est que l'orthographe.
2. Vérifiez les données d'entrée — forme, types et valeurs exactes contre lesquelles vous allez comparer.
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(orders.dtypes)(8, 7)
order_id int64
date str
branch str
product str
category str
quantity int64
price float64
dtype: objectquantity est de type int64, donc > 3 comparera des nombres. Si la colonne avait été lue comme str — parce que quelqu'un avait tapé "3 pcs" dans une cellule — la comparaison échouerait avec un TypeError, et vous voulez le savoir maintenant, pas en plein milieu. branch est de type str, donc la comparaison se fera sur le texte, et la comparaison textuelle est exacte : "north", "North" et "north " sont trois valeurs différentes. Regardez donc quelles valeurs existent réellement :
print(orders["branch"].unique())<StringArray>
['north', 'south', 'east']
Length: 3, dtype: strTrois succursales, chacune avec une orthographe unique, sans espaces parasites. La valeur "north" dans le code est garantie de correspondre à quelque chose.
3. Esquissez le résultat attendu avant de le produire. Sur un petit fichier, répondez d'abord à la question à la main. Les commandes de la succursale nord sont 1001 (12 articles), 1003 (2), 1005 (30) et 1008 (4). Plus de trois articles : 1001, 1005, 1008. Le résultat attendu est donc 3 lignes, 7 colonnes, avec des numéros de commande connus. Si le code renvoie plus tard 4 lignes ou 0, vous saurez immédiatement que le code est faux plutôt que de lui faire confiance. Sur un grand fichier vous ne pouvez pas tout vérifier à la main, mais vous pouvez estimer : « la succursale nord représente environ la moitié des commandes, le résultat devrait donc être bien en dessous de la moitié des lignes. »
4. Choisissez l'outil en fonction de la forme de la condition.
- une comparaison → un masque :
df["col"] > value - plusieurs conditions → masques combinés avec
&(et),|(ou),~(non) - « une de ces valeurs » →
df["col"].isin([...]) - « entre deux valeurs » →
df["col"].between(low, high) - un motif dans du texte →
df["col"].str.contains(...),.str.startswith(...) - des lignes et seulement certaines colonnes →
df.loc[mask, ["col1", "col2"]] - modifier les valeurs de ces lignes →
df.loc[mask, "col"] = new_value
La question du responsable comporte deux comparaisons jointes par et, donc : deux masques et &.
5. Décidez comment vous vérifierez le résultat. Après le filtrage, trois vérifications rapides permettent de détecter presque tous les problèmes : le nombre de lignes correspond à votre esquisse ; la condition est bien vérifiée dans chaque ligne survivante ((result["quantity"] > 3).all() doit afficher True) ; et rien de ce que vous attendiez ne manque (cherchez 1008 dans le résultat).
Passons maintenant au code — une idée à la fois.
Une comparaison produit un masque
Comparez une colonne avec une valeur, et pandas ne vous donne pas un seul True ou False. Il compare chaque ligne et vous renvoie une colonne entière de réponses :
big = orders["quantity"] > 5
print(big)0 True
1 False
2 False
3 True
4 True
5 False
6 True
7 False
Name: quantity, dtype: boolC'est une Series de bool — un masque booléen. Il a le même index que orders (0 à 7), une réponse par ligne : la ligne 0 a 12 articles, donc True ; la ligne 1 en a 5, et 5 n'est pas supérieur à 5, donc False.
Pourquoi pandas fonctionne-t-il ainsi, plutôt que de vous faire boucler ? Parce qu'une colonne est stockée comme un seul bloc de nombres d'un même type, et comparer tout un bloc avec 5 est une opération unique réalisée en code compilé rapide. Votre boucle demandait à Python d'examiner chaque ligne séparément, huit fois — ou douze mille fois. Le masque demande une seule fois.
Chaque opérateur de comparaison fonctionne de la même façon : ==, !=, <, <=, >, >=. Ils fonctionnent aussi sur les colonnes de texte — orders["branch"] == "north" est un masque avec True aux lignes 0, 2, 4 et 7.
Rien n'a encore été filtré. Un masque n'est que les réponses. L'étape suivante consiste à les utiliser.
Le masque conserve les lignes
Placez le masque entre crochets, et pandas conserve les lignes où le masque vaut True :
print(orders[big])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
3 1004 2024-03-02 east bottle accessories 7 120.0
4 1005 2024-03-03 north eraser stationery 30 8.0
6 1007 2024-03-04 east pen stationery 20 15.0Quatre lignes sur huit. Lisez les index à gauche : 0, 3, 4, 6. Les lignes conservées gardent leurs étiquettes d'origine. Le filtrage ne renumérote rien — la ligne 3 est toujours la ligne 3, la même commande 1004 qu'elle était dans le tableau complet. Cela a son importance, nous y reviendrons.
Comment pandas aligne-t-il le masque sur les lignes ? Pas par position — par étiquette d'index. L'étiquette 0 du masque décide de la ligne 0, l'étiquette 3 décide de la ligne 3. Ici les deux index sont identiques, car le masque a été construit à partir de orders lui-même, ce qui rend cela invisible. Cela devient visible seulement quand vous utilisez un masque construit à partir d'un tableau différent — c'est dans la section « Quand ça plante ».
Le masque est généralement écrit de façon inline, sans nom :
print(orders[orders["branch"] == "north"])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
2 1003 2024-03-02 north bag accessories 2 850.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0Lisez-le de l'intérieur vers l'extérieur : `orders["branch"] == "north"` construit le masque, `orders[ … ]` conserve les lignes True. Nommer le masque (big = …) est utile quand la condition est longue ou quand on l'utilise deux fois ; l'inline convient pour les conditions courtes.
Compter avec un masque, sans filtrer
Le responsable ne veut souvent pas les lignes, seulement un nombre : combien de commandes portaient sur plus de cinq articles ? Pas besoin de filtrer pour ça. En arithmétique, True vaut 1 et False vaut 0, donc :
print(big.sum())
print(big.mean())4
0.5sum() additionne les uns : 4 commandes. mean() divise par le nombre de lignes : 4 sur 8 donne 0.5, donc la moitié des commandes portait sur plus de cinq articles. « Combien » correspond à .sum() d'un masque ; « quelle proportion » correspond à .mean() d'un masque. Les deux tiennent en une ligne et ne construisent aucun nouveau tableau.
Et quand vous voulez un total sur les lignes filtrées — le nombre d'articles dans ces grandes commandes — filtrez d'abord, puis sélectionnez la colonne, puis faites la somme :
print(orders[big]["quantity"].sum())6912 + 7 + 30 + 20 = 69. Lire une colonne d'un tableau filtré de cette façon est correct. Y écrire de cette façon ne l'est pas — c'est la dernière section du chapitre.
Plusieurs conditions : &, |, ~
La question du responsable comporte deux conditions. Pandas combine les masques avec trois opérateurs :
&signifie et — la ligne est conservée quand les deux masques valentTrue|signifie ou — la ligne est conservée quand au moins un masque vautTrue~signifie non — il inverse chaqueTrueenFalseet vice versa
Voici enfin la question du responsable :
north_big = orders[(orders["branch"] == "north") & (orders["quantity"] > 3)]
print(north_big)order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0Trois lignes : 1001, 1005, 1008 — exactement l'esquisse de l'étape de planification. Voici maintenant la vérification de l'étape 5 :
print(len(north_big))
print((north_big["quantity"] > 3).all())
print((north_big["branch"] == "north").all())3
True
TrueLe nombre de lignes correspond, et les deux conditions sont vérifiées dans chaque ligne survivante. .all() demande « chaque valeur de ce masque est-elle True ? » — c'est le moyen le moins coûteux de prouver qu'un filtre a fait ce qu'on voulait.
Une question avec ou : commandes de la succursale est, ou toute commande d'un article coûtant 850 ou plus.
print(orders[(orders["branch"] == "east") | (orders["price"] >= 850)])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0Quatre lignes : les deux commandes de la succursale est, et les deux commandes de sacs provenant des succursales nord et sud. Une ligne qui aurait satisfait les deux conditions (aucune ici) n'apparaîtrait qu'une seule fois — un filtre ne fait que conserver ou supprimer une ligne.
Une question avec non : tout sauf la papeterie.
print(orders[~(orders["category"] == "stationery")])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
7 1008 2024-03-04 north bottle accessories 4 120.0~ transforme chaque True en False et vice versa. Pour une simple égalité vous pourriez écrire != à la place, ce qui est plus simple ; ~ justifie son usage quand la chose que vous niez est une condition plus longue ou un isin (vous verrez ça bientôt).
Pourquoi les parenthèses sont obligatoires
Chaque condition ci-dessus est placée entre ses propres parenthèses. Ce n'est pas un choix stylistique. Retirez-les et le code plante :
print(orders[orders["quantity"] > 3 & orders["price"] < 100])TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool]Ce que vous voyez ci-dessus est la dernière ligne de la trace d'erreur, celle qui nomme l'erreur ; les lignes au-dessus indiquent seulement où c'est arrivé. La raison est la priorité des opérateurs — l'ordre dans lequel Python applique les opérateurs. En Python, & est prioritaire sur > et <. Cela a été décidé bien avant l'existence de pandas, car & a été conçu pour les nombres entiers (« et » bit à bit), où il doit être appliqué en premier. Python lit donc la ligne comme :
orders["quantity"] > (3 & orders["price"]) < 100Il essaie de calculer 3 & orders["price"] en premier — un « et » bit à bit entre le nombre 3 et une colonne de décimaux — et c'est là l'origine de l'erreur. Le message mentionne rand_ (le « and » à droite) et un tableau, mais la cause est toujours la même : une paire de parenthèses manquante.
L'erreur est le bon scénario. Entourez seulement la première condition et oubliez la seconde, et il n'y a aucune erreur du tout :
print(len(orders[(orders["quantity"] > 3) & orders["price"] < 100]))
print(len(orders[(orders["quantity"] > 3) & (orders["price"] < 100)]))8
4La première ligne est lue comme ((quantity > 3) & price) < 100. Le & mélange un masque avec les prix, et chaque résultat se trouve par hasard être inférieur à 100, donc le « filtre » conserve les huit lignes. Le filtre correct en conserve quatre. Mêmes colonnes, mêmes nombres, une paire de parenthèses manquante — et la mauvaise version s'exécute sans le moindre bruit. Entourez chaque condition de ses propres parenthèses, à chaque fois. Ni l'erreur ni la version silencieuse ne peuvent alors se produire.
Pourquoi pas and ?
L'autre réflexe naturel est d'écrire le mot anglais :
print(orders[(orders["branch"] == "north") and (orders["quantity"] > 3)])ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().Les mots-clés and, or et not de Python ont besoin d'un seul True ou False de chaque côté — ils ont été conçus pour les instructions if. Pour utiliser and, Python demande au premier masque « es-tu vrai ? », et une colonne de huit réponses ne peut pas répondre par une seule. Est-ce vrai si une valeur est True ? Si toutes le sont ? Pandas refuse de deviner, et l'erreur liste les façons dont vous pourriez trancher (a.any(), a.all()). Mais vous ne vouliez pas une seule réponse ; vous vouliez un « et » ligne par ligne. C'est &. La règle à retenir : dans un filtre, and/or/not deviennent &/|/~.
Une valeur parmi plusieurs : isin
Commandes des succursales sud ou est. Vous pourriez écrire deux égalités jointes par |. Avec cinq succursales, cela ferait cinq comparaisons. isin prend une liste et demande « la valeur de cette ligne est-elle dans la liste ? » :
print(orders[orders["branch"].isin(["east", "south"])])order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0Et voici où ~ révèle tout son intérêt — toutes les succursales sauf nord et est s'écrit avec ~ devant un isin :
print(orders[~orders["branch"].isin(["north", "east"])]["order_id"].tolist())[1002, 1006]Deux commandes, toutes deux de la succursale sud. Notez que ~orders["branch"].isin([...]) n'a pas besoin de parenthèses supplémentaires : ~ s'applique au résultat de l'appel de méthode, et il n'y a pas de & ou | avec lequel entrer en conflit. Dès que vous le combinez avec une autre condition, les parenthèses reviennent : (~orders["branch"].isin([...])) & (orders["quantity"] > 3).
Un intervalle : between
Commandes au prix compris entre 15 et 120. Ce sont deux comparaisons, >= 15 et <= 120. between le dit en une seule fois :
print(orders[orders["price"].between(15, 120)])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
6 1007 2024-03-04 east pen stationery 20 15.0
7 1008 2024-03-04 north bottle accessories 4 120.0between inclut les deux bornes par défaut — les stylos à exactement 15.0 et les bouteilles à exactement 120.0 sont inclus. C'est conforme à la façon dont les gens disent habituellement « de 15 à 120 », mais vérifiez par rapport à la question. Si la question signifie strictement entre, précisez-le :
print(orders["price"].between(15, 120, inclusive="neither").sum())1Seul le carnet à 60.0 est strictement entre les deux bornes. inclusive accepte aussi "left" et "right" pour les intervalles semi-ouverts — le choix habituel pour les tranches de prix, où vous voulez 0–100, 100–200, etc. sans qu'une valeur limite tombe dans deux tranches.
Les motifs textuels : les méthodes .str
L'égalité sur le texte est exacte. L'accesseur .str donne à une colonne de texte les méthodes de chaînes que vous connaissez en Python, appliquées à chaque ligne, et chacune renvoie un masque.
Produits dont le nom commence par « b » :
print(orders[orders["product"].str.startswith("b")])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
7 1008 2024-03-04 north bottle accessories 4 120.0Produits contenant la lettre « o », quelle que soit la casse :
print(orders[orders["product"].str.contains("O", case=False)])order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
7 1008 2024-03-04 north bottle accessories 4 120.0Il n'y a aucun O majuscule dans les données, pourtant le carnet et les deux bouteilles sont trouvés, parce que case=False ignore la casse. Sans cette option, .str.contains("O") ne trouverait rien.
L'égalité exacte ne dispose pas d'une telle option. C'est le bug « mon filtre ne renvoie rien » le plus courant :
print(orders[orders["branch"] == "North"])Empty DataFrame
Columns: [order_id, date, branch, product, category, quantity, price]
Index: []Pas d'erreur, pas de lignes. Le tableau n'est pas faux et pandas n'est pas faux — il n'existe tout simplement pas de succursale orthographiée "North" avec une majuscule. C'est pourquoi l'étape 2 du plan affichait unique() : les valeurs contre lesquelles vous comparez doivent être copiées de ce qui est réellement dans la colonne, pas tapées de mémoire.
Les dates textuelles se comparent correctement — quand elles sont au format ISO
La colonne date a été lue comme str. Pouvez-vous la filtrer avec >= ? Commandes à partir du 3 mars :
print(orders["date"] >= "2024-03-03")0 False
1 False
2 False
3 False
4 True
5 True
6 True
7 True
Name: date, dtype: boolÇa fonctionne, mais comprenez pourquoi. Ce sont des chaînes de caractères, donc >= les compare comme du texte, caractère par caractère, comme dans un dictionnaire. Cela donne le bon résultat uniquement parce que les dates sont écrites année-mois-jour avec un zéro de remplissage (2024-03-03), de sorte que l'ordre textuel correspond par hasard à l'ordre chronologique. Une colonne écrite 3/3/2024 comparerait mal — "10/1/2024" se classerait avant "9/1/2024" parce que "1" vient avant "9". Pour tout ce qui va au-delà de simples seuils, convertissez en vraies dates avec pd.to_datetime, sujet abordé dans le chapitre sur l'ajout de colonnes.
Lignes et colonnes ensemble : .loc[mask, columns]
Les filtres précédents conservaient toutes les colonnes. Le responsable voulait les commandes de la succursale nord, mais le rapport n'a besoin que du produit et de la quantité. Vous avez vu .loc[rows, columns] au chapitre précédent, avec des étiquettes. La partie « lignes » accepte également un masque :
print(orders.loc[orders["branch"] == "north", ["product", "quantity"]])product quantity
0 pen 12
2 bag 2
4 eraser 30
7 bottle 4Une étape, un objet, et le lecteur de votre code voit toute la question — quelles lignes, quelles colonnes — en un seul endroit. .loc[mask, "quantity"] (un nom de colonne unique, pas une liste) vous donne une Series prête pour les calculs :
print(orders.loc[orders["branch"] == "north", "quantity"].sum())4812 + 2 + 30 + 4 = 48 articles vendus par la succursale nord.
query() : le même filtre sous forme de phrase
Il existe une seconde syntaxe pour les filtres, query(), qui prend la condition sous forme de chaîne de caractères :
print(orders.query("branch == 'north' and quantity > 3"))order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0Les mêmes trois lignes. À l'intérieur de la chaîne, vous écrivez les noms de colonnes directement, et and/or/not sont autorisés, parce que query analyse la chaîne lui-même au lieu de la confier aux opérateurs Python. Pour utiliser une variable Python, faites-la précéder de @ :
min_qty = 5
print(orders.query("quantity >= @min_qty")["order_id"].tolist())[1001, 1002, 1004, 1005, 1007]query se lit bien pour les conditions longues. Ses inconvénients : une faute de frappe dans la chaîne n'est découverte qu'au moment où la ligne s'exécute, votre éditeur ne peut pas vous aider à l'intérieur, et les noms de colonnes avec des espaces nécessitent des backticks. Ce cours utilise les masques comme outil principal, car les masques sont ce sur quoi tout le reste dans pandas — .loc, l'affectation, le comptage — est construit. Sachez ce qu'est query pour pouvoir le lire dans le code des autres.
Le tableau filtré conserve ses anciennes étiquettes
Revenons à l'index. Sauvegardez les commandes de la succursale nord et regardez-les :
north = orders[orders["branch"] == "north"]
print(north.index.tolist())[0, 2, 4, 7]Étiquettes 0, 2, 4, 7. Alors, qu'est-ce que north.loc[1] ? Il n'y a pas d'étiquette 1 dans ce tableau — la commande 1002 venait de la succursale sud et a été filtrée :
print(north.loc[1])KeyError: 1.loc recherche une étiquette, et l'étiquette 1 n'existe pas ici. Si vous vouliez dire « la deuxième commande de la succursale nord », c'est une position, et les positions s'obtiennent avec .iloc :
print(north.iloc[1])order_id 1003
date 2024-03-02
branch north
product bag
category accessories
quantity 2
price 850.0
Name: 2, dtype: objectLa deuxième ligne par position est la commande du sac — et son Name est 2, son étiquette dans le tableau d'origine. C'est exactement la distinction loc/iloc du chapitre précédent, et le filtrage est l'endroit où elle fait mal, car après un filtre les étiquettes et les positions ne correspondent plus.
Conserver les étiquettes est délibéré : cela vous permet de retrouver n'importe quelle ligne d'un résultat dans le tableau d'origine. Quand vous n'en avez plus besoin — par exemple si le tableau filtré est le rapport final — renumérotez-le :
print(north.reset_index(drop=True))order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
1 1003 2024-03-02 north bag accessories 2 850.0
2 1005 2024-03-03 north eraser stationery 30 8.0
3 1008 2024-03-04 north bottle accessories 4 120.0drop=True jette les anciennes étiquettes. Sans cette option, elles seraient conservées comme une nouvelle colonne appelée index, ce qui est rarement ce que l'on veut dans un rapport.
Un résultat vide n'est pas une erreur
none = orders[orders["branch"] == "west"]
print(none)
print(none.shape)
print(none.empty)Empty DataFrame
Columns: [order_id, date, branch, product, category, quantity, price]
Index: []
(0, 7)
TrueIl n'y a pas de succursale ouest, donc pas de lignes — et pas d'erreur. C'est le comportement correct (zéro est une réponse valide), mais cela signifie qu'un filtre mal orthographié échoue silencieusement. Les sommes sur un résultat vide valent 0 et les moyennes valent nan, ce qui peut se propager loin dans un rapport avant que quiconque ne le remarque. Quand un filtre alimente quelque chose d'important, vérifiez result.empty ou le nombre de lignes, et dites-le clairement dans la sortie : "No orders from the west branch." vaut mieux qu'un tableau vide.
Modifier les lignes filtrées : .loc[mask, column] = value
Il arrive que l'on filtre pour modifier quelque chose. Les stylos vendus par la succursale est avaient un prix erroné ; ils auraient dû coûter 13.5 et non 15.0. Le code qui vient naturellement à l'esprit fait d'abord le filtre puis sélectionne la colonne — deux paires de crochets, comme lorsque vous lisiez des valeurs précédemment :
import pandas as pd
orders = pd.read_csv("orders.csv")
orders[orders["branch"] == "east"]["price"] = 13.5
print(orders["price"].tolist())[15.0, 60.0, 850.0, 120.0, 8.0, 850.0, 15.0, 120.0]
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment.
Such chained assignment never works to update the original DataFrame or Series, because the intermediate object on which we are setting values always behaves as a copy (due to Copy-on-Write).
Try using '.loc[row_indexer, col_indexer] = value' instead, to perform the assignment in a single step.
See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.html#chained-assignmentLisez les prix : rien n'a changé. Et pandas vous a prévenu, longuement. (Dans un terminal, l'avertissement va vers stderr, donc il apparaît au-dessus de la liste, au moment où la ligne d'affectation s'exécute, et commence par le nom du fichier et le numéro de ligne. Il est affiché ici après la sortie, sans eux.) C'est une affectation chaînée — deux étapes d'indexation, orders[mask] puis ["price"], avec l'affectation à la fin de la chaîne. La première étape, orders[mask], construit un nouveau tableau contenant les lignes de la succursale est. La deuxième étape définit les prix dans ce nouveau tableau. Puis le nouveau tableau est jeté, car rien ne le retient. orders lui-même n'a jamais été touché.
Dans pandas 3, c'est un comportement garanti, appelé Copy-on-Write : tout tableau obtenu par indexation se comporte comme sa propre copie, de sorte qu'y écrire ne peut jamais affecter l'original. La ChainedAssignmentError est un avertissement, pas une exception — le programme continue — et c'est précisément pourquoi c'est dangereux : un script contenant cette ligne se termine « avec succès » avec les anciens prix toujours en place.
(Si vous lisez d'anciens tutoriels, vous verrez cette situation décrite avec un SettingWithCopyWarning et une note indiquant que cela « peut fonctionner ou non ». Dans pandas 3, la réponse est simple : ça ne fonctionne jamais, alors ne l'écrivez jamais.)
La correction est le conseil de l'avertissement lui-même : une seule étape d'indexation, .loc[rows, column], avec l'affectation dessus :
orders.loc[orders["branch"] == "east", "price"] = 13.5
print(orders["price"].tolist())[15.0, 60.0, 850.0, 13.5, 8.0, 850.0, 13.5, 120.0]Attendez — cela a changé les deux commandes de la succursale est, le stylo et la bouteille. La bouteille n'avait pas de prix erroné. Le code a fait exactement ce qu'il disait ; c'est la question dans le code qui était fausse. C'est l'étape 1 du plan qui revient : « stylos vendus par la succursale est » représente deux conditions, et une seule a été écrite. Recommencez à partir d'une copie fraîche du fichier et écrivez les deux :
import pandas as pd
orders = pd.read_csv("orders.csv")
fix = (orders["branch"] == "east") & (orders["product"] == "pen")
print(fix.sum())
orders.loc[fix, "price"] = 13.5
print(orders.loc[orders["branch"] == "east", ["product", "price"]])1
product price
3 bottle 120.0
6 pen 13.5Deux habitudes sont visibles ici. Le masque reçoit un nom (fix) parce qu'il sert à quelque chose d'important ; et avant d'écrire, fix.sum() indique combien de lignes vont changer — une seule, ce qui correspond à ce que l'on attendait. Comptez avant d'écraser, à chaque fois.
Pour lire,df[mask]["col"]convient. Pour écrire, utilisez toujoursdf.loc[mask, "col"] = value. La forme chaînée ne lève aucune exception, et le changement disparaît silencieusement.
Sauvegarder un tableau filtré pour travailler dessus est correct
Qu'en est-il de ceci ?
import pandas as pd
orders = pd.read_csv("orders.csv")
east = orders[orders["branch"] == "east"]
east["price"] = 0.0
print(east["price"].tolist())
print(orders["price"].tolist())[0.0, 0.0]
[15.0, 60.0, 850.0, 120.0, 8.0, 850.0, 15.0, 120.0]Pas d'avertissement, et c'est correct. Ici vous vouliez créer un tableau séparé : vous l'avez nommé east, et le modifier ne change que east. orders reste intact. Copy-on-Write rend ce comportement prévisible — un tableau obtenu par filtrage est toujours indépendant de l'original. Le bug n'est que dans la forme chaînée, où le tableau intermédiaire n'a pas de nom et où le changement disparaît avec lui.
Un exemple complet
Le responsable veut maintenant un court rapport. report.py :
import pandas as pd
orders = pd.read_csv("orders.csv")
# Check what arrived before asking it anything.
print("Rows, columns:", orders.shape)
print("Branches:", list(orders["branch"].unique()))
print()
# 1. The original question: north-branch orders of more than three items.
north_big = orders.loc[
(orders["branch"] == "north") & (orders["quantity"] > 3),
["order_id", "product", "quantity"],
]
print("North orders over 3 items:")
print(north_big.reset_index(drop=True))
print()
# 2. Counting with masks: no new table needed.
accessories = orders["category"] == "accessories"
print("Accessory orders :", accessories.sum())
print("Share of all orders:", accessories.mean())
print("Accessory items :", orders.loc[accessories, "quantity"].sum())
print()
# 3. Outside north, cheap items (under 100).
cheap_outside = orders[
(orders["branch"] != "north") & (orders["price"] < 100)
]
print("Cheap orders outside north:", cheap_outside["order_id"].tolist())
# 4. A branch we do not have: say so instead of printing an empty table.
west = orders[orders["branch"] == "west"]
if west.empty:
print("No orders from the west branch.")Rows, columns: (8, 7)
Branches: ['north', 'south', 'east']
North orders over 3 items:
order_id product quantity
0 1001 pen 12
1 1005 eraser 30
2 1008 bottle 4
Accessory orders : 4
Share of all orders: 0.5
Accessory items : 14
Cheap orders outside north: [1002, 1007]
No orders from the west branch.Pourquoi c'est écrit ainsi :
- Vérifier d'abord, puis demander.
shapeet la liste des succursales sont affichés avant tout filtre. Si le fichier était arrivé avecNorthen majuscule, la ligne des succursales l'aurait montré avant que le rapport n'affiche un tableau vide. .loc[mask, columns]pour la question principale. Les lignes et les colonnes de la réponse sont décidées en un seul endroit, etreset_index(drop=True)renumérote le résultat parce que c'est le rapport final, pas une étape intermédiaire.- Un masque nommé, utilisé trois fois.
accessoriesest calculé une fois, puis compté (sum), mesuré (mean) et utilisé pour sélectionner une colonne. Nommer un masque permet d'éviter d'écrire trois fois la même condition en risquant qu'une d'elles soit légèrement différente. - Chaque condition entre ses propres parenthèses, même dans les cas courts, pour que l'ajout d'une troisième condition ultérieurement ne puisse pas déclencher l'erreur de priorité.
- Le cas vide est traité explicitement. Zéro ligne est une réponse valide, et le rapport le dit en clair.
Quand ça plante
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all(). Vous avez utilisé and, or ou not entre deux masques — ou un if sur un masque entier. Les mots-clés Python ont besoin d'un seul True/False, et un masque en a un par ligne. Dans un filtre, écrivez &, |, ~. Dans un if, décidez ce que vous voulez dire : if mask.any(): (« au moins une ligne est-elle vraie ? ») ou if mask.all(): (« toutes les lignes sont-elles vraies ? »).
TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool] (ou 'ror_', ou [int64]) Parenthèses manquantes autour d'une condition à côté de & ou |. Python a appliqué & avant >, donc il a essayé un « et » bit à bit entre un nombre et une colonne. Mettez chaque condition entre ses propres parenthèses : (a > 3) & (b < 100).
Le filtre renvoie un tableau vide, sans erreur Votre valeur n'est pas présente dans la colonne exactement comme vous l'avez tapée. Affichez df["col"].unique() — ou mieux, list(df["col"].unique()), qui montre chaque valeur entre guillemets, ce qui rend visible un espace à la fin dans 'north '. Corrigez les données avec df["col"].str.strip(), ou faites une correspondance souple avec .str.contains(..., case=False).
KeyError: 1 (ou un autre nombre) juste après un filtrage Vous avez utilisé .loc[n] sur un tableau filtré comme s'il avait encore les étiquettes 0, 1, 2…. Il conserve les étiquettes d'origine. Utilisez .iloc[n] pour « la n-ième ligne », ou faites d'abord un reset_index(drop=True) si les anciennes étiquettes ne sont plus utiles.
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment. Vous avez écrit df[mask]["col"] = value. Dans pandas 3, cela ne modifie jamais df — l'avertissement vous dit que la ligne n'a rien fait. Écrivez df.loc[mask, "col"] = value.
UserWarning: Boolean Series key will be reindexed to match DataFrame index. Le masque a été construit à partir d'un tableau différent de celui que vous filtrez — typiquement un masque de orders complet utilisé sur le north filtré. Pandas l'aligne par étiquette et ignore silencieusement les étiquettes qu'il ne trouve pas :
import pandas as pd
orders = pd.read_csv("orders.csv")
north = orders[orders["branch"] == "north"]
big = orders["quantity"] > 5
print(north[big])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
UserWarning: Boolean Series key will be reindexed to match DataFrame index.(Dans un terminal, l'avertissement apparaît au-dessus du tableau.) Cela donne par chance une réponse sensée ici, mais uniquement grâce au hasard des étiquettes. Construisez le masque à partir du tableau que vous filtrez : north[north["quantity"] > 5].
Étape 4 sur 6 — Prédire
Vérifiez votre compréhension
Qu'est-ce qui est affiché ?
from io import StringIO
import pandas as pd
# Stands in for orders.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
orders = pd.read_csv(StringIO(RAW))
mask = orders["quantity"] > 5
print(mask.sum(), len(orders[mask]))- A5 5
- B4 8
- C4 4
- DTrue 4
Le but est de conserver les commandes de la succursale nord portant sur plus de trois articles. Que se passe-t-il lors de l'exécution ?
from io import StringIO
import pandas as pd
# Stands in for orders.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
orders = pd.read_csv(StringIO(RAW))
north_big = orders[orders["branch"] == "north" & orders["quantity"] > 3]- AUne `ValueError: The truth value of a Series is ambiguous`
- BIl renvoie un tableau vide
- CLe code fonctionne et conserve les commandes 1001, 1005 et 1008
- DUne `TypeError` : sans parenthèses, `&` s'applique avant `==` et `>`
Vous devez réduire de moitié le prix de chaque sac dans orders lui-même. Quelle ligne permet de le faire ?
- Aorders[orders["product"] == "bag"]["price"] = orders["price"] / 2
- Bbags = orders[orders["product"] == "bag"] bags["price"] = bags["price"] / 2
- Corders["price"][orders["product"] == "bag"] /= 2
- Dorders.loc[orders["product"] == "bag", "price"] = orders["price"] / 2
Répondre nécessite un compte
Connectez-vous pour vérifier vos réponses
Les questions sont ci-dessus, et les résoudre de tête est ce qui compte vraiment. Connectez-vous pour voir les réponses, les explications et les indices à trois niveaux.
À vous de jouer
Utilisez le même fichier orders.csv. Écrivez filters.py qui répond à quatre questions, chacune affichée sous un court titre :
- Combien de commandes portaient sur 10 articles ou plus ? Affichez un nombre, pas un tableau.
- Commandes d'accessoires hors de la succursale nord — uniquement
order_id,branch,productetprice, renumérotées à partir de 0. - Commandes des succursales sud ou est dont le prix est inférieur à 100. Utilisez
isinpour les succursales. - Le siège accorde une remise de 10 % sur chaque commande de papeterie de 20 articles ou plus. Définissez le
pricede ces lignes à 90 % de leur prix actuel, mais affichez d'abord combien de lignes vous allez modifier. Puis affichezorder_id,quantityetpricepour toutes les commandes de papeterie.
Avant d'écrire la moindre ligne de code, planifiez chaque question sur papier : quelles colonnes, quels tests, reliés par quoi, et quelles lignes vous attendez en regardant le fichier. Quand votre programme est correct, il affiche exactement ceci :
1. Orders of 10+ items:
3
2. Accessory orders outside north:
order_id branch product price
0 1004 east bottle 120.0
1 1006 south bag 850.0
3. South or east, price under 100:
order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
6 1007 2024-03-04 east pen stationery 20 15.0
4. Stationery discount:
Rows to change: 2
order_id quantity price
0 1001 12 15.0
1 1002 5 60.0
4 1005 30 7.2
6 1007 20 13.5Cassez-le ensuite exprès, une fois pour chaque cas :
- Supprimez les parenthèses dans la question 3. Quelle erreur apparaît ?
- Écrivez la question 4 sous la forme
orders[mask]["price"] = .... Que pandas affiche-t-il, et un prix a-t-il changé ? - Remplacez
"east"par"East"dans la question 3. Que se passe-t-il, et pourquoi est-ce pire qu'une erreur ?
Solution
Planifiez d'abord. Chaque question, décomposée :
quantity >= 10, une condition. À l'œil : 12, 30 et 20, donc 3.category == "accessories"etbranch != "north". À l'œil : 1004 (est, bouteille) et 1006 (sud, sac).branchdans sud/est etprice < 100. À l'œil : 1002 (carnet, 60.0) et 1007 (stylo, 15.0).category == "stationery"etquantity >= 20. À l'œil : 1005 (gomme, 30) et 1007 (stylo, 20), donc 2 lignes.
La question 4 demande de la rigueur : « 20 articles ou plus » correspond à >= 20, donc la commande 1007, avec exactement 20 articles, est incluse. Et elle écrit, donc elle utilise .loc[mask, "price"] avec un masque nommé, compté d'abord.
filters.py :
import pandas as pd
orders = pd.read_csv("orders.csv")
print("1. Orders of 10+ items:")
print((orders["quantity"] >= 10).sum())
print()
print("2. Accessory orders outside north:")
outside = orders.loc[
(orders["category"] == "accessories") & (orders["branch"] != "north"),
["order_id", "branch", "product", "price"],
]
print(outside.reset_index(drop=True))
print()
print("3. South or east, price under 100:")
cheap = orders[
(orders["branch"].isin(["south", "east"])) & (orders["price"] < 100)
]
print(cheap)
print()
print("4. Stationery discount:")
discount = (orders["category"] == "stationery") & (orders["quantity"] >= 20)
print("Rows to change:", discount.sum())
orders.loc[discount, "price"] = orders.loc[discount, "price"] * 0.9
print(orders.loc[orders["category"] == "stationery", ["order_id", "quantity", "price"]])1. Orders of 10+ items:
3
2. Accessory orders outside north:
order_id branch product price
0 1004 east bottle 120.0
1 1006 south bag 850.0
3. South or east, price under 100:
order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
6 1007 2024-03-04 east pen stationery 20 15.0
4. Stationery discount:
Rows to change: 2
order_id quantity price
0 1001 12 15.0
1 1002 5 60.0
4 1005 30 7.2
6 1007 20 13.5Chaque réponse correspond au plan. Notes sur les choix :
- La question 1 utilise
sum()sur un masque, paslen(orders[...]). Les deux donnent 3, mais le masque dit « compter » directement et ne construit aucun tableau. - La question 2 utilise
!=pour « hors de la succursale nord ».~(orders["branch"] == "north")signifie la même chose ;!=est plus simple pour une seule valeur. - La question 3 conserve les étiquettes d'origine
1et6, car la tâche ne demandait pas de renuméroter. Ces étiquettes indiquent de quelles lignes deordersil s'agissait. - La question 4 compte avant d'écrire, et la nouvelle valeur est calculée à partir des mêmes lignes dans le membre de droite :
orders.loc[discount, "price"] * 0.9. Les deux côtés utilisent le même masque, donc chaque ligne reçoit 90 % de son propre prix : 8.0 devient 7.2, 15.0 devient 13.5. La commande de stylos1001(12 articles) et le carnet (5 articles) conservent leurs prix.
Et les trois casses volontaires :
- Sans parenthèses, la question 3 lève
TypeError: Cannot perform 'rand_' …, parce que&a été appliqué avant<. orders[discount]["price"] = …affiche l'avertissementChainedAssignmentError, le script continue, et le tableau final affiche toujours les anciens prix8.0et15.0. La ligne n'a rien fait.- Avec
"East", la question 3 perd silencieusement le stylo de la succursale est et affiche une ligne au lieu de deux. Pas d'erreur, pas d'avertissement : juste une mauvaise réponse qui semble juste. C'est pourquoi le plan note les lignes attendues avant que le code ne s'exécute. La seule chose qui détecte ce bug est une personne qui savait déjà que la réponse devrait comporter deux lignes.
Étape 6 sur 6
Défi — le quiz du chapitre
Dix questions, de la plus facile à la plus difficile. Les dernières sont difficiles exprès.
Connectez-vous pour faire le quiz