Ottimizzazione aziendale con Python: 3 problemi MILP con PuLP

Cerca:

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
Ottimizzazione aziendale con Python e MILP

Dalla scelta dei progetti alla pianificazione robusta: come la Ricerca Operativa trasforma i problemi aziendali in decisioni matematiche

Prendere decisioni in azienda raramente è una questione di bianco o nero.

Molto spesso significa trovare il miglior compromesso possibile tra risorse limitate, obiettivi contrastanti, dipendenze tecniche, capacità produttive e incertezza sul futuro.

Quali progetti finanziare quando il budget non è sufficiente per realizzarli tutti?
Quale impianto utilizzare per soddisfare la domanda al minor costo?
Come distribuire un investimento quando il rendimento dipende da scenari futuri che non possiamo conoscere con certezza?

Quando le alternative diventano numerose, l’intuito manageriale e i tradizionali fogli di calcolo iniziano a mostrare i propri limiti.

È qui che entra in gioco la Ricerca Operativa, combinata con la flessibilità di Python.

L’idea fondamentale è semplice:

trasformare una decisione aziendale in un problema matematico composto da variabili, obiettivi e vincoli, e affidare a un solver la ricerca di una soluzione ottimale.

Non significa che l’algoritmo “prenda la decisione al posto del manager”.

Significa piuttosto che il manager può definire con precisione le regole del problema, mentre il modello matematico ricerca sistematicamente una configurazione che le rispetti.

Questo passaggio è fondamentale.

Un foglio Excel può mostrarci quanto costa una determinata configurazione.

Un modello di ottimizzazione può confrontare sistematicamente le alternative e individuare una soluzione ottimale rispetto all’obiettivo definito.

Naturalmente, c’è una condizione fondamentale:

un solver può trovare una soluzione ottima al modello, ma non può correggere un modello che rappresenta male il problema aziendale.

Per questo la parte più importante dell’ottimizzazione non è soltanto scrivere il codice. È costruire correttamente il modello.

In questo articolo vedremo tre scenari progressivi:

  1. Project Portfolio Management, con vincoli logici e sinergie;
  2. allocazione produttiva, con costi fissi, costi variabili e capacità;
  3. ottimizzazione sotto incertezza, introducendo un vincolo di robustezza.

Utilizzeremo la libreria Python pulp.

Pubblicità

Dalla decisione aziendale al modello MILP

Prima di affrontare gli esercizi è utile chiarire la struttura generale di un problema di Mixed Integer Linear Programming, o MILP.

Un modello MILP contiene generalmente:

Variabili decisionali

Rappresentano ciò che l’azienda deve decidere.

Per esempio:

[math]x_i=1[/math] se un progetto viene finanziato e [math]x_i=0[/math] altrimenti.

Le variabili possono essere:

  • binarie;
  • intere;
  • continue.

Funzione obiettivo

Descrive ciò che vogliamo massimizzare o minimizzare.

Ad esempio: [math]\max Z[/math] per massimizzare il VAN, oppure [math]\min Z[/math] per minimizzare i costi.

Vincoli

Rappresentano le regole del problema:

  • budget;
  • capacità;
  • domanda;
  • dipendenze;
  • incompatibilità;
  • disponibilità di risorse;
  • livelli minimi di performance.

Il solver cerca quindi una soluzione ammissibile che ottimizzi la funzione obiettivo.

È importante distinguere due concetti:

  • soluzione ammissibile: rispetta tutti i vincoli;
  • soluzione ottima: tra le soluzioni ammissibili, raggiunge il miglior valore possibile della funzione obiettivo.

Esercizio 1: allocazione di un portafoglio progetti con sinergie

Il problema

L’azienda di logistica FastRoute deve scegliere tra cinque progetti di innovazione.

L’obiettivo è massimizzare il Valore Attuale Netto (VAN) del portafoglio, tenendo conto non soltanto del valore individuale dei progetti, ma anche delle interazioni tra essi.

I progetti sono:

Progetto Costo (€) VAN (€)
P1 80.000 150.000
P2 70.000 130.000
P3 60.000 110.000
P4 50.000 90.000
P5 40.000 80.000

Il budget disponibile è: [math]B=200.000[/math]

Ma esistono anche tre condizioni strategiche.

Sinergia P1-P2

Se vengono selezionati entrambi P1 e P2, il portafoglio riceve un bonus di [math]25.000€[/math].

Mutua esclusività P3-P4

P3 e P4 sono alternative: [math]x_3+x_4 \leq 1[/math]

Dipendenza P5-P3

P5 può essere scelto soltanto se viene scelto P3: [math]x_5 \leq x_3[/math]

Questo è un esempio molto utile perché mostra come una frase manageriale apparentemente semplice come:

“P5 può essere fatto solo se realizziamo P3”

possa essere tradotta direttamente in un vincolo lineare.

Formulazione matematica

