Nella prima parte della serie abbiamo affrontato il problema più intuitivo: rappresentare una rete logistica come un grafo e trovare un percorso conveniente.
Nella seconda parte il problema è diventato più realistico. Non bastava più individuare il percorso più corto o meno costoso, perché sono entrati in gioco vincoli, costi e condizioni operative che modificavano il significato stesso della soluzione.
In questa terza parte compiamo un passo ulteriore. Non introduciamo semplicemente un algoritmo più sofisticato e non aumentiamo la difficoltà aggiungendo qualche riga di codice. Cambiamo il modo in cui rappresentiamo il problema.
È un passaggio importante perché, nelle applicazioni reali, un nodo raramente contiene tutte le informazioni necessarie per prendere una decisione.
Un hub logistico, per esempio, non è descritto soltanto dalla sua posizione nella rete. Può essere importante sapere con quale modalità di trasporto siamo arrivati, perché questa informazione determina i costi della decisione successiva. Allo stesso modo, un centro di distribuzione non è descritto soltanto dalla quantità di merce disponibile: conta anche quando quella merce sarà disponibile.
Da qui nascono due idee fondamentali:
- un problema di percorso può richiedere un grafo esteso nello spazio degli stati;
- un problema di supply chain può richiedere una rete estesa nel tempo.
I due esercizi di questa parte affrontano proprio queste due situazioni.
Nel primo costruiremo una rete intermodale in cui ogni hub deve ricordare la modalità di trasporto utilizzata. Nel secondo passeremo a una supply chain multi-periodo, dove inventari e trasferimenti evolvono giorno dopo giorno.
L’obiettivo finale non sarà soltanto trovare una soluzione ottima. Vedremo anche perché un costo ottimo può corrispondere a più piani diversi e perché, in un contesto aziendale, la soluzione matematicamente ottima non coincide automaticamente con la decisione migliore.
In questa Parte 3: una nuova dimensione per ogni esercizio
La progressione rispetto agli articoli precedenti può essere riassunta in questo modo.
Esercizio 5 — Stato estesoLa posizione non basta più. Dobbiamo rappresentare:
[math]\displaystyle Hub \times Modalità[/math]
Esercizio 6 — Tempo
La posizione e la quantità disponibile non bastano più. Dobbiamo rappresentare:
[math]\displaystyle Centro \times Tempo[/math]
Il filo conduttore è quindi semplice:
spesso non miglioriamo una decisione scegliendo un algoritmo più sofisticato. La miglioriamo rappresentando in modo più completo il problema che vogliamo risolvere.
Esercizio 5: rete di distribuzione con rotte intermodali
Il problema
Una compagnia di logistica gestisce una rete intermodale utilizzando tre modalità di trasporto:
- Gomma (G)
- Ferrovia (F)
- Nave (N)
I nodi rappresentano hub intermodali e ogni collegamento può essere disponibile con una o più modalità.
| Tratta | Gomma G (€ , h) | Ferrovia F (€ , h) | Nave N (€ , h) |
|---|---|---|---|
| Hub1 → Hub2 | 4, 2 | 6, 1 | — |
| Hub1 → Hub3 | 3, 3 | — | 5, 4 |
| Hub2 → Hub3 | 2, 2 | 3, 1 | — |
| Hub2 → Hub4 | 5, 4 | — | 7, 3 |
| Hub3 → Hub4 | 4, 3 | 5, 2 | — |
| Hub1 → Hub4 | 8, 6 | — | 6, 5 |
Ogni volta che una spedizione arriva in un hub utilizzando una modalità e riparte utilizzandone un’altra deve effettuare un trasbordo.
Il trasbordo comporta:
- un costo di 2 €;
- un tempo aggiuntivo di 1 ora.
L’obiettivo è trovare il percorso migliore da Hub1 a Hub4, ma la parola migliore richiede subito una precisazione: migliore rispetto a quale criterio?
Perché un normale grafo non basta?
In un problema di percorso classico potremmo rappresentare semplicemente la rete:
[math]\displaystyle Hub1 \rightarrow Hub2 \rightarrow Hub3 \rightarrow Hub4[/math]
e applicare Dijkstra ai nodi e agli archi.
In questo caso, però, Hub2 non contiene tutte le informazioni necessarie per valutare la decisione successiva.
Supponiamo infatti di arrivare a Hub2. Non è la stessa cosa arrivare:
- in gomma;
- in ferrovia;
- via nave.
La modalità utilizzata per arrivare determina infatti se la tratta successiva potrà essere percorsa senza costi aggiuntivi oppure se sarà necessario effettuare un trasbordo.
Il vero stato del sistema non è quindi semplicemente:
[math]\displaystyle Hub2[/math]
ma può essere:
[math]\displaystyle (Hub2,G)[/math]
oppure:
[math]\displaystyle (Hub2,F)[/math]
oppure:
[math]\displaystyle (Hub2,N)[/math]
La posizione fisica è la stessa, ma lo stato operativo è diverso.
Questo tipo di situazione compare spesso anche al di fuori della logistica. In un algoritmo di navigazione potrebbe essere necessario ricordare il carburante residuo. In un problema produttivo potrebbe essere necessario ricordare la configurazione corrente di una macchina. In un sistema di customer journey potrebbe essere importante sapere non soltanto dove si trova un cliente, ma anche attraverso quale canale è arrivato.
La regola generale è sempre la stessa:
se un’informazione modifica il costo o la fattibilità delle decisioni future, probabilmente deve entrare nello stato del modello.
Il grafo esteso: Hub × Modalità
Per rappresentare correttamente il problema trasformiamo ogni hub in più stati.
Per esempio:
[math]\displaystyle Hub2[/math]
diventa:
[math]\displaystyle (Hub2,G)[/math]
[math]\displaystyle (Hub2,F)[/math]
[math]\displaystyle (Hub2,N)[/math]
Un arco di trasporto mantiene la modalità:
[math]\displaystyle (Hub1,G)\rightarrow(Hub2,G)[/math]
Un cambio di modalità diventa invece un arco interno allo stesso hub:
[math]\displaystyle (Hub2,G)\rightarrow(Hub2,F)[/math]
con:
[math]\displaystyle Costo=2€[/math]
e:
[math]\displaystyle Tempo=1h[/math]
Questo è il vero passaggio concettuale dell’esercizio.
Non abbiamo modificato Dijkstra. Abbiamo modificato il grafo sul quale Dijkstra lavora.
L’algoritmo continua a cercare un percorso minimo; ciò che cambia è il modo in cui definiamo gli stati e le transizioni possibili.
Dal problema bicriterio a una decisione scalare
Il problema contiene due criteri naturali:
[math]\displaystyle Costo[/math]
e:
[math]\displaystyle Tempo[/math]
Potremmo quindi essere tentati di scrivere:
[math]\displaystyle \min(Costo,Tempo)[/math]
ma il problema è più complesso. Non sempre esiste infatti una soluzione che sia contemporaneamente la meno costosa e la più veloce.
Consideriamo due percorsi.
| Percorso | Costo | Tempo |
|---|---|---|
| A | 6 € | 5 h |
| B | 14 € | 4 h |
Il percorso A costa meno, mentre il percorso B richiede meno tempo.
Nessuno dei due domina completamente l’altro.
Per utilizzare Dijkstra in modo diretto dobbiamo quindi trasformare il problema in un criterio singolo, ma farlo scegliendo semplicemente pesi arbitrari come 0,6 e 0,4 sarebbe poco interpretabile. Il problema è che euro e ore sono grandezze con unità diverse.
La domanda utile diventa allora un’altra:
Quanto vale economicamente un’ora?
Il valore economico del tempo
Introduciamo:
[math]\displaystyle \lambda[/math]
dove:
[math]\displaystyle \lambda = valore\ economico\ di\ un’ora[/math]
La funzione decisionale diventa:
[math]\displaystyle Score=C+\lambda T[/math]
dove:
- [math]C[/math] è il costo monetario;
- [math]T[/math] è il tempo;
- [math]\lambda[/math] esprime quanti euro attribuiamo a un’ora.
Ora entrambi i termini sono espressi nella stessa unità:
[math]\displaystyle €[/math]
Questa formulazione è più interessante di una semplice somma pesata perché il parametro ha un significato operativo. Non stiamo dicendo genericamente che il tempo pesa “il 40%”. Stiamo dichiarando quanto siamo disposti a spendere per risparmiare un’ora.
Questa differenza è importante perché rende il modello discutibile e calibrabile con chi dovrà utilizzare la decisione.
Tre scenari decisionali
Consideriamo tre situazioni.
Scenario 1 — Il tempo non ha un costo economico
[math]\displaystyle \lambda=0[/math]
L’azienda minimizza esclusivamente il costo monetario.
Questo può essere ragionevole per spedizioni non urgenti, prodotti con scorte abbondanti o clienti senza particolari vincoli temporali.
Scenario 2 — Il tempo ha un valore moderato
[math]\displaystyle \lambda=5€/h[/math]
Ogni ora aggiuntiva viene trasformata in un costo economico equivalente di 5 €.
Scenario 3 — Il tempo è molto importante
[math]\displaystyle \lambda=10€/h[/math]
Ogni ora aggiuntiva ha un costo opportunità maggiore e il modello sarà quindi disposto a sostenere una spesa monetaria più elevata pur di ridurre il tempo.
Prima dell’algoritmo: analizziamo le alternative
Prima di eseguire il codice possiamo analizzare due percorsi particolarmente interessanti.
Percorso A — Nave diretta
[math]\displaystyle Hub1\rightarrow Hub4[/math]
Modalità:
[math]\displaystyle N[/math]
Costo:
[math]\displaystyle 6€[/math]
Tempo:
[math]\displaystyle 5h[/math]
Score:
[math]\displaystyle S_N=6+5\lambda[/math]
Percorso B — Ferrovia
[math]\displaystyle Hub1\rightarrow Hub2\rightarrow Hub3\rightarrow Hub4[/math]
Modalità:
[math]\displaystyle F[/math]
Costo:
[math]\displaystyle 6+3+5=14€[/math]
Tempo:
[math]\displaystyle 1+1+2=4h[/math]
Score:
[math]\displaystyle S_F=14+4\lambda[/math]
La ferrovia costa quindi 8 € in più, ma permette di risparmiare un’ora.
La domanda economica è molto concreta:
quell’ora vale più o meno di 8 €?
La soglia che cambia la decisione
Troviamo il valore di [math]\lambda[/math] per il quale le due alternative diventano equivalenti:
[math]\displaystyle 6+5\lambda=14+4\lambda[/math]
da cui:
[math]\displaystyle \lambda=8€/h[/math]
Questa soglia è più interessante dei singoli risultati ottenuti provando valori diversi del parametro, perché ci permette di costruire direttamente una regola decisionale.
Se:
[math]\displaystyle \lambda<8€/h[/math]
conviene la nave diretta.
Se:
[math]\displaystyle \lambda>8€/h[/math]
conviene la ferrovia.
Se:
[math]\displaystyle \lambda=8€/h[/math]
le due alternative hanno lo stesso score.
Il modello non può quindi scegliere tra le due utilizzando soltanto costo e tempo. A quel punto sarebbe necessario introdurre un criterio aggiuntivo, come:
- affidabilità;
- rischio di ritardo;
- capacità disponibile;
- emissioni;
- rischio di interruzione;
- flessibilità operativa.
Il risultato più utile non è quindi:
il percorso migliore è X.
Il risultato è:
sappiamo esattamente in quali condizioni una rotta diventa preferibile all’altra.
Questa è già una forma di sensitivity analysis.
Risultati attesi
| Scenario | Valore del tempo | Percorso preferibile |
|---|---|---|
| Solo costo | 0 €/h | Nave diretta |
| Tempo moderato | 5 €/h | Nave diretta |
| Tempo molto importante | 10 €/h | Ferrovia |
Non esiste quindi un percorso ottimo in assoluto.
Esiste un percorso ottimo rispetto a una funzione decisionale dichiarata.
Non è ancora vera ottimizzazione multi-obiettivo
Il metodo utilizzato qui è una scalarizzazione.
Partiamo da due obiettivi:
[math]\displaystyle (Costo,Tempo)[/math]
e costruiamo:
[math]\displaystyle Score=Costo+\lambda Tempo[/math]
Dijkstra minimizza quindi una singola funzione.
Un approccio realmente multi-obiettivo dovrebbe invece mantenere diverse soluzioni non dominate e permettere di costruire una frontiera di Pareto.
Per questo esercizio la scalarizzazione è una scelta consapevole. Ci permette infatti di utilizzare Dijkstra mantenendo un significato economico chiaro e di concentrarci sul problema principale: la rappresentazione dello stato.
Codice Python: Dijkstra su un grafo esteso
Il codice seguente costruisce esplicitamente gli stati (hub, modalità). Gli archi rappresentano sia i trasferimenti tra hub mantenendo la modalità sia i trasbordi interni allo stesso hub.
La scelta iniziale della modalità è libera e il primo arco determina quella effettivamente utilizzata.
import heapq
from typing import Dict, List, Tuple, Optional, Set
class IntermodalDijkstra:
MODALITA = ["G", "F", "N"]
def __init__(self):
# graph[(hub, modalità)]
# = [((hub_successivo, modalità), costo, tempo)]
self.graph = {}
def costruisci_grafo_esteso(
self,
tratte,
costo_trasbordo=2.0,
tempo_trasbordo=1.0
):
self.graph = {}
# Raccogliamo tutti gli hub
hubs = set()
for h1, h2 in tratte:
hubs.add(h1)
hubs.add(h2)
# Creiamo gli stati:
# (hub, modalità)
for hub in hubs:
for m in self.MODALITA:
self.graph[(hub, m)] = []
# Archi di trasporto
for (h1, h2), info in tratte.items():
for modalita, (costo, tempo) in info.items():
self.graph[(h1, modalita)].append(
(
(h2, modalita),
costo,
tempo
)
)
# Archi di trasbordo
for hub in hubs:
for m1 in self.MODALITA:
for m2 in self.MODALITA:
if m1 != m2:
self.graph[(hub, m1)].append(
(
(hub, m2),
costo_trasbordo,
tempo_trasbordo
)
)
def dijkstra(
self,
start,
end,
valore_tempo=0.0
):
# Score economico:
#
# costo + valore_tempo * tempo
def peso(costo, tempo):
return costo + valore_tempo * tempo
score = {
nodo: float("inf")
for nodo in self.graph
}
costo_cum = {
nodo: float("inf")
for nodo in self.graph
}
tempo_cum = {
nodo: float("inf")
for nodo in self.graph
}
pred = {
nodo: None
for nodo in self.graph
}
priority_queue = []
# Possiamo iniziare con qualsiasi modalità.
#
# Il primo arco determinerà quella effettivamente usata.
for m in self.MODALITA:
stato = (start, m)
score[stato] = 0.0
costo_cum[stato] = 0.0
tempo_cum[stato] = 0.0
heapq.heappush(
priority_queue,
(0.0, stato)
)
visitati = set()
while priority_queue:
cur_score, cur_stato = heapq.heappop(
priority_queue
)
if cur_stato in visitati:
continue
visitati.add(cur_stato)
for (
vicino,
costo_arco,
tempo_arco
) in self.graph[cur_stato]:
nuovo_score = (
score[cur_stato]
+ peso(
costo_arco,
tempo_arco
)
)
if nuovo_score < score[vicino]:
score[vicino] = nuovo_score
costo_cum[vicino] = (
costo_cum[cur_stato]
+ costo_arco
)
tempo_cum[vicino] = (
tempo_cum[cur_stato]
+ tempo_arco
)
pred[vicino] = cur_stato
heapq.heappush(
priority_queue,
(
nuovo_score,
vicino
)
)
stati_finali = [
(end, m)
for m in self.MODALITA
]
best = min(
stati_finali,
key=lambda s: score[s]
)
if score[best] == float("inf"):
return None
# Ricostruzione percorso
path = []
cur = best
while cur is not None:
path.append(cur)
cur = pred[cur]
path.reverse()
return {
"percorso": path,
"costo": costo_cum[best],
"tempo": tempo_cum[best],
"score": score[best]
}
@staticmethod
def formatta_percorso(path):
if not path:
return "Nessun percorso"
risultato = [path[0][0]]
for i in range(
1,
len(path)
):
hub_prev, mod_prev = path[i - 1]
hub_cur, mod_cur = path[i]
if hub_prev == hub_cur:
risultato.append(
f" --trasbordo "
f"{mod_prev}->{mod_cur}--> "
)
else:
risultato.append(
f" -[{mod_cur}]-> "
)
risultato.append(hub_cur)
return "".join(risultato)
Dati della rete
tratte = {
("Hub1", "Hub2"):
{
"G": (4, 2),
"F": (6, 1)
},
("Hub1", "Hub3"):
{
"G": (3, 3),
"N": (5, 4)
},
("Hub2", "Hub3"):
{
"G": (2, 2),
"F": (3, 1)
},
("Hub2", "Hub4"):
{
"G": (5, 4),
"N": (7, 3)
},
("Hub3", "Hub4"):
{
"G": (4, 3),
"F": (5, 2)
},
("Hub1", "Hub4"):
{
"G": (8, 6),
"N": (6, 5)
}
}
Esecuzione
rete = IntermodalDijkstra()
rete.costruisci_grafo_esteso(
tratte,
costo_trasbordo=2,
tempo_trasbordo=1
)
scenari = [
("Solo costo", 0),
("Tempo = 5 €/h", 5),
("Tempo = 10 €/h", 10)
]
for nome, valore_tempo in scenari:
risultato = rete.dijkstra(
"Hub1",
"Hub4",
valore_tempo=valore_tempo
)
print("\n" + "=" * 60)
print(nome)
print("=" * 60)
print(
"Percorso:",
IntermodalDijkstra.formatta_percorso(
risultato["percorso"]
)
)
print(
"Costo:",
risultato["costo"],
"€"
)
print(
"Tempo:",
risultato["tempo"],
"h"
)
print(
"Score:",
risultato["score"],
"€ equivalenti"
)
L’esecuzione produce:
============================================================
Solo costo
============================================================
Percorso: Hub1 -[N]-> Hub4
Costo: 6.0 €
Tempo: 5.0 h
Score: 6.0 € equivalenti
============================================================
Tempo = 5 €/h
============================================================
Percorso: Hub1 -[N]-> Hub4
Costo: 6.0 €
Tempo: 5.0 h
Score: 31.0 € equivalenti
============================================================
Tempo = 10 €/h
============================================================
Percorso: Hub1 -[F]-> Hub2 -[F]-> Hub3 -[F]-> Hub4
Costo: 14.0 €
Tempo: 4.0 h
Score: 54.0 € equivalenti
Quando il valore del tempo cambia la rotta
L’output non rappresenta tre esecuzioni casuali dell’algoritmo. La rete è sempre la stessa, così come i costi e i tempi delle tratte. Cambia invece il criterio con cui l’azienda valuta le alternative.
Scenario 1 — Solo costo
Con:
[math]\displaystyle \lambda=0[/math]
la funzione obiettivo diventa:
[math]\displaystyle Score=C[/math]
L’algoritmo sceglie la nave diretta:
[math]\displaystyle Hub1\rightarrow Hub4[/math]
con:
- costo: 6 €;
- tempo: 5 ore;
- score: 6 €.
Questa è la soluzione meno costosa, ma non significa che sia la migliore in qualsiasi contesto. Richiede infatti un’ora in più rispetto all’alternativa ferroviaria.
Se la spedizione riguarda merci non urgenti o prodotti disponibili in quantità sufficiente presso il cliente, quell’ora aggiuntiva potrebbe non avere alcuna conseguenza economica. In questo caso spendere 8 € in più per risparmiarla non sarebbe razionale.
Scenario 2 — Il tempo entra nel modello
Con:
[math]\displaystyle \lambda=5€/h[/math]
la nave diretta ottiene:
[math]\displaystyle Score=6+5\times5=31€[/math]
La ferrovia ottiene:
[math]\displaystyle Score=14+5\times4=34€[/math]
La decisione non cambia.
Questo risultato è importante perché dimostra che introdurre un nuovo criterio non implica automaticamente modificare la soluzione ottima. Il tempo viene considerato, ma il beneficio di risparmiare un’ora vale ancora meno degli 8 € necessari per utilizzare la ferrovia.
Il modello sta quindi traducendo una preferenza economica molto concreta:
non tutte le ore hanno lo stesso valore e non tutti i risparmi di tempo giustificano un costo aggiuntivo.
Scenario 3 — Il tempo cambia la decisione
Con:
[math]\displaystyle \lambda=10€/h[/math]
la nave diretta ottiene:
[math]\displaystyle Score=6+10\times5=56€[/math]
La ferrovia ottiene:
[math]\displaystyle Score=14+10\times4=54€[/math]
Ora la ferrovia diventa la soluzione ottimale.
L’azienda accetta quindi di spendere:
[math]\displaystyle 14€\quad contro\quad6€[/math]
per risparmiare un’ora.
La situazione potrebbe essere realistica quando il ritardo comporta una conseguenza economica concreta, come:
- una penale contrattuale;
- un fermo produttivo;
- la perdita di una finestra di consegna;
- un aumento del rischio di stockout;
- un peggioramento del livello di servizio;
- un possibile impatto sulla relazione con il cliente.
Il modello non sta quindi affermando che la ferrovia sia intrinsecamente migliore della nave. Sta mostrando che diventa preferibile quando il costo opportunità del tempo supera una determinata soglia.
Sensitivity analysis: la soglia di 8 €/h
Possiamo sintetizzare il comportamento delle due alternative.
| Valore di λ | Nave diretta | Ferrovia | Decisione |
|---|---|---|---|
| 0 €/h | 6 € | 14 € | Nave |
| 5 €/h | 31 € | 34 € | Nave |
| 8 €/h | 46 € | 46 € | Indifferente |
| 10 €/h | 56 € | 54 € | Ferrovia |
La rete non cambia. L’algoritmo non cambia. Cambia il parametro che traduce il contesto economico nella funzione obiettivo.
Questo suggerisce una domanda migliore rispetto a:
Qual è il percorso migliore?
La domanda utile è:
Qual è il percorso migliore dato il valore economico che attribuiamo al tempo in questa situazione?
Nella realtà quel valore può dipendere dal prodotto, dal cliente, dal livello delle scorte, dalla presenza di ritardi precedenti o dal rischio di fermare una linea produttiva.
Una stessa rete fisica può quindi generare decisioni diverse senza che l’algoritmo commetta alcun errore. Cambia il contesto economico, quindi deve cambiare anche la valutazione delle alternative.
Il valore di λ non dovrebbe essere arbitrario
Uno degli errori più comuni nei problemi multicriterio consiste nell’introdurre pesi senza spiegare da dove provengano.
In questo esercizio [math]\lambda[/math] ha invece un significato preciso:
quanti euro siamo disposti a spendere per risparmiare un’ora?
In un’applicazione reale potrebbe essere stimato utilizzando elementi come:
- costo atteso del ritardo;
- penali;
- costo di immobilizzazione;
- valore del servizio;
- rischio economico;
- costo di un fermo produttivo.
Naturalmente questi elementi non dovrebbero essere sommati automaticamente, perché potrebbero rappresentare conseguenze parzialmente sovrapposte. Il punto importante è un altro: il parametro deve essere collegato a una conseguenza economica osservabile o almeno discutibile con chi utilizzerà il modello.
La lezione
Possiamo leggere il risultato su tre livelli.
Livello algoritmico
Dijkstra trova il minimo della funzione assegnata agli archi.
Livello di modellazione
Il problema può essere rappresentato correttamente solo perché lo stato contiene anche la modalità:
[math]\displaystyle (Hub,Modalità)[/math]
Livello decisionale
L’algoritmo non può decidere autonomamente quanto vale un’ora di ritardo.
Dijkstra risponde alla domanda:
Qual è il percorso con score minimo?
Ma non può rispondere da solo alla domanda:
Quanto vale un’ora per questa specifica spedizione?
È qui che il problema diventa un problema di Decision Value.
Il risultato più utile non è soltanto una rotta, ma una policy:
utilizzare la nave quando il valore economico del tempo è inferiore alla soglia critica e utilizzare la ferrovia quando il valore supera quella soglia.
Mini Quiz
Domanda 1
Perché rappresentiamo:
[math]\displaystyle (Hub2,G)[/math]
e:
[math]\displaystyle (Hub2,F)[/math]
come due stati diversi?
Domanda 2
Perché:
[math]\displaystyle Costo+0.4Tempo[/math]
può essere metodologicamente difficile da interpretare?
Domanda 3
Se:
[math]\displaystyle \lambda=8€/h[/math]
quale decisione prende il modello tra nave e ferrovia?
Domanda 4
Se:
[math]\displaystyle \lambda=6€/h[/math]
quale delle due alternative viene scelta?
Risposte
R1
Perché la modalità con cui arriviamo nell’hub influenza il costo e il tempo delle decisioni successive. Un cambio di modalità può richiedere un trasbordo.
R2
Perché euro e ore hanno unità differenti. È preferibile normalizzare le variabili oppure attribuire al tempo un valore economico esplicito.
R3
Le due alternative hanno lo stesso score. Il modello presenta quindi almeno due soluzioni equivalenti rispetto alla funzione obiettivo.
R4
Nave:
[math]\displaystyle 6+5\times6=36€[/math]
Ferrovia:
[math]\displaystyle 14+4\times6=38€[/math]
Conviene quindi la nave.
Esercizio 6: supply chain multi-periodo con rete tempo-espansa
Nel primo esercizio abbiamo aggiunto una nuova dimensione allo stato. Lo stesso hub poteva infatti esistere in più configurazioni a seconda della modalità utilizzata.
Ora aggiungiamo un’altra dimensione:
[math]\displaystyle Tempo[/math]
Una unità disponibile presso un centro di distribuzione il lunedì non rappresenta necessariamente la stessa decisione di una unità disponibile nello stesso centro il venerdì.
Questa differenza è fondamentale nelle supply chain reali. Un inventario sufficiente per soddisfare la domanda settimanale può essere insufficiente per soddisfare la domanda di domani. Un centro può avere un surplus complessivo alla fine della settimana ma non poter trasferire immediatamente tutta la merce senza compromettere le consegne dei giorni successivi.
Per questo dobbiamo introdurre il tempo nel modello.
Il problema
Un’azienda di e-commerce gestisce quattro centri di distribuzione:
- CD1
- CD2
- CD3
- CD4
La domanda giornaliera è:
| Giorno | CD1 | CD2 | CD3 | CD4 | Totale |
|---|---|---|---|---|---|
| Lunedì | 40 | 25 | 20 | 15 | 100 |
| Martedì | 35 | 20 | 15 | 10 | 80 |
| Mercoledì | 30 | 15 | 15 | 10 | 70 |
| Giovedì | 25 | 15 | 10 | 10 | 60 |
| Venerdì | 25 | 10 | 10 | 5 | 50 |
Domanda totale:
[math]\displaystyle 360[/math]
Scorte iniziali
| Centro | Scorte |
|---|---|
| CD1 | 120 |
| CD2 | 100 |
| CD3 | 80 |
| CD4 | 60 |
Totale:
[math]\displaystyle 120+100+80+60=360[/math]
La quantità totale disponibile è quindi esattamente uguale alla domanda totale settimanale.
Non esiste produzione aggiuntiva e non consideriamo perdite. Tutto ciò che viene venduto durante la settimana deve quindi provenire dalle scorte iniziali.
Costi di trasporto
| Da \ A | CD1 | CD2 | CD3 | CD4 |
|---|---|---|---|---|
| CD1 | — | 2 | 4 | 6 |
| CD2 | 3 | — | 1 | 4 |
| CD3 | 5 | 2 | — | 3 |
| CD4 | 6 | 5 | 2 | — |
Gli altri parametri sono:
- capacità massima: 150 unità per CD;
- costo di holding: 0,5 €/unità/giorno;
- tempo di trasferimento intra-giornaliero trascurabile.
Assumiamo che la capacità sia applicata all’inventario disponibile a fine giornata.
Prima osservazione: il costo totale di holding è fisso
A prima vista il modello sembra dover decidere contemporaneamente:
- quanto trasferire;
- quando trasferire;
- dove mantenere l’inventario.
La terza decisione, però, sotto le ipotesi di questo esercizio non modifica il costo totale di holding.
Indichiamo con:
[math]\displaystyle I_{i,t}[/math]
l’inventario del centro [math]i[/math] alla fine del giorno [math]t[/math].
L’inventario totale è:
[math]\displaystyle \sum_i I_{i,t}[/math]
Poiché non esistono produzione e perdite:
[math]\displaystyle I_t^{tot}=I_{t-1}^{tot}-D_t^{tot}[/math]
I trasferimenti tra centri non modificano la quantità totale disponibile. Ogni unità che esce da un centro entra in un altro.
Quindi:
[math]\displaystyle \sum_i Entrate_{i,t}-\sum_i Uscite_{i,t}=0[/math]
La distribuzione cambia, ma la quantità totale rimane fissata dalla domanda cumulata.
Inventario totale durante la settimana
Alla fine di ogni giornata otteniamo:
| Fine giornata | Inventario totale |
|---|---|
| Lunedì | 260 |
| Martedì | 180 |
| Mercoledì | 110 |
| Giovedì | 50 |
| Venerdì | 0 |
Il costo totale di holding è quindi:
[math]\displaystyle 0.5\cdot(260+180+110+50+0)[/math]
ossia:
[math]\displaystyle 300€[/math]
Questo valore è importante perché non dipende da come distribuiamo l’inventario tra i centri, purché rimangano valide tutte le ipotesi del problema.
Di conseguenza possiamo separare due elementi:
Decisione da ottimizzare
[math]\displaystyle CostoTrasporto[/math]
Costo costante da aggiungere successivamente
[math]\displaystyle CostoHolding=300€[/math]
Questa distinzione semplifica il modello e rende più chiaro ciò che il solver sta realmente decidendo.
Quando il costo di holding tornerebbe a essere una decisione?
Questa proprietà vale soltanto sotto le ipotesi dichiarate.
Se il costo di holding fosse differente tra i centri, indicando con [math]h_i[/math] il costo specifico del centro [math]i[/math], avremmo:
[math]\displaystyle CostoHolding=\sum_t\sum_i h_iI_{i,t}[/math]
In questo caso la distribuzione dell’inventario modificherebbe il costo totale e il timing dei trasferimenti diventerebbe nuovamente una decisione economica.
Lo stesso accadrebbe se introducessimo:
- produzione;
- deterioramento;
- perdite;
- costi di holding non lineari;
- capacità differenti;
- tempi di trasferimento maggiori di zero.
Questo è un buon esempio di un principio generale della modellazione:
prima di ottimizzare una componente della funzione obiettivo dobbiamo verificare se quella componente può davvero cambiare al variare delle decisioni.
Analisi del surplus settimanale
Confrontiamo ora scorte iniziali e domanda totale della settimana.
| CD | Scorte | Domanda | Saldo |
|---|---|---|---|
| CD1 | 120 | 155 | −35 |
| CD2 | 100 | 85 | +15 |
| CD3 | 80 | 70 | +10 |
| CD4 | 60 | 50 | +10 |
CD1 presenta quindi un deficit complessivo di:
[math]\displaystyle 35[/math]
unità.
Gli altri centri hanno complessivamente:
[math]\displaystyle 15+10+10=35[/math]
unità di surplus.
A prima vista sembrerebbe quindi sufficiente trasferire tutto il surplus verso CD1. Il problema, però, non è soltanto da dove trasferire la merce.
Dobbiamo capire anche quando è possibile farlo.
Surplus settimanale ≠ disponibilità immediata
Dire che CD2 ha un surplus settimanale di 15 unità significa:
[math]\displaystyle Scorte\ iniziali-Domanda\ settimanale=15[/math]
Non significa automaticamente che quelle 15 unità siano disponibili in qualsiasi giorno della settimana.
Se trasferissimo troppa merce troppo presto, CD2 potrebbe non avere inventario sufficiente per soddisfare la propria domanda nei giorni successivi.
Per capire quando la merce può essere trasferita dobbiamo quindi osservare l’evoluzione temporale dell’inventario.
Ed è qui che entra in gioco il concetto di rete tempo-espansa.
Una rete per ogni momento
Possiamo immaginare:
[math]\displaystyle CD1_{Lun}[/math]
[math]\displaystyle CD1_{Mar}[/math]
[math]\displaystyle CD1_{Mer}[/math]
e così via.
Lo stesso centro appare quindi più volte nel modello perché rappresenta stati diversi in momenti diversi.
Un arco temporale può rappresentare la conservazione dell’inventario:
[math]\displaystyle CD1_t\rightarrow CD1_{t+1}[/math]
Un arco di trasferimento può rappresentare invece:
[math]\displaystyle CD2_t\rightarrow CD1_t[/math]
Il problema diventa quindi:
[math]\displaystyle Spazio\times Tempo[/math]
Nel codice che utilizzeremo non costruiamo esplicitamente tutti questi nodi come oggetti di un grafo. Utilizziamo una formulazione equivalente attraverso variabili di flusso e inventario indicizzate nel tempo.
Il concetto, però, è lo stesso: una unità presente in CD1 al tempo [math]t[/math] e una unità presente nello stesso centro al tempo [math]t+1[/math] rappresentano due stati diversi.
Formulazione matematica
Indichiamo con:
[math]\displaystyle x_{i,j,t}[/math]
la quantità trasferita dal centro [math]i[/math] al centro [math]j[/math] nel giorno [math]t[/math].
Indichiamo con:
[math]\displaystyle I_{i,t}[/math]
l’inventario a fine giornata.
Il bilancio di massa è:
[math]\displaystyle I_{i,t+1}=I_{i,t}+\sum_jx_{j,i,t}-\sum_jx_{i,j,t}-d_{i,t}[/math]
dove:
[math]\displaystyle d_{i,t}[/math]
è la domanda.
La formula può essere letta in modo molto semplice:
inventario finale = inventario iniziale + entrate − uscite − domanda.
Supponiamo, per esempio, che un centro abbia:
- 80 unità disponibili;
- 15 unità in entrata;
- nessuna uscita;
- 35 unità di domanda.
L’inventario finale sarà:
[math]\displaystyle 80+15-0-35=60[/math]
La formulazione matematica non fa altro che applicare questa stessa logica contemporaneamente a tutti i centri e a tutti i giorni.
Vincoli
Inventario non negativo
[math]\displaystyle I_{i,t}\ge0[/math]
Non possiamo utilizzare merce che non possediamo.
Capacità
[math]\displaystyle I_{i,t}\le150[/math]
Ogni centro ha una capacità massima.
Trasferimenti non negativi
[math]\displaystyle x_{i,j,t}\ge0[/math]
Le quantità trasferite non possono essere negative.
La Funzione obiettivo
Abbiamo dimostrato che, sotto le ipotesi del problema, il costo totale di holding è costante.
Il solver deve quindi minimizzare soltanto il costo di trasporto:
[math]\displaystyle \min\sum_t\sum_i\sum_jc_{i,j}x_{i,j,t}[/math]
Dopo aver trovato la soluzione possiamo calcolare il costo complessivo:
[math]\displaystyle CostoTotale=CostoTrasporto+CostoHolding[/math]
Nel nostro caso:
[math]\displaystyle CostoHolding=300€[/math]
è una costante.
Questa separazione rende visibile una proprietà teorica importante:
[math]\displaystyle \arg\min(f(x)+K)=\arg\min f(x)[/math]
quando [math]K[/math] non dipende dalle decisioni.
Costi minimi verso CD1
Vediamo i costi diretti.
| Sorgente | Destinazione | Costo |
|---|---|---|
| CD2 | CD1 | 3 € |
| CD3 | CD1 | 5 € |
| CD4 | CD1 | 6 € |
CD1 ha bisogno esattamente di:
[math]\displaystyle 35[/math]
unità e il surplus complessivo degli altri centri è anch’esso pari a:
[math]\displaystyle 35[/math]
Il costo minimo di trasporto associato ai surplus è quindi:
[math]\displaystyle 15\cdot3+10\cdot5+10\cdot6[/math]
ossia:
[math]\displaystyle 155€[/math]
Il costo di holding sarà poi aggiunto separatamente:
[math]\displaystyle 155+300=455€[/math]
Ma quando trasferire la merce?
Qui emerge uno degli aspetti più interessanti dell’esercizio.
Dal punto di vista del costo di trasporto, trasferire una certa quantità:
- martedì;
- giovedì;
- venerdì;
può avere lo stesso costo unitario.
Il costo di holding totale, sotto le ipotesi dichiarate, rimane anch’esso invariato.
Il timing deve però rispettare un vincolo fondamentale: la merce deve essere realmente disponibile nel momento in cui viene trasferita.
Questo significa che più piani possono essere:
- fattibili;
- economicamente equivalenti;
- ma operativamente diversi.
Ed è proprio per questo che possono esistere più soluzioni ottime.
Dal modello matematico al vettore del solver
linprog non riceve direttamente variabili simboliche come:
[math]\displaystyle x_{2,1,3}[/math]
oppure:
[math]\displaystyle I_{4,2}[/math]
Il solver lavora con un unico vettore di variabili.
Possiamo immaginarlo come:
[math]\displaystyle z=(x_1,x_2,\ldots,I_1,I_2,\ldots)[/math]
Le funzioni:
idx_x(i, j, t)
e:
idx_h(i, t)
servono proprio a tradurre le variabili matematiche nelle posizioni del vettore utilizzato da SciPy.
Questa parte del codice può sembrare tecnica, ma rappresenta un passaggio importante quando si costruiscono modelli di programmazione lineare direttamente con linprog.
Codice Python con programmazione lineare
Nel codice seguente la struttura originale viene mantenuta. La correzione metodologica riguarda la funzione obiettivo: il vettore c contiene soltanto i costi di trasporto, mentre il costo di holding viene calcolato separatamente dopo la soluzione.
import numpy as np
from scipy.optimize import linprog
def risolvi_supply_chain_multiperiodo(
costi_trasporto,
scorte_iniziali,
domande_giornaliere,
capacita_stoccaggio,
costo_stoccaggio
):
n_cd = costi_trasporto.shape[0]
n_giorni = domande_giornaliere.shape[0]
n_coppie = n_cd * (n_cd - 1)
n_x = n_giorni * n_coppie
n_h = (n_giorni + 1) * n_cd
n_var = n_x + n_h
# ------------------------------
# Mappatura degli indici
# ------------------------------
def idx_x(i, j, t):
assert i != j
j_compact = j if j < i else j - 1
return (
t * n_coppie
+ i * (n_cd - 1)
+ j_compact
)
def idx_h(i, t):
return n_x + t * n_cd + i
# ------------------------------
# Funzione obiettivo
# ------------------------------
c = np.zeros(n_var)
# Trasporti
for t in range(n_giorni):
for i in range(n_cd):
for j in range(n_cd):
if i != j:
c[idx_x(i, j, t)] = (
costi_trasporto[i, j]
)
# Il costo di holding non viene inserito
# nella funzione obiettivo perché, sotto
# le ipotesi del problema, è costante e
# non modifica la soluzione ottima.
# ------------------------------
# Vincoli di uguaglianza
# ------------------------------
A_eq_rows = []
b_eq_vals = []
# Scorte iniziali
for i in range(n_cd):
row = np.zeros(n_var)
row[idx_h(i, 0)] = 1.0
A_eq_rows.append(row)
b_eq_vals.append(scorte_iniziali[i])
# Bilancio di massa
for t in range(n_giorni):
for i in range(n_cd):
row = np.zeros(n_var)
row[idx_h(i, t + 1)] = 1.0
row[idx_h(i, t)] = -1.0
for j in range(n_cd):
if i != j:
# Entrate
row[idx_x(j, i, t)] -= 1.0
# Uscite
row[idx_x(i, j, t)] += 1.0
A_eq_rows.append(row)
b_eq_vals.append(
-domande_giornaliere[t, i]
)
A_eq = np.array(A_eq_rows)
b_eq = np.array(b_eq_vals)
# ------------------------------
# Capacità
# ------------------------------
A_ub_rows = []
b_ub_vals = []
for t in range(1, n_giorni + 1):
for i in range(n_cd):
row = np.zeros(n_var)
row[idx_h(i, t)] = 1.0
A_ub_rows.append(row)
b_ub_vals.append(
capacita_stoccaggio
)
A_ub = np.array(A_ub_rows)
b_ub = np.array(b_ub_vals)
# Tutte le variabili non negative
bounds = [
(0.0, None)
] * n_var
# ------------------------------
# Risoluzione
# ------------------------------
res = linprog(
c=c,
A_ub=A_ub,
b_ub=b_ub,
A_eq=A_eq,
b_eq=b_eq,
bounds=bounds,
method="highs"
)
if not res.success:
raise RuntimeError(
f"LP non risolto: {res.message}"
)
# ------------------------------
# Estrazione flussi
# ------------------------------
X = np.zeros(
(
n_giorni,
n_cd,
n_cd
)
)
for t in range(n_giorni):
for i in range(n_cd):
for j in range(n_cd):
if i != j:
X[t, i, j] = (
res.x[idx_x(i, j, t)]
)
# ------------------------------
# Estrazione inventari
# ------------------------------
H = np.zeros(
(
n_giorni + 1,
n_cd
)
)
for t in range(n_giorni + 1):
for i in range(n_cd):
H[t, i] = (
res.x[idx_h(i, t)]
)
costo_trasporto = sum(
costi_trasporto[i, j]
* X[t, i, j]
for t in range(n_giorni)
for i in range(n_cd)
for j in range(n_cd)
if i != j
)
costo_holding = (
costo_stoccaggio
* H[1:].sum()
)
return (
X,
H,
costo_trasporto,
costo_holding
)
Esecuzione
nomi_cd = [
"CD1",
"CD2",
"CD3",
"CD4"
]
nomi_giorni = [
"Lun",
"Mar",
"Mer",
"Gio",
"Ven"
]
costi_trasporto = np.array(
[
[0, 2, 4, 6],
[3, 0, 1, 4],
[5, 2, 0, 3],
[6, 5, 2, 0]
],
dtype=float
)
scorte_iniziali = np.array(
[120, 100, 80, 60],
dtype=float
)
domande = np.array(
[
[40, 25, 20, 15],
[35, 20, 15, 10],
[30, 15, 15, 10],
[25, 15, 10, 10],
[25, 10, 10, 5]
],
dtype=float
)
X, H, ct, ch = (
risolvi_supply_chain_multiperiodo(
costi_trasporto,
scorte_iniziali,
domande,
capacita_stoccaggio=150,
costo_stoccaggio=0.5
)
)
Output degli inventari
print("INVENTARI A FINE GIORNATA")
print()
for t, giorno in enumerate(
nomi_giorni,
start=1
):
print(
giorno,
H[t]
)
Un possibile risultato è:
Lun [80 75 60 45]
Mar [70 40 35 35]
Mer [40 25 20 25]
Gio [15 10 10 15]
Ven [0 0 0 0]
Come leggere gli inventari
La prima osservazione riguarda la quantità totale.
Alla fine del lunedì abbiamo:
[math]\displaystyle 80+75+60+45=260[/math]
unità.
La domanda giornaliera è stata di 100 unità, quindi:
[math]\displaystyle 360-100=260[/math]
Il modello rispetta quindi il bilancio complessivo della rete.
La stessa logica continua nei giorni successivi:
- dopo martedì rimangono 180 unità;
- dopo mercoledì 110;
- dopo giovedì 50;
- dopo venerdì 0.
Questo andamento non è una scelta del solver. È imposto dalla domanda complessiva e dall’assenza di produzione o perdite.
Il solver può decidere come distribuire la quantità residua tra i centri e quando effettuare i trasferimenti, ma non può modificare la quantità totale disponibile nella rete.
Trasferimenti ottimali
for t, giorno in enumerate(nomi_giorni):
for i in range(4):
for j in range(4):
if i != j and X[t, i, j] > 1e-6:
print(
giorno,
nomi_cd[i],
"->",
nomi_cd[j],
X[t, i, j]
)
Una soluzione trovata dal solver può essere:
Martedì
CD2 -> CD1 : 15
CD3 -> CD1 : 10
Venerdì
CD4 -> CD1 : 10
Il piano risolve il deficit complessivo di CD1 trasferendo:
- 15 unità da CD2;
- 10 unità da CD3;
- 10 unità da CD4.
La quantità totale trasferita è quindi:
[math]\displaystyle 15+10+10=35[/math]
esattamente il deficit settimanale di CD1.
Il dettaglio interessante riguarda il momento in cui i trasferimenti vengono effettuati.
Il solver non sta cercando soltanto di individuare le quantità corrette. Deve anche rispettare la disponibilità temporale dell’inventario.
Perché il solver può produrre un piano diverso da quello manuale?
Possiamo costruire anche un altro piano.
Per esempio:
Giovedì
[math]\displaystyle 10[/math]
unità da CD2 a CD1.
Venerdì
[math]\displaystyle 5[/math]
unità da CD2 a CD1;
[math]\displaystyle 10[/math]
unità da CD3 a CD1;
[math]\displaystyle 10[/math]
unità da CD4 a CD1.
Se tutti i vincoli di disponibilità sono rispettati, il costo di trasporto rimane:
[math]\displaystyle 155€[/math]
Il costo di holding rimane:
[math]\displaystyle 300€[/math]
Il costo totale rimane quindi:
[math]\displaystyle 455€[/math]
Questo ci porta a un punto importante.
Non esiste necessariamente una sola soluzione ottima
Il solver non sta affermando:
questo è l’unico modo corretto di gestire la supply chain.
Sta affermando:
questa è una soluzione che minimizza la funzione obiettivo rispettando tutti i vincoli.
Se più configurazioni producono lo stesso valore della funzione obiettivo, possiamo avere più soluzioni ottime.
Nel nostro esercizio:
[math]\displaystyle CostoTrasporto^*=155€[/math]
e:
[math]\displaystyle CostoTotale=155+300=455€[/math]
può essere ottenuto attraverso diversi piani di trasferimento.
È importante non confondere questo concetto con qualsiasi comportamento particolare del solver. Il punto qui è semplicemente che diverse configurazioni decisionali possono produrre lo stesso valore ottimo della funzione obiettivo.
Costo ottimo non significa automaticamente piano migliore
Supponiamo di avere tre piani con lo stesso costo.
Il primo concentra molti trasferimenti all’ultimo momento.
Il secondo anticipa alcune movimentazioni.
Il terzo mantiene una maggiore disponibilità di sicurezza.
Dal punto di vista del modello attuale possono essere equivalenti. Dal punto di vista operativo, però, potrebbero essere molto diversi.
Un responsabile logistico potrebbe considerare anche:
- rischio di ritardo;
- numero di trasferimenti;
- complessità operativa;
- emissioni;
- utilizzo dei mezzi;
- rischio di stockout;
- affidabilità dei centri;
- disponibilità di capacità aggiuntiva.
Qui emerge una distinzione fondamentale.
Soluzione ottima e soluzione robusta
Una soluzione può essere ottima rispetto alla funzione obiettivo e contemporaneamente essere fragile rispetto a eventi non presenti nel modello.
Supponiamo che un piano porti:
[math]\displaystyle CD1=0[/math]
alla fine di una giornata.
Questo non è matematicamente sbagliato. Il problema è che il modello sta assumendo implicitamente che:
- la domanda sia esattamente quella prevista;
- i trasferimenti arrivino senza ritardi;
- non si verifichino interruzioni;
- non esistano variazioni impreviste.
Se una di queste ipotesi viene violata, il piano può diventare problematico.
La fragilità non nasce quindi dall’ottimizzazione in sé. Nasce dalle ipotesi deterministiche utilizzate per costruire il modello.
Una possibile evoluzione consiste nell’introdurre uno stock di sicurezza:
[math]\displaystyle I_{i,t}\ge SafetyStock_i[/math]
oppure nell’aggiungere un secondo criterio che permetta di distinguere tra diverse soluzioni economicamente equivalenti.
Discriminazione, Calibrazione e Decision Value
Questa terza parte permette di applicare tre concetti che utilizziamo spesso nella data science, ma che non appartengono esclusivamente ai modelli predittivi.
Discriminazione
Nei problemi di classificazione la discriminazione misura la capacità di distinguere osservazioni diverse.
Qui possiamo utilizzare un’idea equivalente.
Esercizio 5
Il modello distingue correttamente stati diversi:
[math]\displaystyle (Hub2,G)\neq(Hub2,F)[/math]
perché le decisioni future possono avere costi differenti.
Esercizio 6
Il modello distingue:
- quantità;
- centri;
- giorni;
- flussi;
- inventari.
In entrambi i casi il primo requisito per prendere una buona decisione è rappresentare correttamente le alternative che devono essere confrontate.
Calibrazione
Un modello può essere formalmente corretto e produrre comunque una decisione poco utile se i parametri non rappresentano il sistema reale.
Esercizio 5
Il parametro fondamentale è:
[math]\displaystyle \lambda[/math]
Quanto vale davvero un’ora?
Esercizio 6
La calibrazione riguarda:
- domanda;
- costi di trasporto;
- capacità;
- tempi di trasferimento;
- costo di holding;
- eventuali livelli di sicurezza.
Un modello perfettamente ottimizzato su parametri sbagliati può quindi produrre una soluzione formalmente ottima ma economicamente poco sensata.
Decision Value
Qui si trova probabilmente il messaggio più importante dell’articolo.
Nel primo esercizio il valore non consiste semplicemente nel sapere che una rotta è preferibile.
Il valore consiste nel sapere:
a quale soglia del valore del tempo cambia la decisione.
Nel secondo esercizio il valore non consiste soltanto nel sapere che devono essere trasferite 35 unità.
Il modello ci permette di capire:
quali piani sono economicamente equivalenti e quali criteri aggiuntivi potrebbero servire per scegliere tra essi.
Questo è il passaggio dalla soluzione matematica alla decisione.
Esercizio 6
Modello
Programmazione lineare su una rete concettualmente tempo-espansa.
Risultato
Costo minimo di trasporto:
[math]\displaystyle 155€[/math]
Costo di holding costante:
[math]\displaystyle 300€[/math]
Costo totale:
[math]\displaystyle 455€[/math]
Scoperta
La soluzione ottima non è necessariamente unica.
Decisione
Tra più soluzioni con lo stesso costo possiamo scegliere quella:
- più robusta;
- più semplice da gestire;
- meno rischiosa;
- più affidabile.
Cosa possiamo fare?
Possiamo introdurre una seconda fase decisionale.
Prima:
[math]\displaystyle \min Costo[/math]
Poi, tra tutte le soluzioni ottime:
[math]\displaystyle \min Numero\ Trasferimenti[/math]
oppure:
[math]\displaystyle \max Inventario\ Minimo[/math]
Questo tipo di impostazione permette di passare dall’ottimizzazione di un singolo indicatore alla costruzione di una decisione più completa.
Cosa abbiamo imparato nella Parte 3?
I due esercizi sembrano diversi.
Il primo riguarda un problema di percorso minimo.
Il secondo riguarda inventari e trasferimenti.
Ma condividono la stessa idea.
Esercizio 5
Abbiamo aggiunto una dimensione allo stato:
[math]\displaystyle Hub\times Modalità[/math]
Esercizio 6
Abbiamo aggiunto una dimensione temporale:
[math]\displaystyle Centro\times Tempo[/math]
In entrambi i casi il problema originale diventa più grande, perché aumentano il numero di stati o di variabili.
Allo stesso tempo diventa più facile rappresentarlo correttamente.
Questa è una delle idee più importanti della ricerca operativa e della data science applicata:
un algoritmo sofisticato non può compensare completamente una rappresentazione sbagliata del problema.
Mini Quiz Finale
Domanda 1
Perché nel problema intermodale:
[math]\displaystyle Hub2[/math]
non è sufficiente come stato?
Domanda 2
Perché il costo totale di holding rimane costante in questo esercizio?
Domanda 3
Se il solver restituisce una soluzione con costo totale:
[math]\displaystyle 455€[/math]
significa che non esistono altre soluzioni ottime?
Domanda 4
Qual è la differenza tra:
[math]\displaystyle Costo\ ottimo[/math]
e:
[math]\displaystyle Decisione\ migliore[/math]
in un contesto aziendale?
Risposte
R1
Perché dobbiamo ricordare anche la modalità di trasporto utilizzata. Il cambio di modalità può generare costi e tempi aggiuntivi e modifica quindi il costo delle decisioni successive.
R2
Perché la quantità totale di inventario diminuisce esclusivamente a causa della domanda. I trasferimenti interni modificano la distribuzione della merce ma non la quantità totale e, con costo di holding uniforme, il costo complessivo rimane fisso.
R3
No.
Il problema può avere più configurazioni con lo stesso valore della funzione obiettivo. Il solver restituisce una soluzione ottima, non necessariamente l’unica.
R4
Il costo ottimo dipende dalla funzione obiettivo scelta.
La decisione migliore può dipendere anche da fattori non presenti nel modello, come:
- rischio;
- robustezza;
- semplicità;
- affidabilità;
- servizio al cliente.





