Filtrare le righe — fare a ogni riga la stessa domanda
Conservare solo le righe che rispondono sì: maschere booleane, combinarle con &, | e ~, isin, between e i metodi .str, righe filtrate con .loc — e modificare solo quelle righe senza l'assegnamento a catena che silenziosamente non fa nulla.
Come vuoi imparare?
Il problema che stiamo risolvendo
Il responsabile del negozio invia un messaggio: «Quali ordini della filiale north contenevano più di tre articoli? Voglio vederli.»
Con gli otto ordini del capitolo precedente potresti rispondere a occhio. Fai scorrere il dito lungo la colonna branch, ti fermi a ogni north, dai un'occhiata a quantity. Tre righe. Fatto.
Ora immagina che sia il file reale — dodicimila ordini, un anno intero. Il tuo occhio non funziona più, e la prima idea che viene alla maggior parte dei programmatori Python è un ciclo:
import pandas as pd
orders = pd.read_csv("orders.csv")
keep = []
for i in range(len(orders)):
row = orders.iloc[i]
if row["branch"] == "north" and row["quantity"] > 3:
keep.append(int(row["order_id"]))
print(keep)[1001, 1005, 1008]La risposta è corretta. Ma guarda cosa è costata: un contatore, una lista vuota, un if, un append, e la tabella smontata una riga alla volta. E il risultato non è più una tabella — è una semplice lista di numeri d'ordine. Per mostrare al responsabile anche i prodotti e i prezzi, dovresti fare un altro ciclo. Su dodicimila righe questo approccio è anche lento, perché ogni orders.iloc[i] crea una nuova Series solo per leggerne due valori.
Pandas ha un modo diverso di fare domande, ed è l'operazione più comune che farai mai con un DataFrame: fai a ogni riga la stessa domanda con risposta sì/no contemporaneamente, poi tieni le righe che hanno risposto sì. Questo è il filtraggio, ed è l'argomento di questo intero capitolo. Il ciclo qui sopra diventa una sola riga, il risultato rimane una tabella e si legge quasi come la frase del responsabile.
Alla fine di questo capitolo sarai in grado di
- Trasformare una domanda in una maschera booleana — una colonna di
True/False, uno per riga — e usarla per conservare le righe o per contarle conmask.sum()emask.mean() - Combinare le condizioni con
&,|e~, e spiegare perché le parentesi sono obbligatorie - Usare
isin,betweene i metodi.strper liste, intervalli e pattern di testo - Filtrare le righe e scegliere le colonne in un solo passaggio con
.loc[mask, columns] - Modificare i valori nelle righe filtrate in modo sicuro con
.loc[mask, column] = value, e riconoscere ilChainedAssignmentErrorprodotto dal metodo non sicuro in pandas 3
Prerequisiti: Selezionare colonne e righe.
Crea prima il file
Questo capitolo usa lo stesso file orders.csv di quello precedente. Se non ce l'hai, crealo nella cartella del tuo progetto con esattamente queste nove 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.0Prima di scrivere il codice
Un filtro è una domanda scritta in codice. La maggior parte dei bug di filtraggio non sono bug di pandas — sono una domanda che non è mai stata definita con chiarezza, o una colonna che non contiene ciò che avevi ipotizzato. Quindi, prima della prima riga di filtraggio, fai queste cinque cose.
1. Scrivi la domanda sotto forma di condizioni, una per colonna. Scomponi la frase del responsabile:
- «ordini della filiale north» → colonna
branch, verifica di uguaglianza, valore"north" - «più di tre articoli» → colonna
quantity, verifica maggiore di, valore3 - «voglio vederli» → nessuna verifica; conserva l'intera riga
Questo elenco ti costringe a prendere due decisioni. «Più di tre» è > 3, non >= 3 — un ordine di esattamente tre articoli non conta. E le due condizioni sono unite da and: un ordine deve soddisfarle entrambe. Queste due decisioni costituiscono l'intero filtro; il codice è solo il modo di scriverle.
2. Controlla i dati in ingresso — forma, tipi e i valori esatti con cui farai il confronto.
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(orders.dtypes)(8, 7)
order_id int64
date str
branch str
product str
category str
quantity int64
price float64
dtype: objectquantity è int64, quindi > 3 confronterà numeri. Se fosse arrivato come str — perché qualcuno ha digitato "3 pcs" in una cella — il confronto fallirebbe con un TypeError, e vuoi saperlo adesso, non a metà lavoro. branch è str, quindi verrà confrontato come testo, e il confronto tra testi è esatto: "north", "North" e "north " sono tre valori diversi. Guarda quindi i valori che esistono realmente:
print(orders["branch"].unique())<StringArray>
['north', 'south', 'east']
Length: 3, dtype: strTre filiali, ciascuna scritta in un solo modo, senza spazi spuri. Ora c'è la garanzia che nel codice "north" troverà una corrispondenza.
3. Abbozza l'output prima di produrlo. Su un file piccolo, rispondi prima alla domanda a mano. Gli ordini della filiale north sono 1001 (12 articoli), 1003 (2), 1005 (30) e 1008 (4). Più di tre articoli: 1001, 1005, 1008. Il risultato atteso è quindi di 3 righe, tutte e 7 le colonne, e i loro numeri d'ordine sono noti. Se in seguito il codice restituisce 4 righe o 0, saprai subito che il codice è sbagliato anziché fidartene ciecamente. Su un file grande non puoi fare tutto questo a mano, ma puoi comunque fare una stima: «la filiale north raccoglie circa la metà degli ordini, quindi la risposta dovrebbe essere ben al di sotto della metà delle righe».
4. Scegli lo strumento in base alla forma della condizione.
- un singolo confronto → una maschera:
df["col"] > value - diverse condizioni → maschere unite con
&(and),|(or),~(not) - «uno di questi valori» →
df["col"].isin([...]) - «compreso tra due valori» →
df["col"].between(low, high) - un pattern all'interno del testo →
df["col"].str.contains(...),.str.startswith(...) - righe e solo alcune colonne →
df.loc[mask, ["col1", "col2"]] - modificare i valori in quelle righe →
df.loc[mask, "col"] = new_value
La domanda del responsabile consiste in due confronti uniti da and, quindi: due maschere e &.
5. Decidi come verificherai la risposta. Dopo il filtraggio, tre verifiche rapide ed economiche intercettano quasi tutto: il numero di righe corrisponde al tuo abbozzo; la condizione è davvero soddisfatta in ogni riga superstite ((result["quantity"] > 3).all() dovrebbe stampare True); e non manca nulla di ciò che ti aspettavi (cerca 1008 nel risultato).
Ora passiamo al codice — un'idea alla volta.
Un confronto restituisce una maschera
Confronta una colonna con un valore, e pandas non ti restituirà un solo True o False. Confronta ogni riga e ti restituisce un'intera colonna di risposte:
big = orders["quantity"] > 5
print(big)0 True
1 False
2 False
3 True
4 True
5 False
6 True
7 False
Name: quantity, dtype: boolQuesta è una Series di bool — una maschera booleana. Ha lo stesso indice di orders (da 0 a 7), con una risposta per riga: la riga 0 ha 12 articoli, quindi True; la riga 1 ne ha 5, e 5 non è maggiore di 5, quindi False.
Perché pandas funziona così, invece di farti usare un ciclo? Perché una colonna è memorizzata come un unico blocco di numeri di un solo tipo, e confrontare un intero blocco con 5 è una singola operazione eseguita in codice compilato e veloce. Il tuo ciclo chiedeva a Python di esaminare ogni riga separatamente, otto volte — o dodicimila volte. La maschera fa la domanda una volta sola.
Ogni operatore di confronto funziona nello stesso modo: ==, !=, <, <=, >, >=. Funzionano anche sulle colonne di testo — orders["branch"] == "north" è una maschera con True nelle righe 0, 2, 4 e 7.
Finora non è stato filtrato nulla. Una maschera contiene solo le risposte. Il passo successivo è usarle.
La maschera conserva le righe
Inserisci la maschera tra parentesi quadre e pandas conserverà le righe in cui la maschera è True:
print(orders[big])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
3 1004 2024-03-02 east bottle accessories 7 120.0
4 1005 2024-03-03 north eraser stationery 30 8.0
6 1007 2024-03-04 east pen stationery 20 15.0Quattro righe su otto. Leggi l'indice a sinistra: 0, 3, 4, 6. Le righe superstiti mantengono le loro etichette originali. Il filtraggio non rinumera nulla — la riga 3 è ancora la riga 3, lo stesso ordine 1004 che era nella tabella completa. Questo si rivelerà importante, e ci torneremo sopra.
In che modo pandas allinea la maschera con le righe? Non per posizione — ma per etichetta dell'indice. L'etichetta 0 della maschera decide per la riga 0, l'etichetta 3 decide per la riga 3. Qui i due indici sono identici, perché la maschera è stata creata a partire da orders stesso, quindi la cosa è invisibile. Diventa visibile solo quando usi una maschera creata da una tabella diversa — lo vedremo in «Quando qualcosa va storto».
Di solito vedrai la maschera scritta inline, senza assegnarle un nome:
print(orders[orders["branch"] == "north"])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
2 1003 2024-03-02 north bag accessories 2 850.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0Leggila dall'interno verso l'esterno: `orders["branch"] == "north"` costruisce la maschera, `orders[ … ]` conserva le righe con True. Assegnare un nome alla maschera (big = …) conviene quando la condizione è lunga o quando la usi due volte; inline va benissimo per condizioni brevi.
Contare con una maschera, senza filtrare
Spesso il responsabile non vuole le righe, ma solo un numero: quanti ordini contenevano più di cinque articoli? Non c'è bisogno di filtrare per saperlo. In aritmetica, True vale 1 e False vale 0, quindi:
print(big.sum())
print(big.mean())4
0.5sum() somma gli uno: 4 ordini. mean() divide per il numero di righe: 4 su 8 fa 0.5, quindi la metà degli ordini conteneva più di cinque articoli. «Quanti» è .sum() di una maschera; «quale quota» è .mean() di una maschera. Entrambi richiedono una sola riga e nessuno dei due crea una nuova tabella.
E quando vuoi un totale sulle righe filtrate — il numero di articoli in quei grandi ordini — prima filtri, poi selezioni la colonna, poi sommi:
print(orders[big]["quantity"].sum())6912 + 7 + 30 + 20 = 69. Leggere una colonna da una tabella filtrata, in questo modo, va benissimo. Scriverci dentro in questo modo invece no — questo è l'argomento dell'ultima sezione del capitolo.
Più condizioni: &, |, ~
La domanda del responsabile presenta due condizioni. Pandas unisce le maschere con tre operatori:
&significa and — la riga viene conservata quando entrambe le maschere sonoTrue|significa or — la riga viene conservata quando almeno una maschera èTrue~significa not — inverte ogniTrueinFalsee viceversa
Ecco finalmente la domanda del responsabile:
north_big = orders[(orders["branch"] == "north") & (orders["quantity"] > 3)]
print(north_big)order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0Tre righe: 1001, 1005, 1008 — esattamente l'abbozzo della fase di pianificazione. Ora la verifica del passaggio 5:
print(len(north_big))
print((north_big["quantity"] > 3).all())
print((north_big["branch"] == "north").all())3
True
TrueIl conteggio corrisponde ed entrambe le condizioni sono soddisfatte in ogni riga superstite. .all() chiede: «ogni valore in questa maschera è True?» — è il modo più economico per dimostrare che un filtro ha fatto esattamente ciò che intendevi.
Una domanda con or: ordini della filiale east, oppure qualsiasi ordine di articoli che costano 850 o più.
print(orders[(orders["branch"] == "east") | (orders["price"] >= 850)])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0Quattro righe: i due ordini della filiale east e i due ordini di borse da north e south. Una riga che avesse soddisfatto entrambe le condizioni (qui nessuna) comparirebbe una sola volta, non due — un filtro conserva o scarta una riga, niente di più.
Una domanda con not: tutto tranne la cancelleria.
print(orders[~(orders["category"] == "stationery")])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
7 1008 2024-03-04 north bottle accessories 4 120.0~ trasforma ogni True in False e viceversa. Per una singola uguaglianza potresti scrivere semplicemente !=, che è più lineare; ~ mostra il suo vero valore quando ciò che stai negando è una condizione più lunga o un isin (lo vedrai tra poco).
Perché le parentesi sono obbligatorie
Ogni condizione qui sopra si trova all'interno delle proprie parentesi. Non è una questione di stile. Toglile e il codice smetterà di funzionare:
print(orders[orders["quantity"] > 3 & orders["price"] < 100])TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool]Quella che vedi sopra è l'ultima riga del traceback, la riga che dà un nome all'errore; le righe precedenti indicano solo dove si è verificato. Il motivo è la precedenza degli operatori — l'ordine in cui Python applica gli operatori. In Python, & si lega più strettamente di > e <. È stato stabilito molto prima che esistesse pandas, perché & è nato per i numeri interi ("and" bit a bit), dove deve avere la precedenza. Quindi Python interpreta la riga come:
orders["quantity"] > (3 & orders["price"]) < 100Tenta di calcolare prima 3 & orders["price"] — un "and" bit a bit tra il numero 3 e una colonna di decimali — ed è proprio questo il motivo dell'errore. Il messaggio menziona rand_ (il "right-hand and") e un array, ma la causa è sempre la stessa: una coppia di parentesi mancante.
L'errore è lo scenario fortunato. Se racchiudi tra parentesi solo la prima condizione e dimentichi la seconda, non si verificherà alcun errore:
print(len(orders[(orders["quantity"] > 3) & orders["price"] < 100]))
print(len(orders[(orders["quantity"] > 3) & (orders["price"] < 100)]))8
4La prima riga viene letta come ((quantity > 3) & price) < 100. L'& mescola una maschera con i prezzi, e ogni risultato risulta casualmente inferiore a 100, quindi il "filtro" conserva tutte e otto le righe. Il filtro corretto ne conserva quattro. Stesse colonne, stessi numeri, una sola coppia di parentesi mancante — e la versione sbagliata viene eseguita senza emettere un suono. Racchiudi ogni condizione tra le proprie parentesi, ogni singola volta. Così non potranno verificarsi né l'errore né la versione silenziosa errata.
Perché non and?
L'altro istinto naturale è scrivere la parola inglese:
print(orders[(orders["branch"] == "north") and (orders["quantity"] > 3)])ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().Le parole chiave and, or e not di Python richiedono un singolo True o False su ciascun lato — sono state progettate per le istruzioni if. Per usare and, Python chiede alla prima maschera: «sei vera?», e una colonna di otto risposte non può rispondere con una sola. È vera se qualsiasi valore è True? Se lo sono tutti? Pandas si rifiuta di tirare a indovinare, e l'errore elenca i modi con cui potresti decidere (a.any(), a.all()). Ma tu non volevi una sola risposta; volevi un "and" riga per riga. Questo è &. La regola da ricordare: all'interno di un filtro, and/or/not diventano &/|/~.
Uno tra più valori: isin
Ordini della filiale south o east. Potresti scrivere due uguaglianze unite da |. Con cinque filiali diventerebbero cinque confronti. isin accetta una lista e chiede: «il valore di questa riga è presente nella lista?»:
print(orders[orders["branch"].isin(["east", "south"])])order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0Ed è qui che ~ mostra la sua utilità — ogni filiale tranne north ed east si scrive con ~ davanti a isin:
print(orders[~orders["branch"].isin(["north", "east"])]["order_id"].tolist())[1002, 1006]Due ordini, entrambi della filiale south. Nota che ~orders["branch"].isin([...]) non ha bisogno di parentesi aggiuntive: ~ si applica al risultato della chiamata del metodo, e non ci sono & o | con cui entrare in conflitto. Non appena lo combini con un'altra condizione, le parentesi tornano a essere necessarie: (~orders["branch"].isin([...])) & (orders["quantity"] > 3).
Un intervallo: between
Ordini con prezzo compreso tra 15 e 120. Si tratta di due confronti, >= 15 e <= 120. between lo esprime in una sola volta:
print(orders[orders["price"].between(15, 120)])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
6 1007 2024-03-04 east pen stationery 20 15.0
7 1008 2024-03-04 north bottle accessories 4 120.0Di default between include entrambi gli estremi — le penne a esattamente 15.0 e le bottiglie a esattamente 120.0 sono incluse. Questo corrisponde al modo in cui si intende abitualmente «da 15 a 120», ma verificalo sempre rispetto alla richiesta. Se la richiesta intende un intervallo strettamente compreso tra i due valori, specificalo:
print(orders["price"].between(15, 120, inclusive="neither").sum())1Solo il quaderno a 60.0 è strettamente compreso tra gli estremi. inclusive accetta anche "left" e "right" per intervalli semiaperti — la scelta consueta per le fasce di prezzo, dove vuoi 0–100, 100–200 e così via senza che un valore di confine finisca in due fasce contemporaneamente.
Pattern di testo: i metodi .str
L'uguaglianza sul testo è esatta. L'accessor .str mette a disposizione di una colonna di testo i metodi per stringhe che già conosci da Python, applicandoli a ogni riga, e ciascuno restituisce una maschera.
Prodotti il cui nome inizia con "b":
print(orders[orders["product"].str.startswith("b")])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
7 1008 2024-03-04 north bottle accessories 4 120.0Prodotti che contengono la lettera "o", indipendentemente da maiuscole o minuscole:
print(orders[orders["product"].str.contains("O", case=False)])order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
7 1008 2024-03-04 north bottle accessories 4 120.0Non c'è nessuna O maiuscola nei dati, eppure il quaderno e le due bottiglie vengono trovati, perché case=False ignora la distinzione tra maiuscole e minuscole. Senza questo parametro, .str.contains("O") non troverebbe nulla.
L'uguaglianza esatta non ha un'opzione del genere. Questo è il bug più comune del tipo «il mio filtro non restituisce nulla»:
print(orders[orders["branch"] == "North"])Empty DataFrame
Columns: [order_id, date, branch, product, category, quantity, price]
Index: []Nessun errore, nessuna riga. La tabella non è sbagliata e pandas non è sbagliato — semplicemente non esiste alcuna filiale scritta come "North" con l'iniziale maiuscola. Ecco perché il passaggio 2 del piano stampava unique(): i valori con cui fai il confronto dovrebbero essere copiati da ciò che è effettivamente presente nella colonna, non digitati a memoria.
Le date come testo si confrontano correttamente — se sono in formato ISO
La colonna date è stata importata come str. Puoi filtrarla con >=? Ordini dal 3 marzo in poi:
print(orders["date"] >= "2024-03-03")0 False
1 False
2 False
3 False
4 True
5 True
6 True
7 True
Name: date, dtype: boolFunziona, ma cerchiamo di capire perché. Si tratta di stringhe, quindi >= le confronta come testo, carattere per carattere, proprio come nell'ordine alfabetico di un dizionario. Questo fornisce la risposta corretta solo perché le date sono scritte nel formato anno-mese-giorno con zeri di riempimento (2024-03-03), quindi l'ordine testuale coincide con l'ordine temporale. Una colonna scritta come 3/3/2024 verrebbe confrontata in modo errato — "10/1/2024" viene ordinato prima di "9/1/2024" perché "1" viene prima di "9". Per qualsiasi esigenza che vada oltre i semplici valori limite, converti in vere date con pd.to_datetime, argomento trattato nel capitolo sull'aggiunta di colonne.
Righe e colonne insieme: .loc[mask, columns]
Finora i filtri hanno conservato ogni colonna. Il responsabile voleva gli ordini della filiale north, ma per il report servono solo il prodotto e la quantità. Nel capitolo precedente hai visto .loc[rows, columns] con le etichette. La parte relativa alle righe accetta anche una maschera:
print(orders.loc[orders["branch"] == "north", ["product", "quantity"]])product quantity
0 pen 12
2 bag 2
4 eraser 30
7 bottle 4Un solo passaggio, un solo oggetto, e chi legge il tuo codice vede l'intera domanda — quali righe, quali colonne — in un unico punto. .loc[mask, "quantity"] (un singolo nome di colonna, non una lista) ti restituisce invece una Series, pronta per le operazioni aritmetiche:
print(orders.loc[orders["branch"] == "north", "quantity"].sum())4812 + 2 + 30 + 4 = 48 articoli venduti dalla filiale north.
query(): lo stesso filtro sotto forma di frase
Esiste un secondo modo di scrivere i filtri, query(), che accetta la condizione sotto forma di stringa:
print(orders.query("branch == 'north' and quantity > 3"))order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0Le stesse tre righe. All'interno della stringa scrivi i normali nomi delle colonne, e and/or/not sono consentiti, perché query analizza la stringa autonomamente invece di affidarla agli operatori di Python. Per usare una variabile Python, anteponi il carattere @:
min_qty = 5
print(orders.query("quantity >= @min_qty")["order_id"].tolist())[1001, 1002, 1004, 1005, 1007]query è molto leggibile per condizioni lunghe. I suoi svantaggi: un refuso all'interno della stringa viene scoperto solo quando la riga viene eseguita, l'editor non può aiutarti al suo interno e i nomi di colonna contenenti spazi richiedono i backtick. Questo corso usa le maschere come strumento principale, perché su di esse si basa tutto il resto in pandas: .loc, l'assegnazione, il conteggio. È utile conoscere query per poterlo leggere nel codice scritto da altri.
La tabella filtrata conserva le sue vecchie etichette
Torniamo all'indice. Salva gli ordini della filiale north e osservali:
north = orders[orders["branch"] == "north"]
print(north.index.tolist())[0, 2, 4, 7]Etichette 0, 2, 4, 7. Quindi che cos'è north.loc[1]? In questa tabella non esiste alcuna etichetta 1 — l'ordine 1002 apparteneva alla filiale south ed è stato escluso dal filtro:
print(north.loc[1])KeyError: 1.loc cerca un'etichetta, e qui l'etichetta 1 non esiste. Se intendevi «il secondo ordine della filiale north», si tratta di una posizione, e le posizioni si gestiscono con .iloc:
print(north.iloc[1])order_id 1003
date 2024-03-02
branch north
product bag
category accessories
quantity 2
price 850.0
Name: 2, dtype: objectLa seconda riga per posizione è l'ordine della borsa — e il suo Name è 2, la sua etichetta nella tabella originale. È esattamente la distinzione tra loc e iloc del capitolo precedente, ed è nel filtraggio che si fa sentire, perché dopo un filtro etichette e posizioni smettono di coincidere.
Conservare le etichette è una scelta deliberata: permette di risalire da qualsiasi riga del risultato alla tabella originale. Quando questo non serve più — ad esempio se la tabella filtrata è il report finale — rinumerala:
print(north.reset_index(drop=True))order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
1 1003 2024-03-02 north bag accessories 2 850.0
2 1005 2024-03-03 north eraser stationery 30 8.0
3 1008 2024-03-04 north bottle accessories 4 120.0drop=True elimina le vecchie etichette. Senza questo parametro, verrebbero conservate come una nuova colonna chiamata index, cosa che raramente si desidera in un report.
Un risultato vuoto non è un errore
none = orders[orders["branch"] == "west"]
print(none)
print(none.shape)
print(none.empty)Empty DataFrame
Columns: [order_id, date, branch, product, category, quantity, price]
Index: []
(0, 7)
TrueNon esiste alcuna filiale west, quindi nessuna riga — e nessun errore. È il comportamento corretto (zero è una risposta valida), ma significa che un filtro con un errore di battitura fallisce silenziosamente. Le somme su un risultato vuoto restituiscono 0 e le medie nan, valori che possono propagarsi a lungo in un report prima che qualcuno se ne accorga. Quando un filtro alimenta qualcosa di importante, controlla result.empty o il numero di righe, ed esplicitalo chiaramente nell'output: "No orders from the west branch." è preferibile a una tabella vuota.
Modificare le righe filtrate: .loc[mask, column] = value
A volte filtri per poter modificare qualcosa. I prezzi delle penne vendute dalla filiale east erano errati; avrebbero dovuto costare 13.5, non 15.0. La tentazione è eseguire il filtro e poi selezionare la colonna — due coppie di parentesi quadre, proprio come per leggere i valori in precedenza:
import pandas as pd
orders = pd.read_csv("orders.csv")
orders[orders["branch"] == "east"]["price"] = 13.5
print(orders["price"].tolist())[15.0, 60.0, 850.0, 120.0, 8.0, 850.0, 15.0, 120.0]
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment.
Such chained assignment never works to update the original DataFrame or Series, because the intermediate object on which we are setting values always behaves as a copy (due to Copy-on-Write).
Try using '.loc[row_indexer, col_indexer] = value' instead, to perform the assignment in a single step.
See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.html#chained-assignmentLeggi i prezzi: non è cambiato nulla. E pandas ti ha avvisato, a lungo. (In un terminale l'avviso va su stderr, quindi compare sopra la lista, nel momento in cui la riga di assegnazione viene eseguita, e inizia con il nome del file e il numero di riga. Qui è mostrato dopo l'output, senza di essi.) Questo è l'assegnamento a catena (chained assignment) — due passaggi di indicizzazione, orders[mask] e poi ["price"], con l'assegnazione alla fine della catena. Il primo passaggio, orders[mask], crea una nuova tabella contenente le righe della filiale east. Il secondo passaggio imposta i prezzi in quella nuova tabella. Poi la nuova tabella viene scartata, perché nulla la trattiene in memoria. orders in sé non è mai stato toccato.
In pandas 3 questo è un comportamento garantito, chiamato Copy-on-Write: qualsiasi tabella ottenuta tramite indicizzazione si comporta come una copia a sé stante, quindi scrivere al suo interno non può mai riflettersi sull'originale. Il ChainedAssignmentError è un avviso (warning), non un'eccezione — il programma continua l'esecuzione — ed è proprio per questo che è pericoloso: uno script con questa riga termina con successo apparente, ma con i vecchi prezzi ancora al loro posto.
(Se leggi tutorial più vecchi, vedrai questa situazione descritta con un SettingWithCopyWarning e una nota in cui si dice che «potrebbe funzionare o meno». In pandas 3 la risposta è semplice: non funziona mai, quindi non scriverlo mai.)
La soluzione è il consiglio stesso fornito dall'avviso: un solo passaggio di indicizzazione, .loc[rows, column], con l'assegnazione applicata direttamente:
orders.loc[orders["branch"] == "east", "price"] = 13.5
print(orders["price"].tolist())[15.0, 60.0, 850.0, 13.5, 8.0, 850.0, 13.5, 120.0]Aspetta — questo ha modificato entrambi gli ordini della filiale east, sia la penna che la bottiglia. Il prezzo della bottiglia non era errato. Il codice ha fatto esattamente ciò che gli è stato chiesto; era la domanda nel codice a essere sbagliata. Qui ritorna il passaggio 1 del piano: «penne vendute dalla filiale east» consiste in due condizioni, e ne era stata scritta una sola. Ripartiamo da una copia pulita del file e scriviamole entrambe:
import pandas as pd
orders = pd.read_csv("orders.csv")
fix = (orders["branch"] == "east") & (orders["product"] == "pen")
print(fix.sum())
orders.loc[fix, "price"] = 13.5
print(orders.loc[orders["branch"] == "east", ["product", "price"]])1
product price
3 bottle 120.0
6 pen 13.5Qui sono evidenti due buone abitudini. Alla maschera viene assegnato un nome (fix) perché viene usata per qualcosa di importante; e prima di scrivere, fix.sum() indica quante righe stanno per cambiare — una, esattamente come ci aspettavamo. Conta prima di sovrascrivere, ogni volta.
Per leggere,df[mask]["col"]va benissimo. Per scrivere, usa sempredf.loc[mask, "col"] = value. La forma a catena non solleva eccezioni e la modifica scompare silenziosamente.
Salvare una tabella filtrata su cui lavorare non è un problema
E in questo caso?
import pandas as pd
orders = pd.read_csv("orders.csv")
east = orders[orders["branch"] == "east"]
east["price"] = 0.0
print(east["price"].tolist())
print(orders["price"].tolist())[0.0, 0.0]
[15.0, 60.0, 850.0, 120.0, 8.0, 850.0, 15.0, 120.0]Nessun avviso, ed è corretto. Qui la tua intenzione era creare una tabella separata: le hai dato il nome east, e modificarla cambia solo east. orders non viene toccato. Il Copy-on-Write rende questo comportamento prevedibile — una tabella ottenuta filtrando è sempre indipendente dall'originale. Il bug si verifica soltanto nella forma a catena, in cui la tabella intermedia non ha un nome e la modifica scompare insieme a essa.
Un esempio completo
Il responsabile ora desidera un breve report. report.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
# Check what arrived before asking it anything.
print("Rows, columns:", orders.shape)
print("Branches:", list(orders["branch"].unique()))
print()
# 1. The original question: north-branch orders of more than three items.
north_big = orders.loc[
(orders["branch"] == "north") & (orders["quantity"] > 3),
["order_id", "product", "quantity"],
]
print("North orders over 3 items:")
print(north_big.reset_index(drop=True))
print()
# 2. Counting with masks: no new table needed.
accessories = orders["category"] == "accessories"
print("Accessory orders :", accessories.sum())
print("Share of all orders:", accessories.mean())
print("Accessory items :", orders.loc[accessories, "quantity"].sum())
print()
# 3. Outside north, cheap items (under 100).
cheap_outside = orders[
(orders["branch"] != "north") & (orders["price"] < 100)
]
print("Cheap orders outside north:", cheap_outside["order_id"].tolist())
# 4. A branch we do not have: say so instead of printing an empty table.
west = orders[orders["branch"] == "west"]
if west.empty:
print("No orders from the west branch.")Rows, columns: (8, 7)
Branches: ['north', 'south', 'east']
North orders over 3 items:
order_id product quantity
0 1001 pen 12
1 1005 eraser 30
2 1008 bottle 4
Accessory orders : 4
Share of all orders: 0.5
Accessory items : 14
Cheap orders outside north: [1002, 1007]
No orders from the west branch.Perché è scritto in questo modo:
- Prima controlla, poi chiedi.
shapee la lista delle filiali vengono stampati prima di qualsiasi filtro. Se il file fosse arrivato conNorthmaiuscolo, la riga delle filiali lo mostrerebbe prima che il report stampi una tabella vuota. .loc[mask, columns]per la richiesta principale. Le righe e le colonne della risposta vengono definite in un unico punto, ereset_index(drop=True)rinumera il risultato perché questo è il report finale, non un passaggio intermedio.- Una maschera con un nome, usata tre volte.
accessoriesviene calcolata una sola volta e poi contata (sum), misurata (mean) e usata per selezionare una colonna. Dare un nome a una maschera evita di dover scrivere la stessa condizione tre volte rischiando di renderne una leggermente diversa. - Ogni condizione tra le proprie parentesi, anche quelle brevi, così l'eventuale aggiunta di una terza condizione non potrà causare errori di precedenza.
- Il caso vuoto è gestito in modo esplicito. Zero righe è una risposta valida e il report lo segnala a parole.
Quando qualcosa va storto
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all(). Hai usato and, or o not tra due maschere — o un if su un'intera maschera. Le parole chiave di Python richiedono un singolo True/False, mentre una maschera ne ha uno per riga. All'interno di un filtro, scrivi &, |, ~. All'interno di un if, chiarisci cosa intendi: if mask.any(): («almeno una riga è vera?») o if mask.all(): («tutte le righe sono vere?»).
TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool] (o 'ror_', o [int64]) Mancano le parentesi attorno a una condizione accanto a & o |. Python ha applicato & prima di >, quindi ha tentato un "and" bit a bit tra un numero e una colonna. Inserisci ciascuna condizione tra le proprie parentesi: (a > 3) & (b < 100).
Il filtro restituisce una tabella vuota, senza alcun errore Il valore cercato non compare nella colonna esattamente come è stato digitato. Stampa df["col"].unique() — o ancora meglio, list(df["col"].unique()), che mostra ogni valore tra virgolette, rendendo visibile un eventuale spazio finale in 'north '. Correggi i dati con df["col"].str.strip(), oppure usa una corrispondenza più permissiva con .str.contains(..., case=False).
KeyError: 1 (o un altro numero) subito dopo il filtraggio Hai usato .loc[n] su una tabella filtrata come se avesse ancora le etichette 0, 1, 2…. Conserva invece le etichette originali. Usa .iloc[n] per «la riga n-esima», oppure usa prima reset_index(drop=True) se le vecchie etichette non sono più utili.
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment. Hai scritto df[mask]["col"] = value. In pandas 3 questo non modifica mai df — l'avviso ti sta segnalando che la riga non ha avuto alcun effetto. Scrivi df.loc[mask, "col"] = value.
UserWarning: Boolean Series key will be reindexed to match DataFrame index. La maschera è stata creata a partire da una tabella diversa rispetto a quella che stai filtrando — tipicamente una maschera derivata dall'intero orders usata su north già filtrato. Pandas la allinea per etichetta e scarta silenziosamente le etichette che non riesce a trovare:
import pandas as pd
orders = pd.read_csv("orders.csv")
north = orders[orders["branch"] == "north"]
big = orders["quantity"] > 5
print(north[big])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
UserWarning: Boolean Series key will be reindexed to match DataFrame index.(In un terminale l'avviso compare sopra la tabella.) Qui per coincidenza restituisce una risposta sensata, ma solo per una fortunata coincidenza delle etichette. Costruisci la maschera a partire dalla tabella che stai filtrando: north[north["quantity"] > 5].
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))
mask = orders["quantity"] > 5
print(mask.sum(), len(orders[mask]))- A5 5
- B4 8
- C4 4
- DTrue 4
Questo codice dovrebbe conservare gli ordini della filiale north con più di tre articoli. Cosa succede quando viene eseguito?
from io import StringIO
import pandas as pd
# Stands in for orders.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
orders = pd.read_csv(StringIO(RAW))
north_big = orders[orders["branch"] == "north" & orders["quantity"] > 3]- AUn `ValueError: The truth value of a Series is ambiguous`
- BRestituisce una tabella vuota
- CFunziona e conserva gli ordini 1001, 1005 e 1008
- DUn `TypeError`: senza parentesi, `&` viene applicato prima di `==` e `>`
Devi dimezzare il prezzo di ogni borsa all'interno di orders stesso. Quale riga esegue l'operazione?
- Aorders[orders["product"] == "bag"]["price"] = orders["price"] / 2
- Bbags = orders[orders["product"] == "bag"] bags["price"] = bags["price"] / 2
- Corders["price"][orders["product"] == "bag"] /= 2
- Dorders.loc[orders["product"] == "bag", "price"] = orders["price"] / 2
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
Usa lo stesso file orders.csv. Scrivi filters.py che risponda a quattro domande, ciascuna stampata sotto una breve intestazione:
- Quanti ordini contenevano 10 o più articoli? Stampa un numero, non una tabella.
- Ordini di accessori al di fuori della filiale north — solo
order_id,branch,producteprice, rinumerati a partire da 0. - Ordini della filiale south oppure east con prezzo inferiore a 100. Usa
isinper le filiali. - La sede centrale concede uno sconto del 10% su ogni ordine di cancelleria con 20 o più articoli. Imposta il
priceesattamente di quelle righe al 90% del loro prezzo attuale, ma prima stampa quante righe stai per modificare. Poi stampaorder_id,quantityepriceper tutti gli ordini di cancelleria.
Prima di scrivere qualsiasi riga di codice, pianifica ogni domanda su carta: quali colonne, quali verifiche, unite da cosa, e quali righe ti aspetti guardando il file. Quando il tuo programma è corretto, stampa esattamente questo:
1. Orders of 10+ items:
3
2. Accessory orders outside north:
order_id branch product price
0 1004 east bottle 120.0
1 1006 south bag 850.0
3. South or east, price under 100:
order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
6 1007 2024-03-04 east pen stationery 20 15.0
4. Stationery discount:
Rows to change: 2
order_id quantity price
0 1001 12 15.0
1 1002 5 60.0
4 1005 30 7.2
6 1007 20 13.5Poi rompilo di proposito, una volta per ciascun caso:
- Rimuovi le parentesi nella domanda 3. Quale errore compare?
- Scrivi la domanda 4 come
orders[mask]["price"] = .... Cosa stampa pandas, e i prezzi sono cambiati? - Cambia
"east"in"East"nella domanda 3. Cosa succede, e perché questo è peggio di un errore?
Soluzione
Prima pianifica. Ogni domanda, scomposta:
quantity >= 10, una sola condizione. A occhio: 12, 30 e 20, quindi 3.category == "accessories"andbranch != "north". A occhio: 1004 (east, bottiglia) e 1006 (south, borsa).branchin south/east andprice < 100. A occhio: 1002 (quaderno, 60.0) e 1007 (penna, 15.0).category == "stationery"andquantity >= 20. A occhio: 1005 (gomma, 30) e 1007 (penna, 20), quindi 2 righe.
La domanda 4 richiede attenzione: «20 o più articoli» è >= 20, quindi l'ordine 1007, con esattamente 20 articoli, è incluso. E scrive, quindi usa .loc[mask, "price"] con una maschera a cui è stato assegnato un nome, contata prima.
filters.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
print("1. Orders of 10+ items:")
print((orders["quantity"] >= 10).sum())
print()
print("2. Accessory orders outside north:")
outside = orders.loc[
(orders["category"] == "accessories") & (orders["branch"] != "north"),
["order_id", "branch", "product", "price"],
]
print(outside.reset_index(drop=True))
print()
print("3. South or east, price under 100:")
cheap = orders[
(orders["branch"].isin(["south", "east"])) & (orders["price"] < 100)
]
print(cheap)
print()
print("4. Stationery discount:")
discount = (orders["category"] == "stationery") & (orders["quantity"] >= 20)
print("Rows to change:", discount.sum())
orders.loc[discount, "price"] = orders.loc[discount, "price"] * 0.9
print(orders.loc[orders["category"] == "stationery", ["order_id", "quantity", "price"]])1. Orders of 10+ items:
3
2. Accessory orders outside north:
order_id branch product price
0 1004 east bottle 120.0
1 1006 south bag 850.0
3. South or east, price under 100:
order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
6 1007 2024-03-04 east pen stationery 20 15.0
4. Stationery discount:
Rows to change: 2
order_id quantity price
0 1001 12 15.0
1 1002 5 60.0
4 1005 30 7.2
6 1007 20 13.5Ogni risposta corrisponde alla pianificazione. Note sulle scelte:
- La domanda 1 è il
sum()di una maschera, nonlen(orders[...]). Entrambi danno 3, ma la maschera esprime direttamente il conteggio e non crea alcuna tabella. - La domanda 2 usa
!=per «al di fuori della filiale north».~(orders["branch"] == "north")ha lo stesso significato;!=è più semplice per un singolo valore. - La domanda 3 conserva le etichette originali
1e6, perché l'esercizio non richiedeva di rinumerare. Quelle etichette ti dicono a quali righe diorderscorrispondevano. - La domanda 4 conta prima di scrivere, e il nuovo valore viene calcolato a partire dalle stesse righe nella parte destra dell'assegnazione:
orders.loc[discount, "price"] * 0.9. Entrambi i lati usano la stessa maschera, quindi ogni riga riceve il 90% del proprio prezzo: 8.0 diventa 7.2, 15.0 diventa 13.5. L'ordine delle penne1001(12 articoli) e il quaderno (5 articoli) mantengono i loro prezzi invariati.
E i tre modi in cui si rompe:
- Senza parentesi, la domanda 3 solleva
TypeError: Cannot perform 'rand_' …, perché&è stato applicato prima di<. orders[discount]["price"] = …stampa l'avvisoChainedAssignmentError, lo script prosegue e la tabella finale mostra ancora i vecchi prezzi8.0e15.0. La riga non ha avuto alcun effetto.- Con
"East", la domanda 3 perde silenziosamente la penna della filiale east e stampa una sola riga anziché due. Nessun errore, nessun avviso: solo una risposta sbagliata che sembra corretta. Ecco perché nel piano si annotano le righe attese prima di eseguire il codice. L'unica cosa che intercetta questo bug è una persona che sapeva già che la risposta doveva contenere due righe.
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