Introduciamo: [math]\displaystyle x_i \in \{0,1\}[/math] dove [math]x_i=1[/math] se il progetto [math]i[/math] viene selezionato.

Per rappresentare la sinergia introduciamo inoltre: [math]\displaystyle y \in \{0,1\}[/math] dove [math]y=1[/math] quando e solo quando P1 e P2 sono entrambi selezionati.

La funzione obiettivo è:

[math]\displaystyle \begin{aligned}
\max Z = {} & 150.000x_1 + 130.000x_2 + 110.000x_3 \\
& {} + 90.000x_4 + 80.000x_5 + 25.000y
\end{aligned}[/math]

con vincolo di budget:

[math]\displaystyle \begin{aligned}
80.000x_1 + 70.000x_2 + 60.000x_3 \\
{} + 50.000x_4 + 40.000x_5 \leq 200.000
\end{aligned}[/math]

e:

[math]\displaystyle x_3+x_4 \leq 1[/math]

[math]\displaystyle x_5 \leq x_3[/math]

Per la sinergia:

[math]\displaystyle y \leq x_1[/math]

[math]\displaystyle y \leq x_2[/math]

[math]\displaystyle y \geq x_1+x_2-1[/math]

Questi tre vincoli fanno sì che [math]y[/math] rappresenti l’evento logico: [math]y = x_1 \land x_2[/math].

Pubblicità

Implementazione in Python

import pulp

# 1. Definizione del problema
model = pulp.LpProblem("Logica_e_Sinergie", pulp.LpMaximize)

# 2. Dati
progetti = ['P1', 'P2', 'P3', 'P4', 'P5']
costo = {'P1': 80000, 'P2': 70000, 'P3': 60000, 'P4': 50000, 'P5': 40000}
van = {'P1': 150000, 'P2': 130000, 'P3': 110000, 'P4': 90000, 'P5': 80000}
budget = 200000

# 3. Variabili decisionali
x = pulp.LpVariable.dicts("x", progetti, cat=pulp.LpBinary)
y = pulp.LpVariable("y", cat=pulp.LpBinary)  # Bonus sinergia P1-P2

# 4. Funzione obiettivo
model += pulp.lpSum([van[i] * x[i] for i in progetti]) + 25000 * y

# 5. Vincoli
# Budget
model += pulp.lpSum([costo[i] * x[i] for i in progetti]) <= budget

# Mutua esclusività: P3 e P4 non possono essere entrambi selezionati
model += x['P3'] + x['P4'] <= 1

# Dipendenza: P5 può essere selezionato solo se P3 è selezionato
model += x['P5'] <= x['P3']

# Sinergia: y = 1 se e solo se P1 e P2 sono selezionati
model += y <= x['P1']
model += y <= x['P2']
model += y >= x['P1'] + x['P2'] - 1

# 6. Risoluzione
model.solve(pulp.PULP_CBC_CMD(msg=False))

# 7. Output
print(f"Stato: {pulp.LpStatus[model.status]}")
print(f"VAN Totale: € {pulp.value(model.objective):,.2f}")
print("\nProgetti Selezionati:")
for i in progetti:
    if pulp.value(x[i]) == 1:
        print(f" - {i}")
if pulp.value(y) == 1:
    print("Bonus Sinergia P1-P2 attivato!")
print(f"\nBudget Utilizzato: € {sum(costo[i] * pulp.value(x[i]) for i in progetti):,.2f}")

Output

Stato: Optimal
VAN Totale: € 395,000.00

Progetti Selezionati:
 - P1
 - P2
 - P4
Bonus Sinergia P1-P2 attivato!

Budget Utilizzato: € 200,000.00

Interpretazione dell’output

La parola Optimal è il primo elemento importante.

Significa che il solver non ha semplicemente trovato una combinazione che funziona: ha individuato una soluzione ottimale rispetto al modello formulato.

La configurazione scelta è: [math]P1+P2+P4[/math]

Costo

[math]80.000+70.000+50.000=200.000[/math] → il budget viene utilizzato completamente.

VAN dei singoli progetti

[math]150.000+130.000+90.000=370.000[/math]

Bonus di sinergia

P1 e P2 sono entrambi presenti, quindi [math]y=1[/math] e viene aggiunto [math]25.000[/math]. Il VAN complessivo diventa [math]370.000+25.000=395.000[/math].

Perché non P1 + P2 + P3?

Una configurazione apparentemente molto interessante sarebbe [math]P1+P2+P3[/math]. Il VAN sarebbe [math]150.000+130.000+110.000+25.000=415.000[/math], quindi superiore a €395.000. Ma il costo sarebbe [math]80.000+70.000+60.000=210.000[/math] e quindi [math]210.000 > 200.000[/math]. La configurazione è inammissibile.

Questo è un punto fondamentale nell’ottimizzazione: non basta avere un valore obiettivo elevato. La soluzione deve prima di tutto rispettare tutti i vincoli.

Perché non P1 + P2 + P5?

