Imparare la teoria di Pandas è una passeggiata, finché non ti ritrovi davanti a un CSV pieno di errori, date mancanti e colonne dai nomi improponibili.
È lì che inizi a rimpiangere Excel.
Ma il segreto per non farsi venire il mal di testa non è imparare a memoria la documentazione, è sporcarsi le mani con problemi reali.
Per questo abbiamo preparato 3 esercizi guidati che affrontano i problemi veri: pulire dati sporchi, unire tabelle diverse e filtrare informazioni utili.
Niente giri di parole, solo codice che funziona. 💻🐍
Esercizio 1 – Esplorazione iniziale di un dataset scolastico (Facile)
Testo dell’esercizio
Sei un tutor e devi analizzare i risultati di una classe di 20 studenti in quattro materie (Matematica, Fisica, Chimica, Inglese). I dati sono organizzati in un file CSV con le seguenti colonne:
-
StudentID(identificativo univoco) -
Nome -
Matematica(voto in trentesimi) -
Fisica(voto in trentesimi) -
Chimica(voto in trentesimi) -
Inglese(voto in trentesimi)
Carica i dati in un DataFrame Pandas, visualizza le prime 5 righe, controlla il tipo di dati e le statistiche descrittive di base. Poi seleziona solo le colonne numeriche, filtra gli studenti con un voto di Matematica superiore a 25 e conta quanti sono.
Soluzione
import pandas as pd import numpy as np # 1. Creazione del dataset (simuliamo il file CSV) np.random.seed(42) dati = { 'StudentID': range(1, 21), 'Nome': [f'Studente_{i}' for i in range(1, 21)], 'Matematica': np.random.randint(18, 31, 20), 'Fisica': np.random.randint(18, 31, 20), 'Chimica': np.random.randint(18, 31, 20), 'Inglese': np.random.randint(18, 31, 20) } df = pd.DataFrame(dati) # 2. Visualizzazione prime 5 righe print("Prime 5 righe del DataFrame:") print(df.head()) # 3. Informazioni generali print("\nInformazioni sul DataFrame:") print(df.info()) # 4. Statistiche descrittive print("\nStatistiche descrittive:") print(df.describe()) # 5. Selezione delle sole colonne numeriche # (escludiamo 'StudentID' e 'Nome' che non sono voti) colonne_numeriche = df[['Matematica', 'Fisica', 'Chimica', 'Inglese']] print("\nColonne numeriche (prime 5 righe):") print(colonne_numeriche.head()) # 6. Filtro: studenti con Matematica > 25 studenti_eccellenti_mate = df[df['Matematica'] > 25] print("\nStudenti con voto di Matematica > 25:") print(studenti_eccellenti_mate[['Nome', 'Matematica']]) # 7. Conteggio conteggio = len(studenti_eccellenti_mate) print(f"\nNumero di studenti con Matematica > 25: {conteggio}")
Prime 5 righe del DataFrame: StudentID Nome Matematica Fisica Chimica Inglese 0 1 Studente_1 24 22 29 27 1 2 Studente_2 21 19 20 22 2 3 Studente_3 30 25 29 19 3 4 Studente_4 28 29 24 21 4 5 Studente_5 25 23 21 29 Informazioni sul DataFrame: <class 'pandas.core.frame.DataFrame'> RangeIndex: 20 entries, 0 to 19 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 StudentID 20 non-null int64 1 Nome 20 non-null object 2 Matematica 20 non-null int64 3 Fisica 20 non-null int64 4 Chimica 20 non-null int64 5 Inglese 20 non-null int64 dtypes: int64(5), object(1) memory usage: 1.1+ KB None Statistiche descrittive: StudentID Matematica Fisica Chimica Inglese count 20.00000 20.000000 20.000000 20.000000 20.00000 mean 10.50000 24.600000 25.000000 23.700000 23.55000 std 5.91608 3.118704 4.154896 3.419757 3.88621 min 1.00000 20.000000 18.000000 19.000000 18.00000 25% 5.75000 22.000000 22.000000 20.750000 20.75000 50% 10.50000 24.500000 26.500000 24.000000 23.00000 75% 15.25000 27.250000 29.000000 26.000000 27.00000 max 20.00000 30.000000 30.000000 29.000000 30.00000 Colonne numeriche (prime 5 righe): Matematica Fisica Chimica Inglese 0 24 22 29 27 1 21 19 20 22 2 30 25 29 19 3 28 29 24 21 4 25 23 21 29 Studenti con voto di Matematica > 25: Nome Matematica 2 Studente_3 30 3 Studente_4 28 5 Studente_6 30 8 Studente_9 27 11 Studente_12 28 12 Studente_13 28 Numero di studenti con Matematica > 25: 6
💡 Osservazione
-
df.head()è utile per un primo sguardo ai dati. -
df.info()rivela tipi di dato ed eventuali valori nulli. -
df.describe()fornisce media, deviazione standard, minimo, massimo e quartili per le colonne numeriche. -
Il filtro
df[condizione]restituisce un nuovo DataFrame contenente solo le righe che soddisfano la condizione.
L’esercizio non si limita a mostrare i dati, ma introduce il concetto di Mascheramento Booleano (
df[df['colonna'] > x]). È interessante perché insegna a estrarre valore immediato da una massa informe di numeri, separando il “rumore” dai dati che contano.
Domanda di riflessione
Quale metodo useresti per selezionare solo le righe in cui il voto di Matematica è maggiore di 25 e quello di Fisica è maggiore di 24?
Esercizio 2 – Pulizia di un dataset di vendite (Facile-Medio)
Testo dell’esercizio
Un negozio online ti fornisce un file con i dati delle vendite dell’ultimo mese. Purtroppo il file contiene alcuni valori mancanti, righe duplicate e nomi di colonne poco chiari. Il DataFrame ha le seguenti colonne:
-
Data(stringa nel formato “YYYY-MM-DD”, a volte vuota) -
Prodotto(categoria del prodotto, a volte scritta in modo diverso a causa di errori di battitura) -
Prezzo(float, a volte mancante) -
Quantità(intero, sempre presente) -
Totale(dovrebbe essere Prezzo * Quantità, ma a volte manca o è errato)
Carica i dati, poi:
-
Identifica e gestisci i valori mancanti: per la colonna
Datasostituisci con la data “2023-12-31” (fine mese); perPrezzoeTotalesostituisci con la media dei rispettivi valori. -
Rimuovi le righe duplicate (considerando tutte le colonne).
-
Rinomina le colonne in minuscolo e senza spazi (es.
Data→data,Prezzo→prezzo, ecc.). -
Crea una nuova colonna
ricavocalcolata comeprezzo * quantità(dove ancora non presente) e confrontala con la colonnatotaleoriginale per verificare la coerenza (mostra le righe dove differiscono).
Soluzione
import pandas as pd import numpy as np # 1. Simulazione del dataset problematico dati = { 'Data': ['2023-12-01', '2023-12-01', None, '2023-12-02', '2023-12-03', '2023-12-03'], 'Prodotto': ['Elettronica', 'Elettronica', 'Casa', 'Casa', 'Giardino', 'giardino'], 'Prezzo': [250.0, 250.0, 35.5, None, 15.0, 15.0], 'Quantità': [2, 2, 1, 3, 5, 5], 'Totale': [500.0, 500.0, 35.5, 100.0, 75.0, None] } df = pd.DataFrame(dati) # 2. Gestione valori mancanti (Imputazione) # Riempiamo le date mancanti e usiamo la media per i valori numerici df['Data'] = df['Data'].fillna('2023-12-31') df['Prezzo'] = df['Prezzo'].fillna(df['Prezzo'].mean()) df['Totale'] = df['Totale'].fillna(df['Totale'].mean()) # 3. Rimozione dei duplicati df = df.drop_duplicates() # 4. Pulizia nomi colonne e NORMALIZZAZIONE stringhe # Rinominiamo le colonne in minuscolo df.columns = [col.lower() for col in df.columns] # --- FOCUS: Normalizzazione del testo --- # Trasformiamo i nomi dei prodotti in minuscolo per evitare doppioni logici df['prodotto'] = df['prodotto'].str.lower() # 5. Verifica della coerenza (Audit) # Calcoliamo il ricavo teorico e confrontiamolo con il totale fornito df['ricavo_calcolato'] = df['prezzo'] * df['quantità'] df['differenza'] = df['ricavo_calcolato'] - df['totale'] # Visualizziamo le righe dove c'è un errore di coerenza superiore a 1 centesimo errori_coerenza = df[df['differenza'].abs() > 0.01] print("Dataset Pulito e Normalizzato:") print(df[['data', 'prodotto', 'prezzo', 'quantità', 'totale']]) print("\nAnomalie riscontrate nel dataset originale:") print(errori_coerenza[['data', 'prodotto', 'totale', 'ricavo_calcolato']])
Dataset Pulito e Normalizzato:
data prodotto prezzo quantità totale
0 2023-12-01 elettronica 250.0 2 500.0
2 2023-12-31 casa 35.5 1 35.5
3 2023-12-02 casa 113.1 3 100.0
4 2023-12-03 giardino 15.0 5 75.0
5 2023-12-03 giardino 15.0 5 242.1
Anomalie riscontrate nel dataset originale:
data prodotto totale ricavo_calcolato
3 2023-12-02 casa 100.0 339.3
5 2023-12-03 giardino 242.1 75.0
💡 Osservazioni tecniche: La Normalizzazione
In questo esercizio, il passaggio più critico non è la rimozione dei duplicati, ma la normalizzazione della colonna ‘prodotto’.
Cos’è la Case Sensitivity?
Per un computer, le stringhe "Giardino" e "giardino" sono diverse quanto "Mela" e "Pera". Questo accade perché i caratteri hanno codici numerici differenti (es. ASCII o Unicode).
-
Senza normalizzazione: Se provassi a calcolare quanto hai venduto per ogni categoria usando
df.groupby('prodotto').sum(), otterresti due righe separate per il giardino, rendendo il tuo report sbagliato. -
Con la normalizzazione (
.str.lower()): Uniformiamo tutto allo standard minuscolo. In questo modo, Pandas riconosce che si tratta dello stesso oggetto e aggrega i dati correttamente.
Perché abbiamo calcolato il ‘ricavo’?
Spesso i database contengono colonne calcolate (come totale). Se però un prezzo è stato inserito male o è stato applicato uno sconto non registrato, il totale potrebbe essere “sporco”. Ricalcolare il dato da zero (prezzo * quantità) e confrontarlo con l’originale è la prova del nove di ogni Data Analyst.
Analisi applicativa:
Dal punto di vista professionale, questo esercizio simula il Data Auditing.
Ecco perché è un pilastro per chiunque lavori con Python:
-
Correzione dell’errore umano: Gli esseri umani scrivono “Casa”, “casa “, “CASA” o ” Casà”. Usare
.str.lower()combinato magari con un.str.strip()(per togliere gli spazi bianchi) è l’unico modo per garantire che i dati siano raggruppabili. -
Strategia di riempimento (Filling): Sostituire un valore mancante con la media (
mean()) è una scelta tecnica precisa. In un contesto di vendite, ci permette di non “perdere” una riga di transazione solo perché mancava un prezzo, usando un valore statisticamente plausibile. -
Il potere dei filtri booleani: L’uso di
df[df['differenza'].abs() > 0.01]mostra come identificare istantaneamente solo i problemi. In un dataset di 1 milione di righe, questa riga di codice ti permette di trovare i 10 errori di inserimento in un secondo.
Domanda di riflessione
Cosa succederebbe se usassimo inplace=True in un metodo come drop_duplicates() e poi provassimo a riassegnare il risultato a un’altra variabile?
Esercizio 3 – Fusione di dati da due fonti (Medio)
Testo dell’esercizio
Un’azienda ha due archivi: uno con i dati anagrafici dei dipendenti e uno con le informazioni sui dipartimenti. Il primo DataFrame (dipendenti) contiene:
-
ID_dipendente(intero) -
Nome(stringa) -
ID_dipartimento(intero, chiave verso i dipartimenti) -
Stipendio(float)
Il secondo DataFrame (dipartimenti) contiene:
-
ID_dipartimento(intero) -
Nome_dipartimento(stringa) -
Sede(stringa)
Carica i due DataFrame (puoi crearli con dati fittizi). Poi:
-
Unisci i due DataFrame in modo da ottenere per ogni dipendente anche il nome del dipartimento e la sede.
-
Calcola lo stipendio medio per ogni dipartimento.
-
Conta quanti dipendenti lavorano in ogni sede.
Soluzione
import pandas as pd # 1. Creazione dei due DataFrame dipendenti = pd.DataFrame({ 'ID_dipendente': [101, 102, 103, 104, 105, 106], 'Nome': ['Anna', 'Marco', 'Lucia', 'Giovanni', 'Elena', 'Paolo'], 'ID_dipartimento': [1, 2, 1, 3, 2, 1], 'Stipendio': [2800, 3200, 2900, 4100, 3500, 2700] }) dipartimenti = pd.DataFrame({ 'ID_dipartimento': [1, 2, 3], 'Nome_dipartimento': ['IT', 'Vendite', 'HR'], 'Sede': ['Roma', 'Milano', 'Roma'] }) print("Dipendenti:") print(dipendenti) print("\nDipartimenti:") print(dipartimenti) # 2. Unione (merge) per ottenere i nomi dei dipartimenti e le sedi # Usiamo un left join per conservare tutti i dipendenti (anche se manca il dipartimento, ma qui tutti presenti) df_completo = pd.merge(dipendenti, dipartimenti, on='ID_dipartimento', how='left') print("\nDipendenti con informazioni sul dipartimento:") print(df_completo) # 3. Stipendio medio per dipartimento # Possiamo raggruppare per nome dipartimento (o ID) media_per_dipartimento = df_completo.groupby('Nome_dipartimento')['Stipendio'].mean().reset_index() media_per_dipartimento.columns = ['Dipartimento', 'Stipendio_medio'] print("\nStipendio medio per dipartimento:") print(media_per_dipartimento) # 4. Conteggio dipendenti per sede conteggio_per_sede = df_completo.groupby('Sede').size().reset_index(name='Numero_dipendenti') print("\nNumero di dipendenti per sede:") print(conteggio_per_sede)
Dipendenti:
ID_dipendente Nome ID_dipartimento Stipendio
0 101 Anna 1 2800
1 102 Marco 2 3200
2 103 Lucia 1 2900
3 104 Giovanni 3 4100
4 105 Elena 2 3500
5 106 Paolo 1 2700
Dipartimenti:
ID_dipartimento Nome_dipartimento Sede
0 1 IT Roma
1 2 Vendite Milano
2 3 HR Roma
Dipendenti con informazioni sul dipartimento:
ID_dipendente Nome ID_dipartimento Stipendio Nome_dipartimento \
0 101 Anna 1 2800 IT
1 102 Marco 2 3200 Vendite
2 103 Lucia 1 2900 IT
3 104 Giovanni 3 4100 HR
4 105 Elena 2 3500 Vendite
5 106 Paolo 1 2700 IT
Sede
0 Roma
1 Milano
2 Roma
3 Roma
4 Milano
5 Roma
Stipendio medio per dipartimento:
Dipartimento Stipendio_medio
0 HR 4100.0
1 IT 2800.0
2 Vendite 3350.0
Numero di dipendenti per sede:
Sede Numero_dipendenti
0 Milano 2
1 Roma 4
💡 Osservazione
-
pd.merge()di default esegue un inner join (solo righe con chiave corrispondente in entrambi). Conhow='left'si mantengono tutte le righe del DataFrame di sinistra. -
Il raggruppamento dopo il merge permette di analizzare i dati arricchiti.
-
È importante verificare che le chiavi di join abbiano lo stesso nome o usare i parametri
left_oneright_on.
La Logica Relazionale: Sposta il focus da una singola tabella alla struttura del database. È interessante perché forza il programmatore a pensare come un database SQL pur rimanendo in Python. Insegna che i dati acquistano significato solo quando vengono messi in relazione tra loro (es. stipendio medio non per persona, ma per sede).
Domanda di riflessione
Se avessimo usato un how='right' invece di 'left', cosa sarebbe cambiato nel risultato? E in questo caso specifico, avrebbe avuto senso?
Risposte alle domande di riflessione
Esercizio 1
Per selezionare le righe con Matematica > 25 e Fisica > 24 si usa:
df[(df['Matematica'] > 25) & (df['Fisica'] > 24)].
La condizione composta richiede le parentesi tonde attorno a ogni confronto e l’operatore & (non and).
Esercizio 2
La risposta breve è: distruggeresti il tuo lavoro. Vediamo perché questo concetto è fondamentale per la coerenza dell’Esercizio 2 e perché, nel mondo Python moderno, stiamo smettendo di usare inplace=True.
Il “Suicidio” del DataFrame
Se nell’Esercizio 2 scrivessi: df = df.drop_duplicates(inplace=True)
Accadrebbe questo:
-
L’azione: Pandas rimuove i duplicati direttamente nell’oggetto
df. -
Il ritorno: Poiché il metodo ha modificato l’originale, “restituisce” un valore speciale chiamato
None(il nulla). -
Il disastro: Tu prendi quel
Nonee lo assegni adf.
Risultato?
Il tuo DataFrame sparisce e viene sostituito dal nulla. Qualsiasi riga di codice successiva (come la normalizzazione o il calcolo del ricavo) genererà un errore: AttributeError: 'NoneType' object has no property....
💡 Un consiglio (Il “Modern Pandas”)
Oggi la tendenza è evitare inplace=True.
Ecco due motivi pratici:
-
Niente brutte sorprese: Riassegnando sempre (
df = df.metodo()), sai sempre cosa contiene la variabile. -
Method Chaining: Senza
inplace, puoi scrivere tutto in una riga sola, rendendo il codice elegantissimo:
df = (df.drop_duplicates()
.fillna(0)
.rename(columns=str.lower))
Questo approccio “a cascata” è impossibile se usi inplace=True.
Modifica Diretta vs Assegnazione
| Caratteristica | Metodo con inplace=True | Metodo con Assegnazione (df = …) |
| Cosa restituisce | None (Nulla) |
Un nuovo DataFrame modificato |
| Oggetto Originale | Viene modificato direttamente | Rimane invariato (se non riassegnato) |
| Rischio di Errore | Alto (se provi ad assegnarlo a una variabile) | Molto basso |
| Method Chaining | Impossibile (non puoi concatenare altri metodi) | Sì (es. .drop().fillna().sort()) |
| Sintassi Tipica | df.drop_duplicates(inplace=True) |
df = df.drop_duplicates() |
Perché questa distinzione è vitale?
1. La trappola del None
Se scrivi df = df.drop_duplicates(inplace=True), stai dicendo a Python: “Esegui l’operazione su df e poi assegna il risultato (che è None) di nuovo a df”. In un istante, hai cancellato ore di lavoro. Il DataFrame originale è sparito e la tua variabile ora contiene il nulla.
2. Il mito della memoria
Molti pensano che inplace=True sia più veloce o consumi meno RAM. In realtà, nella maggior parte dei casi, Pandas crea comunque una copia temporanea dei dati “sotto il cofano” prima di sovrascrivere l’originale. Quindi, il risparmio di memoria è spesso un’illusione.
3. L’eleganza del “Method Chaining”
Senza inplace, puoi scrivere codice che sembra una ricetta:
# Leggibile, pulito e sicuro
df_pulito = (df.drop_duplicates()
.dropna(subset=['prezzo'])
.rename(columns=str.lower))
Con inplace=True, dovresti scrivere tre righe separate, ripetendo ogni volta il nome della variabile. Noioso e più esposto ad errori.
Regola d’oro: Se sei in dubbio, non usare
inplace=True. Usa sempre l’assegnazione esplicitadf = df.metodo(). È più sicura, più leggibile e supportata dalle versioni future di Pandas.
Esercizio 3
Con how='right' avremmo mantenuto tutte le righe del DataFrame di destra (dipartimenti), anche se non ci fossero dipendenti associati. Nel nostro caso tutti i dipartimenti hanno dipendenti, quindi il risultato sarebbe identico al left join. Avrebbe senso usare right join se volessimo assicurarci di vedere tutti i dipartimenti, anche quelli senza dipendenti (ad esempio per report completi).




