Trier des lignes — et décider du sort des ex æquo
Mettre des lignes en ordre avec sort_values : une ou plusieurs colonnes, chacune dans sa propre direction, valeurs manquantes et texte qui ressemble à des nombres — plus nlargest pour le « top N », et pourquoi une égalité au seuil de coupure doit être une décision, pas un accident.
Comment voulez-vous apprendre ?
Le problème que nous résolvons
Le propriétaire de la boutique vous envoie un court message : « Quelles ont été nos plus grosses commandes ce mois-ci ? Envoie-moi les trois premières. »
Les données proviennent du fichier habituel orders.csv — huit commandes issues des trois succursales de la boutique : north, south et east. Avec huit lignes, vous pourriez répondre à l'œil nu. Parcourez la colonne quantity du regard, repérez le 30, puis le 20, puis le 12. C'est fait.
Imaginez maintenant le vrai fichier : quatre mille commandes. Parcourir une colonne du regard pour trouver les trois plus grands nombres n'est plus une méthode, c'est un vœu pieux. Et le message suivant du propriétaire est déjà en route : « Peux-tu m'envoyer la liste regroupée par succursale, avec la plus grosse commande en premier dans chaque succursale ? Et les commandes les plus récentes en haut, s'il te plaît. »
Chacune de ces demandes correspond à la même opération : mettre les lignes dans un ordre utile, puis lire à partir du haut. C'est ce qu'on appelle trier, et dans pandas, cela se fait avec une seule méthode : sort_values().
Mais la méthode est la partie facile. Les véritables questions se situent autour, et c'est là que les rapports déraillent :
- « La plus grosse » selon quel critère ? La quantité, le prix, ou le montant que la commande a rapporté ? Chacun donne un top 3 différent.
- Quand deux commandes sont ex æquo, laquelle arrive en premier — et la réponse change-t-elle à chaque exécution du code ?
- Qu'advient-il d'une commande dont la quantité est vide ?
- Si une colonne de nombres a été lue sous forme de texte,
"9"se trie après"30", et rien ne vous avertit.
Ce chapitre enseigne la méthode et, plus important encore, ces décisions.
À la fin de ce chapitre, vous saurez
- Trier un tableau selon une ou plusieurs colonnes avec
sort_values(), chacune dans sa propre direction, et conserver le résultat au lieu de le perdre - Expliquer pourquoi les étiquettes de l'index se déplacent avec leurs lignes, et choisir entre les conserver, utiliser
sort_index()oureset_index(drop=True) - Départager délibérément les ex æquo, et décider où placer les valeurs manquantes avec
na_position - Répondre aux questions de type « top N » avec
head(),nlargest()etnsmallest()— y compris le « top N d'un sous-ensemble » et les ex æquo au seuil de coupure - Trier du texte sans tenir compte de la casse avec
key=, et reconnaître les nombres stockés sous forme de texte d'après leur façon de se trier
Prérequis : Données manquantes — trouver, supprimer et remplir les valeurs manquantes.
Créer le fichier d'abord
Chaque exemple de ce chapitre lit orders.csv, le même fichier utilisé depuis le chapitre quatre. Si vous ne l'avez pas encore, créez-le dans le dossier de votre projet avec exactement ces 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.0Une section ultérieure utilise un second fichier, orders_gaps.csv. Il s'agit du même tableau avec deux quantités laissées vides — les commandes 1002 et 1007 — le genre de manque que le chapitre six vous a appris à repérer :
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,,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,,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0Avant d'écrire le code
Trier semble trop simple pour nécessiter un plan. Ce n'est pas le cas, car un tri n'échoue jamais — il produit toujours un ordre. Si vous avez trié selon le mauvais critère, le résultat paraît tout aussi net et ordonné que le bon. La réflexion doit donc avoir lieu avant d'écrire le code.
1. Écrivez la question en une phrase, en y incluant la colonne. « Les plus grosses commandes » n'est pas une question à laquelle pandas peut répondre. « Les trois commandes ayant la plus grande quantity » en est une. Tout comme « les trois commandes ayant le price le plus élevé ». Elles donnent des réponses différentes — le sac à 850 est la ligne la plus chère, mais seuls deux sacs ont été vendus. Déterminez ce que veut le propriétaire. Si vous ne pouvez pas le deviner, demandez-lui ; cela ne coûte qu'un message, alors qu'un mauvais top 3 coûte la confiance.
2. Examinez les données d'entrée avant de les trier. Trois éléments comptent pour un tri : le nombre de lignes, les types des colonnes selon lesquelles vous allez trier, et si ces colonnes comportent des manques.
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(orders.dtypes)
print(orders["quantity"].isna().sum())(8, 7)
order_id int64
date str
branch str
product str
category str
quantity int64
price float64
dtype: object
0Huit lignes, sept colonnes. quantity est de type int64 et price de type float64, ils seront donc triés comme des nombres. branch et product sont de type str — du texte, qui se trie par ordre alphabétique. date est également de type str. Ce n'est pas un problème ici, et la raison mérite d'être connue : les dates écrites sous la forme YYYY-MM-DD se trient correctement en tant que texte, car la partie la plus significative vient en premier. Des dates écrites sous la forme 4/3/2024 ne le feraient pas. Et quantity ne comporte aucune case vide, nous n'avons donc pas besoin de décider où vont les valeurs manquantes — pour l'instant.
3. Esquissez le résultat attendu. Pour « les trois premières selon la quantité », la réponse doit comporter trois lignes, la plus grande en premier : 30 (gomme, commande 1005), puis 20 (stylo, 1007), puis 12 (stylo, 1001). Noter cela avant d'exécuter quoi que ce soit vous donne un point de repère pour vérifier le code.
4. Déterminez les détails nécessaires au tri.
- Clé — quelle colonne, ou quelles colonnes, décident de l'ordre ? Cela correspond à
by=. - Direction — du plus petit au plus grand ou du plus grand au plus petit, pour chaque clé ? Cela correspond à
ascending=. - Ex æquo — si deux lignes ont la même valeur, laquelle passe en premier ? Cela correspond à une seconde clé, ou à
kind="stable". - Manques — les valeurs manquantes doivent-elles aller au début ou à la fin ? Cela correspond à
na_position=. - Combien — toutes les lignes, ou seulement les N premières ? Cela correspond à
.head(n), ou ànlargest(). - Index — les anciennes étiquettes de lignes ont-elles encore un sens après coup ? Conservez-les, ou supprimez-les avec
reset_index(drop=True).
5. Choisissez le bon outil.
- Toutes les lignes, dans l'ordre d'une ou plusieurs colonnes :
sort_values() - Seulement les N premières ou dernières selon une colonne numérique :
nlargest()/nsmallest() - Les lignes remises dans l'ordre de l'index :
sort_index() - Les N premières au sein d'un sous-ensemble (« dans la succursale nord ») : filtrer d'abord, puis trier, puis
head()
6. Sachez comment vous allez vérifier. Après un tri, les premières lignes doivent correspondre à votre esquisse, shape doit rester inchangé (un tri n'ajoute ni ne supprime jamais de lignes), et pour une clé numérique, la colonne doit être monotone tout au long de la page.
sort_values() — une colonne
Pour que les affichages restent étroits, les exemples ne montrent que cinq des sept colonnes. cols est simplement une liste de noms, utilisée avec la sélection à doubles crochets vue au chapitre quatre.
cols = ["order_id", "branch", "product", "quantity", "price"]
by_qty = orders.sort_values("quantity")
print(by_qty[cols])order_id branch product quantity price
5 1006 south bag 1 850.0
2 1003 north bag 2 850.0
7 1008 north bottle 4 120.0
1 1002 south notebook 5 60.0
3 1004 east bottle 7 120.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0
4 1005 north eraser 30 8.0Par défaut, l'ordre est croissant — le plus petit en premier. Lisez la colonne quantity de haut en bas : 1, 2, 4, 5, 7, 12, 20, 30. C'est la vérification prévue par le plan : une clé numérique ne doit faire qu'augmenter.
Regardez maintenant le bord gauche. L'index n'est plus 0, 1, 2, …. On y lit 5, 2, 7, 1, 3, 0, 6, 4.
Pourquoi l'index suit la ligne
Pandas n'a pas renuméroté les lignes, et c'est délibéré. Une étiquette d'index est le nom de la ligne, pas sa position. L'étiquette 5 correspondait à la commande 1006 avant le tri et correspond toujours à la commande 1006 après. Si le tri renumérotait les lignes, chaque étiquette pointerait soudainement vers une commande différente, et tout ce que vous auriez noté à propos de « la ligne 5 » deviendrait insidieusement faux.
Ainsi, après un tri, il existe deux manières différentes de désigner « la première ligne », et elles ne coïncident plus :
print(by_qty.iloc[0]["order_id"])
print(by_qty.loc[0]["order_id"])1006
1001iloc[0] désigne la position zéro — la première ligne sur la page, la commande 1006. loc[0] désigne la ligne étiquetée 0 — la commande 1001, qui se trouve désormais en sixième position sur la page. Le chapitre quatre a établi cette distinction ; le tri est l'opération qui lui donne toute son importance. Lorsque vous voulez « la première ligne après le tri », utilisez iloc ou head(), qui comptent tous deux des positions.
Le plus grand en premier : ascending=False
print(orders.sort_values("quantity", ascending=False)[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0
3 1004 east bottle 7 120.0
1 1002 south notebook 5 60.0
7 1008 north bottle 4 120.0
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0Les trois premières sont 1005, 1007 et 1001 — exactement l'esquisse de notre plan. Pour n'envoyer que ces trois-là, ajoutez head(3) :
top3 = orders.sort_values("quantity", ascending=False).head(3)
print(top3[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0Lire la chaîne de gauche à droite reproduit fidèlement le plan : prenez orders, placez la plus grande quantité en premier, conservez les trois premières. Ce motif « trier, puis head » répond à la plupart des questions de type « top N ».
Le texte se trie par ordre alphabétique
Une colonne de type str se trie dans l'ordre du dictionnaire :
print(orders.sort_values("product")[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0
7 1008 north bottle 4 120.0
4 1005 north eraser 30 8.0
1 1002 south notebook 5 60.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0bag apparaît avant bottle parce que a vient avant o. Deux sacs, deux bouteilles et deux stylos — ce sont des ex æquo, et nous reviendrons sur la question de savoir lequel d'entre eux passe en premier.
Un tri renvoie un nouveau tableau
C'est l'erreur que presque tout le monde commet au moins une fois, et elle ne lève aucune erreur :
orders.sort_values("quantity", ascending=False)
print(orders[cols].head(3))order_id branch product quantity price
0 1001 north pen 12 15.0
1 1002 south notebook 5 60.0
2 1003 north bag 2 850.0Rien n'a été trié. sort_values() ne modifie pas orders ; il construit et renvoie un nouveau DataFrame dans le nouvel ordre. La première ligne a créé ce tableau trié et, comme personne ne l'a conservé, l'a jeté. orders est resté exactement tel que read_csv l'a produit.
Pourquoi pandas fonctionne-t-il ainsi ? Parce que l'ordre d'origine est lui aussi une information. Dans orders.csv, c'est l'ordre dans lequel les commandes sont arrivées. Si chaque tri réorganisait votre unique copie, vous ne pourriez jamais revenir à « l'état initial du fichier », et un tri écrit pour un rapport modifierait silencieusement les données d'entrée du suivant. Le fait de renvoyer un nouvel objet garantit que l'on peut toujours tester un tri en toute sécurité.
sort_values() ne modifie jamais le tableau sur lequel vous l'appelez. Si vous n'assignez pas le résultat, le tri a bien eu lieu puis a été jeté — et rien ne vous avertit.Vous devez donc conserver le résultat, soit sous un nouveau nom, soit sous le même nom :
by_qty_desc = orders.sort_values("quantity", ascending=False) # keep both
print(by_qty_desc["order_id"].head(3).tolist())
print(orders["order_id"].head(3).tolist())[1005, 1007, 1001]
[1001, 1002, 1003]La copie triée place les grosses commandes en tête ; l'original reste intact. Écrire orders = orders.sort_values(...) convient également lorsque vous êtes certain de ne plus avoir besoin de l'ancien ordre.
Vous rencontrerez inplace=True dans du code plus ancien. Il modifie le tableau sur place et renvoie None, ce qui provoque son propre bug (voir Quand ça plante). Avec le Copy-on-Write de pandas 3, inplace=True ne permet pas non plus d'économiser de la mémoire. Assigner le résultat est plus clair : vous voyez directement sur la ligne où va le tableau trié.
Plusieurs colonnes : priorité et direction
La deuxième demande du propriétaire : « regroupé par succursale, avec la plus grosse en premier dans chaque succursale. » Cela représente deux clés, et l'ordre de la liste correspond à l'ordre de priorité :
by_branch = orders.sort_values(["branch", "quantity"], ascending=[True, False])
print(by_branch[cols])order_id branch product quantity price
6 1007 east pen 20 15.0
3 1004 east bottle 7 120.0
4 1005 north eraser 30 8.0
0 1001 north pen 12 15.0
7 1008 north bottle 4 120.0
2 1003 north bag 2 850.0
1 1002 south notebook 5 60.0
5 1006 south bag 1 850.0Lisez-le de la manière dont pandas l'a construit. D'abord, chaque ligne est positionnée par branch, de A à Z : east, north, south. La deuxième clé, quantity, n'est consultée qu'en cas d'égalité sur la première clé — au sein d'east, au sein de north, au sein de south. Au sein de north, on trouve 30, 12, 4, 2 : la plus grande en premier, car le second élément de ascending vaut False.
ascending prend un booléen True/False par clé, dans le même ordre que by. Un simple False s'appliquerait aux deux clés et placerait south en premier.
Inverser la liste modifie complètement la question :
print(orders.sort_values(["quantity", "branch"], ascending=[False, True])[cols].head(4))order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0
3 1004 east bottle 7 120.0Désormais, c'est la quantité qui fait la loi, et branch n'interviendrait que si deux commandes avaient la même quantité — ce qui n'est le cas d'aucune. Les succursales se retrouvent éparpillées. La question à se poser avant d'écrire la liste est donc : « selon quoi le rapport est-il organisé ? » C'est cette colonne qui doit venir en premier.
Ex æquo : faire de l'ordre un choix délibéré, pas un accident
Triez par price, du plus cher au moins cher :
print(orders.sort_values("price", ascending=False)[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
7 1008 north bottle 4 120.0
3 1004 east bottle 7 120.0
1 1002 south notebook 5 60.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0
4 1005 north eraser 30 8.0Regardez les deux bouteilles à 120. Dans le fichier, la commande 1004 vient avant 1008. Dans le tableau trié, 1008 vient en premier. Rien n'est cassé : pour une seule clé, l'algorithme de tri par défaut de pandas (quicksort) ne garantit pas de conserver les lignes ex æquo dans leur ordre d'origine, et ici il ne l'a pas fait. Les deux sacs à 850 sont restés dans l'ordre du fichier, les deux bouteilles ont permuté — aucune règle n'aurait permis de le prédire.
La plupart du temps, cela n'a pas d'importance. Cela commence à en avoir dès l'instant où vous tronquez la liste. Si le rapport demande « les trois commandes les plus chères », la troisième place est une égalité entre 1004 et 1008, et celle qui figure dans la liste a été choisie par l'algorithme, pas par vous. Exécutez le même code sur un fichier légèrement différent et le résultat peut changer.
Il existe deux manières de reprendre le contrôle de cette décision.
Option 1 : un tri stable. kind="stable" garantit que les lignes ex æquo conservent l'ordre qu'elles avaient avant le tri — ici, l'ordre du fichier :
stable = orders.sort_values("price", ascending=False, kind="stable")
print(stable["order_id"].tolist())[1003, 1006, 1004, 1008, 1002, 1001, 1007, 1005]Option 2 (généralement meilleure) : un critère explicite de départage. Indiquez ce qui doit l'emporter en cas d'égalité. Pour un rapport de ventes, « si le prix est identique, la plus grosse commande en premier » est une règle pleine de bon sens :
tie_broken = orders.sort_values(["price", "quantity"], ascending=[False, False])
print(tie_broken[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0
7 1008 north bottle 4 120.0
1 1002 south notebook 5 60.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0
4 1005 north eraser 30 8.0Regardez à nouveau les bouteilles : 1004 se trouve au-dessus de 1008, parce que 7 l'emporte sur 4. Et les deux stylos à 15 : auparavant, 1001 passait en premier ; désormais 1007 l'emporte, parce que 20 est supérieur à 12 — et il l'emportera à chaque fois, sur n'importe quelle machine, quel que soit l'ordre dans lequel le fichier arrive. Un critère de départage transforme un accident en une règle que vous pouvez expliquer au propriétaire. (Lorsque vous triez selon plusieurs colonnes, le tri de pandas est de toute façon stable, une liste de clés ne mélange donc jamais les véritables ex æquo non plus.)
Si vous n'avez pas de règle métier particulière, order_id constitue une excellente dernière clé : elle est unique, donc après elle, il ne reste plus la moindre égalité.
Valeurs manquantes : na_position
Lisons la version du fichier comportant deux quantités vides :
gaps = pd.read_csv("orders_gaps.csv")
print(gaps["quantity"].isna().sum())
print(gaps.sort_values("quantity", ascending=False)[cols])2
order_id branch product quantity price
4 1005 north eraser 30.0 8.0
0 1001 north pen 12.0 15.0
3 1004 east bottle 7.0 120.0
7 1008 north bottle 4.0 120.0
2 1003 north bag 2.0 850.0
5 1006 south bag 1.0 850.0
1 1002 south notebook NaN 60.0
6 1007 east pen NaN 15.0Deux choses sont à remarquer. Premièrement, quantity s'affiche désormais sous la forme 30.0, 12.0 — les cases vides ont transformé la colonne en float64, comme l'a expliqué le chapitre six, parce que NaN est un flottant. Deuxièmement, les lignes avec NaN sont allées tout en bas, alors même que nous avions demandé la plus grande en premier. C'est le comportement par défaut, na_position="last", et il s'applique dans les deux sens : une valeur manquante n'est ni « petite » ni « grande », elle est inconnue, pandas la met donc de côté plutôt que de deviner.
Remarquez ce que cela a fait au top 3. La commande 1007 a réellement vendu 20 stylos, mais sa quantité est vide dans ce fichier, elle a donc été éjectée du top 3 et 1004 a pris sa place. Un tri ne peut pas réparer des données manquantes ; il peut seulement vous montrer où sont allés les manques. C'est pourquoi l'étape 2 du plan compte les manques avant de trier.
Lorsque vous procédez à un audit des données, vous souhaitez souvent l'inverse — les problèmes tout en haut, là où vous les verrez :
print(gaps.sort_values("quantity", na_position="first")[cols].head(3))order_id branch product quantity price
1 1002 south notebook NaN 60.0
6 1007 east pen NaN 15.0
5 1006 south bag 1.0 850.0na_position n'est donc pas un simple choix de mise en forme. "last" convient pour un rapport, où les manques ne doivent pas évincer les vraies valeurs. "first" convient pour une vérification, où les manques sont précisément ce que vous recherchez.
Retour au point de départ : sort_index() et reset_index()
Comme chaque ligne a conservé son étiquette, l'ordre d'origine n'est jamais perdu. Trier par l'index permet de le rétablir :
restored = by_qty.sort_index()
print(restored["order_id"].tolist())[1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008]sort_index() accepte également ascending=False, ce qui permet d'afficher un tableau avec « l'étiquette la plus récente en premier ».
Parfois, les anciennes étiquettes n'ont plus aucune signification. Si vous effectuez un tri pour produire un classement et le transmettre, les étiquettes 4, 6, 0 sur le côté ne font que semer la confusion. reset_index(drop=True) les supprime et numérote les lignes 0, 1, 2, … dans leur nouvel ordre :
ranked = orders.sort_values("quantity", ascending=False).reset_index(drop=True)
print(ranked[cols].head(3))
print(ranked.loc[0, "order_id"])order_id branch product quantity price
0 1005 north eraser 30 8.0
1 1007 east pen 20 15.0
2 1001 north pen 12 15.0
1005Désormais, loc[0] et iloc[0] sont à nouveau d'accord : tous deux désignent la commande 1005, la plus grande. Sans drop=True, pandas conserverait les anciennes étiquettes dans une nouvelle colonne nommée index — utile si vous souhaitez retracer l'origine des lignes, encombrant si ce n'est pas le cas.
Lequel choisir ? Conservez les étiquettes tant que vous travaillez encore sur les données et que vous pourriez avoir besoin de faire correspondre les lignes avec l'original. Réinitialisez-les lorsque l'ordre lui-même constitue le résultat final — un classement, un top 10.
Le raccourci pour le « top N » : nlargest() et nsmallest()
« Les trois plus grands selon la quantité » est une opération si courante que pandas dispose d'une méthode qui exprime précisément cette intention :
print(orders.nlargest(3, "quantity")[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0Ce sont les mêmes lignes que sort_values("quantity", ascending=False).head(3). Pourquoi la préférer ? La ligne exprime l'intention — « les trois plus grands » — plutôt que le mécanisme. Et sur un grand tableau, c'est plus rapide, car elle n'a pas besoin de trier l'intégralité des lignes simplement pour en retenir trois.
nsmallest() est son pendant exact : les trois lignes les moins chères de la boutique :
print(orders.nsmallest(3, "price")[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0Les ex æquo au seuil de coupure : keep=
Demandez les trois lignes les plus chères et une situation délicate se produit :
print(orders.nlargest(3, "price")[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0La troisième place est à 120 — or deux commandes coûtent 120 : 1004 et 1008. pandas en a renvoyé une seule — avec le comportement par défaut keep="first", celle qui apparaît en premier dans le tableau — et a silencieusement écarté l'autre. Si vous décernez un prix au « top 3 », c'est injuste pour 1008. keep="all" conserve chaque ligne ex æquo au seuil de coupure, même si cela donne plus de N lignes :
print(orders.nlargest(3, "price", keep="all")[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0
7 1008 north bottle 4 120.0Quatre lignes pour un « top 3 ». Ce n'est pas un bug : c'est la réponse honnête lorsque la troisième place est partagée. Que vous souhaitiez ce comportement ou non relève, là encore, d'une décision de planification.
nlargest et nsmallest ne fonctionnent que sur des colonnes numériques. Pour du texte, utilisez sort_values et head.
Top N d'un sous-ensemble : filtrer, puis trier, puis découper
« Quelles ont été les deux plus grosses commandes de la succursale nord ? » Cela combine le chapitre cinq avec celui-ci, et l'ordre des étapes a son importance :
north = orders[orders["branch"] == "north"]
top_north = north.sort_values("quantity", ascending=False).head(2)
print(top_north[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
0 1001 north pen 12 15.0Filtrez d'abord, car la question ne porte que sur la succursale nord. Si vous triiez l'ensemble du tableau et preniez head(2) en premier, vous obtiendriez 1005 et 1007 — or 1007 appartient à la succursale est. Filtrer par la suite ne laisserait plus qu'une seule ligne, et vous présenteriez un « top 2 » ne contenant qu'une seule entrée. La règle : isolez d'abord les lignes sur lesquelles porte la question, classez-les, puis découpez.
Le même enchaînement fonctionne avec nlargest : orders[orders["branch"] == "north"].nlargest(2, "quantity").
Trier du texte sans tenir compte de la casse : key=
Le tri de texte compare les caractères selon leur code, et toute lettre majuscule vient avant n'importe quelle lettre minuscule. Les données saisies par des humains mélangent les deux :
names = pd.DataFrame({"product": ["pen", "Notebook", "bag", "Bottle", "eraser"]})
print(names.sort_values("product"))product
3 Bottle
1 Notebook
2 bag
4 eraser
0 penBottle et Notebook passent devant bag simplement parce qu'ils commencent par une majuscule. Aucun lecteur ne qualifierait cela d'ordre alphabétique.
Vous pourriez corriger les données, mais bien souvent vous ne le devriez pas — cette orthographe est peut-être celle saisie par le client. L'argument key= vous permet de trier selon une version transformée de la colonne tout en conservant les valeurs d'origine. Il accepte une fonction qui reçoit la colonne entière et renvoie la version selon laquelle effectuer le tri :
print(names.sort_values("product", key=lambda col: col.str.lower()))product
2 bag
3 Bottle
4 eraser
1 Notebook
0 penLa copie en minuscules n'est utilisée que pour la comparaison, puis elle est jetée. Le tableau affiche toujours Bottle et Notebook exactement tels qu'ils ont été saisis. (lambda col: col.str.lower() est une fonction d'une ligne : étant donné la colonne col, elle la renvoie en minuscules.)
Quand des nombres sont stockés sous forme de texte
Voici le piège que l'étape de planification sur les dtypes vise à intercepter. Supposons que quantity soit arrivée sous forme de texte — un "N/A" égaré dans le fichier, ou un système qui exporte tout sous forme de chaînes. Pour voir l'effet produit, créons délibérément une telle colonne avec astype :
as_text = orders.astype({"quantity": "str"})
print(as_text["quantity"].dtype)
print(as_text.sort_values("quantity")[["order_id", "quantity"]])str
order_id quantity
5 1006 1
0 1001 12
2 1003 2
6 1007 20
4 1005 30
7 1008 4
1 1002 5
3 1004 71, 12, 2, 20, 30, 4, 5, 7. Aucune erreur, et un ordre qui semble presque correct. Le texte est comparé caractère par caractère : "12" commence par 1, qui est inférieur à 2, donc "12" vient avant "2" — exactement comme « ab » vient avant « b » dans un dictionnaire. Le tri a fait exactement ce qu'on lui a demandé. Mais on lui a demandé la mauvaise chose.
Deux indices le trahissent : quantity est aligné à gauche dans l'affichage, comme pandas affiche le texte, et dtype indique str. La vraie solution consiste à convertir la colonne en nombres (avec pd.to_numeric, vu au chapitre six). Si vous devez impérativement laisser la colonne telle quelle, key= permet de trier selon la valeur numérique à la place :
fixed = as_text.sort_values("quantity", key=pd.to_numeric)
print(fixed["quantity"].tolist())['1', '2', '4', '5', '7', '12', '20', '30']L'ordre est désormais correct — mais les valeurs sont toujours des chaînes, vous ne pourriez donc pas en faire la somme. Préférez convertir la colonne une fois pour toutes, juste après avoir lu le fichier. Ainsi, chaque tri, filtre et somme qui suivra sera correct sans que vous ayez besoin d'y penser.
Un exemple complet
La demande complète du propriétaire, traitée en un seul programme : (1) les trois premières commandes selon le montant rapporté, (2) les commandes les plus récentes en premier, (3) une liste par succursale avec la plus grosse commande en premier dans chacune.
Le « montant rapporté » est la quantité multipliée par le prix. L'ajout de colonnes est abordé en détail plus loin dans le cours ; pour l'instant, une seule ligne suffit — affecter une valeur à un nouveau nom de colonne crée celle-ci.
sorted_report.py :
import pandas as pd
orders = pd.read_csv("orders.csv")
# Check the input before ranking anything
print("Rows, columns:", orders.shape)
print("Blank quantities:", orders["quantity"].isna().sum())
print()
orders["revenue"] = orders["quantity"] * orders["price"]
cols = ["order_id", "date", "branch", "product", "revenue"]
# 1. Top three by revenue, ties broken by the larger quantity, renumbered as a ranking
top3 = (
orders.sort_values(["revenue", "quantity"], ascending=[False, False])
.head(3)
.reset_index(drop=True)
)
print("Top three orders by revenue")
print(top3[cols])
print()
# 2. Newest first; within the same day, the bigger order first
newest = orders.sort_values(["date", "revenue"], ascending=[False, False])
print("Newest first")
print(newest[cols].head(4))
print()
# 3. By branch A-Z, biggest order first inside each branch
by_branch = orders.sort_values(["branch", "revenue"], ascending=[True, False])
print("By branch")
print(by_branch[cols])
print()
print("Still eight rows:", by_branch.shape[0] == orders.shape[0])Rows, columns: (8, 7)
Blank quantities: 0
Top three orders by revenue
order_id date branch product revenue
0 1003 2024-03-02 north bag 1700.0
1 1006 2024-03-03 south bag 850.0
2 1004 2024-03-02 east bottle 840.0
Newest first
order_id date branch product revenue
7 1008 2024-03-04 north bottle 480.0
6 1007 2024-03-04 east pen 300.0
5 1006 2024-03-03 south bag 850.0
4 1005 2024-03-03 north eraser 240.0
By branch
order_id date branch product revenue
3 1004 2024-03-02 east bottle 840.0
6 1007 2024-03-04 east pen 300.0
2 1003 2024-03-02 north bag 1700.0
7 1008 2024-03-04 north bottle 480.0
4 1005 2024-03-03 north eraser 240.0
0 1001 2024-03-01 north pen 180.0
5 1006 2024-03-03 south bag 850.0
1 1002 2024-03-01 south notebook 300.0
Still eight rows: TruePourquoi c'est écrit ainsi :
- La vérification vient en premier.
shapeet le décompte des manques sont affichés avant tout classement. Un top 3 calculé à partir d'un fichier lu à moitié, ou avec des manques repoussés tout en bas, paraîtrait tout aussi assuré. - « La plus grosse » a été convertie en colonne. Classer selon
quantityplacerait les 30 gommes en tête — 240 de chiffre d'affaires. Classer selonrevenueplace les deux sacs valant 1 700 en tête. La première étape du plan — écrire la question en y incluant la colonne — est ce qui détermine la réponse que recevra le propriétaire. - Chaque tri dispose d'un critère de départage. Le 2024-03-04 comporte deux commandes ;
revenuedécide de celle qui est listée en premier, le rapport est donc identique à chaque exécution. - Seul le classement est renuméroté.
top3est une liste finale, ses étiquettes deviennent donc 0, 1, 2.newestetby_branchconservent leurs étiquettes d'origine, de sorte que chaque ligne peut toujours être retracée dans le fichier. orderslui-même n'est jamais retrié. Chaque rapport est un nouveau tableau construit à partir de la même entrée intacte, les trois réponses ne peuvent donc pas interférer entre elles.- La dernière ligne est une assertion peu coûteuse. Un tri ne doit jamais modifier le nombre de lignes. Afficher
Truene coûte rien et permettrait d'intercepter, par exemple, unhead()accidentel resté au mauvais endroit.
Quand ça plante
KeyError: 'Quantity'
orders.sort_values("Quantity")KeyError: 'Quantity'Le nom passé à by= doit correspondre exactement à une colonne — casse et espaces compris. La colonne s'appelle quantity, en minuscules. Affichez list(orders.columns) pour voir les vrais noms, guillemets et espaces superflus inclus, comme le recommandait le chapitre trois.
AttributeError: 'NoneType' object has no attribute 'head'
top = orders.sort_values("quantity", inplace=True)
top.head()AttributeError: 'NoneType' object has no attribute 'head'Avec inplace=True, sort_values modifie orders et renvoie None. Par conséquent, top vaut None, et None ne possède pas de méthode head(). Choisissez un style : soit orders.sort_values("quantity", inplace=True) sur sa propre ligne, soit — mieux — top = orders.sort_values("quantity") sans inplace.
ValueError: Length of ascending (1) != length of by (2)
orders.sort_values(["branch", "quantity"], ascending=[False])ValueError: Length of ascending (1) != length of by (2)Une liste fournie à ascending nécessite un élément par clé. Deux clés, deux booléens : ascending=[True, False]. (Un simple False, hors d'une liste, est autorisé — il s'applique alors à chaque clé.)
ValueError: For argument "ascending" expected type bool, received type str.
orders.sort_values("branch", ascending="False")ValueError: For argument "ascending" expected type bool, received type str."False" entre guillemets est une chaîne de caractères, pas le booléen False. Supprimez les guillemets.
TypeError: Column 'branch' has dtype str, cannot use method 'nlargest' with this dtype
orders.nlargest(3, "branch")TypeError: Column 'branch' has dtype str, cannot use method 'nlargest' with this dtypenlargest et nsmallest ne classent que des nombres. Pour « les trois dernières succursales par ordre alphabétique », utilisez orders.sort_values("branch", ascending=False).head(3). Si vous avez obtenu cette erreur sur une colonne que vous pensiez numérique, le vrai problème est qu'elle a été lue sous forme de texte — vérifiez dtypes.
TypeError: '<' not supported between instances of 'str' and 'int'
mixed = pd.Series([12, "unknown", 5])
mixed.sort_values()TypeError: '<' not supported between instances of 'str' and 'int'Cette colonne contient de vrais nombres et un bout de texte, son dtype est donc object, et Python ne peut pas déterminer si "unknown" est plus grand ou plus petit que 12. Nettoyez la colonne d'abord : pd.to_numeric(mixed, errors="coerce") transforme "unknown" en NaN, après quoi le tri fonctionne et na_position décide où placer le manque.
Étape 4 sur 6 — Prédire
Vérifiez votre compréhension
Deux sacs partagent le prix le plus élevé, 850.0. 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))
top = orders.sort_values("price", ascending=False)
print(list(top.index[:3]))- A[2, 5, 7]
- B[0, 1, 2]
- C[2, 5, 3]
- D[5, 2, 7]
L'objectif est d'afficher la commande contenant le plus d'articles. 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))
orders.sort_values("quantity", ascending=False)
print(orders.iloc[0]["order_id"])- A1001 — `sort_values` a renvoyé une copie triée et personne ne l'a conservée
- B1007
- CUne `TypeError` : le résultat doit être assigné
- D1005
Vous voulez regrouper les commandes par succursale de A à Z, et au sein de chaque succursale, afficher la plus grande quantité en premier. Quelle ligne permet de le faire ?
- Aorders.sort_values("branch").sort_values("quantity", ascending=False)
- Borders.sort_values(["branch", "quantity"], ascending=False)
- Corders.sort_values(["quantity", "branch"], ascending=[False, True])
- Dorders.sort_values(["branch", "quantity"], ascending=[True, False])
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
Un enseignant vous envoie les notes d'un contrôle. Enregistrez ceci sous le nom results.csv. Deux élèves n'ont pas encore de note, et certains prénoms ont été saisis en minuscules :
name,section,marks
Rafi,B,78
anika,A,91
Tanvir,A,
Mitu,B,91
zara,A,65
Karim,B,
Nadia,A,88
Sabbir,B,54Écrivez ranking.py. Avant d'écrire la moindre ligne de code, notez le plan sous forme de commentaires en haut du fichier : pour chaque liste, la colonne clé, la direction, la manière de départager les ex æquo et l'endroit où vont les manques. Faites ensuite en sorte que le programme remplisse ces quatre conditions :
- Avant de trier quoi que ce soit, il affiche
shape,dtypeset le nombre de notes manquantes. - Il affiche un tableau d'honneur : la note la plus élevée en premier, les élèves sans note tout en bas, les ex æquo sur les notes départagés par le nom dans l'ordre alphabétique sans tenir compte de la casse, et les lignes renumérotées 0, 1, 2, …
- Il affiche le top 3 avec
nlargest, en conservant tous les ex æquo à la troisième place. - Il affiche les élèves sans note, triés par nom sans tenir compte de la casse, afin que l'enseignant puisse les relancer — puis le tableau d'origine, pour prouver qu'il n'a jamais été modifié.
Votre programme doit afficher exactement ceci :
(8, 3)
name str
section str
marks float64
dtype: object
Missing marks: 2
Merit list
name section marks
0 anika A 91.0
1 Mitu B 91.0
2 Nadia A 88.0
3 Rafi B 78.0
4 zara A 65.0
5 Sabbir B 54.0
6 Karim B NaN
7 Tanvir A NaN
Top three
name section marks
1 anika A 91.0
3 Mitu B 91.0
6 Nadia A 88.0
No mark yet
name section marks
5 Karim B NaN
2 Tanvir A NaN
Original, unchanged
name section marks
0 Rafi B 78.0
1 anika A 91.0
2 Tanvir A NaN
3 Mitu B 91.0
4 zara A 65.0
5 Karim B NaN
6 Nadia A 88.0
7 Sabbir B 54.0Tentez ensuite deux expériences sur le tableau d'honneur :
- Supprimez
key=. Lequel des deux élèves ayant obtenu 91 arrive en premier désormais, et pourquoi ? - Supprimez complètement
"name"des clés et triez selonmarksuniquement. Qui arrive en premier à 91 — et ce résultat est-il garanti ?
Solution
Le plan. Le fichier compte 8 élèves et 3 colonnes. marks contient des manques, il sera donc lu comme float64, et chaque liste qui effectue un tri selon les notes doit préciser où vont les manques. name mélange majuscules et minuscules, chaque comparaison de noms nécessite donc key=.
- Tableau d'honneur — clés
marks, puisname; notes du plus haut au plus bas, noms de A à Z ; une égalité sur les notes est départagée par le nom ; les manques à la fin (par défaut) ; renuméroté avecreset_index(drop=True). - Top 3 —
nlargest(3, "marks")aveckeep="all", de sorte qu'une troisième place partagée ne soit pas tronquée.nlargestécarte de lui-même les valeurs manquantes. - Pas encore de note — filtrer les lignes où
marksest manquante, comme dans les chapitres précédents, puis trier par nom. Filtrer d'abord, puis trier. - Original — jamais réassigné, ainsi, afficher
resultsà la fin montre l'ordre initial du fichier.
Un détail : une fonction passée à key= est appliquée à chaque colonne clé. Pour le tableau d'honneur, cela s'applique aussi à marks, et .str.lower() échouerait sur des nombres. La fonction passe donc en minuscules uniquement les colonnes textuelles et renvoie n'importe quelle autre colonne inchangée.
ranking.py :
import pandas as pd
# Merit list : marks high->low, ties by name A->Z ignoring case, blanks last, renumber
# Top three : nlargest on marks, keep="all" so a shared third place is not cut
# No mark yet : filter rows with no mark, then name A->Z ignoring case
# Original : never reassigned
results = pd.read_csv("results.csv")
print(results.shape)
print(results.dtypes)
print("Missing marks:", results["marks"].isna().sum())
print()
def lower_text(col):
return col.str.lower() if col.dtype == "str" else col
merit = results.sort_values(
["marks", "name"], ascending=[False, True], key=lower_text
).reset_index(drop=True)
print("Merit list")
print(merit)
print()
print("Top three")
print(results.nlargest(3, "marks", keep="all"))
print()
print("No mark yet")
missing = results[results["marks"].isna()]
print(missing.sort_values("name", key=lower_text))
print()
print("Original, unchanged")
print(results)(8, 3)
name str
section str
marks float64
dtype: object
Missing marks: 2
Merit list
name section marks
0 anika A 91.0
1 Mitu B 91.0
2 Nadia A 88.0
3 Rafi B 78.0
4 zara A 65.0
5 Sabbir B 54.0
6 Karim B NaN
7 Tanvir A NaN
Top three
name section marks
1 anika A 91.0
3 Mitu B 91.0
6 Nadia A 88.0
No mark yet
name section marks
5 Karim B NaN
2 Tanvir A NaN
Original, unchanged
name section marks
0 Rafi B 78.0
1 anika A 91.0
2 Tanvir A NaN
3 Mitu B 91.0
4 zara A 65.0
5 Karim B NaN
6 Nadia A 88.0
7 Sabbir B 54.0Vérifions la sortie par rapport au plan :
marksestfloat64, pasint64, etMissing marks: 2— les manques ont été repérés avant tout classement, le lecteur du tableau d'honneur sait ainsi que deux élèves en sont absents, et non relégués en queue de peloton.- L'égalité à 91 est tranchée par le nom.
anikapasse avantMitucar la comparaison s'est faite sur"anika"et"mitu". - Le top 3 comporte trois lignes ici, car la troisième place (88) n'est pas partagée. Si Nadia avait également eu 91,
keep="all"aurait renvoyé quatre lignes plutôt que d'écarter silencieusement un élève. - La liste des élèves sans note filtre d'abord, puis trie — le même ordre « restreindre d'abord, puis classer » que pour le motif du top N d'un sous-ensemble.
- Le dernier tableau correspond à l'ordre du fichier. Trois listes ont été produites, et
resultsn'a jamais été réassigné.
Les expériences :
no_key = results.sort_values(["marks", "name"], ascending=[False, True])
marks_only = results.sort_values("marks", ascending=False)
print(no_key["name"].head(2).tolist())
print(marks_only["name"].head(2).tolist())['Mitu', 'anika']
['anika', 'Mitu']Sans key=, les noms sont comparés caractère par caractère, et toute majuscule vient avant n'importe quelle minuscule, donc "Mitu" l'emporte sur "anika" — un ordre fondé sur la manière dont le prénom a été saisi, et non sur l'alphabet. Avec marks seul, anika se trouve passer en premier par hasard car elle apparaît plus tôt dans le fichier. C'est la même réponse que dans le tableau d'honneur correct, mais uniquement par accident : avec une seule clé, le tri par défaut ne garantit pas de conserver les lignes ex æquo dans l'ordre du fichier, comme l'ont montré les deux bouteilles à 120 plus haut. Une règle que vous avez formulée est garantie ; un résultat qui a simplement l'air juste ne l'est pas.
É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