Qui entra in gioco la logica. Il costo sarebbe [math]80.000+70.000+40.000=190.000[/math], quindi il budget sarebbe rispettato. Anche il VAN sarebbe interessante: [math]150.000+130.000+80.000+25.000=385.000[/math]. Ma P5 richiede P3. Il vincolo [math]x_5 \leq x_3[/math] impone infatti che se [math]x_5=1[/math] allora necessariamente [math]x_3=1[/math]. La combinazione P1-P2-P5 senza P3 viene quindi esclusa dal modello.

Forse potrebbe interessarti anche:  Ottimizzazione del Budget Marketing con Simulazione Monte Carlo

Interpretazione manageriale

Questo esempio mostra perché il Project Portfolio Management non può essere ridotto alla semplice classifica dei progetti in base al ROI o al VAN.

Se selezionassimo semplicemente i progetti con VAN individuale più elevato, potremmo ottenere una soluzione incompatibile con:

  • il budget;
  • le dipendenze tecnologiche;
  • le strategie organizzative;
  • le incompatibilità tra iniziative;
  • le sinergie.

Il modello introduce invece la possibilità di rappresentare formalmente la strategia.

Ad esempio:

“Questi due progetti insieme producono un valore aggiuntivo”

diventa una variabile binaria ausiliaria.

Allo stesso modo:

“Questo progetto può partire soltanto se è già stata adottata quella piattaforma”

diventa [math]x_{\text{dipendente}} \leq x_{\text{prerequisito}}[/math].

È uno dei motivi per cui i modelli MILP sono estremamente interessanti per la pianificazione aziendale.


Esercizio 2: costi fissi, costi variabili e capacità produttiva

Il problema

La società EcoPrint deve decidere come organizzare la produzione utilizzando tre potenziali impianti.

Ogni impianto presenta:

  • un costo fisso di attivazione;
  • un costo variabile per unità prodotta;
  • una capacità massima.
Impianto Costo fisso Costo variabile Capacità
1 €50.000 €100 500
2 €80.000 €70 800
3 €40.000 €120 400

La domanda è:

Prodotto Domanda
A 300
B 200
C 250

La domanda complessiva è quindi: [math]300+200+250=750[/math]

L’obiettivo è minimizzare: costi fissi + costi variabili.

Una precisazione importante sulla modellazione

Nel modello utilizzato in questo articolo, ogni prodotto viene assegnato interamente a un singolo impianto.

Per questo viene utilizzata una variabile binaria: [math]w_{j,p}\in\{0,1\}[/math] che indica se il prodotto [math]p[/math] viene assegnato all’impianto [math]j[/math].

Non stiamo quindi modellando un vero e proprio flusso frazionabile. In un problema produttivo più generale potremmo introdurre una variabile continua [math]q_{j,p}\geq0[/math] per rappresentare la quantità effettivamente prodotta dall’impianto [math]j[/math].

La formulazione presente è invece volutamente più semplice e didattica: ogni domanda di prodotto viene trattata come un lotto indivisibile.

Formulazione matematica

Introduciamo: [math]\displaystyle y_j \in \{0,1\}[/math] dove [math]y_j=1[/math] se l’impianto [math]j[/math] viene attivato.

Introduciamo inoltre: [math]\displaystyle w_{j,p} \in \{0,1\}[/math] dove [math]w_{j,p}=1[/math] se il prodotto [math]p[/math] viene assegnato all’impianto [math]j[/math].

La funzione obiettivo è:

[math]\displaystyle \min Z = \sum_j F_j y_j + \sum_j \sum_p v_j D_p w_{j,p}[/math]

Ogni prodotto deve essere assegnato a un solo impianto: [math]\displaystyle \sum_j w_{j,p}=1[/math]

La capacità deve essere rispettata: [math]\displaystyle \sum_p D_p w_{j,p} \leq M_j y_j[/math]

e l’assegnazione è possibile soltanto se l’impianto è attivo: [math]\displaystyle w_{j,p} \leq y_j[/math]

Implementazione in Python

import pulp

model = pulp.LpProblem("Costi_Fissi_Variabili", pulp.LpMinimize)

impianti = [1, 2, 3]
prodotti = ['A', 'B', 'C']
F = {1: 50000, 2: 80000, 3: 40000}
v = {1: 100, 2: 70, 3: 120}
M = {1: 500, 2: 800, 3: 400}
D = {'A': 300, 'B': 200, 'C': 250}

# Variabili
y = pulp.LpVariable.dicts("Attiva", impianti, cat=pulp.LpBinary)  # y_j
w = pulp.LpVariable.dicts("Assegna", (impianti, prodotti), cat=pulp.LpBinary)  # w_{j,p}

# Funzione Obiettivo
model += pulp.lpSum([F[j] * y[j] for j in impianti]) + \
         pulp.lpSum([v[j] * D[p] * w[j][p] for j in impianti for p in prodotti])

