Dati mancanti — trovare i vuoti, poi decidere cosa significano
Trovare ogni valore mancante in una tabella, compresi quelli nascosti sotto forma di testo come - o ?, comprendere come NaN modifica somme, medie e tipi, e scegliere intenzionalmente — colonna per colonna — se rimuovere, riempire o mantenere ciascun dato mancante.
Come vuoi imparare?
Il problema che stiamo risolvendo
Gli ordini del negozio per la prima settimana di marzo sono stati inseriti a mano in tre filiali ed esportati come orders_raw.csv. Il titolare vuole due numeri entro sera: quante unità sono state vendute e quanto denaro è entrato.
Leggi il file e, per abitudine dai capitoli precedenti, sommi prima la colonna dei prezzi, solo per farti un'idea dell'ordine di grandezza:
import pandas as pd
orders = pd.read_csv("orders_raw.csv")
print(orders["price"].sum())15.060.0850.08.0-15.0120.0Quello non è un numero. È ogni prezzo incollato all'altro come testo — e pandas non ha sollevato alcun errore per avvisarti. Ora la colonna delle quantità:
print(orders["quantity"].sum())
print(orders["quantity"].mean())76.0
10.857142857142858Questo sembra a posto, ed è ancora peggio. Ci sono otto ordini nel file, eppure 76 / 8 fa 9.5, non 10.86. Una quantità è vuota, e pandas l'ha esclusa silenziosamente sia dal totale che dalla media. E nemmeno di questo ti ha avvisato nessuno.
Il file presenta quindi dei dati mancanti, e questi vuoti sono di due tipi. Alcuni sono visibili: una cella vuota, che pandas trasforma in uno speciale marcatore di valore "mancante" e poi ignora silenziosamente nei calcoli aritmetici. Altri sono nascosti: qualcuno ha digitato - invece di lasciare la cella vuota, pandas l'ha preso alla lettera e l'intera colonna dei prezzi è diventata testo.
Nessuno dei due tipi solleva un errore. Entrambi cambiano il risultato. Questo capitolo spiega come trovare ogni dato mancante, capire come lo tratta pandas e prendere una decisione intenzionale — eliminarlo, riempirlo o mantenerlo — invece di lasciare che sia il comportamento predefinito a decidere per te.
Alla fine di questo capitolo sarai in grado di
- Spiegare cos'è
NaN, perchéNaN == NaNèFalsee perché una colonna di numeri interi diventa decimale quando una cella è vuota - Contare i valori mancanti per colonna con
isna().sum()ed estrarre le righe incomplete - Trasformare marcatori nascosti come
-o?in veri valori mancanti, conna_values=opd.to_numeric(errors="coerce") - Prevedere come
sum,mean,counte l'aritmetica tra colonne trattano i valori mancanti - Rimuovere i valori mancanti con
dropna(), usandosubset=ehow=per eliminare solo ciò che intendi rimuovere - Sostituire i valori mancanti con
fillna()— un unico valore, un valore diverso per ciascuna colonna o una statistica - Decidere, colonna per colonna, se eliminare, riempire o mantenere — e spiegare perché
- Evitare la trappola di
inplace=Trueche non fa nulla in pandas 3
Prerequisiti: Filtrare le righe.
Crea prima il file
Nella cartella del tuo progetto, crea orders_raw.csv esattamente con queste righe. I vuoti sono intenzionali — osserva attentamente la terza, la quinta e la settima riga:
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,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0Sono gli stessi otto ordini di orders.csv dei capitoli precedenti, ma con quattro problemi: l'ordine 1002 non ha una quantità, l'ordine 1004 non ha una filiale e il suo prezzo riporta N/A, e l'ordine 1006 ha - come prezzo.
Prima di scrivere il codice
I dati mancanti sono l'unico argomento in cui scrivere prima il codice è davvero pericoloso: sia dropna() che fillna() vengono eseguiti senza lamentele ed entrambi cambiano il risultato. Il lavoro inizia quindi osservando e decidendo.
1. Formula la domanda in una frase. «Qual è stata la quantità totale venduta e il fatturato totale (quantità × prezzo) della settimana — e quanti ordini non è stato possibile conteggiare?» L'ultima parte è fondamentale. Un totale che tralascia silenziosamente due ordini non è la stessa risposta di un totale che specifica «6 ordini su 8 conteggiati».
2. Guarda cosa è arrivato, prima di qualsiasi altra cosa. Insieme, shape e info() ti forniscono informazioni su ogni colonna in un colpo solo:
print(orders.shape)
orders.info()(8, 7)
<class 'pandas.DataFrame'>
RangeIndex: 8 entries, 0 to 7
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 order_id 8 non-null int64
1 date 8 non-null str
2 branch 7 non-null str
3 product 8 non-null str
4 category 8 non-null str
5 quantity 7 non-null float64
6 price 7 non-null str
dtypes: float64(1), int64(1), str(5)
memory usage: 580.0 bytesLeggilo confrontandolo con ciò che ti aspettavi, colonna per colonna:
branch— ci si aspettavano 8 non-nullstr, ottenuti 7 non-null. Manca una filiale.quantity— ci si aspettavano 8 non-nullint64, ottenuti 7 non-nullfloat64. Manca una quantità, ed è questo il motivo per cui è diventatafloat64(spiegato più avanti).price— ci si aspettavano 8 non-nullfloat64, ottenuti 7 non-nullstr. Manca un prezzo e del testo è finito nella colonna.
Due elementi in questo elenco sono indizi più che fatti certi. Una colonna di numeri interi che mostra float64 indica quasi sempre un dato mancante. Una colonna di numeri che mostra str indica sempre che vi è mescolato del testo — e il testo in una colonna numerica è solitamente un marcatore di dato "mancante" nascosto.
3. Delinea l'output desiderato. Qualcosa del tipo:
Missing values per column: branch 1, quantity 1, price 2
Orders counted for revenue: 6 of 8
Total quantity (known): 76
Total revenue (counted): ...Nota come riporti sia i dati mancanti che i totali. Chiunque legga il report deve sapere che il fatturato è una stima al ribasso.
4. Decidi lo strumento per ogni dato mancante — prima di scriverlo. Per ogni colonna, chiediti: cosa significa una cella vuota in questo punto, e si può comunque rispondere alla domanda senza di essa?
branch— un dato mancante significa che l'ordine è avvenuto ma la filiale non è stata registrata. Decisione: riempi con"Unknown", perché l'ordine è reale e il denaro incassato conta comunque.quantity— un dato mancante significa che non sappiamo quante unità sono state vendute. Decisione: mantienilo mancante ed escludi la riga dal fatturato, perché inventare un numero (0? la media?) significherebbe inventare vendite.price— un dato mancante significa che non sappiamo a quale prezzo sia stato venduto. Decisione: mantienilo mancante ed escludi la riga dal fatturato per la stessa ragione — e segnala quante righe sono state escluse.
5. Decidi cosa controllerai dopo. Dopo ogni passaggio che rimuove o sostituisce valori, stampa di nuovo i conteggi dei dati mancanti e le dimensioni (shape). Se un dropna() ti porta da 8 righe a 2, vorrai vederlo subito, non dopo che il report è già stato inviato.
Il resto del capitolo ti presenta ciascuno strumento a turno, e alla fine riuniremo questo piano in un unico programma.
Come si presenta il dato mancante: NaN
Quando pandas trova una cella vuota, non memorizza una stringa vuota o zero. Memorizza uno speciale valore a virgola mobile chiamato NaN — "not a number" (non un numero). Puoi crearne uno tu stesso inserendo None in una Series:
import pandas as pd
quantity = pd.Series([12, None, 2])
print(quantity)0 12.0
1 NaN
2 2.0
dtype: float64Guarda il dtype. Gli hai passato due numeri interi e un valore vuoto, e hai ottenuto in cambio float64, con 12.0 e 2.0. Questo è il motivo dietro l'indizio in info(): NaN è un valore float, e una classica colonna di interi non ha modo di contenerlo. Quindi, nel momento in cui una cella è mancante, pandas promuove l'intera colonna a float64 per fare spazio a NaN. Ecco perché quantity nel nostro file è stata stampata come 12.0, 2.0 e così via.
Le colonne di testo si comportano diversamente. Una colonna str può contenere un valore mancante senza cambiare tipo:
branch = pd.Series(["north", None, "east"])
print(branch)0 north
1 NaN
2 east
dtype: strViene comunque stampata come NaN, e il dtype rimane str. Quindi in pandas 3 incontrerai NaN sia nelle colonne numeriche che in quelle di testo, e gli stessi strumenti lo individuano in entrambe.
NaN non è uguale a niente — nemmeno a se stesso
Questo è il fatto in assoluto più sorprendente sui valori mancanti, e manda all'aria il modo più naturale di cercarli:
import numpy as np
import pandas as pd
orders = pd.read_csv("orders_raw.csv")
print(np.nan == np.nan)
print((orders["branch"] == np.nan).sum())False
0NaN significa "un valore che non conosciamo". Di due valori sconosciuti non si sa se siano uguali, quindi il confronto restituisce False — ogni singola volta. Ciò rende == np.nan un test che non corrisponde mai a nulla: la colonna branch ha un dato mancante, e il conteggio dice 0. Chiedilo invece direttamente a pandas.
Non cercare mai i valori mancanti con==.== np.nanviene eseguito senza errori e non trova mai nulla, facendo sembrare completa una colonna piena di dati mancanti. Usaisna().
Trovare i valori mancanti: isna()
isna() pone la domanda che == non può fare: "questa cella è mancante?". Su una colonna restituisce una colonna di True/False — una maschera, esattamente come quelle del capitolo sul filtraggio:
print(orders["branch"].isna())0 False
1 False
2 False
3 True
4 False
5 False
6 False
7 False
Name: branch, dtype: boolSull'intero DataFrame fa la stessa cosa per ogni cella. Raramente però vorrai leggere otto righe di True/False. Vuoi un conteggio, e poiché True vale 1, .sum() te lo fornisce:
print(orders.isna().sum())order_id 0
date 0
branch 1
product 0
category 0
quantity 1
price 1
dtype: int64Questa è la riga da eseguire dopo info() su qualsiasi nuovo file. Fornisce le stesse informazioni del Non-Null Count, ribaltate per contare direttamente i dati mancanti.
Ma guarda price: dice 1. Sappiamo che due prezzi sono problematici — N/A e -. Tienilo a mente; ci torneremo nella prossima sezione.
Quali righe sono incomplete?
Un conteggio ti dice quanti sono; per vedere quali, usa la maschera come filtro. isna().any(axis=1) chiede, per ogni riga: "manca qualcosa in questa riga?":
incomplete = orders[orders.isna().any(axis=1)]
print(incomplete)order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery NaN 60.0
3 1004 2024-03-02 NaN bottle accessories 7.0 NaNaxis=1 significa "guarda lungo ogni riga" anziché lungo ogni colonna. E nota chi non è nell'elenco: l'ordine 1006, il cui prezzo è -. Per quanto ne sa pandas, quella riga è completa. Tieni a mente questo dettaglio. Guardare le righe effettive vale quella riga di codice in più: è così che noti, per esempio, che ogni quantità mancante proviene dalla stessa filiale, il che è una domanda da porre alle persone di quella filiale piuttosto che a pandas.
In proporzione, non come conteggio
Con otto righe, i conteggi vanno benissimo. Con ottantamila, "312 mancanti" significa poco finché non sai che è lo 0,4%. La funzione .mean() di una colonna True/False rappresenta la frazione di True:
print(orders.isna().mean())order_id 0.000
date 0.000
branch 0.125
product 0.000
category 0.000
quantity 0.125
price 0.125
dtype: float64Una cella su otto di branch, quantity e price è mancante — il 12,5% ciascuna. Una colonna vuota al 90% e una vuota allo 0,1% richiedono decisioni completamente diverse, ed è così che le distingui.
Valori mancanti nascosti: quando il dato vuoto è scritto come testo
isna() ha contato un solo prezzo mancante, ma due prezzi sono inutilizzabili. Il secondo è il - nell'ordine 1006. Per pandas, - non è un dato mancante — è una stringa di testo a tutti gli effetti. E un singolo elemento di testo in una colonna di numeri è sufficiente per rendere l'intera colonna str, che è esattamente ciò che ha mostrato info().
Ecco come scoprire cosa c'è effettivamente in una colonna:
print(orders["price"].unique())<StringArray>
['15.0', '60.0', '850.0', nan, '8.0', '-', '120.0']
Length: 7, dtype: strOgni valore è tra virgolette — sono tutti testo, compreso '15.0'. E ci sono due tipi di vuoto affiancati: nan, che pandas ha riconosciuto, e '-', che non ha riconosciuto.
Perché N/A è diventato nan mentre - no? Perché read_csv include un elenco integrato di stringhe che tratta come mancanti: una cella vuota, NA, N/A, n/a, NaN, null, None e poche altre. -, ?, missing, 0 e unknown non sono in quell'elenco. Qualunque cosa la persona che ha inserito i dati abbia usato per indicare "non lo so" decide quale tipo di dato mancante otterrai.
Ci sono due modi per trasformare i valori nascosti in veri NaN.
Soluzione 1: segnala il marcatore a read_csv — na_values=
Se conosci il marcatore, la correzione più pulita avviene al momento della lettura. na_values= aggiunge le tue stringhe all'elenco integrato:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
print(orders["price"].dtype)
print(orders.isna().sum())float64
order_id 0
date 0
branch 1
product 0
category 0
quantity 1
price 2
dtype: int64Confrontalo con il conteggio precedente. branch e quantity sono invariate — na_values= aggiunge elementi all'elenco, non lo sostituisce, quindi le celle vuote e N/A vengono ancora riconosciute. Ciò che è cambiato è price: ora è float64, e segnala 2 valori mancanti — sia N/A che - sono ora veri NaN. La colonna è di nuovo numerica, quindi le operazioni aritmetiche su di essa hanno un senso aritmetico.
Nota che na_values=["-"] si applica a ogni colonna. In questo caso è ciò che vogliamo. Se un - potesse essere un valore reale altrove — ad esempio un codice prodotto — passa invece un dizionario, na_values={"price": ["-"]}, e solo la colonna price ne sarà influenzata.
Soluzione 2: converti dopo la lettura — pd.to_numeric(errors="coerce")
A volte non sai in anticipo quali stringhe anomale siano presenti in una colonna, oppure il file è stato letto dal codice di qualcun altro. In tal caso converti la colonna dopo la lettura. Prima di tutto, osserva cosa fa una conversione semplice:
raw = pd.read_csv("orders_raw.csv")
price = pd.to_numeric(raw["price"])ValueError: Unable to parse string "-" at position 5Questo errore è utile: indica il valore incriminato e la sua posizione. Aggiungere errors="coerce" dice "qualsiasi cosa tu non riesca a trasformare in un numero, trasformala in NaN":
price = pd.to_numeric(raw["price"], errors="coerce")
print(price)0 15.0
1 60.0
2 850.0
3 NaN
4 8.0
5 NaN
6 15.0
7 120.0
Name: price, dtype: float64Per modificare effettivamente la tabella, assegna nuovamente il risultato alla colonna — to_numeric restituisce una nuova Series e lascia intatto raw:
raw["price"] = pd.to_numeric(raw["price"], errors="coerce")
print(raw["price"].dtype, raw["price"].isna().sum())float64 2Quale soluzione scegliere
errors="coerce" è potente e sbrigativo. Trasforma qualsiasi cosa non numerica in NaN — compreso un prezzo reale digitato male come 1O0 (con la lettera O) o 1,200 con la virgola delle migliaia. Quelli non sono valori mancanti; sono valori con un errore di battitura, e forzarli elimina silenziosamente informazioni utili.
L'abitudine da adottare è quindi: conta prima, conta dopo e spiega la differenza.
- Conosci il marcatore (
-,?,missing): usana_values=inread_csv. È preciso — solo quella stringa diventa un valore mancante. - Spazzatura sconosciuta e la colonna deve essere numerica: usa
pd.to_numeric(errors="coerce"). Intercetta tutto, quindi controlla cosa ha intercettato. - Alcuni valori sono errori di battitura che puoi correggere: esamina prima
unique(), correggili, poi converti. La coercizione distruggerebbe dati reali.
Prima della coercizione, raw["price"].unique() mostrava esattamente una stringa non numerica ('-') più un nan. Dopo la coercizione, ci sono esattamente 2 NaN. I numeri concordano, quindi non è andato perso nient'altro. Quel controllo fa tutta la differenza tra pulire i dati e danneggiarli.
Da qui in poi useremo la versione letta con na_values=["-"].
Come i calcoli trattano NaN
Ora che i dati mancanti sono veri NaN, devi sapere cosa fa pandas con essi — perché non si ferma a chiedere.
I riepiloghi delle colonne ignorano NaN
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
q = orders["quantity"]
print("len :", len(q))
print("count:", q.count())
print("sum :", q.sum())
print("mean :", q.mean())
print("check:", q.sum() / q.count())len : 8
count: 7
sum : 76.0
mean : 10.857142857142858
check: 10.857142857142858Tre comportamenti da ricordare:
lenconta le righe;count()conta i valori. La differenza tra i due è il numero di dati mancanti. È lo stesso concetto delNon-Null Countininfo().sum()ignoraNaN. Somma le sette quantità note. È ragionevole — ma significa che "76" è il totale delle quantità note, non di tutti gli ordini.mean()divide percount(), non perlen. La media è calcolata sui sette valori noti. Di solito è ciò che si desidera; trattare il dato mancante come 0 abbasserebbe la media senza motivo.
Se preferisci essere avvisato della presenza di un valore mancante, skipna=False fa sì che il risultato sia NaN non appena un qualsiasi valore lo è:
print(q.sum(skipna=False))nanL'aritmetica tra colonne propaga NaN
I riepiloghi ignorano i dati mancanti; l'aritmetica li propaga. Qualsiasi elemento combinato con NaN restituisce NaN, perché "7 volte un prezzo sconosciuto" è a sua volta sconosciuto:
revenue = orders["quantity"] * orders["price"]
print(revenue)
print("total:", revenue.sum())
print("rows counted:", revenue.count(), "of", len(revenue))0 180.0
1 NaN
2 1700.0
3 NaN
4 240.0
5 NaN
6 300.0
7 480.0
dtype: float64
total: 2900.0
rows counted: 5 of 8Unisci i due comportamenti e vedrai la trappola menzionata all'inizio del capitolo. revenue.sum() ha stampato 2900.0 senza alcun avviso — ma tre degli otto ordini non hanno contribuito a nulla. All'ordine 1002 manca una quantità, a 1004 e 1006 manca un prezzo. Il totale è onesto solo se specifichi «5 ordini su 8». Ecco perché il piano richiedeva un conteggio accanto a ogni totale.
Rimuovere i valori mancanti: dropna()
dropna() rimuove le righe che contengono NaN. Senza argomenti rimuove ogni riga con qualsiasi dato mancante in qualsiasi colonna:
print(orders.shape)
print(orders.dropna().shape)(8, 7)
(5, 7)Tre delle otto righe sono sparite — e l'ordine 1004 è stato eliminato perché mancava la sua filiale, anche se per una domanda sulle quantità era una riga perfettamente valida. Questo è il problema di un dropna() semplice: risponde a "rimuovi qualsiasi cosa imperfetta", che raramente è la vera richiesta.
subset= — solo le colonne necessarie alla domanda
Specifica quali colonne devono essere obbligatoriamente presenti:
known_qty = orders.dropna(subset=["quantity"])
print(known_qty.shape)
print(known_qty["quantity"].sum())(7, 7)
76.0Viene rimosso solo l'ordine 1002 — l'unica riga in cui la quantità è davvero sconosciuta. Per il fatturato sono necessarie entrambe le colonne:
countable = orders.dropna(subset=["quantity", "price"])
print(countable[["order_id", "quantity", "price"]])order_id quantity price
0 1001 12.0 15.0
2 1003 2.0 850.0
4 1005 30.0 8.0
6 1007 20.0 15.0
7 1008 4.0 120.0Nota l'indice: 0, 2, 4, 6, 7. dropna() mantiene le etichette originali, proprio come fa il filtraggio, così puoi sempre risalire alla riga nel file. Se hai nuovamente bisogno di 0, 1, 2…, aggiungi .reset_index(drop=True).
how="all" — solo le righe completamente vuote
I fogli di calcolo esportati terminano spesso con alcune righe vuote in ogni colonna. how="all" rimuove solo quelle e lascia qualsiasi riga che contenga anche un solo valore:
print(orders.dropna(how="all").shape)(8, 7)Qui non viene rimosso nulla — nessuna delle nostre righe è completamente vuota — che è esattamente ciò che vuoi verificare.
dropna() restituisce una nuova tabella
Come la maggior parte dei metodi di pandas, dropna() non modifica orders; ti restituisce un nuovo DataFrame. Se scrivi orders.dropna() su una riga da solo, a orders non succede nulla. Assegna il risultato — a un nuovo nome se avrai ancora bisogno della tabella completa, cosa che in un report come il nostro ti servirà (per indicare quante righe sono state scartate).
Sostituire i valori mancanti: fillna()
A volte un dato mancante ha una sostituzione sensata. fillna() inserisce un valore dove c'era NaN.
Un unico valore per una colonna
La filiale mancante è il caso più limpido. L'ordine è avvenuto e il suo incasso è reale; semplicemente non sappiamo quale filiale lo abbia gestito. Riempirlo con un'etichetta conserva la riga e rende visibile il dato mancante in qualsiasi conteggio successivo per filiale:
orders["branch"] = orders["branch"].fillna("Unknown")
print(orders["branch"].tolist())['north', 'south', 'north', 'Unknown', 'north', 'south', 'east', 'north']La struttura di questa riga è importante: prendi la colonna, applichi fillna, e la riassegni alla stessa colonna. fillna restituisce una nuova Series; senza orders["branch"] = davanti, non viene salvato nulla.
Valori diversi per colonne diverse: un dizionario
Passa un dizionario {column: value} per riempire più colonne contemporaneamente, ciascuna con il proprio valore. Le colonne non indicate rimangono intatte:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
filled = orders.fillna({"branch": "Unknown", "quantity": 0})
print(filled.isna().sum())order_id 0
date 0
branch 0
product 0
category 0
quantity 0
price 2
dtype: int64price ha ancora i suoi due valori mancanti, perché non l'abbiamo inclusa. Questo è il vantaggio della forma a dizionario rispetto a orders.fillna(0), che riempirebbe ogni colonna, prezzi inclusi, con 0 — e un prezzo pari a 0 corrisponde a una borsa regalata.
Il riempimento modifica il risultato
Riempire la quantità con 0 sembrava innocuo. Confronta le medie:
print("skip the gap :", orders["quantity"].mean())
print("fill with 0 :", orders["quantity"].fillna(0).mean())
print("fill with median:", orders["quantity"].fillna(orders["quantity"].median()).mean())skip the gap : 10.857142857142858
fill with 0 : 9.5
fill with median: 10.375Tre medie diverse dalla stessa colonna, e l'unica differenza è ciò che hai deciso significhi una quantità sconosciuta:
- 0 dice "questo ordine non ha venduto nulla". Per l'ordine 1002, un ordine di quaderni che ha sicuramente venduto qualcosa, questo è falso e trascina la media verso il basso.
- La mediana (7 in questo caso) dice "ipotizziamo un ordine tipico". Mantiene la media all'incirca stabile, ma ora il totale include 7 quaderni che nessuno ha mai contato.
- Ignorare dice "non lo sappiamo, quindi lasciamolo fuori". La media è calcolata sui sette ordini che conosciamo.
Nessuna di queste è "corretta" in senso assoluto. Ciascuna risponde a una domanda leggermente diversa. Ecco perché la decisione deve essere inserita nel tuo piano, messa per iscritto, anziché affidata al primo valore predefinito che ti capita sottomano.
Una regola pratica valida nella maggior parte dei casi:
- Un'etichetta (filiale, categoria, nome): riempi con
"Unknown"o simile. La riga viene mantenuta e il dato mancante rimane visibile come gruppo a sé stante. - Un conteggio in cui vuoto significa davvero nessuno (resi, reclami): riempi con
0— ma solo quando la fonte intende davvero zero lasciando vuoto. - Una misurazione o denaro (quantità venduta, prezzo, un voto): mantieni
NaN, escludilo dal calcolo e segnala quanti elementi sono stati esclusi. I numeri inventati diventano totali inventati. - Una colonna necessaria alla domanda, per la maggior parte presente:
dropna(subset=[...])solo per quella specifica domanda. Una perdita minima per una risposta onesta.
Tornare ai numeri interi
Dopo il riempimento, quantity non ha più valori mancanti, ma è ancora float64 — pandas non la riconverte automaticamente. Una volta che non ci sono più NaN, astype(int) funziona regolarmente:
filled["quantity"] = filled["quantity"].astype(int)
print(filled["quantity"].tolist())
print(filled["quantity"].dtype)[12, 0, 2, 7, 30, 1, 20, 4]
int64Prova a farlo prima del riempimento e fallirà, perché non esiste un intero che significhi "sconosciuto" — vedi "Quando qualcosa va storto" più sotto.
Un esempio completo
clean_orders.py riunisce il piano delineato in "Prima di scrivere il codice" in un unico programma: legge con il marcatore noto, segnala ogni dato mancante, riempie la filiale, esclude le righe che non possono essere conteggiate e riporta i totali specificando quanti ordini coprono.
import pandas as pd
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
# 1. Look first
print("Rows, columns:", orders.shape)
missing = orders.isna().sum()
print("Missing per column:")
print(missing[missing > 0])
print()
# 2. Decide per column: branch -> label; quantity and price -> keep, exclude
orders["branch"] = orders["branch"].fillna("Unknown")
# 3. Only rows with both numbers can be counted for revenue
countable = orders.dropna(subset=["quantity", "price"])
skipped = orders[orders[["quantity", "price"]].isna().any(axis=1)]
revenue = countable["quantity"] * countable["price"]
print("Orders counted for revenue:", len(countable), "of", len(orders))
print("Total quantity (known): ", int(orders["quantity"].sum()))
print("Total revenue (counted): ", revenue.sum())
print()
print("Not counted - check these with the branch:")
print(skipped[["order_id", "branch", "quantity", "price"]])Rows, columns: (8, 7)
Missing per column:
branch 1
quantity 1
price 2
dtype: int64
Orders counted for revenue: 5 of 8
Total quantity (known): 76
Total revenue (counted): 2900.0
Not counted - check these with the branch:
order_id branch quantity price
1 1002 south NaN 60.0
3 1004 Unknown 7.0 NaN
5 1006 south 1.0 NaNPerché è scritto in questo modo:
na_values=["-"]si trova nella primissima riga. Il marcatore è stato trovato una volta per tutte esaminandounique(). Inserirlo inread_csvfa sì che la colonna sia numerica fin dall'inizio, e chiunque legga il programma vede quale marcatore usa il file.missing[missing > 0]stampa solo le colonne che hanno dati mancanti. Su un file con quaranta colonne, è la differenza tra un report leggibile e un muro di zeri.- La filiale viene riempita, i numeri no. Questo è l'elenco delle decisioni del piano tradotto in codice. Chi legge può non essere d'accordo, ma può vederlo chiaramente.
dropna(subset=...)specifica le due colonne necessarie al fatturato, quindi l'ordine 1004 viene escluso per il prezzo mancante, non per la filiale mancante.- Le righe scartate vengono stampate, non solo contate. "3 ordini non conteggiati" fa sorgere spontanea la domanda "quali?" — rispondi nel report. Quei tre ordini sono un elenco che qualcuno può effettivamente andare a verificare e correggere.
- "Total quantity (known)" e "(counted)" dichiarano esplicitamente cosa sono quei numeri. Entrambi sono stime al ribasso; le etichette impediscono a chiunque di considerarli completi.
Quando qualcosa va storto
TypeError: Cannot perform reduction 'mean' with string dtype Hai richiesto la media di una colonna che pandas ha letto come testo — di solito perché una cella contiene un marcatore come -, ? o missing. Esegui df["col"].unique() per trovare il colpevole, poi rileggi il file con na_values=["-"] o converti con pd.to_numeric(df["col"], errors="coerce").
raw = pd.read_csv("orders_raw.csv")
print(raw["price"].mean())TypeError: Cannot perform reduction 'mean' with string dtypesum() ha restituito una lunga stringa di cifre, come 15.060.0850.0... Stessa causa, sintomo peggiore: su una colonna di testo sum() unisce le stringhe invece di sommare numeri, e non genera alcun errore. Qualsiasi totale con questo aspetto proviene da una colonna str. Controlla df.dtypes prima di sommare qualsiasi cosa.
ValueError: Unable to parse string "-" at position 5 pd.to_numeric ha incontrato del testo che non è riuscito a convertire, e ti ha indicato esattamente cosa e dove. Aggiungi errors="coerce" (dopo aver verificato con unique() che i valori anomali siano davvero marcatori di dati "sconosciuti" e non errori di battitura), oppure correggi prima gli errori di battitura.
(df["col"] == np.nan).sum() restituisce 0, ma ci sono valori mancanti NaN non è mai uguale a nulla, se stesso compreso, quindi == np.nan non corrisponde ad alcuna cella. Usa df["col"].isna().
IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer Hai chiamato astype(int) su una colonna che contiene ancora NaN. Non esiste alcun numero intero che significhi "sconosciuto". Prima riempi o elimina i valori mancanti, poi converti:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
orders["quantity"] = orders["quantity"].astype(int)pandas.errors.IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer.Replace or remove non-finite values or cast to an integer typethat supports these values (e.g. 'Int64')(Gli spazi mancanti in "integer.Replace" e "typethat" sono presenti nel messaggio originale di pandas.) Il messaggio menziona anche 'Int64' con la I maiuscola: un tipo di interi a parte fornito da pandas che può contenere valori mancanti. È utile, ma riempire o eliminare prima i valori mancanti è la soluzione più semplice ed è quella da adottare.
ChainedAssignmentError — e il dato mancante è ancora lì Hai scritto la forma inplace=True che si trova in molti vecchi tutorial:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
orders["quantity"].fillna(0, inplace=True)
print(orders["quantity"].isna().sum())1
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment using an inplace method.
Such inplace method 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).
For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' instead, to perform the operation inplace on the original object, or try to avoid an inplace operation using 'df[col] = df[col].method(value)'.
See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.htmlIl conteggio è ancora 1: nulla è stato riempito. In pandas 3, orders["quantity"] si comporta sempre come una copia a sé stante (Copy-on-Write), quindi riempirla "sul posto" (in place) modifica quella copia, che viene poi scartata. Le versioni precedenti di pandas a volte facevano funzionare questo pattern, motivo per cui lo si trova ovunque online. Scrivi invece l'assegnazione per esteso — funziona in tutte le versioni ed esprime chiaramente cosa cambia:
orders["quantity"] = orders["quantity"].fillna(0)
print(orders["quantity"].isna().sum())0dropna() ha rimosso molte più righe del previsto Un dropna() semplice rimuove una riga per un dato mancante in qualsiasi colonna — compresa una colonna notes o discount quasi del tutto vuota che non stavi nemmeno usando. Stampa df.isna().sum() per trovare la colonna con molti valori mancanti, poi usa dropna(subset=[...]) specificando solo le colonne necessarie alla tua domanda.
TypeError: can't multiply sequence by non-int of type 'float' Hai moltiplicato una colonna numerica per una colonna di testo (quantity * price mentre price è str). La soluzione è identica a quella del primo errore: rendi prima numerica la colonna price.
Passo 4 di 6 — Prevedi
Verifica cosa hai capito
Una quantità è vuota. Cosa viene stampato?
from io import StringIO
import pandas as pd
# Stands in for orders_raw.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,,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
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["quantity"].sum(), orders["quantity"].count(), len(orders))- A76.0 7 8
- B76.0 8 8
- C76 8 8
- Dnan 7 8
L'obiettivo è trovare gli ordini senza prezzo. Mancano due prezzi. Cosa viene stampato?
from io import StringIO
import pandas as pd
# Stands in for orders_raw.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,,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
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), na_values=["-"])
missing_price = orders[orders["price"] == float("nan")]
print(len(missing_price))- A0 — `NaN` non è uguale a niente, nemmeno a `NaN`, quindi `==` non lo trova mai
- B1
- CUn `TypeError`: non puoi confrontare con `nan`
- D2
La colonna dei prezzi è stata letta come str perché una cella contiene - e un'altra N/A. Quale riga la trasforma in numeri, lasciando entrambi i dati mancanti come NaN?
- Aorders["price"] = pd.to_numeric(orders["price"], errors="coerce")
- Borders["price"] = orders["price"].fillna(0)
- Corders["price"] = orders["price"].astype(float)
- Dorders["price"] = orders["price"].dropna()
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 negozio ha avviato le consegne a domicilio e il registro dei corrieri per la prima settimana arriva come deliveries.csv. Crealo esattamente in questo modo:
order_id,branch,distance_km,fee,rider
2001,north,3.5,60,R1
2002,north,,60,R2
2003,south,8.0,?,R1
2004,east,12.0,120,
2005,north,2.0,40,R2
2006,south,6.5,90,?
2007,east,,110,R3
2008,north,4.0,60,R3Il titolare chiede: quanto è stato incassato in tariffe di consegna, a quante consegne manca la tariffa e qual è la tariffa media per chilometro?
Prima di scrivere qualsiasi codice, esamina il file con unique() e decidi qual è il marcatore nascosto. Poi scrivi deliveries_report.py che:
- Legga il file in modo che il marcatore nascosto diventi un vero valore mancante in ogni colonna in cui compare
- Stampi il numero di valori mancanti per colonna e le righe incomplete
- Riempia i
ridermancanti con"unassigned" - Stampi il totale delle tariffe note, il numero di consegne senza tariffa e la tariffa per chilometro — tariffa totale divisa per la distanza totale — calcolata solo sulle consegne in cui sia la tariffa che la distanza sono note
Le ultime tre righe stampate dal programma devono essere esattamente:
Fees collected (known): 540.0
Deliveries with no fee: 1
Fee per km: 13.21 from 5 of 8 deliveriesInoltre, prima del codice, scrivi un elenco di decisioni come quello in "Prima di scrivere il codice": per ciascuno tra distance_km, fee e rider, cosa significa un dato mancante e deciderai di riempire, scartare o mantenere?
Poi prova un esperimento: calcola la tariffa per chilometro con un dropna() semplice invece di dropna(subset=...). Il numero cambia? Quante righe ha utilizzato e perché?
Soluzione
Prima il piano. La domanda ha tre parti: un totale, un conteggio di dati mancanti e un rapporto. La lettura di info() mostrerebbe fee come str — una colonna delle tariffe dovrebbe contenere numeri, quindi del testo si nasconde al suo interno. Decisioni per colonna:
rider— la consegna è avvenuta, il corriere non è stato registrato. Riempi con"unassigned".fee— non sappiamo quanto sia stato addebitato. MantieniNaN; somma le tariffe note e conta quelle sconosciute.distance_km— non sappiamo quanta strada sia stata percorsa. MantieniNaN; escludi queste righe dal rapporto per km.
Per il rapporto, entrambi i numeri devono essere noti per la stessa consegna — altrimenti divideresti le tariffe di sette consegne per le distanze di sei.
Passaggio 1 — leggi senza opzioni ed esamina, per trovare il marcatore:
import pandas as pd
raw = pd.read_csv("deliveries.csv")
print(raw.shape)
print(raw.dtypes)
print(raw["fee"].unique())
print(raw["rider"].unique())(8, 5)
order_id int64
branch str
distance_km float64
fee str
rider str
dtype: object
<StringArray>
['60', '?', '120', '40', '90', '110']
Length: 6, dtype: str
<StringArray>
['R1', 'R2', nan, '?', 'R3']
Length: 5, dtype: strfee è str a causa del punto interrogativo ?. E ? compare anche come rider — lo stesso marcatore, usato in due colonne. distance_km è già float64: i suoi vuoti sono celle vuote, che pandas ha riconosciuto automaticamente. Poiché il marcatore è lo stesso ovunque, na_values=["?"] al momento della lettura sistema entrambe le colonne contemporaneamente.
Il programma completo, deliveries_report.py:
import pandas as pd
deliveries = pd.read_csv("deliveries.csv", na_values=["?"])
print("Types after reading:")
print(deliveries.dtypes)
print()
print("Missing per column:")
print(deliveries.isna().sum())
print()
print("Incomplete rows:")
print(deliveries[deliveries.isna().any(axis=1)])
print()
deliveries["rider"] = deliveries["rider"].fillna("unassigned")
fee_total = deliveries["fee"].sum()
fee_missing = deliveries["fee"].isna().sum()
print("Fees collected (known):", fee_total)
print("Deliveries with no fee:", fee_missing)
both = deliveries.dropna(subset=["fee", "distance_km"])
per_km = both["fee"].sum() / both["distance_km"].sum()
print("Fee per km:", round(per_km, 2), "from", len(both), "of", len(deliveries), "deliveries")Types after reading:
order_id int64
branch str
distance_km float64
fee float64
rider str
dtype: object
Missing per column:
order_id 0
branch 0
distance_km 2
fee 1
rider 2
dtype: int64
Incomplete rows:
order_id branch distance_km fee rider
1 2002 north NaN 60.0 R2
2 2003 south 8.0 NaN R1
3 2004 east 12.0 120.0 NaN
5 2006 south 6.5 90.0 NaN
6 2007 east NaN 110.0 R3
Fees collected (known): 540.0
Deliveries with no fee: 1
Fee per km: 13.21 from 5 of 8 deliveriesControlla i numeri a mano almeno una volta, perché è così che impari a fidarti di un totale ripulito. Tariffe note: 60 + 60 + 120 + 40 + 90 + 110 + 60 = 540, da sette consegne — 2003 non ne ha nessuna. Per il rapporto, dropna(subset=["fee", "distance_km"]) scarta la 2003 (nessuna tariffa) e le consegne 2002 e 2007 (nessuna distanza). Chiedi a pandas quali righe sono rimaste e quanto sommano, invece di affidarti al calcolo a mente:
print(both["order_id"].tolist())
print(both["fee"].sum(), both["distance_km"].sum())[2001, 2004, 2005, 2006, 2008]
370.0 28.0370 / 28.0 = 13.21, che è il numero stampato dal programma. Nota cosa non ha fatto il calcolo del rapporto: non ha diviso i 540 di sette tariffe per le distanze di sei consegne. Entrambi i numeri provengono dalle stesse cinque righe, che è l'unico modo in cui un valore "per chilometro" ha un significato sensato.
Altre due decisioni degne di nota. rider è stato riempito prima del calcolo, ma non lo ha mai influenzato — subset= ha guardato solo fee e distance_km. E il totale delle tariffe è contrassegnato con "(known)", con il conteggio delle tariffe mancanti stampato subito sotto, così nessuno scambierà 540 per l'importo totale incassato.
L'esperimento. Un semplice dropna() rimuove anche la 2004 e la 2006 — le cui tariffe e distanze erano entrambe corrette — perché mancava il loro rider. Se lo esegui sulla tabella prima di riempire rider:
deliveries = pd.read_csv("deliveries.csv", na_values=["?"])
strict = deliveries.dropna()
print(strict["order_id"].tolist())
print(round(strict["fee"].sum() / strict["distance_km"].sum(), 2))[2001, 2005, 2008]
16.84Sopravvivono solo tre consegne e il rapporto balza da 13.21 a 16.84 — non perché sia cambiata una qualche tariffa, ma perché i due viaggi più lunghi (12 e 6,5 km), che per coincidenza sono più economici al chilometro, sono stati scartati a causa di un nome mancante. La colonna del corriere non ha nulla a che fare con la domanda, quindi non dovrebbe decidere quali righe contare. È a questo che serve subset=.
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