Previsione Carichi Logistica con Python e Prophet: Tutorial pratico su trend, festività e deploy del modello

Cerca:

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
Python Prophet tutorial

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.

Pubblicità

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:

  1. Caricare i dati e verificarne la struttura.

  2. Creare un modello Prophet di base (senza regolazione di parametri) e addestrarlo.

  3. Produrre una previsione per i successivi 30 giorni (gennaio 2024).

  4. 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).

Forse potrebbe interessarti anche:  Esercizi Risolti di Statistica Aziendale: Analisi Dati, Break-Even e Modelli Monte Carlo

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)

Domanda di riflessione:
Perché Prophet ha richiesto che le colonne si chiamino esattamente ds e y? Quale vantaggio pratico ha questa convenzione?
Risposta:
Questa convenzione semplifica l’interfaccia della libreria, rendendo il codice più leggibile e riducendo la necessità di configurazioni aggiuntive. Prophet può così identificare automaticamente le colonne rilevanti senza dover specificare nomi personalizzati. Il vantaggio pratico è una minore possibilità di errori nella preparazione dei dati e una maggiore consistenza tra diversi progetti.
Pubblicità

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

Forse potrebbe interessarti anche:  Teorema di Bayes: 6 Esercizi Pratici per Capire l'Analisi delle Recensioni

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()

 

 

 

Passo 7 – Analisi quantitativa delle differenze

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:

  1. Il modello Baseline avrebbe sovrastimato i volumi durante le feste, portando a sprechi di risorse.

  2. 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%}")

 


  horizon      mape
0  3 days  0.159246
1  4 days  0.121994
2  5 days  0.127168
3  6 days  0.114636
4  7 days  0.150938

Errore percentuale medio (MAPE): 15.98%

Come leggere il risultato: Il MAPE (Mean Absolute Percentage Error) è il nostro indicatore chiave. Se otteniamo, ad esempio, un MAPE del 5%, significa che su un carico previsto di 1.000 colli, il nostro errore medio è di circa 50 colli (in più o in meno).

  • Sotto il 5-10%: Il modello è eccellente per la pianificazione operativa.

  • Sopra il 20%: Il modello necessita di più dati o di ulteriori variabili esterne (es. promozioni marketing, scioperi meteo).

Nota per l’analista: In produzione, monitorare il MAPE mese dopo mese è l’unico modo per accorgersi se il mercato sta cambiando e se il modello va riaddestrato.

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?

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

  2. È comunque utile? Assolutamente sì. Senza modello, l’errore umano basato sulla sola “sensazione” spesso supera il 25-30% o ignora completamente pattern complessi.

  3. Il fattore Tempo: La tabella horizon ci 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.

Forse potrebbe interessarti anche:  Distribuzione Multinomiale: Guida Pratica per Data Scientist (NLP e Python)

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?

  1. Efficienza: Non devi tenere i dati storici (magari Gigabyte di file CSV) sempre in memoria. Ti basta il piccolo file JSON del modello.

  2. Deployment: Puoi inviare il file .json a un collega o caricarlo su un server (Cloud) dove un’applicazione web lo userà per mostrare una dashboard ai magazzinieri.

  3. Versatilità: Puoi addestrare il modello una volta a settimana (es. la domenica notte) e usarlo per “staccare” previsioni ogni ora durante il giorno.

Pubblicità