# Vincoli di Assegnazione (un prodotto a un solo impianto)
for p in prodotti:
    model += pulp.lpSum([w[j][p] for j in impianti]) == 1

# Vincoli di Capacità (con "big M" = capacità)
for j in impianti:
    model += pulp.lpSum([D[p] * w[j][p] for p in prodotti]) <= M[j] * y[j]
for j in impianti:
    for p in prodotti:
        model += w[j][p] <= y[j]

# Risoluzione
model.solve(pulp.PULP_CBC_CMD(msg=False))

print(f"Stato: {pulp.LpStatus[model.status]}")
print(f"Costo Totale Minimo: € {pulp.value(model.objective):,.2f}")
print("\nAssegnamenti:")
for j in impianti:
    if pulp.value(y[j]) == 1:
        print(f"Impianto {j} (Costo Fisso: €{F[j]:,})")
        for p in prodotti:
            if pulp.value(w[j][p]) == 1:
                qta = D[p]
                print(f"  - Prodotto {p}: {qta} unità (Costo Var: €{v[j]*qta:,})")

Output

Stato: Optimal
Costo Totale Minimo: € 132,500.00

Assegnamenti:
Impianto 2 (Costo Fisso: €80,000)
  - Prodotto A: 300 unità (Costo Var: €21,000)
  - Prodotto B: 200 unità (Costo Var: €14,000)
  - Prodotto C: 250 unità (Costo Var: €17,500)

Analisi dell’output

Anche in questo caso Optimal indica che il solver ha trovato una soluzione ottimale rispetto al modello.

La decisione è apparentemente sorprendente: attivare soltanto l’impianto 2, nonostante sia quello con il costo fisso più elevato.

Questa è esattamente la situazione in cui l’ottimizzazione diventa utile.

Il costo totale dell’impianto 2

L’impianto 2 ha [math]F_2=80.000[/math] e un costo variabile di [math]70€/\text{unità}[/math]. La produzione complessiva è [math]750[/math], quindi il costo variabile è [math]70 \times 750 = 52.500[/math]. Il costo totale è [math]80.000+52.500=132.500[/math], che rappresenta il minimo individuato dal modello.

Perché l’impianto 1 non è sufficiente?

L’impianto 1 ha una capacità di [math]500[/math] mentre la domanda è [math]750[/math], quindi [math]500 < 750[/math]. L’impianto 1 non può coprire da solo il fabbisogno. Lo stesso vale per l’impianto 3: [math]400 < 750[/math]. L’impianto 2 è invece l’unico che possiede una capacità sufficiente: [math]800 \geq 750[/math]. Questo rende possibile una soluzione estremamente semplice: un solo impianto, nessuna duplicazione dei costi fissi e tutta la produzione concentrata sulla struttura con il costo variabile più basso.

Il punto interessante: il costo fisso non deve essere analizzato isolatamente

L’impianto 3 costa soltanto [math]40.000€[/math] per l’attivazione. Potrebbe quindi sembrare inizialmente l’alternativa più economica. Ma ha un costo variabile di [math]120€/\text{unità}[/math]. Se avesse capacità sufficiente, il costo totale sarebbe [math]40.000+120\times750=130.000€[/math], che sarebbe addirittura inferiore ai €132.500 dell’impianto 2. Ma l’impianto 3 può produrre soltanto 400 unità. Quindi quella soluzione è inammissibile.

Questo esempio mostra una delle idee più importanti della modellazione operativa: una soluzione apparentemente conveniente può essere inutilizzabile perché viola un vincolo strutturale.

Pubblicità

E se attivassimo impianto 2 + impianto 3?

A prima vista potrebbe sembrare conveniente sfruttare il costo fisso più basso dell’impianto 3. Ma attivare il secondo impianto comporta almeno [math]80.000+40.000=120.000€[/math] di costi fissi. Il vantaggio del costo variabile inferiore dell’impianto 2 è già sufficiente per coprire tutta la domanda. Aggiungere una seconda struttura significa quindi introdurre un costo fisso che non produce un beneficio sufficiente.

Il modello sta facendo, in sostanza, una valutazione di Total Cost of Ownership: [math]TCO = \text{costi fissi} + \text{costi operativi}[/math], e non una semplice ricerca del prezzo unitario più basso.

Il margine di capacità è un’informazione manageriale

L’impianto 2 produce [math]750[/math] unità su una capacità di [math]800[/math]. Rimane quindi una capacità inutilizzata di [math]800-750=50[/math] unità. In termini percentuali: [math]\frac{50}{800}=6,25\%[/math].

Questo dato è interessante dal punto di vista manageriale. La soluzione è economicamente ottimale, ma l’impianto 2 opera già al [math]\frac{750}{800}=93,75\%[/math] della capacità. Quindi il modello suggerisce anche una possibile fragilità operativa. Se la domanda aumentasse di oltre 50 unità, la soluzione “solo impianto 2” non sarebbe più fattibile.

