Raggruppamento e aggregazione — una risposta per gruppo
Trasformare le domande "per filiale", "per categoria" e "per giorno" in un'unica riga di groupby: la chiave, il valore e l'aggregazione, più calcoli contemporaneamente con le aggregazioni con nome, size contro count, due chiavi e la trappola della media delle medie.
Come vuoi imparare?
Il problema che stiamo risolvendo
Il titolare del negozio ti invia un breve messaggio: "Quante unità abbiamo venduto in ciascuna filiale questa settimana?"
Hai a disposizione orders.csv — otto ordini, una riga ciascuno. Con quanto appreso negli ultimi quattro capitoli, puoi già rispondere. Filtri una filiale, sommi la sua colonna e ripeti:
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 27I numeri sono corretti. Il metodo però presenta tre problemi, e ognuno di essi peggiora man mano che i dati aumentano.
Primo, dovevi conoscere le filiali in anticipo. Hai digitato tu la lista a mano. Quando il file della prossima settimana conterrà un ordine proveniente da una nuova filiale west, questo ciclo non lo vedrà: non segnalerà alcun errore, escluderà semplicemente west e la somma dei parziali non corrisponderà più al totale complessivo.
Secondo, ogni nuova domanda richiede un nuovo ciclo. "Ricavi per categoria" richiede un secondo ciclo su un'altra lista scritta a mano. "Unità per filiale e categoria" richiede un ciclo all'interno di un altro ciclo.
Terzo, la risposta non è una tabella. Sono solo righe di testo stampate a video. Non puoi ordinarle, filtrarle o affiancarle a un'altra risposta senza doverle ricostruire.
Ciò che desideri davvero è esprimere, in una sola riga: dividi le righe per filiale, calcola la somma della quantità all'interno di ciascuna parte e restituiscimi le parti sotto forma di tabella. Questo è groupby, ed è il modo in cui viene scritto quasi ogni riepilogo in pandas: un report, un indicatore per una dashboard, la prima risposta a "da dove arrivano i soldi?".
Alla fine di questo capitolo sarai in grado di
- Scomporre una domanda come "unità per filiale" nelle sue tre parti — chiave, colonna dei valori, aggregazione — e verificare la chiave prima di raggruppare
- Scrivere
df.groupby(key)[column].sum()e interpretare la Series restituita, con la chiave come indice - Calcolare più riepiloghi contemporaneamente con
.agg()e con l'aggregazione con nome - Distinguere
size()dacount()e decidere come gestire chiavi e valori mancanti - Raggruppare in base a due colonne, appiattire o riorganizzare il risultato ed evitare l'errore della "media delle medie"
Prerequisiti: Ordinare le righe.
Il file che utilizziamo
Lo stesso orders.csv dei capitoli precedenti — otto ordini di un piccolo negozio. Se non lo hai ancora, crealo nella cartella del tuo progetto esattamente con queste righe:
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.0Ogni riga rappresenta un ordine: da quale filiale proviene, quale prodotto, la sua categoria, quante unità e il prezzo unitario.
Prima di scrivere il codice
Ogni domanda basata su un raggruppamento ha la stessa struttura. Prima di digitare groupby, scomponi la domanda nei suoi elementi.
1. Formula la domanda in tre parti
Una domanda di raggruppamento contiene sempre tre elementi, e le parole stesse della richiesta ti indicano quali sono:
- Chiave — la colonna i cui valori definiscono i gruppi. È la parola che segue per o per ciascun/a. In "unità per filiale" è
branch. - Valore — la colonna da riepilogare: ciò che viene misurato. Qui è
quantity. - Aggregazione — il modo in cui più valori ne diventano uno solo: totale, media, quanti, massimo. Qui è
sum.
Qualche altro esempio per fissare lo schema:
- "Ricavi per categoria" — chiave
category, valorerevenue, aggregazionesum - "Dimensione media dell'ordine in ogni filiale" — chiave
branch, valorequantity, aggregazionemean - "Quanti ordini al giorno" — chiave
date, nessuna colonna dei valori (si contano le righe), aggregazionesize - "Ordine singolo più grande per prodotto" — chiave
product, valorequantity, aggregazionemax
Se non riesci a identificare tutte e tre le parti, la domanda non è ancora pronta per essere tradotta in codice. "Quale filiale è la migliore?" non ha né valore né aggregazione: la migliore per unità vendute? per ricavi? per numero di ordini? Chiedilo prima di eseguire i calcoli. Le tre risposte possono indicare tre filiali diverse.
2. Osserva i dati in ingresso prima di raggrupparli
Il raggruppamento nasconde le singole righe. Quando gli otto ordini diventano tre totali, una riga difettosa risulta invisibile all'interno di una somma. I controlli vanno quindi eseguiti prima:
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: objectDue aspetti sono importanti qui. La colonna del valore, quantity, è int64 — numerica, quindi sum sommerà i valori anziché concatenare del testo. La colonna della chiave, branch, è str, il che va benissimo per una chiave.
Poi osserva i valori effettivi della chiave. È il controllo che spesso si salta, ed è quello che conta di più:
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: int64Tre filiali distinte, nessun dato mancante in nessuna delle due colonne. groupby crea un gruppo per ciascun valore distinto, carattere per carattere — quindi "north" e "north " formerebbero due gruppi diversi. value_counts() ti permette di accorgertene prima che si trasformi in un report errato.
Ecco cosa accade quando si salta questo controllo. Tre righe che indicano tutte la filiale north — una digitata correttamente, una con uno spazio finale e una con la lettera maiuscola:
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: int64Nessun errore e tre "filiali" dove dovrebbe essercene una sola. Il numero di ciascun gruppo è corretto in base ai dati ricevuti da pandas, eppure il report è sbagliato. Pulire la chiave prima di raggruppare — rimuovendo gli spazi ed eliminando le maiuscole — riunisce tutto in un unico gruppo con il totale corretto di 44.
3. Fai una bozza dell'output prima di crearlo
Stabilisci quale aspetto dovrebbe avere la risposta, così da accorgerti subito se qualcosa non va:
branch units
east ?
north ?
south ?Grazie ai controlli effettuati sopra conosci già tre elementi fondamentali: conterrà esattamente 3 righe (una per ogni filiale distinta), le filiali compariranno in ordine alfabetico (groupby ordina le sue chiavi) e i tre numeri dovranno dare come somma il totale dell'intera colonna. Quest'ultimo punto costituisce la verifica finale.
4. Scegli lo strumento
- Un solo numero per gruppo, da una sola colonna:
df.groupby(key)[col].sum()(omean,max, ...) - Più numeri da una sola colonna:
df.groupby(key)[col].agg(["sum", "mean"]) - Più numeri da colonne diverse, con nomi personalizzati:
df.groupby(key).agg(name=(col, "sum"), ...) - Quante righe contiene ciascun gruppo:
df.groupby(key).size() - Una colonna non ancora presente, come i ricavi: creala prima, poi raggruppa
Ora passiamo al codice.
Split, apply, combine
groupby esegue tre operazioni in sequenza, ed è utile conoscerne i nomi perché ogni operazione di raggruppamento si riduce a questi tre passaggi:
- Split (dividi) le righe in porzioni, una per ogni valore distinto della chiave
- Apply (applica) un calcolo a ciascuna porzione separatamente
- Combine (combina) i risultati in un unico nuovo oggetto, indicizzato dalla chiave
Puoi osservare la suddivisione da sola. È possibile iterare su un oggetto groupby con un ciclo, ottenendo ogni chiave e la relativa porzione della tabella:
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]Ciascuna porzione è un normale DataFrame con tutte e sette le colonne: la porzione north ha quattro righe, le altre due ne hanno due. Nessuno ha dovuto digitare i nomi delle filiali; pandas li ha ricavati direttamente dai dati. Un ordine proveniente da una nuova filiale west la prossima settimana si trasformerebbe semplicemente in una quarta porzione.
Nel codice reale userai raramente un ciclo come questo. Serve qui per visualizzare chiaramente il meccanismo: tutto ciò che segue consiste nel "fare qualcosa su ogni porzione, per poi unire i risultati".
Un numero per gruppo
La domanda "unità per filiale" nelle sue tre parti — chiave branch, valore quantity, aggregazione sum — si scrive esattamente in questo ordine:
units = orders.groupby("branch")["quantity"].sum()
print(units)branch
east 27
north 48
south 6
Name: quantity, dtype: int64Leggila da sinistra a destra: raggruppa per filiale, prendi quantity, calcola la somma. Gli stessi numeri del ciclo, in una sola riga, senza alcuna lista di filiali digitata a mano.
Ora confronta il risultato con la bozza. Tre righe: sì. Ordine alfabetico: sì. Ed ecco la verifica:
print(units.sum(), orders["quantity"].sum())81 81La somma dei gruppi equivale all'intero totale, quindi nessuna riga è andata persa e nessuna è stata contata due volte. Vale la pena far diventare questa singola riga di controllo un'abitudine: ogni volta che raggruppi, confronta il totale del risultato con il totale dei dati in ingresso.
Il risultato è una Series, e la chiave è il suo indice
print(type(units))
print(units.index)
print(units["north"])<class 'pandas.Series'>
Index(['east', 'north', 'south'], dtype='str', name='branch')
48Il passaggio di combinazione ha inserito le filiali nell'indice, non in una colonna. Ecco perché nell'output branch compare su una riga a parte sopra i valori: è il nome dell'indice. Significa anche che puoi cercare un valore in base al nome della filiale, units["north"], esattamente come faresti con .loc nel capitolo quattro.
Trattandosi di una normale Series, tutto ciò che hai appreso nei capitoli precedenti continua a funzionare. Ordinandola si risponde a "quale filiale ha venduto di più":
print(units.sort_values(ascending=False))
print(units.idxmax())branch
north 48
east 27
south 6
Name: quantity, dtype: int64
northidxmax() restituisce l'etichetta del valore più grande: la filiale, non il numero. Per sapere "chi è in cima" di solito è proprio questo ciò che serve.
Perché scegliere prima la colonna
La tentazione di tralasciare ["quantity"] e lasciare che pandas sommi tutto è forte:
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]Nessun errore — e quasi tutti i numeri sono privi di senso. order_id è stato sommato, come se i numeri d'ordine fossero quantità. Le stringhe date sono state concatenate l'una all'altra, perché "sommare" testo significa unirlo. Anche price è stato sommato: quel 993.0 per north è la somma dei prezzi unitari di quattro prodotti diversi, il che non misura nulla.
orders.groupby("branch").sum() non genera alcun errore. Somma numeri identificativi, concatena date e fa il totale dei prezzi unitari, restituendo un risultato che a prima vista ha tutto l'aspetto di un report valido.Solo quantity ha significato, e avresti dovuto richiedere soltanto quella. Ecco perché la regola prevede di selezionare la colonna del valore prima dell'aggregazione: sum esegue qualsiasi cosa l'addizione significhi per ciascun tipo, e per la maggior parte delle colonne non è una domanda sensata.
mean è più severo e si rifiuta di procedere anziché produrre dati senza senso:
print(orders.groupby("branch").mean())TypeError: dtype 'str' does not support operation 'mean'Quell'errore rende evidente lo stesso sbaglio. Leggilo come: mi hai chiesto di calcolare la media di una colonna di testo. La soluzione non è numeric_only=True — calcolerebbe comunque la media di order_id e price — ma indicare esplicitamente quale colonna intendi usare.
Più numeri contemporaneamente: .agg()
Un solo valore di riepilogo è raramente sufficiente. "Quanto ha venduto ciascuna filiale, in media per ordine, e su quanti ordini?" richiede tre aggregazioni sulla stessa colonna. Passa una lista dei loro nomi a .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 2Ora il risultato è un DataFrame — una colonna per ogni aggregazione, una riga per ogni filiale. Rivela una storia che il totale da solo nascondeva: east ha venduto poco più della metà delle unità di north, ma il suo ordine medio è più grande. La filiale north riceve più ordini; east ne riceve meno, ma leggermente più consistenti.
I nomi nella lista sono stringhe: "sum", "mean", "count", "min", "max", "median", "nunique" (numero di valori distinti), "first", "last". Sono gli stessi metodi che hai usato su una singola colonna, applicati a ciascuna porzione.
Aggregazione con nome: le tue colonne, i tuoi nomi
I report reali riepilogano colonne diverse in modi diversi e richiedono nomi di colonna chiari e leggibili. L'aggregazione con nome fa entrambe le cose in una sola chiamata. Ogni parola chiave è il nome di una colonna di output, e il suo valore è una coppia: (input column, aggregation).
I ricavi sono il dato più naturale da inserire nel report, ma non sono ancora una colonna: corrispondono a quantità per prezzo su ciascuna riga. La creazione di colonne avrà un capitolo dedicato, il prossimo; per ora basta una sola riga, che mostra in azione il passaggio 4 del nostro piano: se la colonna del valore non esiste, creala prima di raggruppare.
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.0Questo è un report pronto da consegnare. Ogni riga della chiamata ad agg si legge come una frase: orders è il conteggio di order_id, units è la somma di quantity, revenue è la somma di revenue. I nomi delle colonne di output sono scelti da te anziché ereditati come sum e count, evitando di doverli rinominare dopo.
E questo cambia la risposta precedente. Per unità vendute east era nettamente seconda; per ricavi south passa in vantaggio — una borsa da 850 supera venti penne. Ecco perché il passaggio 1 insisteva sul nominare il valore: "migliore filiale" per unità e "migliore filiale" per ricavi sono due domande diverse.
Il risultato è un DataFrame, quindi si ordina come tale:
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.0Contare: size() contro count()
"Quanti ordini per filiale" sembra un'unica domanda. Pandas ha due risposte diverse, che divergono proprio quando ci sono dati mancanti.
size()conta le righe in ciascun gruppo, indipendentemente dal loro contenuto.count()conta i valori non mancanti di una colonna in ciascun gruppo.
Sul file pulito coincidono. Per vederli divergere, ecco una piccola tabella creata manualmente accanto a orders, con una filiale mancante e una quantità mancante:
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 è stata convertita in float a causa del valore mancante — il capitolo sei ha spiegato il perché.)
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 ha un ordine (size = 1) ma zero quantità note (count = 0). Entrambe le affermazioni sono vere; rispondono a domande diverse. Usa size() per "quanti ordini ci sono" e count() per "per quanti ordini ho effettivamente un numero".
Due cose che i dati mancanti fanno silenziosamente
Osserva di nuovo quegli output. L'ordine 1003, quello senza filiale, non compare in nessuno dei due. Sono entrati cinque ordini; ne sono usciti quattro:
print(gaps.groupby("branch").size().sum(), len(gaps))4 5Per impostazione predefinita groupby esclude le righe la cui chiave è mancante. Non esiste un gruppo chiamato "sconosciuto", quindi la riga scompare da ogni risultato. Il controllo del totale visto in precedenza individua subito questa anomalia. Se quelle righe sono importanti — e un ordine senza filiale resta comunque un ordine — richiedile con 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: float64Ora sono presenti tutti e quattro i gruppi, e il gruppo NaN contiene le 2 unità che nessuno ha potuto assegnare.
Il secondo comportamento silenzioso si trova nello stesso output: east mostra 0.0. La filiale east non ha venduto zero unità; la sua unica quantità è semplicemente sconosciuta. sum tratta "niente da sommare" come zero. Se ciò che è sconosciuto deve rimanere tale, indicalo con min_count=1 — "calcola la somma solo se esiste almeno un valore reale":
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: float64Quale sia l'opzione corretta dipende dalla domanda, ed è proprio questo il punto: deciderlo nella fase di pianificazione, non per caso. In ogni caso, la vera soluzione a entrambi i problemi si applica di solito prima: ripulisci i valori mancanti (capitolo sei) prima di raggruppare.
Due chiavi: gruppi all'interno di gruppi
"Ricavi per filiale e categoria" ha due chiavi. Passa una lista:
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: float64Una riga per ogni combinazione che compare effettivamente nei dati — sei in questo caso. L'indice ha ora due livelli, branch e category; pandas lo chiama MultiIndex, e stampa il livello esterno una sola volta per blocco per mantenerlo leggibile. Lo spazio vuoto sotto east significa "ancora east".
Puoi effettuare selezioni esattamente come ti aspetteresti:
print(by_both.loc["north"])
print(by_both.loc[("north", "stationery")])category
accessories 2180.0
stationery 420.0
Name: revenue, dtype: float64
420.0Una sola etichetta restituisce l'intera filiale north; una tupla con entrambe le etichette restituisce un singolo numero.
Riottenere colonne normali
Un MultiIndex è compatto, ma molti passaggi successivi — il salvataggio in un CSV, il filtraggio con una maschera, la combinazione con un'altra tabella, che vedremo più avanti — risultano più semplici con colonne ordinarie. Esistono due modi, ed entrambi producono la stessa tabella:
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 dice a groupby di non spostare fin dall'inizio le chiavi nell'indice; reset_index() le riporta fuori come colonne in seguito. Usa quello che rende il codice più leggibile nel contesto specifico.
Riorganizzare i dati per le persone: unstack()
Per una persona che legge il risultato, una griglia è molto più comoda di un lungo elenco: le filiali lungo il lato verticale, le categorie in alto in orizzontale:
print(by_both.unstack())category accessories stationery
branch
east 840.0 300.0
north 2180.0 420.0
south 850.0 300.0unstack() prende il livello più interno dell'indice, category, e trasforma i suoi valori in colonne. I numeri sono esattamente i sei visti in precedenza. Se una filiale non avesse mai venduto una categoria, la rispettiva cella conterrebbe NaN; unstack(fill_value=0) scrive invece 0.
La trappola della media delle medie
Una domanda allettante: quanto costa un'unità, in media, in ciascuna filiale? La risposta sbrigativa è calcolare la media della colonna dei prezzi:
print(orders.groupby("branch")["price"].mean())branch
east 67.50
north 248.25
south 455.00
Name: price, dtype: float64Quella è la media dei prezzi sulle singole righe d'ordine, e ogni riga conta una sola volta: la singola borsa della filiale south (1 unità a 850) ha lo stesso peso dei suoi quaderni (5 unità a 60). Ma i clienti non hanno acquistato una quantità unitaria per ciascuna riga: hanno acquistato pezzi. Il prezzo medio per unità venduta è il totale dei ricavi diviso per il totale delle unità, e deve essere calcolato a partire dalle somme:
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.67Il valore per south scende da 455 a circa 192. Dal punto di vista aritmetico nessuno dei due numeri è "sbagliato"; solo uno dei due risponde alla domanda posta. La regola: quando ciò che vuoi ottenere è un rapporto, aggrega il numeratore e il denominatore separatamente, poi dividi. Calcolare la media di qualcosa che è già una media, o di un prezzo che è già unitario, assegna a ogni riga lo stesso peso, che si tratti di un'unica unità o di trenta.
Un esempio completo
branch_report.py risponde alla domanda del titolare in modo accurato — per filiale: numero di ordini, unità, ricavi, percentuale sul totale dei ricavi e prodotto più venduto per unità — e verifica i propri totali.
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 == 8Perché è scritto in questo modo:
- I controlli vengono prima di tutto e sono stampati. Tre filiali distinte e zero valori mancanti sono le condizioni affinché il report sia affidabile. Se il file della settimana successiva presentasse quattro filiali o due valori mancanti, lo vedresti subito in cima, prima di leggere qualsiasi totale.
revenueviene creata prima del raggruppamento. Calcolare la somma diquantity * priceper riga e poi raggruppare restituisce i ricavi reali. Moltiplicare a posteriori una quantità sommata per un prezzo medio ricadrebbe di nuovo nella trappola della media delle medie.- Una sola chiamata ad
agg, colonne con nome. Ciascuna colonna di output è dichiarata una sola volta, con il nome che il lettore vedrà nel report. Aggiungere una quarta metrica richiede una sola riga in più. share_pctviene calcolata sul risultato, non sull'input. Dopo il raggruppamento,reportè un normale DataFrame indicizzato per filiale, quindi la comune aritmetica tra colonne funziona senza problemi. La somma delle percentuali fa 100.0.- Il prodotto più venduto sfrutta il raggruppamento su due chiavi, seguito dall'ordinamento.
groupby(["branch", "product"])fornisce le unità per ciascuna coppia filiale-prodotto; ordinando perquantitydecrescente e conservando la prima riga per ogni filiale (drop_duplicates("branch")) si ottiene il prodotto più venduto di ciascuna filiale. L'assegnazionetop.set_index("branch")["product"]allinea i prodotti in base all'etichetta della filiale, non per posizione: è l'indice a eseguire la corrispondenza. - Le ultime due righe costituiscono la verifica. I ricavi e il conteggio degli ordini dei gruppi sono uguali a quelli dell'input. Se una filiale fosse andata persa o se il file contenesse una filiale vuota, i numeri differirebbero e te ne accorgeresti prima che il report venga inviato.
Quando qualcosa va storto
KeyError: 'Branch' Il nome della colonna chiave è scritto in modo diverso rispetto al file — qui con una B maiuscola. I nomi delle colonne fanno distinzione tra maiuscole e minuscole, e groupby li controlla immediatamente. Stampa list(orders.columns) e copia il nome esatto; le virgolette in una lista mostrano anche gli spazi finali nascosti come 'branch '.
KeyError: 'Column not found: Quantity' Stesso errore, un passaggio dopo: la chiave era corretta, ma la colonna del valore in groupby("branch")["Quantity"] non esiste. Il messaggio specifica "Column not found" anziché solo il nome, indicando che a fallire è stata la selezione successiva a groupby.
KeyError: "Label(s) ['qty'] do not exist" Nell'aggregazione con nome, il primo elemento di ogni coppia deve essere una colonna realmente esistente: units=("qty", "sum") fallisce perché non esiste alcuna colonna qty. Il nome di output a sinistra (units) può essere qualsiasi cosa; l'input a destra no.
ValueError: Cannot subset columns with a tuple with more than one element. Use a list instead. Hai scritto orders.groupby("branch")["quantity", "price"]. Due nomi all'interno di un'unica coppia di parentesi quadre formano una tupla. Per selezionare più colonne serve una lista — doppie parentesi quadre, esattamente come nel capitolo quattro: orders.groupby("branch")[["quantity", "price"]].sum().
TypeError: dtype 'str' does not support operation 'mean' Un'aggregazione ha raggiunto una colonna di testo — o perché non è stata selezionata alcuna colonna (groupby("branch").mean()), o perché la colonna selezionata è stata letta come testo. Nel secondo caso, info() mostrerà str dove ti aspettavi un numero; la correzione è pd.to_numeric(..., errors="coerce") vista nel capitolo sei.
KeyError: 'quantity' durante la lettura del risultato Dopo units = orders.groupby("branch")["quantity"].sum(), le filiali sono l'indice di units e quantity è soltanto il suo nome. units["quantity"] cerca una filiale chiamata quantity. Usa units["north"], oppure units.sum() per il totale complessivo.
Passo 4 di 6 — Prevedi
Verifica cosa hai capito
Cosa viene stampato?
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'obiettivo è la quantità totale e il prezzo totale per filiale. Cosa succede?
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.`
- BFunziona e restituisce una tabella con due colonne
- C`KeyError: ('quantity', 'price')`
- DRestituisce solo la colonna quantity
Qual è la differenza tra size() e count() dopo groupby?
- ANessuna; sono due nomi per la stessa cosa
- B`size()` misura la memoria utilizzata da ciascun gruppo
- C`size()` conta le righe in ciascun gruppo, inclusi i valori mancanti; `count()` conta solo i valori non mancanti
- D`count()` conta le righe; `size()` conta i valori distinti
Per rispondere serve un account
Accedi per verificare le tue risposte
Le domande sono qui sopra, e ragionarci a mente è la parte che conta. Accedi per vedere le risposte, le spiegazioni e i suggerimenti a tre livelli.
Tocca a te
Il titolare ora desidera lo stesso tipo di risposta per categoria e per giorno. Utilizzando orders.csv, scrivi category_report.py che esegua esattamente queste quattro operazioni:
- Legge il file e stampa
shape, la lista ordinata delle categorie distinte e il numero di valori mancanti complessivi incategory,quantityeprice - Crea una colonna
revenue, quindi costruisce una tabella con una riga per categoria e le seguenti colonne, ordinate per ricavi dal più alto al più basso:orders(numero di ordini),units(quantità totale),revenue(ricavi totali),avg_order_units(quantità media per ordine) eavg_unit_price(ricavi divisi per unità, arrotondati a 2 decimali) - Stampa i ricavi per giorno, poi il singolo giorno con i ricavi più alti
- Stampa una riga di verifica: i ricavi della tabella per categoria, della tabella giornaliera e dell'intero file
Prima di scrivere il codice, pianifica su carta: per i punti 2 e 3, indica la chiave, il valore e l'aggregazione per ciascuna colonna, e annota quante righe deve avere ogni risultato.
Quando il codice è corretto, il programma stampa esattamente questo:
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.0Poi introduci appositamente degli errori e osserva cosa succede ogni volta:
- Modifica la categoria dell'ordine 1001 in
"Stationery"(con la S maiuscola). Quante righe ha adesso la tua tabella? La verifica dei totali ha ancora esito positivo? - Sostituisci
"sum"con"mean"perunits. Quale numero cambia e a quale domanda risponde adesso? - Calcola
avg_unit_pricecome media diprice. Quale categoria varia di più e perché?
Soluzione
Prima pianifica.
- Punto 2: chiave
category.orders= conteggio diorder_id;units= somma diquantity;revenue= somma direvenue;avg_order_units= media diquantity;avg_unit_price= non è un'aggregazione su una singola colonna — è un rapporto, quindi va calcolato successivamente a partire darevenueeunits. Il file contiene due categorie, quindi la tabella ha 2 righe. - Punto 3: chiave
date, valorerevenue, aggregazionesum. Quattro date distinte, quindi 4 righe; il giorno migliore è l'etichetta dell'indice corrispondente al massimo, data daidxmax(). - La verifica: i ricavi di entrambe le tabelle devono corrispondere a 4890.0, il totale del file.
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.0Due righe e quattro righe, esattamente come previsto, e tutti e tre i totali dei ricavi coincidono. Nota cosa rivela la tabella che nessuna delle due colonne da sola avrebbe mostrato: stationery muove quasi cinque volte più pezzi, ma gli accessori generano quasi quattro volte più incasso.
Cosa dimostrano i tre test di errore.
- Con
"Stationery"su una riga la tabella ha 3 righe —Stationeryforma un gruppo a sé stante con un solo ordine. La verifica dei totali ha comunque successo, perché nessuna riga è andata persa: è stata semplicemente catalogata sotto un'altra voce. Ecco perché la riga con le categorie distinte viene stampata in cima: ora elencherebbe tre categorie, facendoti notare l'anomalia prima ancora di leggere qualsiasi numero. - Con
"mean",unitsdiventa 3.5 e 16.75 — esattamente uguale adavg_order_units. Ora risponde a "quanto è grande un ordine tipico", non a "quanto abbiamo venduto". Ancheavg_unit_pricecambia, perché divide i ricavi per il dato sbagliato. - Calcolare la media di
priceattribuisce ad accessories(850 + 120 + 850 + 120) / 4 = 485.0, molto al di sopra del valore reale di 276.43, perché i due ordini di borse da una e due unità pesano quanto gli ordini di bottiglie da sette e quattro unità. Stationery sale da 15.22 a 24.5 — una variazione meno marcata solo perché i suoi prezzi sono tutti bassi. Fare la media di un valore unitario sulle righe d'ordine è la trappola; dividere le somme è la soluzione.
Passo 6 di 6
Sfida — il quiz del capitolo
Dieci domande dalla più facile alla più difficile. Le ultime sono difficili di proposito.
Accedi per fare il quiz