Risk Measures and Portfolio Optimization: Expanded#
Prepared by: Andrew Marquardt and Yohannes Mariam (amarquar@nd.edu,ymariam@nd.edu, 2024). Original source material by Madelynn Watson.
Primary source: Risk Measures and Portfolio Optimization
# Imports
import sys
if "google.colab" in sys.modules:
!wget "https://raw.githubusercontent.com/ndcbe/optimization/main/notebooks/helper.py"
import helper
helper.easy_install()
helper.install_idaes()
helper.install_ipopt()
else:
sys.path.insert(0, '../')
import helper
helper.set_plotting_style()
import pandas as pd
import numpy as np
import pyomo.environ as pyo
import matplotlib.pyplot as plt
--2024-10-31 21:43:08-- https://raw.githubusercontent.com/ndcbe/optimization/main/notebooks/helper.py
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.109.133, 185.199.111.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 6493 (6.3K) [text/plain]
Saving to: ‘helper.py.2’
helper.py.2 0%[ ] 0 --.-KB/s
helper.py.2 100%[===================>] 6.34K --.-KB/s in 0s
2024-10-31 21:43:08 (52.8 MB/s) - ‘helper.py.2’ saved [6493/6493]
idaes was found! No need to install.
idaes was found! No need to install.
Learning Objectives#
Understand how more complicated supply and demand scenarios that affect marginal profit change the way in which decisions are made during optimization
Learn how a variable marginal price functionally dependent on the optimum solution can be implemented into a pyomo model
Observe how stochastic uncertainty in the input feed affects the optimization decision flow
Apply stochastic modeling to a scenario with both inherent model uncertainty/noise and increased market regulation
The Base Problem#
Our work expands upon the notebook prepared by Madelyn Watson for Portfolio management. It includes the Mean Value (MV), Mean-Absolute Deviation, and Condition Value-at-Risk (CVaR) risk measurements, taken directly from Madelyn’s work. For a summary of those risk measurements, We have included the equations from her work, though for greater analysis, the source notebook is excellent. The base idea is that a Sugar Cane mill in Brazil must decide how to process its products into sugar cane, ethanol, and free market and regulated electricity. With different risk management metrics comes different profiles, though interesting results do follow with the inclusion of more complicated scenarios. Below is the mill flowchart prepared in the original notebook, describing the system and decision tree, of sorts.
Visualize Input Data#
The following data is copied from the original notebook and depicts the historical price data for each market as well as the mill constraints. More detailed information is presented in that notebook, but it is important to at least briefly take time to understand the data sets and constraints placed on the optimization we will utilize.
#Load Data From CSV files stored on Github
path_cap='https://raw.githubusercontent.com/MadelynnWatson/RiskMeasures/main/Capacity.csv'
path_opex = 'https://raw.githubusercontent.com/MadelynnWatson/RiskMeasures/main/OPEX.csv'
path_conv = 'https://raw.githubusercontent.com/MadelynnWatson/RiskMeasures/main/conversions.csv'
path_gen = 'https://raw.githubusercontent.com/MadelynnWatson/RiskMeasures/main/generation.csv'
path_hp = 'https://raw.githubusercontent.com/MadelynnWatson/RiskMeasures/main/historicalprices.csv'
df_maxcap = pd.read_csv(path_cap)
df_prodcost = pd.read_csv(path_opex)
df_conv = pd.read_csv(path_conv)
df_gen = pd.read_csv(path_gen)
df_hp = pd.read_csv(path_hp)
## Uncomment to Display Additional Data in Tables
display(df_maxcap)
display(df_prodcost)
# display(df_conv)
# display(df_gen)
process | capacity | units | |
---|---|---|---|
0 | mill | 3000000 | tonne sugarcane |
1 | fact | 1541400 | tonne juice |
2 | dist | 2202000 | tonne juice |
saleable_product | cost | units | |
---|---|---|---|
0 | sug | 321.92 | $/tonne |
1 | eth | 502.01 | $/m3 |
2 | fre | 1.98 | $/MWh |
3 | reg | 1.98 | $/MWh |
#Plot historical prices
x = np.arange(1,len(df_hp['eth'])+ 1)
fig, ax = plt.subplots(figsize=(6.4, 4))
plt.scatter(x,df_hp['eth'],1, label = 'Ethanol')
plt.xlabel('Week (2013 - 2022)', fontsize = 16, fontweight='bold')
plt.ylabel('USD/m$^3$', fontsize = 16, fontweight = 'bold')
plt.legend(fontsize = 14)
ax.xaxis.set_tick_params(labelsize=15)
ax.yaxis.set_tick_params(labelsize=15)
ax.tick_params(direction="in")
plt.show()
fig, ax = plt.subplots(figsize=(6.4, 4))
plt.scatter(x,df_hp['sug'],1, label = 'Sugar')
plt.xlabel('Week (2013 - 2022)', fontsize = 16, fontweight='bold')
plt.ylabel('USD/tonne', fontsize = 16, fontweight = 'bold')
plt.legend(fontsize = 14)
ax.xaxis.set_tick_params(labelsize=15)
ax.yaxis.set_tick_params(labelsize=15)
ax.tick_params(direction="in")
plt.show()
fig, ax = plt.subplots(figsize=(6.4, 4))
plt.scatter(x,df_hp['fre'],1, label = 'Electricity to Free Market')
plt.xlabel('Week (2013 - 2022)', fontsize = 16, fontweight='bold')
plt.ylabel('USD/MWh', fontsize = 16, fontweight = 'bold')
plt.legend(fontsize = 14)
ax.xaxis.set_tick_params(labelsize=15)
ax.yaxis.set_tick_params(labelsize=15)
ax.tick_params(direction="in")
plt.show()
Define Plotting Functions#
These plotting functions will be used multiple times down the notebook to display results. For the sake of brevity, they are defined here and then called later.
final_EP = {}
final_min ={}
final_diff ={}
def profit_dist(m,obj_name,final_EP,final_min,final_diff):
#Collect the profit distribution
profits = []
for i in m.price_time_obs:
profits.append(pyo.value(m.profit[i]))
#Collect the minimum profit
min_prof = min(profits)
#Plot the Profit Distribution
fig, ax = plt.subplots(figsize=(6.4, 4))
plt.hist(np.array(profits), alpha = 0.5)
plt.vlines(ymin = 0,ymax= 220, x= pyo.value(m.EP), label = 'Expected Profit', color = 'green')
plt.vlines(ymin = 0, ymax = 220, x=min_prof, label = ('Minimum Profit'), color = 'red')
plt.xlabel('profit$_q$ M USD', fontsize = 16, fontweight='bold')
plt.ylabel('Frequency', fontsize = 16, fontweight='bold')
plt.legend(fontsize = 14)
ax.xaxis.set_tick_params(labelsize=15)
ax.yaxis.set_tick_params(labelsize=15)
ax.tick_params(direction="in")
plt.show()
print('Results')
print('-------------------------------------')
print('Expected Profit:', np.round(pyo.value(m.EP)/1e6,2), 'M USD')
print('Minimum Profit:', np.round(min_prof/1e6,2), 'M USD')
difference = pyo.value(m.EP) - min_prof
print('Difference:', np.round(difference/1e6,2), 'M USD')
#Collect results for conclusion
final_EP[obj_name] = pyo.value(m.EP)/1e6
final_min[obj_name] = min_prof/1e6
final_diff[obj_name] = difference/1e6
return final_EP,final_min,final_diff
def revenue_breakdown(m):
fig, ax = plt.subplots(figsize=(6.4, 4))
plt.bar('Sugar',pyo.value(m.avg_sug_prof)/1e6)
plt.bar('Ethanol', pyo.value(m.avg_eth_prof)/1e6)
plt.bar('Electricity \n Free \n Market', pyo.value(m.avg_el_prof)/1e6)
plt.bar('Electricity \n Regulated \n Market', pyo.value(m.x['reg'])*72.5/1e6)
plt.xlabel('Product', fontsize = 16, fontweight = 'bold')
plt.ylabel('Revenue from \n Each Profit (USD)', fontsize = 16, fontweight='bold')
ax.xaxis.set_tick_params(labelsize=15)
ax.yaxis.set_tick_params(labelsize=15)
ax.tick_params(direction="in")
plt.show()
Define Base Model in Pyomo#
The following function for building the pyomo model contains 3 options: The original (‘det’), the stochastic model designed for this project (‘stoc’), and the supply and demand adjustment designed for this project (the final else).
#Create Base Model In Pyomo
def get_prodCost(i):
og_price = df_prodcost['cost'][i]
cost_uncertainty = np.random.uniform(0.85, 1.15)
return og_price*cost_uncertainty
def create_model(mode, scale_hyp=0.5,bin_count=200):
'''
This function buils a superstructure model in Pyomo for a sugarcane mill that can produce jet fuel.
Inputs:
product: desired product to maximize
pathway: Excel sheet containing Jet fuel
det: True if deterministic model, false if stocahstic model
Returns: Pyomo model m
'''
m=pyo.ConcreteModel()
#SETS
resources = ['sug', 'eth', 'fre', 'reg', 'cane', 'jui', 'mol', 'bag', 'el-r']
saleable_products = ['sug', 'eth', 'fre', 'reg']
processes = ['mill', 'fact', 'dist','cogen']
commodity = ['fre','sug','eth']
#PARAMETERS
#Scalars
Ca = 3000000 # Annual Sugarcane Capacity
beta = 0.9 # Confidence Level For CVaR Formulation
price_reg = 72.5 # Price of electricity sold to the regulated market
#Fill in Dictionaries with Excel Data
max_cap = {}
prodcost = {}
conv = {}
gen = {}
hp = {}
for i in range(len(df_maxcap['process'])):
max_cap[df_maxcap['process'][i]] = df_maxcap['capacity'][i]
for i in range(len(df_prodcost['saleable_product'])):
prodcost[df_prodcost['saleable_product'][i]] = get_prodCost(i)
for k in resources:
for i in range(len(df_conv[k])):
conv[(df_conv['resource'][i],df_conv['process'][i],k)] = df_conv[k][i]
for k in resources:
for i in range(len(df_gen[k])):
gen[(df_gen['process'][i],k)] = df_gen[k][i]
for k in commodity:
for i in range(len(df_hp[k])):
hp[(df_hp['q'][i],k)] = df_hp[k][i]
N = len(df_hp['q'])
q=[]
for i in range(1,N+1):
q.append('t%d' %(i))
if mode == "det":
#PYOMO SETS
m.resources = pyo.Set(initialize = resources)
m.saleable_products = pyo.Set(initialize = saleable_products)
m.processes = pyo.Set(initialize = processes)
m.price_time_obs = pyo.Set(initialize = q)
m.commodities = pyo.Set(initialize = commodity)
m.N = pyo.Param(initialize = N)
#VARIABLES
#Positive Variables
m.x = pyo.Var(m.resources, within = pyo.NonNegativeReals)
m.r = pyo.Var(m.resources,m.processes, within = pyo.NonNegativeReals) #For juice split point
m.profit = pyo.Var(m.price_time_obs)
m.EP = pyo.Var()
# Adding randomness in yield ranging from -20% to +20%
yield_factors = np.random.uniform(0.8, 1.2, 6)
jui_yield, bag_yield, sug_yield, mol_yield, el_yield, eth_yield = yield_factors
#CONSTRAINTS
#Superstructure Constraints
def mill1(m):
return m.x['jui']*jui_yield == Ca * conv['cane','mill','jui']
m.milleq1 = pyo.Constraint(rule = mill1)
def mill2(m):
return m.x['bag']*bag_yield == Ca * conv['cane','mill','bag']
m.milleq2 = pyo.Constraint(rule = mill2)
def juice1(m):
return m.x['jui']*jui_yield == m.r['jui','fact'] + m.r['jui','dist']
m.juiceeq1 = pyo.Constraint(rule = juice1)
def juice2(m):
return m.r['jui','fact'] <= max_cap['fact']
m.juiceeq2 = pyo.Constraint(rule = juice2)
def juice3(m):
return m.r['jui','dist'] <= max_cap['dist']
m.juiceeq3 = pyo.Constraint(rule = juice3)
def sugar1(m):
return m.x['sug']*sug_yield == conv['jui', 'fact', 'sug'] * m.r['jui','fact']
m.su1 = pyo.Constraint(rule = sugar1)
def sugar2(m):
return m.x['mol']*mol_yield == gen['fact','mol'] * m.x['sug']
m.su2 = pyo.Constraint(rule = sugar2)
def ethanol1(m):
return m.x['eth']*eth_yield == conv['jui','dist','eth'] * m.r['jui','dist'] + conv['mol','dist','eth']*m.x['mol']
m.et1 = pyo.Constraint(rule = ethanol1)
def el_prod(m):
return m.x['el-r']*el_yield == Ca * 0.053 #53 kWh produced per ton of sugarcane processes
m.el_produced = pyo.Constraint(rule = el_prod)
def el_sales(m):
return m.x['fre'] + m.x['reg'] == m.x['el-r']*el_yield
m.electricity = pyo.Constraint(rule = el_sales)
#Expected Profit
def profit(m,q):
return m.profit[q] == sug_yield*m.x['sug']*hp[q,'sug'] + eth_yield*m.x['eth']*hp[q,'eth'] + m.x['fre']*hp[q,'fre'] + m.x['reg']*price_reg - sum(m.x[j]*prodcost[j] for j in m.saleable_products)
m.profiteq = pyo.Constraint(m.price_time_obs, rule = profit)
def eprofit1(m):
return m.EP == (1/N)*sum(m.profit[q] for q in m.price_time_obs)
m.ep1 = pyo.Constraint(rule = eprofit1)
#Product Revenues to be used in analysis
def sug_prof(m):
return sum(m.x['sug']*hp[q,'sug'] for q in m.price_time_obs) * (1/N)
m.avg_sug_prof = pyo.Expression(rule = sug_prof)
def eth_prof(m):
return sum(m.x['eth']*hp[q,'eth'] for q in m.price_time_obs) * (1/N)
m.avg_eth_prof = pyo.Expression(rule = eth_prof)
def el_prof(m):
return sum(m.x['fre']*hp[q,'fre'] for q in m.price_time_obs) * (1/N)
m.avg_el_prof = pyo.Expression(rule = el_prof)
elif mode == "stoc":
reg_sugar_limit = 150000
reg_eth_limit = 120000
yield_range = (0.6, 1.4)
mean_yield = 1
m.SugarYieldSample = pyo.Var(within=pyo.Reals, bounds=yield_range, initialize = mean_yield)
# PYOMO SETS
m.resources = pyo.Set(initialize = resources)
m.saleable_products = pyo.Set(initialize = saleable_products)
m.processes = pyo.Set(initialize = processes)
m.price_time_obs = pyo.Set(initialize = q)
m.commodities = pyo.Set(initialize = commodity)
m.N = pyo.Param(initialize = N)
m.r = pyo.Var(m.resources,m.processes, within = pyo.NonNegativeReals)
# VARIABLES
# Positive Variables
# First-Stage Decision Variables (Scenario-independent)
m.x_bag = pyo.Var(within=pyo.UnitInterval) # Quantity of bagasse
m.x_jui = pyo.Var(within=pyo.UnitInterval)
m.x_juiF = pyo.Var(within=pyo.UnitInterval)
m.x_juiD = pyo.Var(within=pyo.UnitInterval)
m.profit = pyo.Var(m.price_time_obs)
m.EP1 = pyo.Var()
m.EP = pyo.Var()
initial_price_reg_sug = 800
initial_price_reg_eth = 770
price_reg_el = 72.5
m.price_reg_sug = pyo.Param(m.price_time_obs, initialize=initial_price_reg_sug, within=pyo.NonNegativeReals)
m.price_reg_eth = pyo.Param(m.price_time_obs, initialize=initial_price_reg_eth, within=pyo.NonNegativeReals)
m.costs = pyo.Var(within=pyo.NonNegativeReals)
m.q_sugar_reg = pyo.Var(within = pyo.NonNegativeReals) # Sugar to regulated market
m.q_sugar_free = pyo.Var(within=pyo.NonNegativeReals) # Sugar to free market
m.q_eth_reg = pyo.Var(within=pyo.NonNegativeReals) # Ethanol to regulated market
m.q_eth_free = pyo.Var(within=pyo.NonNegativeReals) # Ethanol to free market
m.q_el_reg = pyo.Var(within=pyo.NonNegativeReals) # Electricity to regulated market
m.q_el_free = pyo.Var(within=pyo.NonNegativeReals) # Electricity to free market
m.q_mol = pyo.Var(within=pyo.NonNegativeReals) # Molasses produced
#CONSTRAINTS
#Superstructure
def CoP(m):
return m.costs == (
(m.q_sugar_free + m.q_sugar_reg) * prodcost['sug'] +
(m.q_eth_free + m.q_eth_reg) * prodcost['eth'] +
(m.q_el_free + m.q_el_reg) * prodcost['fre']
)
m.c = pyo.Constraint(rule=CoP)
def mass_balance(m):
return m.x_bag + m.x_jui == 1
m.mass_balance = pyo.Constraint(rule=mass_balance)
def juice_production(m):
return m.r['jui', 'mill'] == conv['cane', 'mill', 'jui'] * Ca * m.x_jui * m.SugarYieldSample
m.juice_production = pyo.Constraint(rule=juice_production)
def bagasse_production(m):
return m.r['bag', 'mill'] == conv['cane', 'mill', 'bag'] * Ca * m.x_bag * m.SugarYieldSample
m.bagasse_production = pyo.Constraint(rule=bagasse_production)
def juice1(m):
return m.x_jui == m.x_juiF + m.x_juiD
m.juiceeq1 = pyo.Constraint(rule = juice1)
def juice_flow_fact(m):
return m.r['jui', 'fact'] == m.x_juiF * m.r['jui', 'mill']
m.juice_flow_fact = pyo.Constraint(rule=juice_flow_fact)
def capacity_fact(m):
return m.x_juiF * m.x_jui * Ca <= max_cap['fact']
m.capacity_fact = pyo.Constraint(rule=capacity_fact)
def juice_flow_dist(m):
return m.r['jui', 'dist'] == m.x_juiD * m.r['jui', 'mill']
m.juice_flow_dist = pyo.Constraint(rule=juice_flow_dist)
def juice25(m):
return m.r['jui','fact'] <= max_cap['fact']
m.juiceeq25 = pyo.Constraint(rule = juice25)
def capacity_dist(m):
return m.x_juiD * m.x_jui * Ca <= max_cap['dist']
m.capacity_dist = pyo.Constraint(rule=capacity_dist)
def juice35(m):
return m.r['jui','dist'] <= max_cap['dist']
m.juiceeq35 = pyo.Constraint(rule = juice35)
def sugar_production(m):
return m.q_sugar_reg + m.q_sugar_free == conv['jui', 'fact', 'sug'] * m.r['jui', 'fact']
m.sugar_production = pyo.Constraint(rule=sugar_production)
def sugar2(m):
return m.q_mol == gen['fact','mol'] * (m.q_sugar_reg + m.q_sugar_free)
m.su2 = pyo.Constraint(rule = sugar2)
def sugar3(m):
return m.q_sugar_reg <= 90000
m.su3 = pyo.Constraint(rule=sugar3)
def ethanol_production(m):
return m.q_eth_reg + m.q_eth_free == (conv['jui', 'dist', 'eth'] * m.r['jui', 'dist'] + conv['mol', 'dist', 'eth'] * m.q_mol)
m.ethanol_production = pyo.Constraint(rule=ethanol_production)
def ethanol2(m):
return m.q_eth_reg <= 77000
m.et2 = pyo.Constraint(rule = ethanol2)
def el_prod(m):
return m.q_el_reg + m.q_el_free == Ca * m.SugarYieldSample * 0.053 #53 kWh produced per ton of sugarcane processes
m.el_produced = pyo.Constraint(rule = el_prod)
def min_sugar_reg_sales(m):
return m.q_sugar_reg >= 20000
m.min_sugar_reg_sales = pyo.Constraint(rule=min_sugar_reg_sales)
def min_eth_reg_sales(m):
return m.q_eth_reg >= 20000
m.min_eth_reg_sales = pyo.Constraint(rule=min_eth_reg_sales)
def min_sugar_free_sales(m):
return m.q_sugar_free >= 1000
m.min_sugar_free_sales = pyo.Constraint(rule=min_sugar_free_sales)
def min_eth_free_sales(m):
return m.q_eth_free >= 1000
m.min_eth_free_sales = pyo.Constraint(rule=min_eth_free_sales)
M = 1e6
m.exceeds_sugar_limit = pyo.Var(within=pyo.Binary)
m.exceeds_eth_limit = pyo.Var(within=pyo.Binary)
def sugar_limit_rule(m):
return m.q_sugar_reg <= reg_sugar_limit + M * m.exceeds_sugar_limit
m.sugar_limit_constraint = pyo.Constraint(rule=sugar_limit_rule)
def sugar_over_limit_rule(m):
return m.q_sugar_reg >= reg_sugar_limit * (1 - m.exceeds_sugar_limit)
m.sugar_over_limit_constraint = pyo.Constraint(rule=sugar_over_limit_rule)
# Add constraints to enforce binary variable logic for ethanol
def eth_limit_rule(m):
return m.q_eth_reg <= reg_eth_limit + M * m.exceeds_eth_limit
m.eth_limit_constraint = pyo.Constraint(rule=eth_limit_rule)
def eth_over_limit_rule(m):
return m.q_eth_reg >= reg_eth_limit * (1 - m.exceeds_eth_limit)
m.eth_over_limit_constraint = pyo.Constraint(rule=eth_over_limit_rule)
#Expected Profit
def profit(m, q):
sugar_reg_price = m.price_reg_sug[q] * (1 - 0.5 * m.exceeds_sugar_limit)
ethanol_reg_price = m.price_reg_eth[q] * (1 - 0.5 * m.exceeds_eth_limit)
revenue = (
m.q_sugar_free * hp[q, 'sug'] +
m.q_sugar_reg * sugar_reg_price +
m.q_eth_free * hp[q, 'eth'] +
m.q_eth_reg * ethanol_reg_price +
m.q_el_free * hp[q, 'fre'] +
m.q_el_reg * price_reg_el
)
cost = m.costs
return m.profit[q] == revenue - cost
m.profiteq = pyo.Constraint(m.price_time_obs, rule=profit)
def eprofit1(m):
total_profit = sum(m.profit[q] for q in m.price_time_obs)
return m.EP == total_profit / (m.N)
m.ep2 = pyo.Constraint(rule=eprofit1)
#Product Revenues to be used in analysis
def sug_Fprof(m):
return sum(m.q_sugar_free*hp[q,'sug'] for q in m.price_time_obs) * (1/N)
m.avg_sug_Fprof = pyo.Expression(rule = sug_Fprof)
def eth_Fprof(m):
return sum(m.q_eth_free*hp[q,'eth'] for q in m.price_time_obs) * (1/N)
m.avg_eth_Fprof = pyo.Expression(rule = eth_Fprof)
def sug_Rprof(m):
return sum(m.q_sugar_reg* (m.price_reg_sug[q]* (1 - 0.5 * m.exceeds_sugar_limit)) for q in m.price_time_obs) * (1/N)
m.avg_sug_Rprof = pyo.Expression(rule = sug_Rprof)
def eth_Rprof(m):
return sum(m.q_eth_reg* (m.price_reg_eth[q]* (1 - 0.5 * m.exceeds_eth_limit)) for q in m.price_time_obs) * (1/N)
m.avg_eth_Rprof = pyo.Expression(rule = eth_Rprof)
def el_prof(m):
return sum(m.q_el_free*hp[q,'fre'] for q in m.price_time_obs) * (1/N)
m.avg_el_prof = pyo.Expression(rule = el_prof)
else:
#PYOMO SETS
bin_width = [1541400/bin_count for i in range(bin_count)]
bins = [0 for i in range(bin_count)]
bin_indices = [i for i in range(bin_count)]
m.resources = pyo.Set(initialize = resources)
m.saleable_products = pyo.Set(initialize = saleable_products)
m.processes = pyo.Set(initialize = processes)
m.price_time_obs = pyo.Set(initialize = q)
m.commodities = pyo.Set(initialize = commodity)
m.N = pyo.Param(initialize = N)
#VARIABLES
#Positive Variables
m.x = pyo.Var(m.resources, within = pyo.NonNegativeReals)
m.r = pyo.Var(m.resources,m.processes, within = pyo.NonNegativeReals) #For juice split point
m.profit = pyo.Var(m.price_time_obs)
m.EP = pyo.Var()
m.S_bins = pyo.Var(bin_indices,within=pyo.NonNegativeReals)
# Adding randomness in yield ranging from -20% to +20%
yield_factors = np.random.uniform(0.8, 1.2, 6)
jui_yield, bag_yield, sug_yield, mol_yield, el_yield, eth_yield = yield_factors
#CONSTRAINTS
#Superstructure Constraints
def mill1(m):
return m.x['jui']*jui_yield == Ca * conv['cane','mill','jui']
m.milleq1 = pyo.Constraint(rule = mill1)
def mill2(m):
return m.x['bag']*bag_yield == Ca * conv['cane','mill','bag']
m.milleq2 = pyo.Constraint(rule = mill2)
def juice1(m):
return m.x['jui']*jui_yield == m.r['jui','fact'] + m.r['jui','dist']
m.juiceeq1 = pyo.Constraint(rule = juice1)
def juice2(m):
return m.r['jui','fact'] <= max_cap['fact']
m.juiceeq2 = pyo.Constraint(rule = juice2)
def juice3(m):
return m.r['jui','dist'] <= max_cap['dist']
m.juiceeq3 = pyo.Constraint(rule = juice3)
def sugar1(m):
return m.x['sug']*sug_yield == conv['jui', 'fact', 'sug'] * m.r['jui','fact']
m.su1 = pyo.Constraint(rule = sugar1)
def sugar2(m):
return m.x['mol']*mol_yield == gen['fact','mol'] * m.x['sug']
m.su2 = pyo.Constraint(rule = sugar2)
def ethanol1(m):
return m.x['eth']*eth_yield == conv['jui','dist','eth'] * m.r['jui','dist'] + conv['mol','dist','eth']*m.x['mol']
m.et1 = pyo.Constraint(rule = ethanol1)
def el_prod(m):
return m.x['el-r']*el_yield == Ca * 0.053 #53 kWh produced per ton of sugarcane processes
m.el_produced = pyo.Constraint(rule = el_prod)
def el_sales(m):
return m.x['fre'] + m.x['reg'] == m.x['el-r']*el_yield
m.electricity = pyo.Constraint(rule = el_sales)
# sugar cane reformulation
def sugar_sum(m):
return m.x['sug'] == sum(m.S_bins[i] for i in range(bin_count))
m.sugar_tot_sum = pyo.Constraint(rule=sugar_sum)
for i in range(bin_count):
m.add_component(f'bin_{i+1}_max',pyo.Constraint(expr=m.S_bins[i] <= bin_width[i]))
# Cap on regulated electricity
def el_reg_cap(m):
return m.x['reg'] <= 100000
m.reg_cap = pyo.Constraint(rule=el_reg_cap)
#Expected Profit
def profit(m,q):
return m.profit[q] == sum(m.S_bins[z]*(hp[q,'sug']-(hp[q,'sug']-scale_hyp*321.92)*(bin_count-z)/bin_count) for z in range(bin_count)) + eth_yield*m.x['eth']*hp[q,'eth'] + m.x['fre']*hp[q,'fre'] + m.x['reg']*price_reg - sum(m.x[j]*prodcost[j] for j in m.saleable_products)
m.profiteq = pyo.Constraint(m.price_time_obs, rule = profit)
def eprofit1(m):
return m.EP == (1/N)*sum(m.profit[q] for q in m.price_time_obs)
m.ep1 = pyo.Constraint(rule = eprofit1)
#Product Revenues to be used in analysis
def sug_prof(m):
return sum(sum(m.S_bins[z]*(hp[q,'sug']-(hp[q,'sug']-2/3*321.92)*(bin_count-z)/bin_count) for z in range(bin_count)) for q in m.price_time_obs) * (1/N)
m.avg_sug_prof = pyo.Expression(rule = sug_prof)
def eth_prof(m):
return sum(m.x['eth']*hp[q,'eth'] for q in m.price_time_obs) * (1/N)
m.avg_eth_prof = pyo.Expression(rule = eth_prof)
def el_prof(m):
return sum(m.x['fre']*hp[q,'fre'] for q in m.price_time_obs) * (1/N)
m.avg_el_prof = pyo.Expression(rule = el_prof)
return m
Risk measurement equations#
MV (Mean value)
MAD (Mean Absolute Deviation)
CVaR (Conditional Value-at-Risk)
Implementing a new scenario: model supply and demand and government caps#
Say the historical prices are only averages and the true reality is that the market supply and demand curves are more active. Specifically, this imiplies that the market prices provided are anchor prices, but the true profit a certain mill recieves is functionally dependent on the amount produced.
Generally, supply and demand curves can take any one of a variety of functional forms relating production/quantity and price, but for the sake of not repeating stale analysis, we primarily considered a linearly decreasing market price. The actual total profit requires that the production curve be integrated from 0 to the amount produced, but this amount is unknown and dependent on the optimization, so pyomo cannot handle an implicit integration system- normally, this would require a lambda function-type code or a significant nondimensionalization reformatting. Alternatively, the sale prices could be binned, with a more precise integral resulting from more bins (with the accompanying increase in computational cost from a larger number of variables). This approach was utilized. The bins used a left-side rule, so the bin approximation of a true integral is larger than a middle-point rule or the true integral itself, but this is more reasonable from a real-life scenario, since the maximum price in any real-life binning scenario would be applied to the top bin instead of some estimated median value. The equation used is listed in the profit calculations section below.
There are 2 ways to initiate the model: either use the historical data as a middle point somewhere along the curve, or use the historical data as the demand cost for 1 unit. We believe it is impossible to provide any practical justification for the former strategy, since it is unclear where along the curve that anchor should be placed without knowing how much will be produced. We could use the previous optimization solution, but it is still unclear how aggressive to be in this design. Since we are already designing the functions ourselves, we decided to focus any hypothetical on this.
We ensured that the market price is still positive for all values that could be produced using the mill capacity, but that if the mill produced too much sugar, it would start to lose money at some point. The aggressiveness of this decay is a hyperparameter that effectively determines the floor price of sugar (which is named in our parameter set), though the effect on production of a more aggressive regime is easily deduced from the a baseline decay regime.
We also applied a maximum cap on the amount of electricity that could possibly go to the regulated market, assuming that some government regulation would limit the amount that could be sold there. This is a farily simple change, but it does increase the considerations that the system must work with.
System Description/Model#
Sets
Parameters
Variables
Constraints
Mass Balances
Additional Electricity Constraint
Profit Calculations
DOF Analysis#
Number of Variables
Number of Constraints
DOF
#Reload the base Model
m = create_model(mode = "s&d")
#Drop the scenario number column
price = df_hp.drop(columns = ['q'])
#Calculate Returns
Returns = price.diff()/price.shift(1)
covar = Returns.cov()
print('Covariance Matrix')
print('-------------------')
print(covar)
#Define MV as the objective
def mean_value(m):
return sum(m.x[i]*covar.loc[i,j]*m.x[j] for i in m.commodities for j in m.commodities)
m.obj = pyo.Objective(rule = mean_value, sense = pyo.minimize)
#Solve the Model
sol =pyo.SolverFactory('ipopt')
sol.solve(m)
print('Results')
print('--------------------------------------')
print('Expected Profit', np.round(pyo.value(m.EP),2), 'USD')
print('Ethanol Produced', np.round(pyo.value(m.x['eth']),2), 'm3')
print('Sugar Produced', np.round(pyo.value(m.x['sug']),2), 'tonne')
print('Electricity Produced', np.round(pyo.value(m.x['el-r']),2), 'MWh')
print('Electricity to Free Market', np.round(pyo.value(m.x['fre']),2), 'MWh')
print('Electricity to Regulated Market', np.round(pyo.value(m.x['reg']),2), 'MWh')
Covariance Matrix
-------------------
fre sug eth
fre 0.094993 -0.000350 -0.000756
sug -0.000350 0.000922 0.000390
eth -0.000756 0.000390 0.001578
Results
--------------------------------------
Expected Profit 38373444.1 USD
Ethanol Produced 151564.09 m3
Sugar Produced 151445.92 tonne
Electricity Produced 160620.53 MWh
Electricity to Free Market 59000.0 MWh
Electricity to Regulated Market 100000.0 MWh
final_EP,final_min,final_diff = profit_dist(m,'MV',final_EP,final_min,final_diff)
Results
-------------------------------------
Expected Profit: 38.37 M USD
Minimum Profit: -32.64 M USD
Difference: 71.01 M USD
revenue_breakdown(m)
#Reload the base Model
m = create_model(mode = "s&d")
#Create set of return indicies
J = np.arange(1,m.N)
#Create new variables
m.y_aux = pyo.Var(J, within = pyo.NonNegativeReals)
m.z_aux = pyo.Var(J, within = pyo.NonNegativeReals)
#Constrain y - z to be the abs portion
def aux_con(m,j):
return m.y_aux[j] - m.z_aux[j] == sum(m.x[i]*(Returns.loc[j,i]-((1/m.N)*sum(Returns.loc[j,i] for j in J))) for i in m.commodities)
m.aux = pyo.Constraint(J, rule = aux_con)
#Define MAD as the objective
def mad(m):
return 1/m.N * sum(m.y_aux[j] + m.z_aux[j] for j in J)
m.obj = pyo.Objective(rule = mad, sense = pyo.minimize)
#Solve the Model
sol =pyo.SolverFactory('ipopt')
results = sol.solve(m)
print('Results')
print('------------------------------')
print('Expected Profit', np.round(pyo.value(m.EP),2), 'USD')
print('Ethanol Produced', np.round(pyo.value(m.x['eth']),2), 'm3')
print('Sugar Produced', np.round(pyo.value(m.x['sug']),2), 'tonne')
print('Electricity Produced', np.round(pyo.value(m.x['el-r']),2), 'MWh')
print('Electricity to Free Market', np.round(pyo.value(m.x['fre']),2), 'MWh')
print('Electricity to Regulated Market', np.round(pyo.value(m.x['reg']),2), 'MWh')
Results
------------------------------
Expected Profit 11452693.54 USD
Ethanol Produced 196693.51 m3
Sugar Produced 182111.45 tonne
Electricity Produced 135856.58 MWh
Electricity to Free Market 59000.0 MWh
Electricity to Regulated Market 100000.0 MWh
final_EP,final_min,final_diff = profit_dist(m,'MAD',final_EP,final_min,final_diff)
Results
-------------------------------------
Expected Profit: 11.45 M USD
Minimum Profit: -64.31 M USD
Difference: 75.77 M USD
revenue_breakdown(m)
#Reload the base Model
m = create_model(mode = "s&d")
#Add parameters
beta = 0.9 #confidence interval
#Add variables
m.shortfall = pyo.Var(m.price_time_obs)
m.alpha = pyo.Var()
m.CVaR = pyo.Var()
#Add CVaR Constraints
def CVaR1(m):
return m.CVaR == m.alpha - ((1/(m.N*(1-beta)))*sum(m.shortfall[q] for q in m.price_time_obs))
m.cvar1eq = pyo.Constraint(rule = CVaR1)
def CVaR2(m,q):
return m.shortfall[q] >= 0
m.cvar2eq = pyo.Constraint(m.price_time_obs, rule = CVaR2)
def CVaR4(m,q):
return m.profit[q] + m.shortfall[q] - m.alpha >= 0
m.cvar4eq = pyo.Constraint(m.price_time_obs, rule = CVaR4)
#OBJECTIVE
def obj_rule(m):
return m.CVaR
m.obj = pyo.Objective(rule=obj_rule, sense=pyo.maximize)
#Solve the Model
sol =pyo.SolverFactory('ipopt')
results = sol.solve(m)
print('Results')
print('------------------------------')
print('Expected Profit', np.round(pyo.value(m.EP),2), 'USD')
print('Ethanol Produced', np.round(pyo.value(m.x['eth']),2), 'm3')
print('Sugar Produced', np.round(pyo.value(m.x['sug']),2), 'tonne')
print('Electricity Produced', np.round(pyo.value(m.x['el-r']),2), 'MWh')
print('Electricity to Free Market', np.round(pyo.value(m.x['fre']),2), 'MWh')
print('Electricity to Regulated Market', np.round(pyo.value(m.x['reg']),2), 'MWh')
WARNING:pyomo.core:Loading a SolverResults object with a warning status into model.name="unknown";
- termination condition: infeasible
- message from solver: Ipopt 3.13.2\x3a Converged to a locally infeasible point. Problem may be infeasible.
Results
------------------------------
Expected Profit -1187931.9 USD
Ethanol Produced 1136.87 m3
Sugar Produced 114923.73 tonne
Electricity Produced 152404.67 MWh
Electricity to Free Market 10622.36 MWh
Electricity to Regulated Market 148377.64 MWh
final_EP,final_min,final_diff = profit_dist(m,'MCVaR',final_EP,final_min,final_diff)
Results
-------------------------------------
Expected Profit: -1.19 M USD
Minimum Profit: -4.69 M USD
Difference: 3.5 M USD
revenue_breakdown(m)
Summarizing what happened#
It isn’t always easy to see what happened differently, but compared to the results returned by the original notebook (the source material linked at the top of this notebook), there are a few things to note. First, the amount of profit from Sugar sold to market decreased significantly using the MAD objective, but not the MV objective. This suggests the difference in priority carries over between targets, but sugar profit still becomes capped and affects the overall portfolio in certain circumstances. Second, as expected the electricity to the free market increases to compensate for the regulated market cap. Nothing particularly special there, but it is a good sanity check that the algorithm is functioning as intended. Finally, it should be noted that profit is down overall for all metrics- as expected. For a better visualization of how these results change, one only needs to change the model specification for a given objective and re-run the code.
Implementing a new scenario: Stochastic Modeling#
The true market landscape is more complex than what is depicted in the original model, influenced by both production decisions and regulatory constraints. This is particularly evident in the commitment to the ratio of juice to bagasse, a key feature that was absent in the previous model. In the old model, we assumed flexibility in how the production process allocated sugarcane between juice and bagasse, but in reality, this ratio is often fixed. This commitment limits operational choices and requires a more careful balance in production planning, as optimizing one part of the process (such as juice) directly affects the availability of resources for others (like bagasse).
Additionally, the new model introduces more markets—both free and regulated—which operate under distinct pricing schemes. Free markets fluctuate based on supply and demand, while regulated markets are constrained by set prices and government-imposed caps on how much can be sold. By imposing constraints on both free and regulated markets, we can better capture the practical challenges that businesses face. For instance, surpassing the regulatory limit on sugar or ethanol sales can lead to reduced prices, which the model now reflects through binary variables. These variables allow for price adjustments when production exceeds regulatory thresholds, making the model more responsive to real-world market conditions.
Sets
Parameters
Variables
Constraints
Cost of Production
Mass Balances
Minimum and Maximum Sales Constraints
Profit Calculations
Number of Variables
Number of Constraints
DOF
#Load the base model from the function created above
m = create_model(mode = "stoc")
#Set the objective to maximize the expected value of the profit
def obj_rule(m):
return m.EP
m.obj = pyo.Objective(rule=obj_rule, sense=pyo.maximize)
#Solve the Model
sol=pyo.SolverFactory('bonmin', tee=True)
sol.solve(m)
print('Results')
print('----------------------------------------------------')
print('Expected Profit', np.round(pyo.value(m.obj),2), 'USD')
print(f" Sugar to Regulated Market: {pyo.value(m.q_sugar_reg)}")
print(f" Sugar to Free Market: {pyo.value(m.q_sugar_free)}")
print(f" Ethanol to Regulated Market: {pyo.value(m.q_eth_reg)}")
print(f" Ethanol to Free Market: {pyo.value(m.q_eth_free)}")
print(f" Electricity to Free Market: {pyo.value(m.q_el_free)}")
print(f" Electricity to Regulated Market: {pyo.value(m.q_el_reg)}")
Results
----------------------------------------------------
Expected Profit 88177260.07 USD
Sugar to Regulated Market: 19999.9998
Sugar to Free Market: 169129.78209129782
Ethanol to Regulated Market: 19999.9998
Ethanol to Free Market: 243048.3211674899
Electricity to Free Market: 222600.00222601
Electricity to Regulated Market: 0.0
#Collect the profit distribution
profits = []
for i in m.price_time_obs:
profits.append(pyo.value(m.profit[i]))
#Collect the minimum profit
min_prof = min(profits)
#Plot the Profit Distribution
fig, ax = plt.subplots(figsize=(6.4, 4))
plt.hist(np.array(profits), alpha = 0.5)
plt.vlines(ymin = 0,ymax= 160, x= pyo.value(m.obj), label = 'Expected Profit', color = 'green', linewidth =3)
plt.vlines(ymin = 0, ymax = 160, x=min_prof, label = ('Minimum Profit'), color = 'red', linewidth = 3)
plt.xlabel('profit$_q$ M USD', fontsize = 16, fontweight='bold')
plt.ylabel('Frequency', fontsize = 16, fontweight='bold')
plt.legend(fontsize = 14)
ax.xaxis.set_tick_params(labelsize=15)
ax.yaxis.set_tick_params(labelsize=15)
ax.tick_params(direction="in")
plt.show()
print('Results')
print('-------------------------------------')
print('Expected Profit:', np.round(pyo.value(m.obj)/1e6,2), 'M USD')
print('Minimum Profit:', np.round(min_prof/1e6,2), 'M USD')
difference = pyo.value(m.obj) - min_prof
print('Difference:', np.round(difference/1e6,2), 'M USD')
#Collect Results for Conclusion
final_EP = {}
final_min = {}
final_diff = {}
final_EP['No Risk'] = pyo.value(m.obj)/1e6
final_min['No Risk'] = min_prof/1e6
final_diff['No Risk'] = difference/1e6
Results
-------------------------------------
Expected Profit: 88.18 M USD
Minimum Profit: -41.95 M USD
Difference: 130.13 M USD
fig, ax = plt.subplots(figsize=(6.4, 4))
plt.bar('Sugar \n Free \n Market',pyo.value(m.avg_sug_Fprof)/1e6)
plt.bar('Sugar \n Regulated \n Market',pyo.value(m.avg_sug_Rprof)/1e6)
plt.bar('Ethanol \n Free \n Market', pyo.value(m.avg_eth_Fprof)/1e6)
plt.bar('Ethanol \n Regulated \n Market', pyo.value(m.avg_eth_Rprof)/1e6)
plt.bar('Electricity \n Free \n Market', pyo.value(m.avg_el_prof)/1e6)
plt.bar('Electricity \n Regulated \n Market', pyo.value(m.q_el_reg)*72.5/1e6)
plt.ylabel('Product', fontsize = 16, fontweight='bold')
plt.ylabel('Revenue from \n Each Profit (USD)', fontsize = 16, fontweight='bold')
ax.xaxis.set_tick_params(labelsize=10)
ax.yaxis.set_tick_params(labelsize=10)
ax.tick_params(direction="in")
plt.show()
#Calculate the Covariance Matrix
#Drop the scenario number column
price = df_hp.drop(columns = ['q'])
#Calculate Returns
Returns = price.diff()/price.shift(1)
covar = Returns.cov()
print('Covariance Matrix')
print('-------------------')
print(covar)
Covariance Matrix
-------------------
fre sug eth
fre 0.094993 -0.000350 -0.000756
sug -0.000350 0.000922 0.000390
eth -0.000756 0.000390 0.001578
#Reload the base Model
m = create_model(mode = "stoc")
#Define MV as the objective
def mean_value(m):
# Assume covar is the covariance matrix dataframe
return (
2 * (m.q_sugar_free * covar.loc['sug', 'eth'] * m.q_eth_free) +
2 * (m.q_sugar_free * covar.loc['sug', 'fre'] * m.q_el_free) +
2 *(m.q_eth_free * covar.loc['eth', 'fre'] * m.q_el_free)
)
m.obj = pyo.Objective(rule = mean_value, sense = pyo.minimize)
#Solve the Model
sol =pyo.SolverFactory('bonmin', tee = True)
sol.solve(m)
print('Results')
print('--------------------------------------')
print('Expected Profit', np.round(pyo.value(m.EP),2), 'USD')
print(f"Sugar to Regulated Market: {pyo.value(m.q_sugar_reg)}")
print(f"Sugar to Free Market: {pyo.value(m.q_sugar_free)}")
print(f"Ethanol to Regulated Market: {pyo.value(m.q_eth_reg)}")
print(f"Ethanol to Free Market: {pyo.value(m.q_eth_free)}")
print(f"Electricity to Free Market: {pyo.value(m.q_el_free)}")
print(f"Electricity to Regulated Market: {pyo.value(m.q_el_reg)}")
Results
--------------------------------------
Expected Profit 55668301.76 USD
Sugar to Regulated Market: 90000.0009
Sugar to Free Market: 18074.16018074838
Ethanol to Regulated Market: 19999.9998
Ethanol to Free Market: 292292.4480869271
Electricity to Free Market: 222600.00222601
Electricity to Regulated Market: 0.0
#Reload the base Model
m = create_model(mode = "stoc")
#Define MV as the objective
def mean_value(m):
# Assume covar is the covariance matrix dataframe
return (
2 * (m.q_sugar_free * covar.loc['sug', 'eth'] * m.q_eth_free) +
2 * (m.q_sugar_free * covar.loc['sug', 'fre'] * m.q_el_free) +
2 *(m.q_eth_free * covar.loc['eth', 'fre'] * m.q_el_free)
)
m.obj = pyo.Objective(rule = mean_value, sense = pyo.minimize)
#Solve the Model
sol =pyo.SolverFactory('bonmin', tee = True)
sol.solve(m)
print('Results')
print('--------------------------------------')
print('Expected Profit', np.round(pyo.value(m.EP),2), 'USD')
print(f"Sugar to Regulated Market: {pyo.value(m.q_sugar_reg)}")
print(f"Sugar to Free Market: {pyo.value(m.q_sugar_free)}")
print(f"Ethanol to Regulated Market: {pyo.value(m.q_eth_reg)}")
print(f"Ethanol to Free Market: {pyo.value(m.q_eth_free)}")
print(f"Electricity to Free Market: {pyo.value(m.q_el_free)}")
print(f"Electricity to Regulated Market: {pyo.value(m.q_el_reg)}")
Results
--------------------------------------
Expected Profit 39008019.39 USD
Sugar to Regulated Market: 90000.0009
Sugar to Free Market: 18074.16018074838
Ethanol to Regulated Market: 19999.9998
Ethanol to Free Market: 292292.4480869271
Electricity to Free Market: 222600.00222601
Electricity to Regulated Market: 0.0
#Collect the profit distribution
profits = []
for i in m.price_time_obs:
profits.append(pyo.value(m.profit[i]))
#Collect the minimum profit
min_prof = min(profits)
#Plot the Profit Distribution
fig, ax = plt.subplots(figsize=(6.4, 4))
plt.hist(np.array(profits), alpha = 0.5)
plt.vlines(ymin = 0,ymax= 160, x= pyo.value(m.EP), label = 'Expected Profit', color = 'green', linewidth =3)
plt.vlines(ymin = 0, ymax = 160, x=min_prof, label = ('Minimum Profit'), color = 'red', linewidth = 3)
plt.xlabel('profit$_q$ M USD', fontsize = 16, fontweight='bold')
plt.ylabel('Frequency', fontsize = 16, fontweight='bold')
plt.legend(fontsize = 14)
ax.xaxis.set_tick_params(labelsize=15)
ax.yaxis.set_tick_params(labelsize=15)
ax.tick_params(direction="in")
plt.show()
print('Results')
print('-------------------------------------')
print('Expected Profit:', np.round(pyo.value(m.EP)/1e6,2), 'M USD')
print('Minimum Profit:', np.round(min_prof/1e6,2), 'M USD')
difference = pyo.value(m.EP) - min_prof
print('Difference:', np.round(difference/1e6,2), 'M USD')
final_EP['MV'] = pyo.value(m.obj)/1e6
final_min['MV'] = min_prof/1e6
final_diff['MV'] = difference/1e6
Results
-------------------------------------
Expected Profit: 39.01 M USD
Minimum Profit: -69.71 M USD
Difference: 108.72 M USD
fig, ax = plt.subplots(figsize=(6.4, 4))
plt.bar('Sugar \n Free \n Market',pyo.value(m.avg_sug_Fprof)/1e6)
plt.bar('Sugar \n Regulated \n Market',pyo.value(m.avg_sug_Rprof)/1e6)
plt.bar('Ethanol \n Free \n Market', pyo.value(m.avg_eth_Fprof)/1e6)
plt.bar('Ethanol \n Regulated \n Market', pyo.value(m.avg_eth_Rprof)/1e6)
plt.bar('Electricity \n Free \n Market', pyo.value(m.avg_el_prof)/1e6)
plt.bar('Electricity \n Regulated \n Market', pyo.value(m.q_el_reg)*72.5/1e6)
plt.ylabel('Product', fontsize = 16, fontweight='bold')
plt.ylabel('Revenue from \n Each Profit (USD)', fontsize = 16, fontweight='bold')
ax.xaxis.set_tick_params(labelsize=10)
ax.yaxis.set_tick_params(labelsize=10)
ax.tick_params(direction="in")
plt.show()
#Reload the base Model
m = create_model(mode = "stoc")
#Create set of return indicies
J = np.arange(1,m.N)
#Create new variables
m.y_aux = pyo.Var(J, within = pyo.NonNegativeReals)
m.z_aux = pyo.Var(J, within = pyo.NonNegativeReals)
#Constrain y - z to be the abs portion
def aux_con(m, j):
return m.y_aux[j] - m.z_aux[j] == (
m.q_sugar_free * (Returns.loc[j, 'sug'] - (1/m.N) * Returns['sug'].mean()) +
m.q_eth_free * (Returns.loc[j, 'eth'] - (1/m.N) * Returns['eth'].mean()) +
m.q_el_free * (Returns.loc[j, 'fre'] - (1/m.N) * Returns['fre'].mean())
)
m.aux_constraint = pyo.Constraint(J, rule=aux_con)
#Define MAD as the objective
def mad(m):
return (1/m.N) * sum(m.y_aux[j] + m.z_aux[j] for j in J)
m.obj = pyo.Objective(rule=mad, sense=pyo.minimize)
#Solve the Model
sol =pyo.SolverFactory('bonmin', tee = True)
results = sol.solve(m)
print('Results')
print('------------------------------')
print('Expected Profit', np.round(pyo.value(m.EP),2), 'USD')
print(f"Sugar to Regulated Market: {pyo.value(m.q_sugar_reg)}")
print(f"Sugar to Free Market: {pyo.value(m.q_sugar_free)}")
print(f"Ethanol to Regulated Market: {pyo.value(m.q_eth_reg)}")
print(f"Ethanol to Free Market: {pyo.value(m.q_eth_free)}")
print(f"Electricity to Free Market: {pyo.value(m.q_el_free)}")
print(f"Electricity to Regulated Market: {pyo.value(m.q_el_reg)}")
Results
------------------------------
Expected Profit 8170171.51 USD
Sugar to Regulated Market: 20328.041043691588
Sugar to Free Market: 999.9999900006177
Ethanol to Regulated Market: 20227.029833779416
Ethanol to Free Market: 999.9999900004543
Electricity to Free Market: 7.378656985065415
Electricity to Regulated Market: 106808.29162836997
#Collect the profit distribution
profits = []
for i in m.price_time_obs:
profits.append(pyo.value(m.profit[i]))
#Collect the minimum profit
min_prof = min(profits)
#Plot the Profit Distribution
fig, ax = plt.subplots(figsize=(6.4, 4))
plt.hist(np.array(profits), alpha = 0.5)
plt.vlines(ymin = 0,ymax= 220, x= pyo.value(m.EP), label = 'Expected Profit', color = 'green')
plt.vlines(ymin = 0, ymax = 220, x=min_prof, label = ('Minimum Profit'), color = 'red')
plt.xlabel('profit$_q$ M USD', fontsize = 16, fontweight='bold')
plt.ylabel('Frequency', fontsize = 16, fontweight='bold')
plt.legend(fontsize = 14)
ax.xaxis.set_tick_params(labelsize=15)
ax.yaxis.set_tick_params(labelsize=15)
ax.tick_params(direction="in")
plt.show()
print('Results')
print('-------------------------------------')
print('Expected Profit:', np.round(pyo.value(m.EP)/1e6,2), 'M USD')
print('Minimum Profit:', np.round(min_prof/1e6,2), 'M USD')
difference = pyo.value(m.EP) - min_prof
print('Difference:', np.round(difference/1e6,2), 'M USD')
#Collect results for conclusion
final_EP['MAD'] = pyo.value(m.EP)/1e6
final_min['MAD'] = min_prof/1e6
final_diff['MAD'] = difference/1e6
Results
-------------------------------------
Expected Profit: 8.17 M USD
Minimum Profit: 7.62 M USD
Difference: 0.55 M USD
fig, ax = plt.subplots(figsize=(6.4, 4))
plt.bar('Sugar \n Free \n Market',pyo.value(m.avg_sug_Fprof)/1e6)
plt.bar('Sugar \n Regulated \n Market',pyo.value(m.avg_sug_Rprof)/1e6)
plt.bar('Ethanol \n Free \n Market', pyo.value(m.avg_eth_Fprof)/1e6)
plt.bar('Ethanol \n Regulated \n Market', pyo.value(m.avg_eth_Rprof)/1e6)
plt.bar('Electricity \n Free \n Market', pyo.value(m.avg_el_prof)/1e6)
plt.bar('Electricity \n Regulated \n Market', pyo.value(m.q_el_reg)*72.5/1e6)
plt.ylabel('Product', fontsize = 16, fontweight='bold')
plt.ylabel('Revenue from \n Each Profit (USD)', fontsize = 16, fontweight='bold')
ax.xaxis.set_tick_params(labelsize=10)
ax.yaxis.set_tick_params(labelsize=10)
ax.tick_params(direction="in")
plt.show()
# Reload the base Model
m = create_model(mode = "stoc")
# Add parameters
beta = 0.9 # confidence interval
# Add variables for CVaR
m.shortfall = pyo.Var(m.price_time_obs, within=pyo.NonNegativeReals, initialize=0)
m.alpha = pyo.Var(within=pyo.Reals, initialize=0)
m.CVaR = pyo.Var(within=pyo.Reals, initialize=0)
# Add CVaR Constraints
def CVaR1(m):
return m.CVaR == m.alpha - (1 / (m.N * (1 - beta))) * sum(m.shortfall[q] for q in m.price_time_obs)
m.cvar1eq = pyo.Constraint(rule=CVaR1)
def CVaR2(m, q):
return m.shortfall[q] >= 0
m.cvar2eq = pyo.Constraint(m.price_time_obs, rule=CVaR2)
def CVaR4(m, q):
return m.profit[q] + m.shortfall[q] - m.alpha >= 0
m.cvar4eq = pyo.Constraint(m.price_time_obs, rule=CVaR4)
# Objective: Maximize CVaR
def obj_rule(m):
return m.CVaR
m.obj = pyo.Objective(rule=obj_rule, sense=pyo.maximize)
# Solve the Model
sol = pyo.SolverFactory('bonmin', tee = True)
results = sol.solve(m)
# Display Results
print('Results')
print('------------------------------')
print('Expected Profit:', np.round(pyo.value(m.EP), 2), 'USD')
print(f"Sugar to Regulated Market: {pyo.value(m.q_sugar_reg)}")
print(f"Sugar to Free Market: {pyo.value(m.q_sugar_free)}")
print(f"Ethanol to Regulated Market: {pyo.value(m.q_eth_reg)}")
print(f"Ethanol to Free Market: {pyo.value(m.q_eth_free)}")
print(f"Electricity to Free Market: {pyo.value(m.q_el_free)}")
print(f"Electricity to Regulated Market: {pyo.value(m.q_el_reg)}")
Results
------------------------------
Expected Profit: 55885788.25 USD
Sugar to Regulated Market: 90000.0009
Sugar to Free Market: 99129.7809912978
Ethanol to Regulated Market: 19999.9998
Ethanol to Free Market: 54072.67927772556
Electricity to Free Market: 0.0
Electricity to Regulated Market: 222600.00222600996
#Collect the profit distribution
profits = []
for i in m.price_time_obs:
profits.append(pyo.value(m.profit[i]))
#Collect the minimum profit
min_prof = min(profits)
#Plot the Profit Distribution
fig, ax = plt.subplots(figsize=(6.4, 4))
plt.hist(np.array(profits), alpha = 0.5)
plt.vlines(ymin = 0,ymax= 220, x= pyo.value(m.EP), label = 'Expected Profit', color = 'green')
plt.vlines(ymin = 0, ymax = 220, x=min_prof, label = ('Minimum Profit'), color = 'red')
plt.xlabel('profit$_q$ M USD', fontsize = 16, fontweight='bold')
plt.ylabel('Frequency', fontsize = 16, fontweight='bold')
plt.legend(fontsize = 14)
ax.xaxis.set_tick_params(labelsize=15)
ax.yaxis.set_tick_params(labelsize=15)
ax.tick_params(direction="in")
plt.show()
print('Results')
print('-------------------------------------')
print('Expected Profit:', np.round(pyo.value(m.EP)/1e6,2), 'M USD')
print('Minimum Profit:', np.round(min_prof/1e6,2), 'M USD')
difference = pyo.value(m.EP) - min_prof
print('Difference:', np.round(difference/1e6,2), 'M USD')
#Collect results for conclusion
final_EP['CVaR'] = pyo.value(m.EP)/1e6
final_min['CVaR'] = min_prof/1e6
final_diff['CVaR'] = difference/1e6
Results
-------------------------------------
Expected Profit: 55.89 M USD
Minimum Profit: 15.15 M USD
Difference: 40.74 M USD
fig, ax = plt.subplots(figsize=(6.4, 4))
plt.bar('Sugar \n Free \n Market',pyo.value(m.avg_sug_Fprof)/1e6)
plt.bar('Sugar \n Regulated \n Market',pyo.value(m.avg_sug_Rprof)/1e6)
plt.bar('Ethanol \n Free \n Market', pyo.value(m.avg_eth_Fprof)/1e6)
plt.bar('Ethanol \n Regulated \n Market', pyo.value(m.avg_eth_Rprof)/1e6)
plt.bar('Electricity \n Free \n Market', pyo.value(m.avg_el_prof)/1e6)
plt.bar('Electricity \n Regulated \n Market', pyo.value(m.q_el_reg)*72.5/1e6)
plt.ylabel('Product', fontsize = 16, fontweight='bold')
plt.ylabel('Revenue from \n Each Profit (USD)', fontsize = 16, fontweight='bold')
ax.xaxis.set_tick_params(labelsize=10)
ax.yaxis.set_tick_params(labelsize=10)
ax.tick_params(direction="in")
plt.show()
Summary#
The analysis shows that the free market channels for sugar, ethanol, and electricity yield distinct revenue distributions compared to the regulated channels. For instance, in the free market, the absence of pricing controls allows for higher revenue potential; however, this also introduces greater variability, as prices are susceptible to market fluctuations. In contrast, the regulated markets provide a more predictable revenue stream but cap the profit potential due to controlled pricing. These distinctions underscore a crucial trade-off between risk and revenue stability in choosing between market types.
The results further highlight that sugar and ethanol are particularly sensitive to regulatory constraints, with the regulated market setting a ceiling on potential earnings, especially when production surpluses increase supply. This condition may compel producers to prioritize free markets when aiming for higher profitability, albeit with increased exposure to market risks.
Overall, the model provides a comparative framework, guiding decision-makers to balance between predictable returns and potential profits within an uncertain market environment. This perspective is especially valuable for industries where the regulated and free markets can operate in tandem, allowing for diversified approaches that mitigate risk while maximizing revenue opportunities.