Questa osservazione apre naturalmente la porta a un’analisi di scenario: quanto deve aumentare la domanda prima che diventi conveniente o necessario attivare un secondo impianto?

È proprio qui che l’ottimizzazione può passare dalla semplice ricerca di una soluzione alla vera decision analysis.


Esercizio 3: allocazione robusta con domanda e rendimento incerti

Il problema

Passiamo ora a un problema più complesso.

Forse potrebbe interessarti anche:  Computer Vision: Come Iniziare con OpenCV Parte1

L’azienda AgriTech deve decidere come allocare un budget di [math]150.000€[/math] per l’acquisto di quattro tipi di sensori.

Il valore economico dei sensori non è certo: dipende dalle condizioni meteorologiche future.

Consideriamo tre scenari:

  • Ottimistico
  • Normale
  • Pessimistico

e assumiamo che siano equiprobabili.

Sensore Costo Ottimistico Normale Pessimistico
S1 €40.000 €100.000 €80.000 €60.000
S2 €60.000 €140.000 €100.000 €70.000
S3 €50.000 €110.000 €90.000 €50.000
S4 €30.000 €70.000 €60.000 €40.000

L’obiettivo è massimizzare il VAN atteso. Ma introduciamo anche un requisito di robustezza.

Vogliamo che il VAN ottenuto in ogni scenario sia almeno il 70% del VAN che sarebbe possibile ottenere ottimizzando separatamente quello specifico scenario.

Definiamo quindi [math]Z_s^*[/math] come il miglior VAN ottenibile nello scenario [math]s[/math] rispettando soltanto il vincolo di budget.

Il vincolo di robustezza sarà: [math]Z_s \geq 0,7 Z_s^*[/math] per ogni scenario.

Una precisazione importante: robustezza non significa necessariamente “minimizzare il caso peggiore”

Esistono diverse forme di ottimizzazione robusta. Quella utilizzata qui è una formulazione del tipo:

massimizza il rendimento atteso, ma imponi una soglia minima di performance rispetto all’ottimo specifico di ciascuno scenario.

È quindi diversa, ad esempio, da una strategia puramente pessimistica come [math]\max \min_s Z_s[/math] che cercherebbe di massimizzare direttamente il peggior risultato. Questa distinzione è importante perché “robusto” non significa automaticamente “pessimista”.

Formulazione matematica

Introduciamo: [math]\displaystyle x_i \in \{0,1\}[/math] dove [math]x_i=1[/math] se il sensore [math]i[/math] viene acquistato.

Per ogni scenario: [math]\displaystyle Z_s = \sum_i v_{i,s} x_i[/math]

Il VAN atteso è: [math]\displaystyle E[Z] = \frac{1}{3}\sum_s Z_s[/math]

e il vincolo di budget è: [math]\displaystyle \sum_i c_i x_i \leq 150.000[/math]

Infine: [math]\displaystyle Z_s \geq \alpha Z_s^*[/math] con [math]\alpha=0,7[/math].

Implementazione in Python

import pulp

# Dati
sensori = ['S1', 'S2', 'S3', 'S4']
costo = {'S1': 40000, 'S2': 60000, 'S3': 50000, 'S4': 30000}
budget = 150000
van_scenari = {
    'Ott': {'S1': 100000, 'S2': 140000, 'S3': 110000, 'S4': 70000},
    'Nor': {'S1': 80000,  'S2': 100000, 'S3': 90000,  'S4': 60000},
    'Pes': {'S1': 60000,  'S2': 70000,  'S3': 50000,  'S4': 40000}
}
alpha = 0.7

# Funzione per calcolare Z* per ogni scenario (senza vincoli di robustezza)
def calcola_ottimo(scenario_van):
    mod = pulp.LpProblem("Temp", pulp.LpMaximize)
    x = pulp.LpVariable.dicts("x", sensori, cat=pulp.LpBinary)
    mod += pulp.lpSum([scenario_van[i] * x[i] for i in sensori])
    mod += pulp.lpSum([costo[i] * x[i] for i in sensori]) <= budget
    mod.solve(pulp.PULP_CBC_CMD(msg=False))
    return pulp.value(mod.objective)

z_star = {s: calcola_ottimo(van_scenari[s]) for s in van_scenari}
print("Ottimi per scenario (Z*):", z_star)

# Modello principale
model = pulp.LpProblem("Robustezza", pulp.LpMaximize)

x = pulp.LpVariable.dicts("x", sensori, cat=pulp.LpBinary)
Z = pulp.LpVariable.dicts("Z", van_scenari.keys(), lowBound=0)

# Obiettivo: massimizzare VAN atteso
model += (1/3) * pulp.lpSum([Z[s] for s in van_scenari])

# Vincolo di budget
model += pulp.lpSum([costo[i] * x[i] for i in sensori]) <= budget

