Ordinare le righe — e decidere cosa fare con le parità
Mettere in ordine le righe con sort_values: una o più colonne, ciascuna nella propria direzione, valori mancanti e testo con l'aspetto di numeri — oltre a nlargest per i "primi N", e perché una parità sulla soglia deve essere una decisione, non un caso.
Come vuoi imparare?
Il problema che stiamo risolvendo
Il titolare del negozio invia un breve messaggio: "Quali sono stati i nostri ordini più grandi questo mese? Mandami i primi tre."
I dati sono quelli del familiare orders.csv — otto ordini dalle tre filiali del negozio: north, south ed east. Con otto righe potresti rispondere a occhio. Scorri verso il basso la colonna quantity, trovi il 30, poi il 20, poi il 12. Fatto.
Ora immagina il file reale: quattromila ordini. Scorrere una colonna verso il basso per trovare i tre numeri più grandi non è più un metodo, è una speranza. E il messaggio successivo del titolare sta già arrivando: "Puoi inviarmi l'elenco raggruppato per filiale, con il più grande per primo all'interno di ciascuna filiale? E gli ordini più recenti in alto, per favore."
Ognuna di queste richieste corrisponde alla stessa operazione: mettere le righe in un ordine utile, per poi leggere dall'alto verso il basso. Questo è l'ordinamento, e in pandas si esegue con un unico metodo: sort_values().
Ma il metodo è la parte facile. Le vere domande ruotano attorno ad esso, ed è lì che i report falliscono:
- "Più grande" in base a cosa? Quantità, prezzo o incasso generato dall'ordine? Ognuna di queste opzioni dà tre ordini diversi in cima.
- Quando due ordini sono a pari merito, quale viene prima — e la risposta cambia ogni volta che esegui il codice?
- Cosa succede a un ordine la cui quantità è vuota?
- Se una colonna di numeri è stata letta come testo,
"9"viene ordinato dopo"30", e nessun avviso ti mette in guardia.
Questo capitolo insegna il metodo e, cosa ancora più importante, queste decisioni.
Alla fine di questo capitolo sarai in grado di
- Ordinare una tabella in base a una o più colonne con
sort_values(), ciascuna nella propria direzione, e conservare il risultato anziché perderlo - Spiegare perché le etichette dell'indice viaggiano insieme alle rispettive righe, e scegliere se mantenerle, usare
sort_index()oreset_index(drop=True) - Risolvere le parità in modo intenzionale, e decidere dove collocare i valori mancanti con
na_position - Rispondere alle domande sui "primi N" con
head(),nlargest()ensmallest()— inclusi i "primi N di un sottoinsieme" e le parità sulla soglia - Ordinare il testo senza fare distinzione tra maiuscole e minuscole con
key=, e riconoscere i numeri memorizzati come testo dal modo in cui vengono ordinati
Prerequisiti: Dati mancanti — individuare, eliminare e riempire i vuoti.
Crea prima il file
Ogni esempio in questo capitolo legge orders.csv, lo stesso file usato a partire dal quarto capitolo. 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.0Una sezione successiva utilizza un secondo file, orders_gaps.csv. Si tratta della stessa tabella con due quantità lasciate vuote — gli ordini 1002 e 1007 — il genere di vuoto che il capitolo sei ti ha insegnato a individuare:
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.0Prima di scrivere il codice
L'ordinamento sembra troppo semplice per richiedere una pianificazione. Non è così, perché un ordinamento non fallisce mai — produce sempre un ordine. Se hai ordinato in base alla colonna sbagliata, il risultato apparirà comunque ordinato e pulito esattamente come quello corretto. Quindi la riflessione deve avvenire prima di scrivere il codice.
1. Scrivi la domanda in una sola frase, indicando la colonna. "Gli ordini più grandi" non è una domanda a cui pandas può rispondere. "I tre ordini con la maggiore quantity" lo è. Così come "i tre ordini con il price più alto". Forniscono risposte diverse — la borsa da 850 è l'articolo più costoso, ma sono state vendute solo due borse. Decidi quale intende il titolare. Se non sei sicuro, chiediglielo; costa solo un messaggio, mentre una terna errata costa la fiducia.
2. Osserva i dati in ingresso prima di ordinarli. Tre aspetti contano per un ordinamento: il numero di righe, i tipi delle colonne in base alle quali eseguirai l'ordinamento e l'eventuale presenza di valori vuoti in quelle colonne.
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
0Otto righe, sette colonne. quantity è int64 e price è float64, quindi verranno ordinati come numeri. branch e product sono str — testo, che viene ordinato alfabeticamente. Anche date è str. Qui non rappresenta un problema, e il motivo è utile da conoscere: le date scritte come YYYY-MM-DD si ordinano correttamente come testo, perché la parte più significativa compare per prima. Le date scritte come 4/3/2024 non lo farebbero. E quantity non ha valori vuoti, quindi non dobbiamo ancora decidere dove collocare i valori mancanti.
3. Fai una bozza dell'output. Per i "primi tre per quantità" la risposta dovrebbe essere composta da tre righe, dalla più grande alla più piccola: 30 (eraser, ordine 1005), poi 20 (pen, 1007), poi 12 (pen, 1001). Annotarlo prima di eseguire qualsiasi riga di codice ti fornisce un termine di confronto con cui verificare il risultato.
4. Decidi i dettagli necessari per l'ordinamento.
- Chiave — quale colonna, o quali colonne, decidono l'ordine? Questo diventa
by=. - Direzione — dal più piccolo al più grande o dal più grande al più piccolo, per ciascuna chiave? Questo diventa
ascending=. - Parità — se due righe hanno lo stesso valore, quale compare per prima? Questo diventa una seconda chiave, oppure
kind="stable". - Valori vuoti — i valori mancanti devono andare per primi o per ultimi? Questo diventa
na_position=. - Quante righe — tutte le righe o solo le prime N? Questo diventa
.head(n)oppurenlargest(). - Indice — le vecchie etichette di riga hanno ancora un significato in seguito? Mantienile, oppure eliminale con
reset_index(drop=True).
5. Scegli lo strumento.
- Tutte le righe, nell'ordine di una o più colonne:
sort_values() - Solo le prime o le ultime N righe in base a una colonna numerica:
nlargest()/nsmallest() - Le righe ripristinate nell'ordine dell'indice:
sort_index() - Le prime N righe all'interno di un sottoinsieme ("nella filiale north"): prima filtra, poi ordina, poi usa
head()
6. Stabilisci come effettuerai la verifica. Dopo un ordinamento, le prime righe devono corrispondere alla tua bozza, lo shape non deve cambiare (un ordinamento non aggiunge né rimuove mai righe) e, per una chiave numerica, la colonna deve scorrere in modo monotono lungo la pagina.
sort_values() — una sola colonna
Per mantenere le stampe compatte, gli esempi mostrano cinque delle sette colonne. cols è semplicemente una lista di nomi, utilizzata con la selezione a doppie parentesi quadre del capitolo quattro.
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.0Il comportamento predefinito è crescente — dal più piccolo al più grande. Leggi la colonna quantity dall'alto verso il basso: 1, 2, 4, 5, 7, 12, 20, 30. Questa è la verifica prevista dal piano: una chiave numerica deve solo crescere.
Ora osserva il bordo sinistro. L'indice non è più 0, 1, 2, …. Riporta invece 5, 2, 7, 1, 3, 0, 6, 4.
Perché l'indice viaggia insieme alla riga
Pandas non ha rinumerato le righe, e questa è una scelta intenzionale. Un'etichetta dell'indice è il nome della riga, non la sua posizione. L'etichetta 5 era l'ordine 1006 prima dell'ordinamento ed è ancora l'ordine 1006 dopo di esso. Se l'ordinamento rinumerasse le righe, ogni etichetta punterebbe improvvisamente a un ordine diverso, e qualsiasi annotazione tu avessi fatto sulla "riga 5" diventerebbe silenziosamente errata.
Quindi, dopo un ordinamento, ci sono due modi diversi per indicare "la prima riga", e non concordano più:
print(by_qty.iloc[0]["order_id"])
print(by_qty.loc[0]["order_id"])1006
1001iloc[0] indica la posizione zero — la prima riga sulla pagina, l'ordine 1006. loc[0] indica la riga con etichetta 0 — l'ordine 1001, che ora è il sesto sulla pagina. Il capitolo quattro ha introdotto questa distinzione; l'ordinamento è l'operazione che la rende fondamentale. Quando vuoi "la prima riga dopo l'ordinamento", usa iloc o head(), entrambi i quali contano le posizioni.
Dal più grande al più piccolo: 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.0I primi tre sono 1005, 1007 e 1001 — esattamente la bozza del piano. Per inviare solo questi tre, aggiungi 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.0Leggendo la catena da sinistra a destra si ritrova il piano: prendi orders, metti la quantità più grande per prima, tieni le prime tre. Questo pattern "ordina, poi head" risponde alla maggior parte delle domande sui "primi N".
Il testo viene ordinato alfabeticamente
Una colonna str si ordina in ordine di dizionario:
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 prima di bottle perché a viene prima di o. Due borse, due bottiglie e due penne — queste sono parità, e torneremo sulla questione di quale elemento della coppia a pari merito compaia per primo.
L'ordinamento restituisce una nuova tabella
Questo è l'errore che quasi tutti fanno almeno una volta, e non genera alcun errore:
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.0Nulla è stato ordinato. sort_values() non modifica orders; costruisce e restituisce un nuovo DataFrame nel nuovo ordine. La prima riga ha creato quella tabella ordinata e, dato che nessuno l'ha memorizzata, l'ha scartata. orders è esattamente ciò che read_csv ha prodotto.
Perché pandas funziona così? Perché anche l'ordine originale è un'informazione. In orders.csv è l'ordine in cui sono arrivati gli ordini. Se ogni operazione di ordinamento riorganizzasse l'unica copia a disposizione, non potresti mai tornare a "come era il file in origine", e un ordinamento scritto per un report cambierebbe silenziosamente i dati in ingresso di quello successivo. Restituire un nuovo oggetto significa che provare a ordinare è sempre un'operazione sicura.
sort_values() non modifica mai la tabella su cui viene chiamato. Se non assegni il risultato, l'ordinamento è stato eseguito e subito scartato — e nessun messaggio ti avvisa.Quindi conservi il risultato, assegnandolo a un nuovo nome oppure allo stesso nome:
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 copia ordinata ha gli ordini grandi in cima; l'originale è intatto. Scrivere orders = orders.sort_values(...) va altrettanto bene quando sei sicuro di non aver più bisogno del vecchio ordine.
Troverai inplace=True nel codice più datato. Modifica la tabella sul posto e restituisce None, il che causa un proprio bug (vedi Quando qualcosa va storto). Con Copy-on-Write in pandas 3, inplace=True non fa risparmiare neanche memoria. Assegnare il risultato è più chiaro: vedi chiaramente sulla riga dove va a finire la tabella ordinata.
Più colonne: priorità e direzione
La seconda richiesta del titolare: "raggruppato per filiale, con il più grande per primo all'interno di ciascuna filiale." Si tratta di due chiavi, e l'ordine della lista rappresenta l'ordine di 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.0Leggilo nel modo in cui pandas lo ha costruito. Innanzitutto, ogni riga viene posizionata in base a branch, dalla A alla Z: east, north, south. La seconda chiave, quantity, viene consultata solo quando la prima chiave produce una parità — all'interno di east, all'interno di north, all'interno di south. All'interno di north l'ordine è 30, 12, 4, 2: dal più grande al più piccolo, perché il secondo elemento di ascending è False.
ascending accetta un valore True/False per ciascuna chiave, nello stesso ordine di by. Un singolo False si applicherebbe a entrambe le chiavi e metterebbe south per prima.
Invertire la lista cambia completamente la domanda:
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.0Ora comanda la quantità, e branch conterebbe solo se due ordini avessero la stessa quantità — cosa che qui non accade. Le filiali sono sparse. Quindi la domanda da porsi prima di scrivere la lista è: "in base a cosa è organizzato il report?" Quella colonna va messa per prima.
Parità: rendi l'ordine una decisione, non un caso
Ordina per price, dal più costoso al meno costoso:
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.0Osserva le due bottiglie a 120. Nel file, l'ordine 1004 precede il 1008. Nella tabella ordinata, 1008 compare per primo. Non c'è nulla di rotto: per una singola chiave, l'algoritmo di ordinamento predefinito di pandas (quicksort) non garantisce di mantenere le righe a pari merito nel loro ordine originale, e qui non lo ha fatto. Le due borse da 850 sono rimaste nell'ordine del file, le due bottiglie si sono scambiate di posto — non c'era alcuna regola che avresti potuto prevedere.
Di solito questo non importa. Comincia a importare nel momento in cui tagli la lista. Se il report richiede "i tre ordini più costosi", il terzo posto è una parità tra 1004 e 1008, e quale dei due entra nella lista è stato deciso dall'algoritmo, non da te. Esegui lo stesso codice su un file leggermente diverso e la risposta può cambiare.
Ci sono due modi per riprendere il controllo di questa decisione.
Opzione 1: un ordinamento stabile. kind="stable" garantisce che le righe a pari merito mantengano l'ordine che avevano prima dell'ordinamento — in questo caso, l'ordine del file:
stable = orders.sort_values("price", ascending=False, kind="stable")
print(stable["order_id"].tolist())[1003, 1006, 1004, 1008, 1002, 1001, 1007, 1005]Opzione 2 (di solito migliore): uno spareggio esplicito. Dichiara cosa deve vincere in caso di parità. Per un report sulle vendite, "a parità di prezzo, l'ordine più grande per primo" è una regola ragionevole:
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.0Guarda di nuovo le bottiglie: 1004 si trova sopra 1008, perché 7 batte 4. E le due penne da 15: prima 1001 veniva per primo; ora vince 1007, perché 20 batte 12 — e vincerà ogni volta, su qualsiasi macchina, a prescindere dall'ordine in cui arriva il file. Un criterio di spareggio trasforma un caso fortuito in una regola che puoi spiegare al titolare. (Quando ordini per più di una colonna, l'ordinamento di pandas è comunque stabile, quindi una lista di chiavi non rimescola mai le vere parità.)
Se non hai una regola di business, order_id è un'ottima ultima chiave: è univoco, quindi dopo di esso non rimane alcuna parità.
Valori mancanti: na_position
Leggi la versione del file con due quantità vuote:
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.0Due cose da notare. Primo, quantity ora viene stampata come 30.0, 12.0 — i vuoti hanno trasformato la colonna in float64, come spiegato nel capitolo sei, poiché NaN è un float. Secondo, le righe con NaN sono finite in fondo, anche se avevamo chiesto di mettere il più grande per primo. Questa è l'impostazione predefinita, na_position="last", e si applica in entrambe le direzioni: un valore mancante non è "piccolo" o "grande", è sconosciuto, quindi pandas lo mette da parte invece di tirare a indovinare.
Nota cosa ha comportato questo per i primi tre ordini. L'ordine 1007 ha effettivamente venduto 20 penne, ma la sua quantità è vuota in questo file, quindi è uscito dai primi tre e il 1004 ha preso il suo posto. Un ordinamento non può correggere i dati mancanti; può solo mostrarti dove sono finiti i vuoti. Ecco perché il punto 2 del piano prevede di contare i valori mancanti prima di ordinare.
Quando stai eseguendo un controllo sui dati, spesso desideri il contrario — i problemi in cima, dove puoi vederli subito:
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.0Quindi na_position non è una scelta di formattazione. "last" è adatto a un report, dove i vuoti non devono oscurare i valori reali. "first" è adatto a una verifica, dove i vuoti sono esattamente ciò che stai cercando.
Ritorno al punto di partenza: sort_index() e reset_index()
Poiché ogni riga ha conservato la propria etichetta, l'ordine originale non è mai andato perduto. Ordinare in base all'indice lo ripristina:
restored = by_qty.sort_index()
print(restored["order_id"].tolist())[1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008]Anche sort_index() accetta ascending=False, ed è così che elencheresti una tabella "dall'etichetta più recente alla prima".
A volte le vecchie etichette non hanno più alcun significato. Se ordini per produrre una classifica da inviare ad altri, le etichette 4, 6, 0 sul lato sinistro creano solo confusione. reset_index(drop=True) le scarta e numera le righe 0, 1, 2, … nel loro nuovo ordine:
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
1005Ora loc[0] e iloc[0] concordano di nuovo: entrambi indicano l'ordine 1005, il più grande. Senza drop=True, pandas conserverebbe le vecchie etichette come una nuova colonna chiamata index — utile se vuoi risalire alle righe originali, disordinato se non ti serve.
Cosa scegliere? Mantieni le etichette finché stai ancora lavorando con i dati e potresti aver bisogno di ricollegare le righe all'originale. Reimpostale quando l'ordine stesso costituisce il risultato — una classifica, un elenco dei primi dieci.
La scorciatoia per i "primi N": nlargest() e nsmallest()
"I primi tre per quantità" è una richiesta così comune che pandas mette a disposizione un metodo che dice esattamente questo:
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.0Le stesse identiche righe di sort_values("quantity", ascending=False).head(3). Perché preferirlo? La riga esprime l'intenzione — "i tre più grandi" — anziché il meccanismo. E su una tabella di grandi dimensioni è più veloce, perché non ha bisogno di ordinare tutte le righe solo per conservarne tre.
nsmallest() è l'esatto speculare: i tre articoli più economici del negozio:
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.0Parità sulla soglia: keep=
Chiedi i tre articoli più costosi e succede qualcosa di imbarazzante:
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.0Il terzo posto è 120 — ma due ordini costano 120, il 1004 e il 1008. pandas ne ha restituito uno solo — con il valore predefinito keep="first", quello che compare per primo nella tabella — e ha silenziosamente escluso l'altro. Se stai assegnando un premio ai "primi tre", questo è ingiusto nei confronti del 1008. keep="all" conserva ogni riga a pari merito sulla soglia di taglio, anche se ciò significa ottenere più di N righe:
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.0Quattro righe per un "podio dei primi tre". Non è un bug: è la risposta corretta e trasparente quando il terzo posto è condiviso. Che tu la voglia o meno è, ancora una volta, una decisione di pianificazione.
nlargest e nsmallest funzionano solo su colonne numeriche. Per il testo, usa sort_values e head.
I primi N di un sottoinsieme: filtra, ordina e taglia
"Quali sono stati i due ordini più grandi nella filiale north?" Questo combina il capitolo cinque con questo, e l'ordine dei passaggi è fondamentale:
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.0Filtra prima, perché la domanda riguarda esclusivamente la filiale north. Se ordinassi l'intera tabella e prendessi prima head(2), otterresti 1005 e 1007 — e 1007 appartiene alla filiale east. Filtrare successivamente lascerebbe una sola riga, e finiresti per presentare dei "primi due" con una sola voce. La regola: restringi il campo alle righe a cui si riferisce la domanda, poi ordinale e infine taglia.
La stessa catena funziona con nlargest: orders[orders["branch"] == "north"].nlargest(2, "quantity").
Ordinare il testo senza considerare maiuscole e minuscole: key=
L'ordinamento del testo confronta i caratteri in base al loro codice, e ogni lettera maiuscola viene prima di qualsiasi lettera minuscola. I dati inseriti dalle persone spesso mescolano le due cose:
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 e Notebook scavalcano bag semplicemente perché iniziano con una lettera maiuscola. Nessun lettore definirebbe questo ordine alfabetico.
Potresti correggere i dati, ma spesso non dovresti farlo — la grafia potrebbe essere esattamente quella usata dal cliente. L'argomento key= ti consente di ordinare in base a una versione trasformata della colonna mantenendo i valori originali. Accetta una funzione che riceve l'intera colonna e restituisce la versione in base a cui effettuare l'ordinamento:
print(names.sort_values("product", key=lambda col: col.str.lower()))product
2 bag
3 Bottle
4 eraser
1 Notebook
0 penLa copia in minuscolo viene utilizzata solo per il confronto e poi scartata. La tabella mostra ancora Bottle e Notebook esattamente come sono stati digitati. (lambda col: col.str.lower() è una funzione di una riga: data la colonna col, la restituisce in minuscolo.)
Quando i numeri sono memorizzati come testo
Ecco la trappola che il passaggio di pianificazione sui dtypes serve a evitare. Supponi che quantity sia arrivata come testo — un "N/A" isolato nel file, o un sistema che esporta qualsiasi cosa come stringa. Per vedere cosa comporta, creiamo una colonna del genere appositamente con 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. Nessun errore, e un ordine che sembra quasi corretto. Il testo viene confrontato carattere per carattere: "12" inizia con 1, che è minore di 2, quindi "12" viene prima di "2" — esattamente come "ab" viene prima di "b" in un dizionario. L'ordinamento ha fatto esattamente quello che gli è stato chiesto. Ma gli è stata chiesta la cosa sbagliata.
Due indizi rivelano l'inganno: quantity è allineata a sinistra nella stampa, che è il modo in cui pandas stampa il testo, e il dtype indica str. La vera soluzione è convertire la colonna in numeri (pd.to_numeric del capitolo sei). Se devi lasciare la colonna così com'è, key= può ordinare in base al valore numerico:
fixed = as_text.sort_values("quantity", key=pd.to_numeric)
print(fixed["quantity"].tolist())['1', '2', '4', '5', '7', '12', '20', '30']Ora l'ordine è corretto — ma i valori sono ancora stringhe, quindi non potresti sommarli. È preferibile convertire la colonna una volta sola, subito dopo aver letto il file. In questo modo ogni ordinamento, filtro e somma successivi saranno corretti senza doverti ricordare di intervenire ogni volta.
Un esempio completo
La richiesta completa del titolare, soddisfatta in un unico programma: (1) i primi tre ordini per incasso generato, (2) gli ordini più recenti per primi, (3) un elenco per filiale con l'ordine più grande per primo in ciascuna.
L'"incasso generato" è la quantità moltiplicata per il prezzo. L'aggiunta di colonne viene trattata in dettaglio più avanti nel corso; per ora è sufficiente una sola riga — l'assegnazione a un nuovo nome di colonna la crea automaticamente.
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: TruePerché è scritto in questo modo:
- La verifica viene prima.
shapee il conteggio dei vuoti vengono stampati prima di qualsiasi classifica. Una top tre calcolata da un file letto a metà, o con i vuoti spinti in fondo, apparirebbe altrettanto convincente e sicura di sé. - "Più grande" è stato trasformato in una colonna. Ordinare per
quantitymetterebbe le 30 gomme per cancellare in cima — 240 di fatturato. Ordinare perrevenuemette in cima le due borse del valore di 1.700. Il primo punto del piano — scrivi la domanda indicando la colonna — è ciò che determina quale risposta riceverà il titolare. - Ogni ordinamento ha un criterio di spareggio. Il 2024-03-04 ci sono due ordini;
revenuedecide quale viene elencato per primo, così il report è identico a ogni esecuzione. - Solo la classifica viene rinumerata.
top3è una lista definitiva, quindi le sue etichette diventano 0, 1, 2.newesteby_branchmantengono le loro etichette originali, così qualsiasi riga può ancora essere ricollegata al file. ordersnon viene mai riordinato direttamente. Ogni report è una nuova tabella costruita a partire dagli stessi dati originali intatti, quindi le tre risposte non possono interferire tra loro.- L'ultima riga è un'asserzione a basso costo. Un ordinamento non deve mai modificare il numero di righe. Stampare
Truenon costa nulla e intercetta, ad esempio, unhead()rimasto per errore nel posto sbagliato.
Quando qualcosa va storto
KeyError: 'Quantity'
orders.sort_values("Quantity")KeyError: 'Quantity'Il nome in by= deve corrispondere esattamente a una colonna — maiuscole, minuscole, spazi e tutto il resto. La colonna è quantity, minuscola. Stampa list(orders.columns) per visualizzare i nomi reali, comprese virgolette ed eventuali spazi residui, come consigliato nel capitolo tre.
AttributeError: 'NoneType' object has no attribute 'head'
top = orders.sort_values("quantity", inplace=True)
top.head()AttributeError: 'NoneType' object has no attribute 'head'Con inplace=True, sort_values modifica orders e restituisce None. Di conseguenza top vale None, e None non possiede alcun metodo head(). Scegli un unico stile: o orders.sort_values("quantity", inplace=True) su una riga a sé stante, o — preferibilmente — top = orders.sort_values("quantity") senza 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)Una lista per ascending richiede un elemento per ciascuna chiave. Due chiavi, due booleani: ascending=[True, False]. (Un singolo False semplice, non all'interno di una lista, è consentito — si applica a ogni chiave.)
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" tra virgolette è una stringa, non il booleano False. Rimuovi le virgolette.
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 e nsmallest ordinano solo numeri. Per "le ultime tre filiali in ordine alfabetico" usa orders.sort_values("branch", ascending=False).head(3). Se hai riscontrato questo errore su una colonna che ti aspettavi fosse numerica, il vero problema è che è stata letta come testo — controlla 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'Questa colonna contiene numeri reali e una stringa di testo, quindi il suo dtype è object, e Python non è in grado di stabilire se "unknown" sia maggiore o minore di 12. Pulisci prima la colonna: pd.to_numeric(mixed, errors="coerce") trasforma "unknown" in NaN, dopodiché l'ordinamento funziona e na_position decide dove collocare il valore vuoto.
Passo 4 di 6 — Prevedi
Verifica cosa hai capito
Due borse condividono il prezzo più alto, 850.0. 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))
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'obiettivo è stampare l'ordine con il maggior numero di articoli. 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))
orders.sort_values("quantity", ascending=False)
print(orders.iloc[0]["order_id"])- A1001 — `sort_values` ha restituito una copia ordinata e nessuno l'ha salvata
- B1007
- CUn `TypeError`: il risultato deve essere assegnato
- D1005
Vuoi gli ordini raggruppati per filiale dalla A alla Z, e all'interno di ogni filiale la quantità maggiore per prima. Quale riga ottiene questo risultato?
- 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])
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
Un insegnante ti invia i voti di una prova. Salva questo contenuto come results.csv. Due studenti non hanno ancora un voto, e alcuni nomi sono stati digitati in minuscolo:
name,section,marks
Rafi,B,78
anika,A,91
Tanvir,A,
Mitu,B,91
zara,A,65
Karim,B,
Nadia,A,88
Sabbir,B,54Scrivi ranking.py. Prima di qualsiasi riga di codice, scrivi il piano sotto forma di commenti in cima al file: per ogni elenco indica la colonna chiave, la direzione, come vengono risolte le parità e dove vanno i valori vuoti. Poi fai in modo che il programma soddisfi queste quattro condizioni:
- Prima di ordinare qualsiasi cosa, stampa
shape,dtypese il numero di voti mancanti. - Stampa una graduatoria di merito: il voto più alto per primo, gli studenti senza voto in fondo, le parità di voto risolte in base al nome in ordine alfabetico senza considerare maiuscole e minuscole, e le righe rinumerate 0, 1, 2, …
- Stampa i primi tre con
nlargest, conservando tutti coloro che si trovano a pari merito al terzo posto. - Stampa gli studenti senza voto, ordinati per nome senza considerare maiuscole e minuscole, così che l'insegnante possa contattarli — e infine la tabella originale, per dimostrare che non è mai stata modificata.
Il tuo programma dovrebbe stampare esattamente questo:
(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.0Poi prova due esperimenti sulla graduatoria di merito:
- Rimuovi
key=. Quale dei due studenti con 91 viene per primo adesso, e perché? - Rimuovi del tutto
"name"dalle chiavi e ordina solo permarks. Chi compare per primo a 91 — e questo risultato è garantito?
Soluzione
Il piano. Il file contiene 8 studenti e 3 colonne. marks presenta valori vuoti, quindi verrà letto come float64, e ogni elenco che ordina per voti deve specificare dove collocare i vuoti. name mescola maiuscole e minuscole, quindi ogni confronto tra nomi richiede key=.
- Graduatoria di merito — chiavi
marks, poiname; voti dal più alto al più basso, nomi dalla A alla Z; una parità sui voti viene risolta in base al nome; valori vuoti per ultimi (comportamento predefinito); rinumerata conreset_index(drop=True). - Primi tre —
nlargest(3, "marks")conkeep="all", affinché un terzo posto condiviso non venga tagliato.nlargestesclude automaticamente i vuoti. - Senza voto — filtra le righe in cui
marksè mancante, come nei capitoli precedenti, poi ordina per nome. Prima filtra, poi ordina. - Originale — mai riassegnato, quindi stampare
resultsalla fine mostra l'ordine del file.
Un dettaglio: una funzione key= viene applicata a ogni colonna chiave. Per la graduatoria di merito questo include anche marks, e .str.lower() fallirebbe sui numeri. Quindi la funzione converte in minuscolo solo le colonne di testo e restituisce qualsiasi altra colonna invariata.
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.0Confrontiamo l'output con il piano:
marksèfloat64, nonint64, eMissing marks: 2— i vuoti sono stati individuati prima di qualsiasi ordinamento, così chi legge la graduatoria sa che due studenti sono assenti da essa, non in fondo alla lista.- La parità a 91 viene decisa in base al nome.
anikaviene prima diMituperché il confronto è stato effettuato tra"anika"e"mitu". - I primi tre contengono tre righe in questo caso, perché il terzo posto (88) non è condiviso. Se anche Nadia avesse avuto 91,
keep="all"avrebbe restituito quattro righe anziché escludere silenziosamente uno studente. - L'elenco degli studenti senza voto prima filtra e poi ordina — lo stesso ordine "restringi il campo, poi ordina" del pattern dei primi N di un sottoinsieme.
- L'ultima tabella rispecchia l'ordine del file. Sono state prodotte tre liste, e
resultsnon è mai stato riassegnato.
Gli esperimenti:
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']Senza key=, i nomi vengono confrontati carattere per carattere, e ogni lettera maiuscola precede qualsiasi lettera minuscola, quindi "Mitu" batte "anika" — un ordine basato su come è stato digitato un nome, non sull'alfabeto. Con marks da solo, anika compare prima semplicemente perché si trova prima nel file. Si tratta della stessa risposta della graduatoria corretta, ma solo per puro caso: con una singola chiave l'ordinamento predefinito non garantisce di conservare le righe a pari merito nell'ordine del file, come hanno mostrato prima le due bottiglie a 120. Una regola che hai definito esplicitamente è garantita; un risultato che sembra semplicemente corretto non lo è.
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