Il caffè del lunedì ha un sapore diverso se sai già cosa ti aspetta.
C’è una sottile linea rossa che divide una settimana produttiva dal caos totale in magazzino: la risposta alla domanda “Quanti camion scarichiamo oggi?”.
Se sbagli la stima per eccesso, hai squadre ferme a guardare il soffitto. Se la sbagli per difetto, ti ritrovi con le ribalte intasate, la merce nei corridoi e i trasportatori che urlano al telefono.
Per anni abbiamo lavorato con file Excel enormi e la memoria storica del capo magazziniere.
Funzionava, più o meno.
Poi il mercato è diventato frenetico, le promozioni improvvise e le festività infrasettimanali hanno fatto saltare i vecchi schemi.
L’intuito non scala. I dati sì.
Non serve essere ingegneri della NASA per cambiare approccio. Serve un metodo per trasformare lo storico dei tuoi DDT in un piano di battaglia. Oggi lasciamo perdere la teoria accademica.
Apriamo Python e costruiamo uno strumento che ci dica non solo se arriverà un picco di lavoro, ma di quanto sarà alto, permettendoci di gestire ferie e turni senza sudori freddi.
Esercizio 1 (Livello Base) – Carico dati e primo modello
Testo:
Sei un analista stagista presso un’azienda di distribuzione. Il tuo responsabile ti fornisce il file carichi_spedizioni.csv contenente i dati giornalieri dei carichi gestiti dal fornitore “LogisticaCelere” nel 2023. Le colonne sono ds (data) e y (totale colli gestiti in quella data). Ti chiede di:
-
Caricare i dati e verificarne la struttura.
-
Creare un modello Prophet di base (senza regolazione di parametri) e addestrarlo.
-
Produrre una previsione per i successivi 30 giorni (gennaio 2024).
-
Visualizzare il grafico delle componenti del modello.
Soluzione
Generazione del file carichi_spedizioni.csv
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# Genera date per tutto il 2023
start_date = datetime(2023, 1, 1)
end_date = datetime(2023, 12, 31)
dates = pd.date_range(start=start_date, end=end_date, freq='D')
# Parametri per simulare trend + stagionalità
base_trend = 2800 # Trend base a gennaio
trend_growth = 0.002 # Crescita giornaliera dello 0.2%
# Stagionalità settimanale (indici 0=Lunedì, 6=Domenica)
weekly_pattern = {
0: 1.0, # Lunedì
1: 1.05, # Martedì
2: 1.03, # Mercoledì
3: 1.02, # Giovedì
4: 1.01, # Venerdì
5: 0.45, # Sabato
6: 0.35 # Domenica
}
# Stagionalità annuale (senza + estiva)
def annual_seasonality(date):
day_of_year = date.timetuple().tm_yday
# Picco estivo (luglio-agosto)
if 180 <= day_of_year <= 240:
return 1.15
# Calo invernale (gennaio)
elif day_of_year <= 30:
return 0.95
else:
return 1.0
# Lista per i dati
data = []
for i, date in enumerate(dates):
# Trend crescente
trend = base_trend * (1 + trend_growth) ** i
# Stagionalità settimanale
day_of_week = date.weekday()
weekly_effect = weekly_pattern[day_of_week]
# Stagionalità annuale
annual_effect = annual_seasonality(date)
# Rumore casuale
noise = np.random.normal(0, 0.05) # 5% di rumore
# Calcolo valore finale
y = trend * weekly_effect * annual_effect * (1 + noise)
# Aggiungi outlier per lo sciopero (15 marzo)
if date == datetime(2023, 3, 15):
y = y * 0.3 # -70%
# Aggiungi festività (riduzioni significative)
festivita = [
datetime(2023, 1, 1), datetime(2023, 1, 6),
datetime(2023, 4, 10), datetime(2023, 4, 25),
datetime(2023, 5, 1), datetime(2023, 6, 2),
datetime(2023, 8, 15), datetime(2023, 11, 1),
datetime(2023, 12, 8), datetime(2023, 12, 25),
datetime(2023, 12, 26)
]
if date in festivita:
y = y * 0.4 # -60% nei giorni festivi
data.append([date.strftime('%Y-%m-%d'), int(y)])
# Crea DataFrame
df = pd.DataFrame(data, columns=['ds', 'y'])
# Salva su CSV
df.to_csv('carichi_spedizioni_completo.csv', index=False)
print(f"Creato file con {len(df)} righe")
print("Primi 10 record:")
print(df.head(10))
print("\nUltimi 10 record:")
print(df.tail(10))
Passo 1 – Caricamento e verifica
import pandas as pd
from prophet import Prophet
# Carico i dati
df = pd.read_csv('carichi_spedizioni.csv')
# Converto la colonna ds in datetime
df['ds'] = pd.to_datetime(df['ds'])
# Stampo le prime righe e le info
print(df.head())
print(df.info())
ds y
0 2023-01-01 366
1 2023-01-02 2639
2 2023-01-03 2831
3 2023-01-04 2750
4 2023-01-05 2916
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 365 entries, 0 to 364
Data columns (total 2 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 ds 365 non-null datetime64[ns]
1 y 365 non-null int64
dtypes: datetime64[ns](1), int64(1)
memory usage: 5.8 KB
None
ds y
0 2023-01-01 366
1 2023-01-02 2639
2 2023-01-03 2831
3 2023-01-04 2750
4 2023-01-05 2916
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 365 entries, 0 to 364
Data columns (total 2 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 ds 365 non-null datetime64[ns]
1 y 365 non-null int64
dtypes: datetime64[ns](1), int64(1)
memory usage: 5.8 KB
None
Osservazione: Prophet richiede di nominare le colonne esattamente ds (per le date) e y (per la variabile da prevedere). Il formato data deve essere riconosciuto da pandas (es. YYYY-MM-DD).
Nota: Esegui questo blocco di codice solo una volta per generare il file di prova se non ne hai uno tuo.
Passo 2 – Creazione e addestramento del modello base
# Istanzio il modello (senza parametri specifici) m = Prophet() # Addestramento m.fit(df)
Richiamo teorico: Prophet è un modello additivo che scompone la serie temporale in trend, stagionalità e festività. Il modello base include automaticamente una stagionalità settimanale e annuale (se i dati sono sufficienti).
Passo 3 – Creazione del futuro e previsione
# Creo un dataframe con le date future (30 giorni dopo l'ultima data disponibile) future = m.make_future_dataframe(periods=30, freq='D') # Previsione forecast = m.predict(future) # Visualizzo le colonne chiave della previsione print(forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail())
ds yhat yhat_lower yhat_upper
390 2024-01-26 5300.604324 4559.027194 5995.403627
391 2024-01-27 3124.183584 2438.879093 3901.597415
392 2024-01-28 2697.740737 1951.063326 3409.385703
393 2024-01-29 5338.628774 4643.389204 6061.711050
394 2024-01-30 5465.209245 4797.586654 6185.114601
Osservazione: make_future_dataframe estende il dataframe delle date. yhat è la previsione, mentre yhat_lower e yhat_upper sono gli intervalli di incertezza (livello default 80%).
Passo 4 – Grafico delle componenti
fig = m.plot_components(forecast)
Il grafico mostrerà:
-
Il trend (andamento di lungo periodo)
-
La stagionalità settimanale (se presente)
-
La stagionalità annuale (se presente)
Perché Prophet ha richiesto che le colonne si chiamino esattamente
ds e y? Quale vantaggio pratico ha questa convenzione?Esercizio 2 (Livello Intermedio-Basso) – Gestione delle festività
Testo:
Continuando con l’analisi del fornitore “LogisticaCelere”, noti che nei periodi festivi (Natale, Capodanno, Ferragosto) i carichi subiscono cali significativi. Il responsabile ti chiede di incorporare nel modello le principali festività italiane per migliorare l’accuratezza.
Crea due modelli: uno senza festività e uno con festività, poi confronta la previsione per dicembre 2023.
Risoluzione:
Analisi Predittiva: Impatto delle Festività sui Carichi
L’obiettivo è creare un modello che non si limiti a guardare il passato “ciecamente”, ma che sappia che in certi giorni dell’anno (come Natale) il business si ferma o cambia ritmo.
Passo 1 – Preparazione dell’ambiente e dei dati
Iniziamo caricando le librerie necessarie. Prophet è il cuore dell’analisi, pandas gestisce i dati e matplotlib serve per i grafici.
import pandas as pd
from prophet import Prophet
import matplotlib.pyplot as plt
# Caricamento del dataset
df = pd.read_csv('carichi_spedizioni.csv')
# Conversione della colonna 'ds' in formato data (essenziale per Prophet)
df['ds'] = pd.to_datetime(df['ds'])
print(f"Dataset pronto: {len(df)} giorni di storico.")
print(f"Intervallo temporale: da {df['ds'].min().date()} a {df['ds'].max().date()}")
Dataset pronto: 365 giorni di storico.
Intervallo temporale: da 2023-01-01 a 2023-12-31
Passo 2 – Modello con festività
Definizione del “Calendario Custom”
Prophet non conosce le festività italiane di default. Dobbiamo creare un dataframe che indichi al modello quali giorni sono “speciali”.
# Creiamo il calendario delle festività
festivita_italiane = pd.DataFrame({
'holiday': 'festivita_italiana',
'ds': pd.to_datetime([
'2023-01-01', '2023-01-06', # Capodanno, Epifania
'2023-04-10', '2023-04-25', # Pasquetta, Liberazione
'2023-05-01', '2023-06-02', # Festa dei Lavoratori, Repubblica
'2023-08-15', '2023-11-01', # Ferragosto, Ognissanti
'2023-12-08', '2023-12-25', '2023-12-26' # Immacolata, Natale, S.Stefano
]),
# lower_window: -1 significa "guarda anche il giorno prima" (es. Vigilia)
'lower_window': -1,
# upper_window: 0 significa "fermati al giorno della festa"
'upper_window': 0
})
Passo 3 – Creazione dei due modelli a confronto
Creazione e Addestramento dei Modelli
Creiamo due “cervelli” diversi: uno ignora le feste, l’altro le tiene in considerazione.
# 3.1 Modello Baseline (Senza festività)
m_senza = Prophet()
m_senza.fit(df)
# 3.2 Modello Advanced (Con festività)
# Passiamo il dataframe festivita_italiane come parametro
m_fest = Prophet(holidays=festivita_italiane)
m_fest.fit(df)
print("Addestramento completato per entrambi i modelli.")
Passo 4 – Creazione dataframe futuro e previsioni
Generazione del Futuro e Previsioni
Chiediamo ai modelli di “immaginare” cosa succederà nei prossimi 60 giorni.
# Creiamo un range di date future (60 giorni oltre l'ultimo dato disponibile) future = m_senza.make_future_dataframe(periods=60, freq='D') # Generiamo le previsioni (yhat è il valore predetto) forecast_senza = m_senza.predict(future) forecast_fest = m_fest.predict(future)
Passo 5 – Filtraggio dicembre 2023 per confronto
Focus su Dicembre 2023
Per capire se il modello funziona, isoliamo il mese di Dicembre 2023, dove sappiamo esserci molte festività.
# Filtriamo i risultati per il solo mese di dicembre mask = (forecast_senza['ds'] >= '2023-12-01') & (forecast_senza['ds'] <= '2023-12-31') dec_senza = forecast_senza[mask].copy() dec_fest = forecast_fest[mask].copy()
Passo 6 – Visualizzazione comparativa
Visualizzazione Grafica
Un grafico vale più di mille righe di dati. Qui mettiamo a confronto le due linee.
plt.figure(figsize=(12, 6))
# Disegniamo le due previsioni
plt.plot(dec_senza['ds'], dec_senza['yhat'], label='Senza Festività (Lineare)', linestyle='--')
plt.plot(dec_fest['ds'], dec_fest['yhat'], label='Con Festività (Reattivo)', color='red')
# Evidenziamo Natale per controllo visivo
plt.axvline(pd.to_datetime('2023-12-25'), color='green', alpha=0.3, label='Natale')
plt.title("Impatto delle festività sulle previsioni di Dicembre")
plt.legend()
plt.show()
Analisi dei Risultati (Perché lo abbiamo fatto?)
Infine, calcoliamo la differenza numerica per dare una risposta al business.
# Uniamo i dati per fare sottrazioni dirette tra le colonne yhat
confronto = pd.merge(
dec_senza[['ds', 'yhat']].rename(columns={'yhat': 'base'}),
dec_fest[['ds', 'yhat']].rename(columns={'yhat': 'holiday_model'}),
on='ds'
)
# Calcoliamo la variazione
confronto['differenza'] = confronto['holiday_model'] - confronto['base']
# Vediamo cosa succede il 25 Dicembre
natale = confronto[confronto['ds'] == '2023-12-25']
print(f"A Natale, il modello con festività prevede {natale['differenza'].values[0]:.0f} colli in meno rispetto al modello base.")
A Natale, il modello con festività prevede -2282 colli in meno rispetto al modello base.
Conclusione dell’esercizio
Grazie a questo codice, abbiamo dimostrato che:
-
Il modello Baseline avrebbe sovrastimato i volumi durante le feste, portando a sprechi di risorse.
-
Il modello Advanced (con
holidays) ha imparato che durante le feste i carichi crollano, permettendo una pianificazione logistica molto più accurata.
Il momento della verità: Quanto sbaglia il nostro modello?
Oltre il grafico: La validazione numerica (Cross-Validation)
Abbiamo visto che il grafico “con festività” sembra seguire meglio la realtà. Ma in azienda “sembrare” non basta. Il Direttore della Logistica ti chiederà: “Qual è il margine di errore medio? Di quanti colli stiamo sbagliando?”.
Per rispondere, non possiamo usare i dati futuri (che non abbiamo ancora), ma possiamo usare la Cross-Validation (validazione incrociata). In pratica, Prophet simula il passato: si posiziona in diverse date del 2023, fa finta di non conoscere il futuro, genera una previsione e la confronta con ciò che è realmente accaduto.
Ecco come calcolare l’errore reale del nostro modello m_fest:
from prophet.diagnostics import cross_validation, performance_metrics
# 1. Eseguiamo la Cross-Validation
# initial: addestriamo sui primi 180 giorni
# period: ripetiamo il test ogni 30 giorni
# horizon: prevediamo i successivi 30 giorni
df_cv = cross_validation(m_fest, initial='180 days', period='30 days', horizon='30 days')
# 2. Calcoliamo le metriche di performance
df_p = performance_metrics(df_cv)
# 3. Guardiamo il MAPE (Mean Absolute Percentage Error)
print(df_p[['horizon', 'mape']].head())
# Calcoliamo la media generale dell'errore percentuale
errore_medio = df_p['mape'].mean()
print(f"\nErrore percentuale medio (MAPE): {errore_medio:.2%}")
Interpretazione dei Risultati: Il Modello è affidabile?
Guardando l’output, notiamo che il MAPE (errore medio) è di circa il 16%.
Cosa significa questo numero per chi gestisce il magazzino?
-
Non è una sfera di cristallo: Il modello non è perfetto. Su un carico previsto di 2.000 colli, potremmo avere uno scostamento di circa 300 colli.
-
È comunque utile? Assolutamente sì. Senza modello, l’errore umano basato sulla sola “sensazione” spesso supera il 25-30% o ignora completamente pattern complessi.
-
Il fattore Tempo: La tabella
horizonci mostra una verità fondamentale: più cerchiamo di prevedere lontano nel tempo, più l’errore tende a variare. Prevedere i carichi di domani (horizon 1 day) è sempre più preciso che prevedere quelli tra una settimana.
Il verdetto: Un errore del 16% è un buon punto di partenza per un modello grezzo (“out of the box”).
Per abbassarlo sotto il 10%, il passo successivo in un progetto reale sarebbe aggiungere i regressori extra: dati meteo (se piove si ordina di più online?), scioperi programmati o campagne di marketing attive.( Che vedremo nel prossimo articolo)
Esercizio Bonus: Il “Salvataggio” del Cervello (Model Persistence)
Scenario: Il responsabile della logistica è entusiasta del tuo modello con le festività. Tuttavia, ti fa notare che i dati storici dei carichi aumentano ogni giorno e l’addestramento sta diventando più lento. Ti chiede: “È possibile salvare il ‘cervello’ del modello oggi, in modo da poter generare le previsioni domattina in pochi secondi senza dover rielaborare mesi di dati?”
Obiettivo: Esportare il modello addestrato m_fest in un file locale e simulare il caricamento per una previsione immediata.
Soluzione: Utilizzo della libreria pickle o json
In Python, il modo più comune per salvare oggetti complessi come i modelli Prophet è la serializzazione. Useremo il formato JSON, che è il metodo ufficiale raccomandato dagli sviluppatori di Prophet perché è più robusto tra diverse versioni del software.
import json
from prophet.serialize import model_to_json, model_from_json
# --- 1. SALVATAGGIO DEL MODELLO ---
# Trasformiamo il modello addestrato in un formato testuale (JSON)
with open('modello_logistica_festivita.json', 'w') as fout:
fout.write(model_to_json(m_fest))
print("Modello salvato con successo sul file: modello_logistica_festivita.json")
# --- 2. SIMULAZIONE DI CARICAMENTO (Scenario 'Domani Mattina') ---
# Immaginiamo di aver chiuso Python e di riaprirlo. Carichiamo il file:
with open('modello_logistica_festivita.json', 'r') as fin:
m_caricato = model_from_json(fin.read())
print("Modello ricaricato correttamente!")
# --- 3. UTILIZZO IMMEDIATO ---
# Ora possiamo generare previsioni senza usare m_fest.fit(df)
future_veloce = m_caricato.make_future_dataframe(periods=7, freq='D')
forecast_veloce = m_caricato.predict(future_veloce)
print("\nPrevisioni per la prossima settimana generate in pochi millisecondi:")
print(forecast_veloce[['ds', 'yhat']].tail(7))
Modello salvato con successo sul file: modello_logistica_festivita.json
Modello ricaricato correttamente!
Previsioni per la prossima settimana generate in pochi millisecondi:
ds yhat
365 2024-01-01 5293.253337
366 2024-01-02 5481.890181
367 2024-01-03 5411.556933
368 2024-01-04 5380.874180
369 2024-01-05 5411.377799
370 2024-01-06 3044.609126
371 2024-01-07 2668.344087
Perché questo passaggio è vitale?
-
Efficienza: Non devi tenere i dati storici (magari Gigabyte di file CSV) sempre in memoria. Ti basta il piccolo file JSON del modello.
-
Deployment: Puoi inviare il file
.jsona un collega o caricarlo su un server (Cloud) dove un’applicazione web lo userà per mostrare una dashboard ai magazzinieri. -
Versatilità: Puoi addestrare il modello una volta a settimana (es. la domenica notte) e usarlo per “staccare” previsioni ogni ora durante il giorno.
Serie temporali & forecasting
- 📘 Facebook Prophet: guida completa al forecasting di serie temporali
- 📘 Previsioni di vendita con Facebook Prophet: 4 esercizi progressivi
- 📘Forecasting Logistico con Python: Usare i Regressori Meteo in Prophet
- 📘 6 esercizi pratici di serie temporali con Pandas: da base ad avanzato
- 📘 Feature engineering temporale in Python: gestire cicli e anni fiscali
- 📘 Analisi serie storiche in Python: modelli additivo e moltiplicativo
- 📘 Pandas oltre la teoria: 4 casi reali per diventare data analyst



