3 Esercizi Pratici per Imparare Pandas: Pulizia Dati, Filtri Booleani e Merge tra DataFrame

Cerca:

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
Analisi Dati con Pandas e Python

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. 💻🐍

Pubblicità

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?

Forse potrebbe interessarti anche:  Test t di Welch: Guida Completa con Esercizi Risolti per Studenti e Professionisti

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:

  1. Identifica e gestisci i valori mancanti: per la colonna Data sostituisci con la data “2023-12-31” (fine mese); per Prezzo e Totale sostituisci con la media dei rispettivi valori.

  2. Rimuovi le righe duplicate (considerando tutte le colonne).

  3. Rinomina le colonne in minuscolo e senza spazi (es. Data → dataPrezzo → prezzo, ecc.).

  4. Crea una nuova colonna ricavo calcolata come prezzo * quantità (dove ancora non presente) e confrontala con la colonna totale originale 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:

  1. 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.

  2. 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.

  3. 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.

Forse potrebbe interessarti anche:  Analisi della Fedeltà dei Clienti con Python: Un Approccio Pratico con Pandas

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:

  1. Unisci i due DataFrame in modo da ottenere per ogni dipendente anche il nome del dipartimento e la sede.

  2. Calcola lo stipendio medio per ogni dipartimento.

  3. 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). Con how='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_on e right_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).

Forse potrebbe interessarti anche:  Python: la libreria Pandas. Le Series ed i DataFrame

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:

  1. L’azione: Pandas rimuove i duplicati direttamente nell’oggetto df.

  2. Il ritorno: Poiché il metodo ha modificato l’originale, “restituisce” un valore speciale chiamato None (il nulla).

  3. Il disastro: Tu prendi quel None e lo assegni a df.

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:

  1. Niente brutte sorprese: Riassegnando sempre (df = df.metodo()), sai sempre cosa contiene la variabile.

  2. 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) (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 esplicita df = 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).

Pubblicità