# Vincoli per ogni scenario
for s in van_scenari:
    # Uguaglianza: Z_s = VAN dello scenario s
    model += Z[s] == pulp.lpSum([van_scenari[s][i] * x[i] for i in sensori])
    # Vincolo di robustezza
    model += Z[s] >= alpha * z_star[s]

# Risoluzione
model.solve(pulp.PULP_CBC_CMD(msg=False))

print(f"Stato: {pulp.LpStatus[model.status]}")
print(f"VAN Atteso Massimizzato: € {pulp.value(model.objective):,.2f}")
print("\nComposizione Portafoglio Robusto:")
for i in sensori:
    if pulp.value(x[i]) == 1:
        print(f" - {i}")
print("\nDettaglio per Scenario:")
for s in van_scenari:
    val_Z = pulp.value(Z[s])
    print(f"  {s}: VAN = € {val_Z:,.2f} (Vincolo: ≥ {alpha * z_star[s]:,.2f})")
print(f"Budget Utilizzato: € {sum(costo[i] * pulp.value(x[i]) for i in sensori):,.2f}")

Analisi dell’output

Il primo risultato è:

Ottimi per scenario (Z*):
{'Ott': 350000.0, 'Nor': 270000.0, 'Pes': 180000.0}

Questi valori sono molto importanti. Il modello ha prima risolto tre problemi distinti: “Se sapessi in anticipo quale scenario si verificherà, qual è il miglior portafoglio che potrei scegliere?”

Otteniamo:

Scenario Ottimo specifico [math]Z_s^*[/math]
Ottimistico €350.000
Normale €270.000
Pessimistico €180.000

Questi valori costituiscono il benchmark teorico contro cui viene valutata la robustezza.

Le soglie di robustezza

Con [math]\alpha=0,7[/math] le soglie diventano:

  • Scenario ottimistico: [math]0,7 \times 350.000 = 245.000[/math]
  • Scenario normale: [math]0,7 \times 270.000 = 189.000[/math]
  • Scenario pessimistico: [math]0,7 \times 180.000 = 126.000[/math]

Il portafoglio scelto dovrà quindi produrre almeno:

Scenario Soglia minima
Ottimistico €245.000
Normale €189.000
Pessimistico €126.000

Questa è una forma di protezione relativa: non chiediamo che il portafoglio abbia un VAN assoluto minimo uguale in tutti gli scenari, ma che rimanga sufficientemente vicino al miglior risultato ottenibile nello scenario.

Soluzione del modello

Il solver restituisce:

Stato: Optimal
VAN Atteso Massimizzato: € 266,666.67

e seleziona: S1 S2 S3.

Il costo è: [math]40.000+60.000+50.000=150.000[/math], quindi l’intero budget viene utilizzato.

Performance del portafoglio

Il portafoglio S1+S2+S3 produce:

  • Ottimistico: [math]100.000+140.000+110.000=350.000[/math]
  • Normale: [math]80.000+100.000+90.000=270.000[/math]
  • Pessimistico: [math]60.000+70.000+50.000=180.000[/math]

Il VAN atteso è quindi: [math]\displaystyle E[Z] = \frac{350.000+270.000+180.000}{3} = 266.666,67€[/math]

Un risultato particolarmente interessante: la robustezza non è vincolante

Qui è necessario fare una precisazione rispetto all’interpretazione iniziale dell’esercizio.

Il portafoglio S1+S2+S3 non soltanto rispetta i vincoli di robustezza.

Raggiunge l’ottimo individuale in tutti e tre gli scenari.

Infatti:

  • [math]Z_{\text{Ott}} = Z_{\text{Ott}}^* = 350.000[/math]
  • [math]Z_{\text{Nor}} = Z_{\text{Nor}}^* = 270.000[/math]
  • [math]Z_{\text{Pes}} = Z_{\text{Pes}}^* = 180.000[/math]

Di conseguenza [math]\frac{Z_s}{Z_s^*} = 1[/math] per ogni scenario. Il portafoglio raggiunge quindi il 100% dell’ottimo scenario-specifico. Questo significa che il vincolo del 70% è ampiamente soddisfatto.

Quanto è ampio il margine?

Possiamo calcolare la distanza tra il VAN ottenuto e la soglia minima.

Scenario VAN ottenuto Soglia 70% Margine
Ottimistico €350.000 €245.000 €105.000
Normale €270.000 €189.000 €81.000
Pessimistico €180.000 €126.000 €54.000

Il margine più piccolo è quello dello scenario pessimistico: [math]180.000-126.000=54.000[/math], ma anche questo margine è significativo.

Una conseguenza matematica importante

Poiché il portafoglio raggiunge [math]Z_s=Z_s^*[/math] in ogni scenario, la soluzione rimane ammissibile non soltanto per [math]\alpha=0,7[/math], ma per qualunque [math]0 \leq \alpha \leq 1[/math]. Infatti, per [math]\alpha=0,9[/math], [math]Z_s \geq 0,9 Z_s^*[/math] è ancora vero. Per [math]\alpha=0,95[/math], ancora vero. Persino per [math]\alpha=1[/math] abbiamo [math]Z_s \geq Z_s^*[/math] e il portafoglio raggiunge esattamente il valore richiesto.

