Pandas in Azienda: 3 Esercizi Pratici per Pulire Dati e Creare Report

Cerca:

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
Pandas 3 Problemi Reali, 3 Soluzioni in Codice

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.

Pubblicità

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 è:

  1. Caricare il dataset in un DataFrame Pandas.
  2. Esplorare la struttura dei dati (prime righe, informazioni generali, statistiche descrittive).
  3. Convertire la colonna delle date nel formato datetime.
  4. Rinominare le colonne con nomi più chiari e leggibili.
  5. Identificare e gestire i valori mancanti nella colonna quantità (sostituendoli con la mediana).
  6. 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  

 

Forse potrebbe interessarti anche:  Data Quality con Python: 5 esercizi per costruire una pipeline di pulizia dati con Pandas

💡 Perché questo esercizio è interessante

Questo script è il “pane quotidiano” dell’analisi dati.

  • Le Date: L’uso del parametro format='%d/%m/%Y' in pd.to_datetime ti 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.

Pubblicità

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 fornitore
  • data_ordine: data di emissione dell’ordine
  • data_consegna: data di effettiva consegna
  • prodotto: categoria merceologica
  • quantita: numero di unità ordinate
  • costo_unitario: costo per unità (€)

Richiesta:

  1. Calcolare per ogni fornitore il tempo medio di consegna (in giorni).
  2. Determinare il tasso di puntualità (% di consegne entro 3 giorni lavorativi).
  3. Creare un report riassuntivo per fornitore con: numero ordini, quantità totale, spesa totale, tempo medio di consegna, tasso di puntualità.
  4. 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_riga a 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 True vale 1 e False vale 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.
Forse potrebbe interessarti anche:  Colloquio Databricks e PySpark: Le Domande Reali e le Risposte per Superarlo

 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 univoco
  • nome_prodotto: descrizione
  • categoria: famiglia merceologica
  • quantita_venduta: unità vendute nell’ultimo anno
  • prezzo_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:

  1. Calcolare il fatturato annuale per singolo prodotto.
  2. Ordinare l’inventario per fatturato decrescente e calcolare la percentuale cumulata.
  3. Assegnare la classe A, B o C a ogni riga secondo la regola 80/15/5.
  4. 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 <= 80 fallisce). 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 funzione assign_abc adattandola esattamente alle politiche commerciali della tua azienda.
Forse potrebbe interessarti anche:  ANOVA Spiegata con Esercizi Risolti: Customer Success, Finanza e Retail

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

 

 

Pubblicità