Quando si lavora con i dati, c’è una linea sottile che separa un foglio di calcolo illeggibile da un’analisi strategica su cui un’azienda può investire del budget. Pandas è lo standard industriale per muoversi oltre quella linea, ma chiunque ci abbia sbattuto la testa sa che basta una virgola fuori posto, un valore nullo non gestito o un’aggregazione mal calcolata per bloccare un’intera pipeline di produzione.
In questa guida analizzeremo direttamente tre scenari reali di complessità crescente: dal filtraggio di base della pipeline delle vendite, passando per la pulizia dei dati mancanti nei database relazionali, fino alla ristrutturazione di serie temporali complesse con le tabelle pivot.
📌 Esercizio 1 – Livello: Medio
Argomento: DataFrame Pandas – selezione, filtri, nuove colonne
💡 Quando un manager ti chiede un report sulle vendite, la tentazione di scaricare il file in Excel e usare i filtri manuali è forte. Ma cosa succede se domani arrivano altri 50 file di dati? Imparare a usare la logica booleana di Pandas per filtrare i dati ti permette di automatizzare questa operazione una volta per tutte. Scrivi il codice oggi, lo riusi per i prossimi dieci anni.
Testo
Sei un analista in una piccola azienda.
Hai il seguente DataFrame con le vendite:
import pandas as pd
df = pd.DataFrame({
'prodotto': ['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B'],
'regione': ['Nord', 'Sud', 'Nord', 'Centro', 'Nord', 'Sud', 'Sud', 'Centro'],
'quantità': [10, 15, 7, 12, 9, 14, 8, 11],
'prezzo_unitario': [5.0, 3.5, 8.0, 5.0, 3.5, 8.0, 5.0, 3.5]
})
- Aggiungi una colonna
ricavo = quantità * prezzo_unitario. - Filtra solo le righe dove
regioneèNord. - Calcola il ricavo medio per prodotto (groupby).
- Quale prodotto ha il ricavo totale più alto?
Soluzione Completa in Python
Ecco il codice commentato passo dopo passo per risolvere tutti i punti dell’esercizio:
import pandas as pd
# Creazione del DataFrame originale
df = pd.DataFrame({
'prodotto': ['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B'],
'regione': ['Nord', 'Sud', 'Nord', 'Centro', 'Nord', 'Sud', 'Sud', 'Centro'],
'quantità': [10, 15, 7, 12, 9, 14, 8, 11],
'prezzo_unitario': [5.0, 3.5, 8.0, 5.0, 3.5, 8.0, 5.0, 3.5]
})
# 1. Aggiungi la colonna ricavo
df['ricavo'] = df['quantità'] * df['prezzo_unitario']
print("--- 1. DataFrame con colonna 'ricavo' ---")
print(df)
print("\n")
# 2. Filtra solo le righe dove regione è Nord
df_nord = df[df['regione'] == 'Nord']
print("--- 2. Righe filtrate per regione 'Nord' ---")
print(df_nord)
print("\n")
# 3. Calcola il ricavo medio per prodotto (usando il DataFrame intero)
ricavo_medio = df.groupby('prodotto')['ricavo'].mean()
print("--- 3. Ricavo medio per prodotto ---")
print(ricavo_medio)
print("\n")
# 4. Quale prodotto ha il ricavo totale più alto?
ricavo_totale = df.groupby('prodotto')['ricavo'].sum()
prodotto_top = ricavo_totale.idxmax()
valore_top = ricavo_totale.max()
print("--- 4. Prodotto con il ricavo totale più alto ---")
print(f"Il prodotto con il ricavo più alto è il prodotto '{prodotto_top}' con un totale di {valore_top}")
Output del Codice e Risposte ai Quesiti
Punto 1 & 2: DataFrame con colonna ‘ricavo’ e filtro Regione ‘Nord’
L’operazione di vettorizzazione calcola il ricavo per tutte le transazioni. Successivamente, il filtro applicato sulla regione Nord isola esclusivamente le seguenti 3 righe dal dataset originale:
- Riga 0 – Prodotto A (Nord): [math]10 \times 5.0 = 50.0[/math]
- Riga 2 – Prodotto C (Nord): [math]7 \times 8.0 = 56.0[/math]
- Riga 4 – Prodotto B (Nord): [math]9 \times 3.5 = 31.5[/math]
💡 Nota di coerenza: Il prodotto A compare anche nelle regioni Centro e Sud, ma l’applicazione del filtro
df['regione'] == 'Nord'esclude correttamente tali record dalla visualizzazione, mantenendo solo la transazione iniziale.
Punto 3: Ricavo medio per prodotto
Il calcolo del groupby applicato alla media del ricavo sull’intero dataset (senza filtri di regione) restituisce:
- Prodotto A: [math]50.0[/math] (media aritmetica tra i ricavi di Nord: 50.0, Centro: 60.0, Sud: 40.0)
- Prodotto B: [math]40.25[/math] (media aritmetica tra i ricavi di Sud: 52.5, Nord: 31.5, Centro: 38.5)
- Prodotto C: [math]84.0[/math] (media aritmetica tra i ricavi di Nord: 56.0, Sud: 112.0)
Punto 4: Prodotto con il ricavo totale più alto
Calcolando la somma complessiva dei ricavi per ogni singolo prodotto su tutto il DataFrame:
- Prodotto A: [math]50.0 + 60.0 + 40.0 = 150.0[/math]
- Prodotto B: [math]52.5 + 31.5 + 38.5 = 122.5[/math]
- Prodotto C: [math]56.0 + 112.0 = 168.0[/math]
Risposta Finale: Il prodotto con il ricavo totale più alto è il prodotto C, con un ricavo complessivo di 168.0.
📌 Esercizio 2 – Livello: Medio/Difficile
Argomento: Merge di DataFrame, handling dati mancanti (missing data)
⚠️ ATTENZIONE ai finti dati puliti: Nei database aziendali reali i dati mancanti (
NaNoNone) sono la norma, non l’eccezione. Molti analisti alle prime armi cancellano brutalmente le righe vuote o le sovrascrivono con la media generale dell’intera colonna. È il modo perfetto per falsare i dati e presentare un report statistico completamente distorto. In questo esercizio vedremo come fare una “pulizia chirurgica” e contestuale.
Testo
Hai a disposizione due dataset relazionali:
studenticon le colonne:id_studente,nome,classevoticon le colonne:id_studente,materia,voto
Alcuni studenti non hanno voti inseriti, mentre alcuni record nei voti presentano valori mancanti (None / NaN).
import pandas as pd
import numpy as np
studenti = pd.DataFrame({
'id_studente': [1, 2, 3, 4, 5],
'nome': ['Anna', 'Luca', 'Maria', 'Giulia', 'Marco'],
'classe': [5, 5, 4, 5, 4]
})
voti = pd.DataFrame({
'id_studente': [1, 1, 2, 3, 3, 4, 5],
'materia': ['Mate', 'Fisica', 'Mate', 'Mate', 'Fisica', 'Fisica', 'Mate'],
'voto': [8, 7, 6, None, 8, 9, None]
})
- Unisci i due DataFrame con un merge esterno (full outer) sulla chiave
id_studente. - Sostituisci i valori
None(NaN) nella colonnavotocon la media dei voti della rispettiva materia. - Calcola la media dei voti per singolo studente.
- Mostra il nome e la media finale degli studenti appartenenti esclusivamente alla classe 5.
Soluzione Completa in Python
Di seguito viene mostrato il codice commentato passo dopo passo per eseguire l’intera pipeline di manipolazione dati:
import pandas as pd
import numpy as np
# Dataset di partenza
studenti = pd.DataFrame({
'id_studente': [1, 2, 3, 4, 5],
'nome': ['Anna', 'Luca', 'Maria', 'Giulia', 'Marco'],
'classe': [5, 5, 4, 5, 4]
})
voti = pd.DataFrame({
'id_studente': [1, 1, 2, 3, 3, 4, 5],
'materia': ['Mate', 'Fisica', 'Mate', 'Mate', 'Fisica', 'Fisica', 'Mate'],
'voto': [8, 7, 6, None, 8, 9, None]
})
# 1. Full Outer Merge
df_merged = pd.merge(studenti, voti, on='id_studente', how='outer')
print("--- 1. DataFrame dopo il Full Outer Merge ---")
print(df_merged)
print("\n")
# 2. Imputazione dei missing values con la media della specifica materia
# Calcoliamo prima le medie per ogni materia per trasparenza algebrica
medie_materie = df_merged.groupby('materia')['voto'].transform('mean')
df_merged['voto'] = df_merged['voto'].fillna(medie_materie)
print("--- 2. DataFrame con NaN sostituiti dalle medie di materia ---")
print(df_merged)
print("\n")
# 3. Calcolo della media voti per ogni studente (mantenendo nome e classe)
# Raggruppiamo per id, nome e classe per non perdere i metadati
df_medie_studenti = df_merged.groupby(['id_studente', 'nome', 'classe'])['voto'].mean().reset_index()
df_medie_studenti.rename(columns={'voto': 'media_voti'}, inplace=True)
print("--- 3. Media voti calcolata per ogni studente ---")
print(df_medie_studenti)
print("\n")
# 4. Filtro per la classe 5 e selezione delle colonne richieste
risultato_classe_5 = df_medie_studenti[df_medie_studenti['classe'] == 5][['nome', 'media_voti']]
print("--- 4. Risultato finale: studenti della classe 5 ---")
print(risultato_classe_5)
Spiegazione dei Passaggi e Calcoli Matematici
Passaggio 1: Il Merge Esterno
L’operazione how='outer' unisce tutte le righe di entrambe le tabelle. In questo caso specifico, i record corrispondono perfettamente per via degli ID, ma la struttura viene linearizzata ripetendo i dati anagrafici dello studente per ogni voto preso.
Passaggio 2: Calcolo e Imputazione delle Medie di Materia
Prima di sostituire i valori mancanti, il motore calcola la media dei voti validi per ciascuna materia presente nel dataset:
- Fisica: I voti validi sono 7 e 9. La media è [math]\frac{7 + 9}{2} = 8.0[/math].
- Mate: I voti validi sono 8 e 6. La media è [math]\frac{8 + 6}{2} = 7.0[/math].
I valori NaN vengono quindi sostituiti in modo mirato:
- Il voto mancante di Maria (id 3) in Mate diventa 7.0.
- Il voto mancante di Marco (id 5) in Mate diventa 7.0.
Passaggio 3 & 4: Aggregazione e Filtro Classe 5
Viene calcolata la media aritmetica dei voti per ogni singolo studente aggregato. Successivamente, isolando solo le righe in cui classe == 5, si ottiene la seguente situazione:
- Anna: Ha i voti 8.0 e 7.0. La sua media è [math]\frac{8 + 7}{2} = 7.5[/math].
- Luca: Ha un solo voto pari a 6.0. La sua media è 6.0.
- Giulia: Ha un solo voto pari a 9.0. La sua media è 9.0.
Output Finale della Classe 5:
- Anna: 7.5
- Luca: 6.0
- Giulia: 9.0
📌 Esercizio 3 – Livello: Difficile
Argomento: Pivot table, analisi temporale (Pandas) e performance
🚀 PUNTA ALLE PERFORMANCE: Molti programmatori che passano a Pandas commettono l’errore di usare i cicli
forper fare calcoli riga per riga (iterrows()). Se lo fai su un dataset di 10 milioni di righe, fai in tempo ad andare a prendere un caffè, fare pranzo e tornare che il codice sta ancora girando. Pandas lavora sfruttando la vettorizzazione nativa in linguaggio C: impara a usarla e il tuo codice passerà da minuti a frazioni di secondo.
Testo
Un supermercato registra gli acquisti giornalieri per una settimana. Il dataset viene generato casualmente tramite il seguente codice:
import pandas as pd
import numpy as np
np.random.seed(42)
date = pd.date_range('2025-01-01', periods=7, freq='D')
prodotti = ['Pasta', 'Latte', 'Pane', 'Uova']
df_vendite = pd.DataFrame({
'data': np.random.choice(date, 50),
'prodotto': np.random.choice(prodotti, 50),
'quantità': np.random.randint(1, 10, 50),
'prezzo': np.random.uniform(0.8, 3.5, 50).round(2)
})
- Crea una colonna
ricavo = quantità * prezzo. - Crea una
pivot tableche mostri il ricavo medio per prodotto per giorno. - Trova il giorno della settimana con il ricavo totale complessivo più alto.
- Per ogni singolo prodotto, individua il giorno in cui si è registrato il massimo ricavo totale.
Soluzione Completa in Python
Ecco il codice sorgente corretto. Nota l’utilizzo della funzione nativa round() di Python per gestire le variabili scalari isolate dal ciclo di aggregazione:
import pandas as pd
import numpy as np
# 0. Generazione guidata del dataset con seed fisso
np.random.seed(42)
date = pd.date_range('2025-01-01', periods=7, freq='D')
prodotti = ['Pasta', 'Latte', 'Pane', 'Uova']
df_vendite = pd.DataFrame({
'data': np.random.choice(date, 50),
'prodotto': np.random.choice(prodotti, 50),
'quantità': np.random.randint(1, 10, 50),
'prezzo': np.random.uniform(0.8, 3.5, 50).round(2)
})
# 1. Creazione della colonna ricavo
df_vendite['ricavo'] = (df_vendite['quantità'] * df_vendite['prezzo']).round(2)
# 2. Creazione della pivot table (ricavo medio per prodotto/giorno)
pivot_medie = df_vendite.pivot_table(
values='ricavo',
index='data',
columns='prodotto',
aggfunc='mean'
).round(2)
print("--- 2. Pivot Table: Ricavo MEDIO per Prodotto e Giorno ---")
print(pivot_medie)
print("\n")
# 3. Giorno con il ricavo totale più alto
ricavo_totale_giornaliero = df_vendite.groupby('data')['ricavo'].sum()
giorno_top = ricavo_totale_giornaliero.idxmax()
valore_giorno_top = ricavo_totale_giornaliero.max()
print("--- 3. Giorno con il ricavo totale più alto ---")
# pd.to_datetime garantisce la formattazione della data, round() nativo evita l'AttributeError
data_formattata = pd.to_datetime(giorno_top).strftime('%Y-%m-%d')
print(f"Giorno top: {data_formattata} con un ricavo totale di {round(valore_giorno_top, 2)}")
print("\n")
# 4. Giorno di massimo ricavo totale per OGNI prodotto
pivot_totali = df_vendite.pivot_table(
values='ricavo',
index='data',
columns='prodotto',
aggfunc='sum'
).fillna(0)
giorni_max_per_prodotto = pivot_totali.idxmax()
print("--- 4. Giorno di massimo ricavo totale per ciascun prodotto ---")
for prodotto, data_max in giorni_max_per_prodotto.items():
valore_max = pivot_totali.loc[data_max, prodotto]
data_max_formattata = pd.to_datetime(data_max).strftime('%Y-%m-%d')
print(f"Prodotto: {prodotto:<6} -> Giorno Top: {data_max_formattata} (Ricavo Totale: {round(valore_max, 2)})")
--- 2. Pivot Table: Ricavo MEDIO per Prodotto e Giorno ---
prodotto Latte Pane Pasta Uova
data
2025-01-01 NaN 13.65 NaN 5.90
2025-01-02 14.22 NaN 12.78 7.31
2025-01-03 15.55 NaN 9.70 5.33
2025-01-04 13.03 15.66 1.10 10.68
2025-01-05 9.55 8.90 26.82 11.65
2025-01-06 14.30 7.05 5.39 23.84
2025-01-07 9.61 NaN 12.20 22.72
--- 3. Giorno con il ricavo totale più alto ---
Giorno top: 2025-01-04 con un ricavo totale di 123.93
--- 4. Giorno di massimo ricavo totale per ciascun prodotto ---
Prodotto: Latte -> Giorno Top: 2025-01-03 (Ricavo Totale: 77.75)
Prodotto: Pane -> Giorno Top: 2025-01-04 (Ricavo Totale: 46.99)
Prodotto: Pasta -> Giorno Top: 2025-01-05 (Ricavo Totale: 26.82)
Prodotto: Uova -> Giorno Top: 2025-01-05 (Ricavo Totale: 34.94)
Analisi e Commento Tecnico del Codice
Questo script rappresenta un caso di studio avanzato sull’utilizzo di Pandas per l’analisi di serie temporali e l’aggregazione di matrici bidimensionali. Vediamo nel dettaglio come funzionano i singoli blocchi di codice e perché sono stati strutturati in questo modo.
1. Inizializzazione del Dataset e Gestione del “Seed”
La prima parte dello script si occupa di generare un dataset fittizio (mock data) che simula le transazioni di un supermercato:
np.random.seed(42)
date = pd.date_range('2025-01-01', periods=7, freq='D')
prodotti = ['Pasta', 'Latte', 'Pane', 'Uova']
Dettaglio tecnico:
np.random.seed(42): Configura il generatore di numeri pseudocasuali di NumPy con uno stato iniziale fisso (42). Questo garantisce la riproducibilità del codice: ogni volta che lo script viene eseguito, genererà gli stessi identici dati, permettendo di verificare la correttezza dei calcoli algebrici.pd.date_range(..., freq='D'): Crea un indice temporale nativo di Pandas composto da 7 giorni consecutivi.
2. Vettorizzazione elementare (Punto 1)
Per calcolare il ricavo di ogni singola transazione, il codice esegue un’operazione nota come vettorizzazione:
df_vendite['ricavo'] = (df_vendite['quantità'] * df_vendite['prezzo']).round(2)
Invece di utilizzare un ciclo iterativo (come un ciclo for) per scorrere le righe una alla volta, Pandas moltiplica gli interi vettori colonna (Series) in un colpo solo. Questa operazione viene eseguita direttamente in memoria a basso livello (linguaggio C), garantendo performance eccezionali anche su milioni di righe. Il metodo .round(2) finale pulisce i decimali fluttuanti tipici delle operazioni binari sui float.
3. Riorganizzazione della Matrice tramite Pivot Table (Punto 2)
La Pivot Table è lo strumento matematico ideale per trasformare dati lineari in strutture cross-tabulari:
pivot_medie = df_vendite.pivot_table(
values='ricavo',
index='data',
columns='prodotto',
aggfunc='mean'
).round(2)
Qui indichiamo a Pandas di proiettare le date sulle righe ([math]\text{index}[/math]) e le categorie di prodotto sulle colonne ([math]\text{columns}[/math]). Il parametro cruciale è aggfunc='mean': se in una stessa data ci sono più vendite dello stesso prodotto, il motore calcolerà la media aritmetica di quei ricavi. Le combinazioni data-prodotto che non hanno registrato vendite verranno popolate con NaN (valore nullo).
4. Aggregazione Temporale e Risoluzione dei Tipi Scalari (Punto 3)
Per trovare il giorno con l’incasso totale più alto, isoliamo l’asse temporale:
ricavo_totale_giornaliero = df_vendite.groupby('data')['ricavo'].sum()
giorno_top = ricavo_totale_giornaliero.idxmax()
valore_giorno_top = ricavo_totale_giornaliero.max()
Logica applicata:
.groupby('data')['ricavo'].sum(): Somma tutti i ricavi del supermercato collassando i dati per giorno..idxmax(): Restituisce l’indice associato al valore massimo (ovvero la data esatta del picco di vendite)..max(): Restituisce il valore numerico (il ricavo effettivo) di quel picco.
Risoluzione del Bug (Scalari vs Vettori): Quando estraiamo
valore_giorno_toptramite.max(), otteniamo un numero float puro (nativo di Python) e non più una Series di Pandas. Per formattarlo senza generare unAttributeError, non possiamo usare il metodo ad oggetti.round(), ma dobbiamo passarlo all’interno della funzione nativa del linguaggio:round(valore_giorno_top, 2).
5. Gestione dei Valori Nulli e Ricerca Multicorrispondenza (Punto 4)
L’ultimo blocco analizza le performance massime di ogni singolo prodotto lungo l’arco della settimana:
pivot_totali = df_vendite.pivot_table(..., aggfunc='sum').fillna(0)
giorni_max_per_prodotto = pivot_totali.idxmax()
In questo caso, la pivot table usa aggfunc='sum' per ottenere i ricavi totali (e non medi). Viene applicato immediatamente .fillna(0): questo passaggio è fondamentale perché la presenza di valori NaN potrebbe falsare l’algoritmo di ricerca del punto di massimo.
Infine, chiamando .idxmax() sull’intero DataFrame pivot_totali, Pandas esegue una scansione verticale su ogni colonna (prodotto), restituendo un dizionario di corrispondenze in cui ad ogni prodotto è associata la data del suo massimo successo commerciale.
Pandas, NumPy & Analisi Dati in Python
👉Vettorializzazione e broadcasting in NumPy: guida pratica con esercizi
👉Pandas in Python: tutorial completo per analisi e manipolazione dati
👉Operazioni sui DataFrame con Pandas
👉🐼 Pandas per principianti: analizzare un dataset in 10 minuti
👉DataFrame in Python: 6 esercizi pratici con Pandas
👉Pandas oltre la teoria: casi reali per diventare Data Analyst