Quindi, con questi dati, il vincolo di robustezza non modifica la soluzione ottimale. Questo non è un errore del modello: è una caratteristica dell’esempio numerico.

Perché questo risultato è comunque didatticamente utile?

Perché mostra un concetto spesso trascurato: un vincolo di robustezza è interessante soprattutto quando esiste un conflitto tra performance media e performance negli scenari sfavorevoli.

Qui quel conflitto non emerge. S1+S2+S3 è contemporaneamente:

  • il miglior portafoglio nello scenario ottimistico;
  • il miglior portafoglio nello scenario normale;
  • il miglior portafoglio nello scenario pessimistico;
  • il miglior portafoglio in termini di VAN atteso;
  • una soluzione che soddisfa anche il requisito di robustezza.

In altre parole, ottimizzazione attesa e robustezza coincidono.

Per un caso reale, invece, potremmo avere una situazione completamente diversa. Ad esempio, un portafoglio potrebbe avere [math]E[Z]=300.000[/math] ma produrre soltanto [math]100.000[/math] nello scenario pessimistico. Un altro portafoglio potrebbe avere [math]E[Z]=280.000[/math] ma [math]180.000[/math] nello scenario pessimistico. Se il management impone una soglia minima di performance, il secondo portafoglio potrebbe diventare preferibile. Ed è proprio in situazioni di questo tipo che la robust optimization produce un vero trade-off decisionale.


Confronto tra le tre tipologie di problema

I tre esercizi rappresentano tre livelli differenti di decisione.

Forse potrebbe interessarti anche:  Guida Pratica a Random Forest in Python: 6 Esercizi con Scikit-Learn per Classificazione e Regressione
Problema Domanda aziendale Strumento
Portfolio Management Quali progetti finanziare? MILP + logica
Produzione Dove produrre? MILP + capacità + costi
Investimenti Come decidere sotto incertezza? MILP + scenari + robustezza

La progressione è importante. Nel primo problema l’incertezza è assente, ma la struttura logica è complessa. Nel secondo problema la difficoltà deriva dal rapporto tra costi fissi e costi variabili, oltre che dai vincoli di capacità. Nel terzo problema introduciamo invece una dimensione ulteriore: incertezza, e quindi non chiediamo più soltanto “Qual è la soluzione migliore?” ma “Qual è la soluzione migliore considerando che il futuro potrebbe essere diverso da quello previsto?”


Il vero valore della modellazione: dalla soluzione alla decisione

È importante non fermarsi all’output del solver.

Un risultato come P1 P2 P4 non è ancora una decisione manageriale completa.

La domanda successiva dovrebbe essere: Perché?

E poi: Quanto è stabile questa soluzione?

Infine: Cosa succede se cambiano le condizioni?

Queste domande portano dall’ottimizzazione statica alla Decision Intelligence.

Nel primo esercizio

Possiamo chiedere:

  • cosa succede se il budget passa da €200.000 a €180.000?
  • quanto deve aumentare il bonus P1-P2 affinché la combinazione diventi ancora più interessante?
  • cosa succede se il VAN di P4 diminuisce?
  • quando diventa conveniente rinunciare a P2?

Queste domande possono essere studiate attraverso una sensitivity analysis.

Nel secondo esercizio

Possiamo modificare:

  • domanda;
  • capacità;
  • costo energetico;
  • costi di attivazione;
  • costo variabile;
  • disponibilità degli impianti.

Una domanda particolarmente interessante è: A partire da quale livello di domanda l’attivazione di un secondo impianto diventa economicamente conveniente? In questo caso non stiamo più cercando una singola soluzione. Stiamo cercando una soglia decisionale.

Nel terzo esercizio

Possiamo invece analizzare:

  • probabilità degli scenari;
  • livello di robustezza [math]\alpha[/math];
  • VAN individuali;
  • costi dei sensori;
  • scenari meteorologici;
  • vincoli minimi di rendimento.

Qui possiamo arrivare a domande come: Quanto rendimento atteso siamo disposti a sacrificare per ottenere una maggiore protezione nello scenario pessimistico? Questa è una domanda manageriale molto più interessante del semplice “Qual è il VAN massimo?”


PuLP non è il solver

Un’altra distinzione importante riguarda il ruolo degli strumenti software.

Nel nostro esempio utilizziamo import pulp.

PuLP è un modeling framework Python. Serve a definire variabili, costruire funzioni obiettivo, aggiungere vincoli, costruire il modello matematico e collegarsi a un solver.

Nel codice utilizziamo pulp.PULP_CBC_CMD(msg=False).

CBC è invece il solver che esegue concretamente l’ottimizzazione.

Lo schema concettuale è:

Problema aziendale
        ↓
Modello matematico
        ↓
