Algoritmo di Dijkstra e Supply Chain – Parte 3: grafi estesi, reti nel tempo e soluzioni ottime non uniche

Cerca:

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
Dijkstra e Supply Chain

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.

Forse potrebbe interessarti anche:  Analisi della Fedeltà dei Clienti con Python: Un Approccio Pratico con Pandas

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.

Forse potrebbe interessarti anche:  Decoratori Python: Guida Essenziale con Esempi Pratici per Ogni Livello

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:

  1. quanto trasferire;
  2. quando trasferire;
  3. 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.

Forse potrebbe interessarti anche:  Metodo Branch and Bound Spiegato: Esempio Semplice di PLI

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.
Pubblicità