Chi lavora con i dati lo sa bene: i tutorial online mostrano quasi sempre dataset perfetti, dove tutto fila liscio al primo colpo e ogni colonna è esattamente dove dovrebbe essere. Poi arrivi in ufficio, apri il file inviato dal cliente e trovi date scritte al contrario, valori mancanti sparsi a caso e intestazioni incomprensibili.
È in questi momenti che si vede la differenza tra chi ha studiato solo la teoria e chi sa mettere le mani nel codice per risolvere problemi reali. In questo articolo faremo esattamente questo: tre casi di studio che potresti tranquillamente trovare domattina sulla tua scrivania. Partiremo dalla sistemazione di un tracciato ordini un po’ caotico, per poi passare all’analisi delle performance dei fornitori, fino ad arrivare alla valutazione della stagionalità tramite tabelle pivot.
Prepara il tuo ambiente Python, apri un nuovo Notebook e cominciamo.
Esercizio 1 – Livello: Base
Tracciato ordini: pulizia e primo sguardo
Testo
Sei un analista dati presso LogiTech Srl, un’azienda di logistica che gestisce spedizioni per conto terzi. Ti viene fornito il file ordini.csv contenente i dati degli ordini elaborati nell’ultimo mese. I dati presentano alcune criticità: date in formato non standard, valori mancanti, e colonne con nomi non chiari.
Il tuo compito è:
- Caricare il dataset in un DataFrame Pandas.
- Esplorare la struttura dei dati (prime righe, informazioni generali, statistiche descrittive).
- Convertire la colonna delle date nel formato datetime.
- Rinominare le colonne con nomi più chiari e leggibili.
- Identificare e gestire i valori mancanti nella colonna
quantità(sostituendoli con la mediana). - Salvare il dataset pulito in un nuovo file CSV.
Dataset di esempio (crealo tu per l’esercizio):
import pandas as pd
import numpy as np
np.random.seed(42)
data = {
'OrderID': range(1001, 1051),
'OrderDate': pd.date_range('2026-01-01', periods=50).strftime('%d/%m/%Y'),
'Customer': [f'Client_{i}' for i in np.random.choice(range(1, 21), 50)],
'Product': np.random.choice(['Laptop', 'Mouse', 'Keyboard', 'Monitor', 'Printer'], 50),
'Qty': np.random.choice([1, 2, 3, np.nan, 5], 50, p=[0.3, 0.25, 0.2, 0.15, 0.1]),
'UnitPrice': np.round(np.random.uniform(10, 500, 50), 2)
}
df = pd.DataFrame(data)
df.to_csv('ordini.csv', index=False)
Soluzione
import pandas as pd
import numpy as np
# 1. Caricamento del dataset
df = pd.read_csv('ordini.csv')
print("=== Prime 5 righe ===")
print(df.head())
print("\n=== Info dataset ===")
print(df.info())
# Output atteso: 50 righe, 6 colonne, Qty ha valori mancanti
# 2. Statistiche descrittive delle colonne numeriche
print("\n=== Statistiche descrittive ===")
print(df.describe())
# 3. Conversione della data in formato datetime
# La data è nel formato gg/mm/aaaa, specifichiamo il format per evitare ambiguità
df['OrderDate'] = pd.to_datetime(df['OrderDate'], format='%d/%m/%Y')
print("\n=== Tipo di OrderDate dopo conversione ===")
print(df['OrderDate'].dtype) # datetime64[ns]
# 4. Rinomina delle colonne per maggiore chiarezza
df = df.rename(columns={
'OrderID': 'id_ordine',
'OrderDate': 'data_ordine',
'Customer': 'cliente',
'Product': 'prodotto',
'Qty': 'quantita',
'UnitPrice': 'prezzo_unitario'
})
# 5. Identificazione e gestione dei valori mancanti in 'quantita'
print(f"\nValori mancanti in 'quantita': {df['quantita'].isna().sum()}")
# Calcolo della mediana (ignorando i NaN)
mediana_quantita = df['quantita'].median()
print(f"Mediana di 'quantita': {mediana_quantita}")
# Sostituzione dei NaN con la mediana
df['quantita'] = df['quantita'].fillna(mediana_quantita)
# Verifica: nessun valore mancante
print(f"Valori mancanti dopo il fill: {df['quantita'].isna().sum()}")
# 6. Calcolo di una nuova colonna: totale riga = quantita * prezzo_unitario
df['totale_riga'] = df['quantita'] * df['prezzo_unitario']
# 7. Salvataggio del dataset pulito
df.to_csv('ordini_pulito.csv', index=False)
print("\n✅ Dataset pulito salvato come 'ordini_pulito.csv'")
# Verifica finale
print("\n=== Prime 5 righe del dataset pulito ===")
print(df.head())
=== Prime 5 righe ===
OrderID OrderDate Customer Product Qty UnitPrice
0 1001 01/01/2026 Client_7 Monitor 3.0 354.48
1 1002 02/01/2026 Client_20 Mouse NaN 188.18
2 1003 03/01/2026 Client_15 Mouse 2.0 486.17
3 1004 04/01/2026 Client_11 Monitor 1.0 481.60
4 1005 05/01/2026 Client_8 Printer 3.0 133.37
=== Statistiche descrittive ===
OrderID Qty UnitPrice
count 50.00000 38.000000 50.000000
mean 1025.50000 1.973684 243.771600
std 14.57738 1.126747 143.135829
min 1001.00000 1.000000 18.130000
25% 1013.25000 1.000000 127.695000
50% 1025.50000 2.000000 251.740000
75% 1037.75000 2.000000 346.922500
max 1050.00000 5.000000 492.970000
=== Tipo di OrderDate dopo conversione ===
datetime64[ns]
Valori mancanti in 'quantita': 12
Mediana di 'quantita': 2.0
Valori mancanti dopo il fill: 0
✅ Dataset pulito salvato come 'ordini_pulito.csv'
=== Prime 5 righe del dataset pulito ===
id_ordine data_ordine cliente prodotto quantita prezzo_unitario \
0 1001 2026-01-01 Client_7 Monitor 3.0 354.48
1 1002 2026-01-02 Client_20 Mouse 2.0 188.18
2 1003 2026-01-03 Client_15 Mouse 2.0 486.17
3 1004 2026-01-04 Client_11 Monitor 1.0 481.60
4 1005 2026-01-05 Client_8 Printer 3.0 133.37
totale_riga
0 1063.44
1 376.36
2 972.34
3 481.60
4 400.11
💡 Perché questo esercizio è interessante
Questo script è il “pane quotidiano” dell’analisi dati.
- Le Date: L’uso del parametro
format='%d/%m/%Y'inpd.to_datetimeti salva la vita. Senza di esso, Pandas cerca di indovinare se il primo numero è un giorno o un mese (formato americano vs europeo), causando errori silenziosi e disastrosi. - L’Imputazione (La Scelta della Mediana): Perché usare la mediana e non la media per riempire i vuoti? La mediana è robusta. In logistica, se hai 99 ordini da 1 pezzo e 1 ordine B2B da 1000 pezzi, la media schizza alle stelle, falsando i dati imputati. La mediana, invece, ignora i valori estremi (outlier) e ti restituisce un valore molto più realistico per un ordine “tipo”.
- La creazione di nuove colonne derivate (
totale_riga) è un’operazione fondamentale nell’analisi dei dati
Domanda di riflessione
Quale proprietà della mediana la rende preferibile alla media per l’imputazione di valori mancanti in presenza di outlier?
Risposta:
La mediana è resistente agli outlier (robusta), mentre la media è sensibile ai valori estremi. In un dataset con pochi ordini di quantità molto elevate, la media verrebbe trascinata verso l’alto, distorcendo l’imputazione.
Esercizio 2 – Livello: Base/Intermedio
Analisi delle performance dei fornitori
Testo
L’ufficio acquisti di GreenFood SpA, un’azienda di distribuzione alimentare, ti chiede di analizzare le performance dei propri fornitori. Hai a disposizione il dataset fornitori.csv con i seguenti dati relativi agli ultimi 6 mesi:
fornitore: nome del fornitoredata_ordine: data di emissione dell’ordinedata_consegna: data di effettiva consegnaprodotto: categoria merceologicaquantita: numero di unità ordinatecosto_unitario: costo per unità (€)
Richiesta:
- Calcolare per ogni fornitore il tempo medio di consegna (in giorni).
- Determinare il tasso di puntualità (% di consegne entro 3 giorni lavorativi).
- Creare un report riassuntivo per fornitore con: numero ordini, quantità totale, spesa totale, tempo medio di consegna, tasso di puntualità.
- Identificare il fornitore con la spesa totale più alta e quello con il costo medio per unità più basso.
Soluzione
import pandas as pd
import numpy as np
# Creazione dataset di esempio
np.random.seed(123)
n_ordini = 200
fornitori = ['Fornitore_A', 'Fornitore_B', 'Fornitore_C', 'Fornitore_D']
prodotti = ['Frutta', 'Verdura', 'Carne', 'Latticini', 'Surgelati']
df = pd.DataFrame({
'fornitore': np.random.choice(fornitori, n_ordini),
'data_ordine': pd.date_range('2025-07-01', periods=n_ordini),
'prodotto': np.random.choice(prodotti, n_ordini),
'quantita': np.random.randint(10, 500, n_ordini),
'costo_unitario': np.round(np.random.uniform(1.5, 25.0, n_ordini), 2)
})
# Aggiunta data_consegna: data_ordine + 1-7 giorni (con qualche ritardo)
df['data_consegna'] = df['data_ordine'] + pd.to_timedelta(
np.random.choice([1, 2, 3, 4, 5, 6, 7, 10, 15], n_ordini), unit='D'
)
# === SOLUZIONE ===
# 1. Calcolo del tempo di consegna in giorni
# Usiamo .dt.days per avere un numero intero pulito su cui fare calcoli
df['tempo_consegna'] = (df['data_consegna'] - df['data_ordine']).dt.days
# 2. Verifica puntualità (consegna <= 3 giorni) restituisce True/False
df['puntuale'] = df['tempo_consegna'] <= 3
# 3. Calcolo preliminare della spesa per singola riga (Best Practice)
df['spesa_riga'] = df['quantita'] * df['costo_unitario']
# 4. Creazione del Report aggregato per fornitore
report = df.groupby('fornitore').agg(
numero_ordini=('fornitore', 'count'),
quantita_totale=('quantita', 'sum'),
spesa_totale=('spesa_riga', 'sum'),
tempo_medio_consegna=('tempo_consegna', 'mean'),
tasso_puntualita=('puntuale', 'mean') # La media dei booleani dà la percentuale!
).reset_index()
# Arrotondamenti per leggibilità
report['tempo_medio_consegna'] = report['tempo_medio_consegna'].round(1)
report['tasso_puntualita'] = (report['tasso_puntualita'] * 100).round(1)
print("=== Report Performance Fornitori ===")
print(report.to_string(index=False))
# 5. Estrazione dei fornitori top/flop
# Per la spesa totale peschiamo direttamente dal report appena creato
fornitore_top_spesa = report.loc[report['spesa_totale'].idxmax()]
# Per il costo medio unitario, calcoliamo la media sul dataframe originale
costo_medio_unitario = df.groupby('fornitore')['costo_unitario'].mean()
print(f"\n🏆 Fornitore con spesa totale più alta: {fornitore_top_spesa['fornitore']} "
f"({fornitore_top_spesa['spesa_totale']:,.2f} €)")
print(f"💰 Fornitore con costo unitario medio più basso: {costo_medio_unitario.idxmin()} "
f"({costo_medio_unitario.min():.2f} €)")
Output Atteso
=== Report Performance Fornitori ===
fornitore numero_ordini quantita_totale spesa_totale tempo_medio_consegna tasso_puntualita
Fornitore_A 51 13466 182370.60 7.3 21.6
Fornitore_B 47 10949 126820.08 5.3 36.2
Fornitore_C 51 12123 168586.54 5.6 29.4
Fornitore_D 51 12721 166254.27 4.8 39.2
🏆 Fornitore con spesa totale più alta: Fornitore_A (182,370.60 €)
💰 Fornitore con costo unitario medio più basso: Fornitore_B (11.72 €)
💡 Perché questo esercizio ti salva la vita in ufficio
Questo script simula in poche righe la costruzione di indicatori di performance (KPI) che in Excel richiederebbero fogli di lavoro incrociati, CERCA.VERT e formule fragili.
- L’ordine delle operazioni conta: Piuttosto che tentare calcoli complessi durante il raggruppamento, abbiamo calcolato la
spesa_rigaa monte. Questa è una best practice assoluta in Pandas: prima prepari tutte le colonne necessarie sul dataset granulare, e solo dopo aggreghi. Il codice diventa più leggibile e l’esecuzione incredibilmente più veloce. - Il trucco dei Booleani: Costruire un KPI sulle consegne puntuali sembra complesso, ma in Python
Truevale 1 eFalsevale 0. Chiedere a Pandas di fare la media (mean) su una colonna di Vero/Falso ti restituisce istantaneamente la percentuale matematica di successo. Riduci righe di logica condizionale a un’operazione statistica istantanea. - Riciclo dei dati: Quando abbiamo cercato il fornitore con la spesa più alta, non abbiamo ricalcolato tutto da zero. Abbiamo usato il report appena creato (
report['spesa_totale'].idxmax()). Ottimizzare significa non far fare al processore due volte lo stesso lavoro.
Domanda di riflessione
Nel calcolo del tasso_puntualita abbiamo usato mean() su una colonna booleana. Cosa succederebbe a questo calcolo se alcuni ordini non avessero ancora una data di consegna (generando un valore mancante o NaN nella colonna tempo_consegna)?
Risposta: Di default, la funzione mean() di Pandas ignora i valori NaN. Questo significa che il fornitore verrebbe valutato solo sugli ordini effettivamente consegnati. A livello aziendale devi chiederti se questo ha senso o se gli ordini non consegnati (che sono tecnicamente in ritardo) debbano essere contati come False per non falsare in positivo le statistiche del fornitore.
Esercizio 3 – Livello: Intermedio/Avanzato
Ottimizzazione dell’inventario: Analisi ABC e Redditività Reale
Testo
TechDistrib è un distributore di componenti elettronici. Chi lavora in magazzino sa che non tutti i prodotti hanno lo stesso peso commerciale: dedicare lo stesso tempo al conteggio di un cavo da 50 centesimi e a una scheda video da 800 euro è uno spreco di risorse.
Il management ti chiede di classificare i prodotti secondo il metodo ABC (basato sul principio di Pareto) per ottimizzare la gestione dell’inventario. Hai a disposizione il dataset prodotti.csv:
codice_prodotto: identificativo univoconome_prodotto: descrizionecategoria: famiglia merceologicaquantita_venduta: unità vendute nell’ultimo annoprezzo_unitario: prezzo di vendita medio (€)costo_acquisto_unitario: costo sostenuto per acquistare il prodotto dal fornitore (€)costo_holding: costo annuale di mantenimento a magazzino per unità (€)
La metodologia ABC classifica i prodotti in base al fatturato cumulato ([math]quantità \times prezzo[/math]). Per questo esercizio useremo una convenzione classica:
- Classe A: i prodotti che generano i primi 80% del fatturato totale (alta priorità).
- Classe B: i prodotti che generano il successivo 15% del fatturato (priorità media).
- Classe C: la coda lunga, ovvero l’ultimo 5% del fatturato (bassa priorità).
Richiesta:
- Calcolare il fatturato annuale per singolo prodotto.
- Ordinare l’inventario per fatturato decrescente e calcolare la percentuale cumulata.
- Assegnare la classe A, B o C a ogni riga secondo la regola 80/15/5.
- Generare un report direzionale con: numero di prodotti, fatturato totale, costo di acquisto, costo di holding e il vero margine operativo per ogni classe.
Soluzione
import pandas as pd
import numpy as np
# Creazione dataset di esempio
np.random.seed(789)
n_prodotti = 200
# Generiamo prezzi di vendita
prezzo_unitario = np.round(np.random.uniform(10, 800, n_prodotti), 2)
# Generiamo un costo di acquisto realistico (tra il 40% e l'85% del prezzo di vendita)
costo_acquisto_unitario = np.round(prezzo_unitario * np.random.uniform(0.40, 0.85, n_prodotti), 2)
df = pd.DataFrame({
'codice_prodotto': [f'P{str(i).zfill(4)}' for i in range(1, n_prodotti + 1)],
'nome_prodotto': [f'Prodotto_{i}' for i in range(1, n_prodotti + 1)],
'categoria': np.random.choice(['CPU', 'RAM', 'Storage', 'GPU', 'Motherboard'], n_prodotti),
'quantita_venduta': np.random.randint(50, 5000, n_prodotti),
'prezzo_unitario': prezzo_unitario,
'costo_acquisto_unitario': costo_acquisto_unitario,
'costo_holding': np.round(np.random.uniform(0.5, 20, n_prodotti), 2)
})
# === SOLUZIONE ===
# 1. Calcolo del fatturato annuale
df['fatturato'] = df['quantita_venduta'] * df['prezzo_unitario']
# 2. Ordinamento per fatturato decrescente e ricalcolo dell'indice
df_sorted = df.sort_values('fatturato', ascending=False).reset_index(drop=True)
# Calcolo del fatturato cumulato e percentuale
df_sorted['fatturato_cumulato'] = df_sorted['fatturato'].cumsum()
fatturato_totale = df_sorted['fatturato'].sum()
df_sorted['percentuale_cumulata'] = (df_sorted['fatturato_cumulato'] / fatturato_totale) * 100
# 3. Assegnazione classe ABC (Soglie convenzionali: 80/15/5)
def assign_abc(pct_cum):
if pct_cum <= 80:
return 'A'
elif pct_cum <= 95:
return 'B'
else:
return 'C'
df_sorted['classe_abc'] = df_sorted['percentuale_cumulata'].apply(assign_abc)
# 4. Calcoli economici a livello di riga (Best Practice)
df_sorted['costo_acquisto_tot'] = df_sorted['quantita_venduta'] * df_sorted['costo_acquisto_unitario']
df_sorted['costo_holding_tot'] = df_sorted['quantita_venduta'] * df_sorted['costo_holding']
# Calcolo del vero margine operativo
df_sorted['margine_operativo_tot'] = (
df_sorted['fatturato']
- df_sorted['costo_acquisto_tot']
- df_sorted['costo_holding_tot']
)
# 5. Creazione del report aggregato
report_abc = df_sorted.groupby('classe_abc').agg(
numero_prodotti=('codice_prodotto', 'count'),
fatturato=('fatturato', 'sum'),
costi_acquisto=('costo_acquisto_tot', 'sum'),
costi_holding=('costo_holding_tot', 'sum'),
margine_operativo=('margine_operativo_tot', 'sum')
).reset_index()
# Aggiunta di metriche percentuali
report_abc['%_fatturato'] = (report_abc['fatturato'] / fatturato_totale * 100).round(1)
report_abc['%_prodotti'] = (report_abc['numero_prodotti'] / n_prodotti * 100).round(1)
print("=== Analisi Economica ABC dell'Inventario ===")
print(report_abc.to_string(index=False))
# 6. Verifica operativa: margine unitario negativo
df_sorted['margine_unitario'] = (
df_sorted['prezzo_unitario']
- df_sorted['costo_acquisto_unitario']
- df_sorted['costo_holding']
)
prodotti_critici = df_sorted[df_sorted['margine_unitario'] < 0]
print(f"\n📊 Distribuzione: il {report_abc.loc[0, '%_prodotti']}% dell'inventario "
f"genera circa l'80% del fatturato (Classe A).")
print(f"⚠️ Attenzione: rilevati {len(prodotti_critici)} prodotti in perdita netta (margine unitario negativo).")
Output Atteso
=== Analisi Economica ABC dell'Inventario ===
classe_abc numero_prodotti fatturato costi_acquisto costi_holding margine_operativo %_fatturato %_prodotti
A 48 17688523.51 11055327.19 242131.50 6391064.82 78.3 24.0
B 44 3613670.36 2258543.97 57182.50 1297943.89 16.0 22.0
C 108 1287614.94 804759.34 65017.00 417838.60 5.7 54.0
📊 Distribuzione: il 24.0% dell'inventario genera circa l'80% del fatturato (Classe A).
⚠️ Attenzione: rilevati 9 prodotti in perdita netta (margine unitario negativo).
💡 Perché questo esercizio fa la differenza in azienda
Questo script trasforma un analista dati in un consulente per il business. Non si limita a ordinare tabelle, ma fa emergere dinamiche economiche nascoste.
- L’Illusione del Fatturato: Avere un prodotto in “Classe A” (alto fatturato) non significa automaticamente che stia sostenendo l’azienda. Inserendo il
costo_acquisto_unitario, il codice ti permette di calcolare il vero margine operativo. Potresti scoprire che un prodotto in Classe A fattura milioni, ma tra costi di acquisto e spese di mantenimento a magazzino (assicurazioni, spazi occupati, deperibilità) genera meno utile netto di un prodotto in Classe B. - Prodotti in Perdita Netta: La formula [math]prezzo – costo_acquisto – costo_holding[/math] svela i “prodotti critici”. Si tratta di articoli che l’azienda paga al fornitore, tiene in magazzino per mesi e, quando finalmente li vende, ci perde soldi. Estrapolare questa singola informazione con Pandas ripaga da solo l’intero stipendio di un data analyst.
- La trappola della “Regola” 80/15/5: Attenzione a non prendere l’80/15/5 come un dogma matematico. È un’eccellente convenzione didattica ed euristica, ma nella pratica aziendale le soglie si adattano. Ad esempio, se un singolo prodotto fa saltare la percentuale cumulata dal 76% all’84%, il nostro script lo categorizzerà strettamente in ‘B’ (perché la condizione
<= 80fallisce). In un software reale, la direzione potrebbe decidere di forzare l’inclusione di quel prodotto in Classe A. Python ti dà la flessibilità per riscrivere la funzioneassign_abcadattandola esattamente alle politiche commerciali della tua azienda.
Domanda di riflessione
Cosa succederebbe alle tue percentuali se i primi due prodotti in Classe A da soli raggiungessero il 60% dell’intero fatturato aziendale?
Risposta: La curva cumulativa salirebbe così verticalmente che pochissimi prodotti (magari il 2% o il 3% dei codici totali) finirebbero in Classe A. Questo ti segnalerebbe un rischio enorme per l’azienda: un “single point of failure”. Se uno di quei due prodotti dovesse avere problemi di approvvigionamento, perderesti immediatamente oltre metà del tuo giro d’affari.
📚 Per approfondire: Pandas, analisi dei dati e DataFrame in Python
Se vuoi consolidare le tue competenze nell’analisi e nella manipolazione dei dati con Pandas, puoi approfondire con queste guide, esercizi pratici e tutorial dedicati a DataFrame, pivot table e serie temporali:
👉 Pandas Python: tutorial completo per l’analisi e la manipolazione dei dati
👉 Pandas per principianti: come analizzare un dataset in 10 minuti
👉 Pivot Table con Pandas: guida avanzata per un’analisi dati efficace in Python
👉 Guida pratica a Pandas: esercizi svolti di data manipulation e Pivot Table
👉 Come analizzare la stagionalità e le serie temporali con Pandas: esercizio pratico