PuLP
        ↓
MILP
        ↓
CBC
        ↓
Soluzione ottimale
        ↓
Interpretazione manageriale

Questa distinzione diventa importante quando si lavora su problemi più grandi, perché PuLP può essere utilizzato con diversi solver compatibili.


Dal modello didattico al modello aziendale reale

Gli esempi utilizzati in questo articolo sono volutamente semplificati.

In un contesto aziendale reale potremmo introdurre molti altri elementi.

Project Portfolio Management

Potremmo aggiungere:

  • budget annuali e trimestrali;
  • disponibilità di personale;
  • competenze necessarie;
  • dipendenze temporali;
  • milestone;
  • rischio;
  • probabilità di successo;
  • benefici non monetari.

Produzione

Potremmo aggiungere:

  • costi di trasporto;
  • tempi di setup;
  • turni;
  • capacità di magazzino;
  • materie prime;
  • tempi di consegna;
  • emissioni;
  • manutenzione.

Investimenti sotto incertezza

Potremmo introdurre:

  • decine o centinaia di scenari;
  • probabilità differenti;
  • correlazioni tra variabili;
  • CVaR;
  • worst-case optimization;
  • robust optimization;
  • stochastic programming.

Il principio, però, rimane lo stesso: [math]\displaystyle \boxed{\text{Decisione} \rightarrow \text{Modello} \rightarrow \text{Ottimizzazione} \rightarrow \text{Interpretazione}}[/math]


Il punto più importante: l’algoritmo non sostituisce il decisore

È facile cadere nella tentazione di pensare che un modello di ottimizzazione produca automaticamente “la decisione migliore”. Non è così.

Produce la migliore soluzione rispetto al modello costruito. Se inseriamo un VAN errato, il solver non può sapere che il dato è sbagliato. Se stimiamo male una capacità, il solver utilizzerà quella capacità. Se dimentichiamo un vincolo strategico, il modello potrebbe proporre una soluzione che l’azienda non potrebbe realmente implementare.

Per questo il valore della Ricerca Operativa non sta soltanto nell’algoritmo.

Sta soprattutto nella capacità di formalizzare correttamente il problema decisionale.


Conclusione

I tre esempi mostrano tre modi diversi di utilizzare la matematica per supportare decisioni aziendali.

Nel primo caso abbiamo trasformato regole strategiche come “questi due progetti generano una sinergia” oppure “questo progetto può essere realizzato soltanto se ne viene realizzato un altro” in vincoli matematici.

Nel secondo abbiamo visto che il costo fisso più basso non implica necessariamente il costo totale più basso. La decisione ottimale nasce dall’interazione tra costi fissi + costi variabili + capacità.

Nel terzo abbiamo introdotto un concetto ancora più importante: l’incertezza. Massimizzare il VAN atteso non è sempre sufficiente. In molte decisioni strategiche bisogna chiedersi anche quanto la soluzione sia capace di resistere a scenari differenti. Nel nostro esempio, la robustezza non modifica la soluzione perché S1+S2+S3 è già ottimale in tutti gli scenari. Questo è un risultato interessante proprio perché mostra una condizione limite: non esiste un trade-off tra rendimento e robustezza quando una stessa soluzione domina tutti gli scenari considerati.

In problemi reali, invece, il vero valore dell’ottimizzazione emerge spesso quando questi obiettivi entrano in conflitto. Ed è qui che la Ricerca Operativa incontra la Data Science.

Il valore non consiste semplicemente nel produrre una risposta numerica. Consiste nel poter chiedere:

  • quale decisione è ottimale?
  • quali vincoli la determinano?
  • quanto è sensibile ai cambiamenti?
  • quanto è robusta?
  • quando cambia la soluzione?
  • quale compromesso siamo disposti ad accettare?

Questa è la transizione dalla semplice analisi dei dati alla Decision Intelligence.

Il solver trova la soluzione ottimale del modello. Il lavoro analitico consiste nel costruire il modello giusto e capire cosa significa quella soluzione nel mondo reale.

E questa è forse la lezione più importante di tutta la Ricerca Operativa.

📚 Per approfondire: Ottimizzazione matematica, MILP e Python

Se vuoi approfondire come trasformare problemi aziendali complessi in modelli di ottimizzazione, questi articoli mostrano applicazioni pratiche della programmazione lineare, della programmazione lineare intera e degli algoritmi di ottimizzazione in Python, dalla gestione del budget alla produzione e all’assegnazione delle risorse:

👉 Allocazione del budget con Python e MILP: modelli, codice e soluzioni pratiche

👉 Ottimizzazione della produzione e gestione scorte: programmazione lineare intera e Python

👉 Ottimizzazione lineare in Python: confronto pratico tra PuLP e SciPy Optimize

👉 Librerie Python per l’ottimizzazione: un viaggio completo

👉 Problema di assegnazione: come risolverlo con Python e PuLP per minimizzare i costi

 

Pubblicità