Regroupement et agrégation — une réponse par groupe
Transformer les questions « par succursale », « par catégorie » et « par jour » en une seule ligne de groupby : la clé, la valeur et l'agrégation, plusieurs nombres à la fois avec l'agrégation nommée, size face à count, deux clés et le piège de la moyenne de moyennes.
Comment voulez-vous apprendre ?
Le problème que nous résolvons
Le propriétaire de la boutique vous envoie un court message : « Combien d'unités avons-nous vendues dans chaque succursale cette semaine ? »
Vous disposez de orders.csv — huit commandes, une ligne chacune. Avec ce que les quatre derniers chapitres vous ont appris, vous pouvez déjà répondre. Filtrer une succursale, faire la somme de sa colonne, recommencer :
import pandas as pd
orders = pd.read_csv("orders.csv")
for branch in ["north", "south", "east"]:
units = orders[orders["branch"] == branch]["quantity"].sum()
print(branch, units)north 48
south 6
east 27Les chiffres sont corrects. La méthode pose toutefois trois problèmes, et chacun empire à mesure que les données grossissent.
Premièrement, vous deviez connaître les succursales à l'avance. Vous avez saisi la liste vous-même. Lorsque le fichier de la semaine prochaine comportera une commande d'une nouvelle succursale west, cette boucle ne la verra pas — elle n'échouera pas, elle laissera simplement west de côté, et la somme des totaux ne correspondra plus à l'ensemble.
Deuxièmement, chaque nouvelle question impose une nouvelle boucle. « Chiffre d'affaires par catégorie » implique une deuxième boucle sur une deuxième liste saisie à la main. « Unités par succursale et par catégorie » implique une boucle imbriquée dans une autre.
Troisièmement, la réponse n'est pas un tableau. Ce sont des lignes de texte affichées dans la console. Vous ne pouvez pas la trier, la filtrer ni la placer à côté d'une autre réponse sans devoir la reconstruire.
Ce que vous voulez réellement, c'est formuler en une seule ligne : découpe les lignes par succursale, fais la somme de la quantité au sein de chaque morceau, et renvoie-moi les morceaux sous forme de tableau. C'est exactement le rôle de groupby, et c'est ainsi que s'écrit presque chaque synthèse dans pandas — un rapport, un indicateur de tableau de bord, la première réponse à « d'où vient l'argent ? ».
À la fin de ce chapitre, vous saurez
- Décomposer une question comme « unités par succursale » en ses trois parties — clé, colonne de valeur, agrégation — et vérifier la clé avant de regrouper
- Écrire
df.groupby(key)[column].sum()et lire la Series renvoyée, avec la clé comme index - Calculer plusieurs synthèses à la fois avec
.agg()et avec l'agrégation nommée - Distinguer
size()decount(), et décider du sort réservé aux clés manquantes et aux valeurs manquantes - Regrouper par deux colonnes, aplatir ou réorganiser le résultat, et éviter l'erreur de la « moyenne de moyennes »
Prérequis : Trier des lignes.
Le fichier que nous utilisons
Le même fichier orders.csv que dans les chapitres précédents — huit commandes d'une petite boutique. Si vous ne l'avez pas, 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 ligne correspond à une commande : de quelle succursale elle provient, quel produit, sa catégorie, combien d'unités et le prix unitaire.
Avant d'écrire le code
Chaque question avec regroupement possède le même squelette. Avant de taper groupby, décomposez la question.
1. Formulez la question en trois parties
Une question avec regroupement contient toujours trois éléments, et les mots de la phrase vous indiquent qui est qui :
- Clé — la colonne dont les valeurs définissent les groupes. C'est le mot situé après par, selon ou pour chaque. Dans « unités par succursale », c'est
branch. - Valeur — la colonne résumée : la grandeur mesurée. Ici,
quantity. - Agrégation — la manière dont plusieurs valeurs se réduisent à une seule : total, moyenne, combien, plus grand. Ici,
sum.
En voici quelques autres, pour bien ancrer le principe :
- « Chiffre d'affaires par catégorie » — clé
category, valeurrevenue, agrégationsum - « Taille moyenne des commandes dans chaque succursale » — clé
branch, valeurquantity, agrégationmean - « Combien de commandes chaque jour » — clé
date, pas de colonne de valeur (vous comptez les lignes), agrégationsize - « Plus grosse commande individuelle par produit » — clé
product, valeurquantity, agrégationmax
Si vous ne pouvez pas nommer ces trois parties, la question n'est pas encore prête à être traduite en code. « Quelle succursale est la meilleure ? » n'a ni valeur ni agrégation — la meilleure en nombre d'unités ? en chiffre d'affaires ? en nombre de commandes ? Demandez des précisions avant de calculer. Les trois réponses peuvent désigner trois succursales différentes.
2. Examinez les données avant de les regrouper
Le regroupement masque les lignes. Une fois que les huit commandes sont réduites à trois totaux, une ligne corrompue devient invisible au sein d'une somme. Les vérifications se font donc avant :
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: objectDeux points importent ici. La colonne de valeur, quantity, est de type int64 — numérique, donc sum réalisera une addition au lieu de concaténer du texte. La colonne de clé, branch, est de type str, ce qui convient parfaitement pour une clé.
Examinez ensuite les valeurs réelles de la clé. C'est la vérification que l'on oublie souvent, et pourtant c'est la plus importante :
print(orders["branch"].value_counts())
print(orders[["branch", "quantity"]].isna().sum())branch
north 4
south 2
east 2
Name: count, dtype: int64
branch 0
quantity 0
dtype: int64Trois succursales distinctes, aucune valeur manquante dans l'une ou l'autre colonne. groupby crée un groupe par valeur distincte, caractère par caractère — ainsi, "north" et "north " formeraient deux groupes. C'est avec value_counts() que vous l'auriez repéré, avant que cela ne donne un rapport erroné.
Voici ce qui se produit lorsqu'on ignore cette vérification. Trois lignes qui désignent toutes la succursale north — l'une correctement saisie, l'autre avec un espace final, et la dernière avec une majuscule :
messy = pd.DataFrame({"branch": ["north", "north ", "North"], "quantity": [12, 2, 30]})
print(messy.groupby("branch")["quantity"].sum())
messy["branch"] = messy["branch"].str.strip().str.lower()
print(messy.groupby("branch")["quantity"].sum())branch
North 30
north 12
north 2
Name: quantity, dtype: int64
branch
north 44
Name: quantity, dtype: int64Aucune erreur, et pourtant trois « succursales » là où il ne devrait y en avoir qu'une. Le chiffre de chaque groupe est exact au vu de ce qui a été fourni à pandas, et le rapport est pourtant faux. Nettoyer la clé — supprimer les espaces avec strip, passer en minuscules — avant de regrouper permet de les rassembler en un seul groupe avec le véritable total de 44.
3. Esquissez le résultat avant de le produire
Déterminez à quoi la réponse doit ressembler, afin de vous apercevoir immédiatement si ce n'est pas le cas :
branch units
east ?
north ?
south ?Grâce aux vérifications précédentes, vous connaissez déjà trois faits à son sujet : il comportera exactement 3 lignes (une par succursale distincte), les succursales apparaîtront dans l'ordre alphabétique (groupby trie ses clés), et les trois nombres doivent égaler le total de la colonne entière. Cette dernière condition constituera votre preuve de contrôle par la suite.
4. Choisissez l'outil
- Un nombre par groupe, issu d'une seule colonne :
df.groupby(key)[col].sum()(oumean,max, ...) - Plusieurs nombres issus d'une seule colonne :
df.groupby(key)[col].agg(["sum", "mean"]) - Plusieurs nombres issus de plusieurs colonnes, avec vos propres noms :
df.groupby(key).agg(name=(col, "sum"), ...) - Le nombre de lignes de chaque groupe :
df.groupby(key).size() - Une colonne qui n'existe pas encore, comme le chiffre d'affaires : créez-la d'abord, puis regroupez
Passons maintenant au code.
Découper, appliquer, combiner
groupby accomplit trois actions dans l'ordre, et il est utile de retenir ces termes car toute question avec regroupement suit ces trois étapes :
- Découper les lignes en morceaux, un par valeur distincte de clé
- Appliquer un calcul à chaque morceau séparément
- Combiner les résultats en un nouvel objet, dont l'index est la clé
Vous pouvez observer le découpage isolément. Il est possible d'itérer sur un objet groupby dans une boucle, ce qui vous donne chaque clé et son morceau de tableau :
for branch, part in orders.groupby("branch"):
print(branch, part.shape, list(part["order_id"]))east (2, 7) [1004, 1007]
north (4, 7) [1001, 1003, 1005, 1008]
south (2, 7) [1002, 1006]Chaque morceau est un DataFrame ordinaire doté de ses sept colonnes — le morceau north compte quatre lignes, les deux autres en comptent deux. Personne n'a eu à saisir le nom des succursales : pandas les a trouvées directement dans les données. Une commande provenant d'une nouvelle succursale west la semaine prochaine constituerait simplement un quatrième morceau.
Vous n'écrirez que rarement une telle boucle dans du vrai code. Elle est présentée ici pour clarifier le concept : tout ce qui suit revient à « faire quelque chose sur chaque morceau, puis coller les réponses ensemble ».
Un nombre par groupe
La question « unités par succursale » décomposée en ses trois parties — clé branch, valeur quantity, agrégation sum — s'écrit exactement dans cet ordre :
units = orders.groupby("branch")["quantity"].sum()
print(units)branch
east 27
north 48
south 6
Name: quantity, dtype: int64Lisez-le de gauche à droite : groupe par succursale, prends la quantité, fais-en la somme. Les mêmes chiffres que la boucle, en une seule ligne, sans aucune liste de succursales saisie à la main.
Vérifions maintenant par rapport à notre esquisse. Trois lignes — oui. Ordre alphabétique — oui. Et la preuve de contrôle :
print(units.sum(), orders["quantity"].sum())81 81La somme des groupes donne bien le total général, donc aucune ligne n'a été perdue ni comptée deux fois. Prenez l'habitude de cette vérification en une ligne : chaque fois que vous effectuez un regroupement, comparez le total du résultat avec le total des données d'entrée.
Le résultat est une Series, et la clé est son index
print(type(units))
print(units.index)
print(units["north"])<class 'pandas.Series'>
Index(['east', 'north', 'south'], dtype='str', name='branch')
48L'étape de combinaison a placé les succursales dans l'index, et non dans une colonne. C'est pourquoi la sortie affiche branch sur une ligne dédiée au-dessus des valeurs — il s'agit du nom de l'index. Cela signifie aussi que vous accédez à une valeur par le nom de la succursale, units["north"], exactement comme vous l'auriez fait avec .loc au chapitre quatre.
Puisqu'il s'agit d'une Series ordinaire, tout ce qui a été vu dans les chapitres précédents s'y applique. La trier permet de répondre à « quelle succursale a le plus vendu » :
print(units.sort_values(ascending=False))
print(units.idxmax())branch
north 48
east 27
south 6
Name: quantity, dtype: int64
northidxmax() renvoie l'étiquette de la valeur maximale — la succursale, et non le nombre. Pour savoir « qui arrive en tête », c'est généralement ce que l'on cherche.
Pourquoi choisir la colonne en premier
Il est tentant d'omettre ["quantity"] et de laisser pandas tout additionner :
print(orders.groupby("branch").sum())order_id date ... quantity price
branch ...
east 2011 2024-03-022024-03-04 ... 27 135.0
north 4017 2024-03-012024-03-022024-03-032024-03-04 ... 48 993.0
south 2008 2024-03-012024-03-03 ... 6 910.0
[3 rows x 6 columns]Aucune erreur — et pourtant presque chaque chiffre est absurde. Les order_id ont été additionnés, comme si les numéros de commande étaient des montants. Les chaînes date ont été collées bout à bout, car « sommer » du texte signifie le concaténer. Les valeurs de price ont également été sommées : les 993.0 pour north correspondent aux prix de quatre produits différents additionnés, ce qui ne mesure rien.
orders.groupby("branch").sum() ne lève aucune erreur. L'instruction additionne des identifiants, concatène des dates et totalise des prix unitaires, puis renvoie un résultat qui ressemble trait pour trait à un rapport.Seule quantity a un sens ici, et vous auriez pu ne demander qu'elle. C'est pourquoi la règle d'or est : choisissez la colonne de valeur avant l'agrégation. sum applique l'addition selon le type de chaque colonne, et pour la plupart d'entre elles, ce n'est pas une question que l'on se pose.
mean est plus stricte et refuse de s'exécuter plutôt que de générer des absurdités :
print(orders.groupby("branch").mean())TypeError: dtype 'str' does not support operation 'mean'Cette erreur rend visible la même maladresse. Interprétez-la ainsi : vous m'avez demandé de calculer la moyenne d'une colonne de texte. La solution ne consiste pas à ajouter numeric_only=True — cela calculerait encore la moyenne de order_id et de price —, mais à préciser quelle colonne vous visez.
Plusieurs nombres à la fois : .agg()
Une seule synthèse suffit rarement. « Combien chaque succursale a-t-elle vendu, en moyenne par commande, et sur combien de commandes ? » représente trois agrégations d'une même colonne. Transmettez la liste de leurs noms à .agg() :
print(orders.groupby("branch")["quantity"].agg(["sum", "mean", "count"]))sum mean count
branch
east 27 13.5 2
north 48 12.0 4
south 6 3.0 2Le résultat est désormais un DataFrame — une colonne par agrégation, une ligne par succursale. Il raconte une histoire que le simple total masquait : east a vendu un peu plus de la moitié des unités de north, mais sa commande moyenne est plus volumineuse. La succursale north enregistre davantage de commandes ; east en reçoit moins, mais de taille légèrement supérieure.
Les noms figurant dans la liste sont des chaînes de caractères : "sum", "mean", "count", "min", "max", "median", "nunique" (nombre de valeurs distinctes), "first", "last". Ce sont les mêmes méthodes que vous utilisiez déjà sur une colonne individuelle, appliquées ici à chaque morceau.
Agrégation nommée : vos colonnes, vos noms
Les vrais rapports synthétisent des colonnes différentes de manières différentes, et exigent des noms de colonnes lisibles. L'agrégation nommée combine les deux en un seul appel. Chaque paramètre nommé correspond au nom d'une colonne de sortie, et sa valeur est une paire : (input column, aggregation).
Le chiffre d'affaires est l'indicateur évident à intégrer au rapport, or ce n'est pas encore une colonne — c'est la quantité multipliée par le prix sur chaque ligne. La création de colonnes fera l'objet d'un chapitre dédié très bientôt ; pour l'instant, une seule ligne suffit, et elle illustre l'étape 4 du plan en action : si la colonne de valeur n'existe pas encore, créez-la avant de regrouper.
orders["revenue"] = orders["quantity"] * orders["price"]
summary = orders.groupby("branch").agg(
orders=("order_id", "count"),
units=("quantity", "sum"),
revenue=("revenue", "sum"),
)
print(summary)orders units revenue
branch
east 2 27 1140.0
north 4 48 2600.0
south 2 6 1150.0Voilà un rapport que vous pourriez envoyer. Chaque ligne de l'appel à agg se lit comme une phrase : orders est le décompte de order_id, units est la somme de quantity, revenue est la somme de revenue. Vous choisissez vous-même le nom des colonnes de sortie au lieu d'hériter de sum ou count, ce qui évite d'avoir à les renommer par la suite.
Et cela modifie la conclusion précédente. En nombre d'unités, east était nettement deuxième ; en chiffre d'affaires, south passe d'un cheveu devant — un seul sac à 850 pèse plus lourd que vingt stylos. Voilà pourquoi l'étape 1 insistait sur le choix de la valeur : « meilleure succursale » en unités et « meilleure succursale » en chiffre d'affaires sont deux questions différentes.
Le résultat étant un DataFrame, il se trie comme tel :
print(summary.sort_values("revenue", ascending=False))orders units revenue
branch
north 4 48 2600.0
south 2 6 1150.0
east 2 27 1140.0Compter : size() face à count()
« Combien de commandes par succursale » semble être une question unique. Pourtant, pandas propose deux réponses, et elles diffèrent précisément lorsqu'il y a des données manquantes.
size()compte les lignes de chaque groupe, quel que soit leur contenu.count()compte les valeurs non manquantes d'une colonne au sein de chaque groupe.
Sur un fichier propre, les deux concordent. Pour les voir diverger, voici un petit tableau construit manuellement à côté de orders, avec une succursale manquante et une quantité manquante :
gaps = pd.DataFrame(
{
"order_id": [1001, 1002, 1003, 1004, 1005],
"branch": ["north", "south", None, "east", "north"],
"quantity": [12, 5, 2, None, 30],
}
)
print(gaps)order_id branch quantity
0 1001 north 12.0
1 1002 south 5.0
2 1003 NaN 2.0
3 1004 east NaN
4 1005 north 30.0(quantity est devenue un float en raison de la valeur manquante — le chapitre six en a expliqué la raison.)
print(gaps.groupby("branch").size())
print(gaps.groupby("branch")["quantity"].count())branch
east 1
north 2
south 1
dtype: int64
branch
east 0
north 2
south 1
Name: quantity, dtype: int64east compte une commande (size = 1) mais zéro quantité connue (count = 0). Les deux sont vrais ; ils répondent à des questions différentes. Utilisez size() pour « combien de commandes », et count() pour « pour combien de commandes dispose-t-on d'un chiffre réel ».
Deux choses que les données manquantes font en silence
Regardez à nouveau ces sorties. La commande 1003, celle qui n'a pas de succursale, n'apparaît dans aucune des deux. Cinq commandes sont entrées ; quatre seulement sont ressorties :
print(gaps.groupby("branch").size().sum(), len(gaps))4 5Par défaut, groupby ignore les lignes dont la clé est manquante. Il n'existe pas de groupe nommé « inconnu », la ligne disparaît donc de chaque résultat. La vérification de total vue plus tôt intercepte cela immédiatement. Si ces lignes comptent — et une commande sans succursale reste une commande —, demandez-les avec dropna=False :
print(gaps.groupby("branch", dropna=False)["quantity"].sum())branch
east 0.0
north 42.0
south 5.0
NaN 2.0
Name: quantity, dtype: float64Désormais, les quatre groupes sont présents, et le groupe NaN recueille les 2 unités qu'on ne savait pas où affecter.
Le second comportement silencieux apparaît dans cette même sortie : east indique 0.0. La succursale east n'en a pas vendu zéro ; sa seule quantité est inconnue. sum interprète « rien à additionner » comme un zéro. Si une valeur « inconnue » doit rester inconnue, précisez-le avec min_count=1 — « ne calcule une somme que si au moins une valeur réelle existe » :
print(gaps.groupby("branch", dropna=False)["quantity"].sum(min_count=1))branch
east NaN
north 42.0
south 5.0
NaN 2.0
Name: quantity, dtype: float64La bonne option dépend de votre besoin, et c'est tout l'intérêt : tranchez la question dès l'étape de planification, et non par accident. De toute façon, la solution à ces deux problèmes intervient généralement plus en amont — nettoyez les valeurs manquantes (chapitre six) avant de regrouper.
Deux clés : des groupes dans des groupes
« Chiffre d'affaires par succursale et par catégorie » comporte deux clés. Passez une liste :
by_both = orders.groupby(["branch", "category"])["revenue"].sum()
print(by_both)branch category
east accessories 840.0
stationery 300.0
north accessories 2180.0
stationery 420.0
south accessories 850.0
stationery 300.0
Name: revenue, dtype: float64Une ligne par combinaison qui apparaît réellement — ici, six. L'index comprend à présent deux niveaux, branch et category ; pandas appelle cela un MultiIndex, et n'affiche le niveau extérieur qu'une seule fois par bloc pour préserver la lisibilité. L'espace vide sous east signifie « toujours east ».
Vous pouvez y sélectionner des éléments comme vous vous y attendez :
print(by_both.loc["north"])
print(by_both.loc[("north", "stationery")])category
accessories 2180.0
stationery 420.0
Name: revenue, dtype: float64
420.0Une seule étiquette vous donne l'ensemble de north ; un tuple des deux étiquettes vous donne un nombre précis.
Retrouver des colonnes ordinaires
Un MultiIndex est compact, mais de nombreuses étapes ultérieures — exporter en CSV, filtrer à l'aide d'un masque, combiner avec un autre tableau (ce qui viendra plus tard) — sont plus simples avec des colonnes ordinaires. Il existe deux manières de procéder, qui aboutissent au même tableau :
flat = orders.groupby(["branch", "category"], as_index=False)["revenue"].sum()
print(flat)
print(flat.equals(by_both.reset_index()))branch category revenue
0 east accessories 840.0
1 east stationery 300.0
2 north accessories 2180.0
3 north stationery 420.0
4 south accessories 850.0
5 south stationery 300.0
Trueas_index=False indique à groupby de ne pas placer les clés dans l'index dès le départ ; reset_index() les en ressort après coup. Utilisez la syntaxe qui s'avère la plus lisible dans votre contexte.
Réorganiser pour les humains : unstack()
Pour une personne lisant le résultat, une grille est plus agréable qu'une longue liste — les succursales en lignes, les catégories en colonnes :
print(by_both.unstack())category accessories stationery
branch
east 840.0 300.0
north 2180.0 420.0
south 850.0 300.0unstack() prend le niveau d'index intérieur, category, et transforme ses valeurs en colonnes. Les chiffres correspondent exactement aux six valeurs précédentes. Si une succursale n'avait jamais vendu une catégorie donnée, sa case afficherait NaN ; unstack(fill_value=0) y inscrit 0 à la place.
Le piège de la moyenne de moyennes
Une question tentante : combien coûte une unité, en moyenne, dans chaque succursale ? La réponse expéditive consisterait à faire la moyenne de la colonne des prix :
print(orders.groupby("branch")["price"].mean())branch
east 67.50
north 248.25
south 455.00
Name: price, dtype: float64C'est la moyenne des prix sur les lignes de commande, et chaque ligne compte une seule fois — l'unique sac de la succursale south (1 unité à 850) pèse autant que ses carnets (5 unités à 60). Or, les clients n'ont pas acheté un exemplaire de chaque ligne. Ils ont acheté des unités. Le prix moyen par unité vendue est le chiffre d'affaires total divisé par le nombre total d'unités, et doit obligatoirement être calculé à partir de sommes :
per_unit = orders.groupby("branch").agg(
units=("quantity", "sum"),
revenue=("revenue", "sum"),
)
per_unit["avg_unit_price"] = (per_unit["revenue"] / per_unit["units"]).round(2)
print(per_unit)units revenue avg_unit_price
branch
east 27 1140.0 42.22
north 48 2600.0 54.17
south 6 1150.0 191.67Le chiffre de south chute de 455 à environ 192. Aucun de ces deux nombres n'est « faux » sur le plan arithmétique ; un seul répond toutefois à la question. La règle : lorsque vous cherchez un ratio, agrégez le numérateur et le dénominateur séparément, puis divisez. Calculer la moyenne de quelque chose qui est déjà une moyenne, ou d'un prix qui est déjà unitaire, donne le même poids à chaque ligne, qu'elle comporte une seule unité ou trente.
Un exemple complet
branch_report.py répond convenablement à la question du propriétaire — par succursale : nombre de commandes, unités, chiffre d'affaires, part du chiffre d'affaires total et produit le plus vendu en unités — et contrôle ses propres totaux.
import pandas as pd
orders = pd.read_csv("orders.csv")
# 1. Check the input before grouping hides anything.
print("Rows, columns:", orders.shape)
print("Branches:", sorted(orders["branch"].unique()))
print("Missing in key/value:", int(orders[["branch", "quantity", "price"]].isna().sum().sum()))
print()
# 2. Create the value column that does not exist yet.
orders["revenue"] = orders["quantity"] * orders["price"]
# 3. One grouped call, one row per branch.
report = orders.groupby("branch").agg(
orders=("order_id", "count"),
units=("quantity", "sum"),
revenue=("revenue", "sum"),
)
report["share_pct"] = (report["revenue"] / report["revenue"].sum() * 100).round(1)
# 4. Best product per branch: group by both keys, then keep each branch's top row.
by_product = orders.groupby(["branch", "product"], as_index=False)["quantity"].sum()
top = by_product.sort_values("quantity", ascending=False).drop_duplicates("branch")
report["top_product"] = top.set_index("branch")["product"]
report = report.sort_values("revenue", ascending=False)
print(report)
print()
# 5. Prove nothing was lost or double-counted.
print("Revenue check:", report["revenue"].sum(), "==", orders["revenue"].sum())
print("Orders check :", report["orders"].sum(), "==", len(orders))Rows, columns: (8, 7)
Branches: ['east', 'north', 'south']
Missing in key/value: 0
orders units revenue share_pct top_product
branch
north 4 48 2600.0 53.2 eraser
south 2 6 1150.0 23.5 notebook
east 2 27 1140.0 23.3 pen
Revenue check: 4890.0 == 4890.0
Orders check : 8 == 8Pourquoi ce code est écrit ainsi :
- Les vérifications viennent en premier et sont affichées. Trois succursales distinctes et zéro valeur manquante sont les conditions indispensables pour que le rapport soit digne de confiance. Si le fichier de la semaine suivante indique quatre succursales ou deux manques, vous le voyez tout en haut, avant de lire le moindre total.
revenueest créée avant de regrouper. Sommerquantity * priceligne par ligne puis regrouper donne le chiffre d'affaires réel. Multiplier après coup une quantité sommée par un prix moyen retomberait dans le piège de la moyenne de moyennes.- Un seul appel à
agg, des colonnes nommées. Chaque colonne de sortie est déclarée une fois, avec le nom que lira le destinataire. Ajouter un quatrième indicateur ne demande qu'une seule ligne de plus. share_pctest calculée sur le résultat, pas sur les données d'entrée. Après regroupement,reportest un DataFrame ordinaire indexé par succursale, donc l'arithmétique classique de colonnes s'y applique. Ses pourcentages totalisent bien 100.0.- Le produit vedette s'obtient par un regroupement sur deux clés, suivi d'un tri.
groupby(["branch", "product"])donne les unités par paire succursale-produit ; trier parquantitydécroissante et ne conserver que la première ligne de chaque succursale (drop_duplicates("branch")) isole la meilleure vente de chacune. L'assignationtop.set_index("branch")["product"]aligne les produits selon l'étiquette de succursale, et non selon leur position — l'index assure la correspondance. - Les deux dernières lignes constituent la preuve de contrôle. Le chiffre d'affaires et le nombre de commandes des groupes égalent ceux des données d'entrée. Si une succursale avait été oubliée, ou si le fichier comportait une succursale vide, les chiffres différeraient et vous le sauriez avant même d'envoyer le rapport.
Quand ça plante
KeyError: 'Branch' La colonne de clé est orthographiée différemment du fichier — ici avec un B majuscule. Les noms de colonnes sont sensibles à la casse, et groupby les vérifie immédiatement. Affichez list(orders.columns) et copiez le nom exact ; les guillemets dans une liste révèlent également les espaces superflus comme 'branch '.
KeyError: 'Column not found: Quantity' La même erreur, une étape plus loin : la clé était correcte, mais la colonne de valeur dans groupby("branch")["Quantity"] n'existe pas. Le message indique « Column not found » plutôt que le simple nom, ce qui vous signale que c'est la sélection après le groupby qui a échoué.
KeyError: "Label(s) ['qty'] do not exist" Dans l'agrégation nommée, le premier élément de chaque paire doit être une vraie colonne : units=("qty", "sum") échoue car il n'y a pas de colonne qty. Le nom de sortie à gauche (units) peut être ce que vous voulez ; l'entrée à droite ne le peut pas.
ValueError: Cannot subset columns with a tuple with more than one element. Use a list instead. Vous avez écrit orders.groupby("branch")["quantity", "price"]. Deux noms à l'intérieur d'une seule paire de crochets forment un tuple. Pour sélectionner plusieurs colonnes, il faut une liste — des doubles crochets, exactement comme au chapitre quatre : orders.groupby("branch")[["quantity", "price"]].sum().
TypeError: dtype 'str' does not support operation 'mean' Une agrégation a atteint une colonne de texte — soit parce qu'aucune colonne n'a été sélectionnée (groupby("branch").mean()), soit parce que la colonne sélectionnée a été chargée sous forme de texte. Dans le second cas, info() indiquera str là où vous attendiez un nombre ; la conversion avec pd.to_numeric(..., errors="coerce") vue au chapitre six permet de corriger le tir.
KeyError: 'quantity' lors de la lecture du résultat Après units = orders.groupby("branch")["quantity"].sum(), les succursales forment l'index de units et quantity n'est que son nom. units["quantity"] recherche une succursale nommée quantity. Utilisez units["north"], ou units.sum() pour obtenir le total.
É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))
print(orders.groupby("branch")["quantity"].sum())- A branch quantity 0 east 27 1 north 48 2 south 6
- B81
- Cbranch north 48 south 6 east 27 Name: quantity, dtype: int64
- Dbranch east 27 north 48 south 6 Name: quantity, dtype: int64
L'objectif est d'obtenir la quantité totale et le prix total par succursale. Que se passe-t-il ?
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))
by_branch = orders.groupby("branch")["quantity", "price"].sum()- A`ValueError: Cannot subset columns with a tuple with more than one element. Use a list instead.`
- BCela fonctionne et renvoie un tableau avec deux colonnes
- C`KeyError: ('quantity', 'price')`
- DCela ne renvoie que la colonne quantity
Quelle est la différence entre size() et count() après un groupby ?
- AIl n'y en a aucune ; ce sont deux noms pour la même chose
- B`size()` mesure la mémoire utilisée par chaque groupe
- C`size()` compte les lignes de chaque groupe, valeurs manquantes incluses ; `count()` ne compte que les valeurs non manquantes
- D`count()` compte les lignes ; `size()` compte les valeurs distinctes
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
Le propriétaire souhaite désormais le même type de réponse par catégorie et par jour. En utilisant orders.csv, écrivez category_report.py qui réalise exactement ces quatre opérations :
- Lit le fichier et affiche
shape, la liste triée des catégories distinctes, ainsi que le nombre total de valeurs manquantes danscategory,quantityetpriceréunies - Crée une colonne
revenue, puis construit un tableau avec une ligne par catégorie et les colonnes suivantes, trié par chiffre d'affaires décroissant :orders(nombre de commandes),units(quantité totale),revenue(chiffre d'affaires total),avg_order_units(quantité moyenne par commande) etavg_unit_price(chiffre d'affaires divisé par les unités, arrondi à 2 décimales) - Affiche le chiffre d'affaires par jour, puis le jour ayant généré le plus gros chiffre d'affaires
- Affiche une ligne de contrôle : le chiffre d'affaires du tableau par catégorie, celui du tableau quotidien et celui du fichier complet
Avant d'écrire le code, planifiez sur papier : pour les conditions 2 et 3, nommez la clé, la valeur et l'agrégation pour chaque colonne, et notez le nombre de lignes que chaque résultat doit comporter.
Lorsque le programme est correct, il affiche exactement ceci :
Rows, columns: (8, 7)
Categories: ['accessories', 'stationery']
Missing: 0
orders units revenue avg_order_units avg_unit_price
category
accessories 4 14 3870.0 3.50 276.43
stationery 4 67 1020.0 16.75 15.22
date
2024-03-01 480.0
2024-03-02 2540.0
2024-03-03 1090.0
2024-03-04 780.0
Name: revenue, dtype: float64
Best day: 2024-03-02 with 2540.0
Proof: 4890.0 4890.0 4890.0Provoquez ensuite des erreurs délibérées, et observez ce qui se produit à chaque fois :
- Modifiez la catégorie de la commande 1001 en
"Stationery"(avec un S majuscule). Combien de lignes votre tableau compte-t-il désormais, et le contrôle de total est-il toujours valide ? - Remplacez
"sum"par"mean"pourunits. Quel nombre change, et à quelle question répond-il désormais ? - Calculez
avg_unit_priceen faisant la moyenne depriceà la place. Quelle catégorie varie le plus, et pourquoi ?
Solution
Planifiez d'abord.
- Condition 2 : clé
category.orders= décompte deorder_id;units= somme dequantity;revenue= somme derevenue;avg_order_units= moyenne dequantity;avg_unit_price= pas une agrégation d'une seule colonne — c'est un ratio, qui est donc calculé après coup à partir derevenueetunits. Le fichier comptant deux catégories, le tableau comporte 2 lignes. - Condition 3 : clé
date, valeurrevenue, agrégationsum. Quatre dates distinctes, donc 4 lignes ; le meilleur jour correspond à l'étiquette d'index du maximum, obtenue avecidxmax(). - La preuve de contrôle : le chiffre d'affaires des deux tableaux doit totaliser les 4890.0 du fichier.
category_report.py :
import pandas as pd
orders = pd.read_csv("orders.csv")
print("Rows, columns:", orders.shape)
print("Categories:", sorted(orders["category"].unique()))
print("Missing:", int(orders[["category", "quantity", "price"]].isna().sum().sum()))
print()
orders["revenue"] = orders["quantity"] * orders["price"]
table = orders.groupby("category").agg(
orders=("order_id", "count"),
units=("quantity", "sum"),
revenue=("revenue", "sum"),
avg_order_units=("quantity", "mean"),
)
table["avg_unit_price"] = (table["revenue"] / table["units"]).round(2)
table = table.sort_values("revenue", ascending=False)
print(table)
print()
daily = orders.groupby("date")["revenue"].sum()
print(daily)
print("Best day:", daily.idxmax(), "with", daily.max())
print()
print("Proof:", table["revenue"].sum(), daily.sum(), orders["revenue"].sum())Rows, columns: (8, 7)
Categories: ['accessories', 'stationery']
Missing: 0
orders units revenue avg_order_units avg_unit_price
category
accessories 4 14 3870.0 3.50 276.43
stationery 4 67 1020.0 16.75 15.22
date
2024-03-01 480.0
2024-03-02 2540.0
2024-03-03 1090.0
2024-03-04 780.0
Name: revenue, dtype: float64
Best day: 2024-03-02 with 2540.0
Proof: 4890.0 4890.0 4890.0Deux lignes et quatre lignes, comme prévu, et les trois totaux de chiffre d'affaires concordent. Remarquez ce que le tableau révèle alors qu'aucune des colonnes prise isolément ne le montrait : les articles de papeterie représentent presque cinq fois plus d'unités vendues, mais les accessoires rapportent près de quatre fois plus d'argent.
Ce que montrent les trois erreurs provoquées.
- Avec
"Stationery"sur une ligne, le tableau compte 3 lignes —Stationeryconstitue son propre groupe avec une seule commande. La preuve de contrôle reste valide, car aucune ligne n'a été perdue ; elle a seulement été mal classée. C'est pourquoi la liste des catégories distinctes est affichée tout en haut : elle listerait désormais trois catégories, et vous repéreriez l'anomalie avant même de lire le moindre chiffre. - Avec
"mean",unitsdevient 3.5 et 16.75 — soit la même valeur queavg_order_units. L'indicateur répond désormais à « quel est le volume d'une commande type », et non plus à « combien avons-nous vendu ».avg_unit_pricechange également, car il divise le chiffre d'affaires par la mauvaise grandeur. - Faire la moyenne de
pricedonne pour les accessoires(850 + 120 + 850 + 120) / 4 = 485.0, bien au-dessus de la valeur réelle de 276.43, parce que les deux commandes de sacs d'une et deux unités pèsent autant que les commandes de bouteilles de sept et quatre unités. Pour la papeterie, la valeur passe de 15.22 à 24.5 — une hausse moins spectaculaire uniquement parce que ses prix unitaires sont tous bas. Faire la moyenne d'un montant unitaire sur des lignes de commande constitue le piège ; diviser des sommes est la solution.
É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