Skip to content

Commit

Permalink
FIX: simple MNL assortment (#74)
Browse files Browse the repository at this point in the history
  • Loading branch information
VincentAuriau authored Apr 26, 2024
1 parent 6e06c2b commit 0719d28
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 30 deletions.
11 changes: 6 additions & 5 deletions choice_learn/toolbox/assortment_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"""


class AssortmentOptimizer(object):
class MNLAssortmentOptimizer(object):
"""Base class for assortment optimization."""

def __init__(self, utilities, itemwise_values, assortment_size, outside_option_given=False):
Expand Down Expand Up @@ -59,7 +59,9 @@ def base_instantiate(self):

for j in range(self.n_items + 1):
y[j] = solver.addVar(
vtype=gp.GRB.CONTINUOUS, obj=self.itemwise_values[j], name="y_%s" % j
vtype=gp.GRB.CONTINUOUS,
obj=self.itemwise_values[j] * self.utilities[j],
name="y_%s" % j,
)
self.y = y
# Integrate new variables
Expand All @@ -78,7 +80,7 @@ def set_base_constraints(self):
self.solver.addConstr(self.y[j] <= self.y[0])

# Base Charnes-Cooper Constraint for Normalization
charnes_cooper = gp.quicksum(self.y[j] for j in range(self.n_items + 1))
charnes_cooper = gp.quicksum(self.y[j] * self.utilities[j] for j in range(self.n_items + 1))
self.solver.addConstr(charnes_cooper == 1)

# Assortment size constraint
Expand Down Expand Up @@ -127,7 +129,6 @@ def solve(self):
for i in range(0, self.n_items + 1):
if self.y[i].x > 0:
assortment[i] = 1

chosen_utilities = assortment * self.utilities
norm = np.sum(chosen_utilities)

Expand Down Expand Up @@ -386,7 +387,7 @@ def solve(self):
return assortment, recomputed_obj


class LatentClassAssortmentOptimizerWithPricing(object):
class LatentClassPricingOptimizer(object):
"""Assortment optimizer for latent class models with additional pricing optimization.
Implementation of the paper:
Expand Down
50 changes: 25 additions & 25 deletions notebooks/auxiliary_tools/assortment_example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@
"text": [
"WARNING:root:L-BFGS Opimization finished:\n",
"WARNING:root:---------------------------------------------------------------\n",
"WARNING:root:Number of iterations: 292\n",
"WARNING:root:Number of iterations: 225\n",
"WARNING:root:Algorithm converged before reaching max iterations: True\n"
]
}
Expand Down Expand Up @@ -379,14 +379,14 @@
"name": "stdout",
"output_type": "stream",
"text": [
"Model Negative Log-Likelihood: tf.Tensor(2.7657237, shape=(), dtype=float32)\n",
"Model Negative Log-Likelihood: tf.Tensor(2.7657256, shape=(), dtype=float32)\n",
"Model Weights:\n",
"Base Utilities u_i: [[ 0.5063553 2.936941 1.9992499 0.5475128 0.72667426 1.0071179\n",
" -0.718513 -0.9683217 -0.00803988 -3.0414693 1.0678933 1.6365201\n",
" -3.6386008 -1.2465692 3.0116532 1.6828817 1.8587259 -1.2654052\n",
" -1.1659362 -0.08459308 -1.7738986 -1.9679959 -1.7954452 1.5047753\n",
" -0.74721426]]\n",
"Price Elasticities: [[-0.06288974 -0.05763549 -0.05428785]]\n"
"Base Utilities u_i: [[ 0.5068263 2.935736 1.998015 0.5470789 0.72602475 1.0055478\n",
" -0.7196758 -0.970541 -0.00946927 -3.042058 1.0770373 1.6368566\n",
" -3.6405432 -1.2479168 3.0117846 1.6831478 1.8547137 -1.2627332\n",
" -1.1671457 -0.08575154 -1.773998 -1.9642268 -1.7941352 1.5037025\n",
" -0.7460297 ]]\n",
"Price Elasticities: [[-0.06286521 -0.05761966 -0.05427208]]\n"
]
}
],
Expand Down Expand Up @@ -454,11 +454,11 @@
"name": "stdout",
"output_type": "stream",
"text": [
"Estimated final utilities for each product: [-0.2500564 -0.39177373 -0.70433736 -0.5409493 -0.48122874 -0.38774762\n",
" -0.6583458 -0.93199795 -0.7261336 -1.5849706 -1.3194016 -0.17794682\n",
" -1.6317085 -0.8343645 -0.5001374 -0.8097935 -1.0557907 -0.8406432\n",
" -0.80748695 -0.69453645 -0.99110276 -1.0177253 -1.0173231 -1.383195\n",
" -0.42993477]\n"
"Estimated final utilities for each product: [-0.24978125 -0.3917887 -0.7043624 -0.5408898 -0.4812412 -0.38806686\n",
" -0.6586153 -0.93256587 -0.72640586 -1.5850058 -1.3158809 -0.17763059\n",
" -1.6322378 -0.83469564 -0.49966928 -0.80931807 -1.0566555 -0.8396344\n",
" -0.8077719 -0.69473463 -0.99102306 -1.0163671 -1.0167683 -1.3830209\n",
" -0.4294889 ]\n"
]
}
],
Expand Down Expand Up @@ -488,9 +488,9 @@
"metadata": {},
"outputs": [],
"source": [
"from choice_learn.toolbox.assortment_optimizer import AssortmentOptimizer\n",
"from choice_learn.toolbox.assortment_optimizer import MNLAssortmentOptimizer\n",
"\n",
"opt = AssortmentOptimizer(utilities=np.exp(final_utilities), # Utilities need to be transformed with exponential function\n",
"opt = MNLAssortmentOptimizer(utilities=np.exp(final_utilities), # Utilities need to be transformed with exponential function\n",
" itemwise_values=future_prices[0][:, 0], # Values to optimize for each item, here price that is used to compute turnover\n",
" assortment_size=12) # Size of the assortment we want"
]
Expand All @@ -507,9 +507,9 @@
"output_type": "stream",
"text": [
"Our Optimal Assortment is:\n",
"[0. 1. 1. 1. 1. 1. 0. 0. 1. 0. 1. 1. 0. 0. 1. 1. 1. 0. 0. 0. 0. 0. 0. 1.\n",
"[0. 1. 1. 1. 0. 0. 0. 1. 1. 1. 1. 0. 1. 0. 1. 1. 1. 0. 0. 0. 0. 0. 0. 1.\n",
" 0.]\n",
"With an estimated average revenue of: 50.05401698375297\n"
"With an estimated average revenue of: 51.57688285623652\n"
]
}
],
Expand Down Expand Up @@ -560,7 +560,7 @@
"Our Optimal Assortment is:\n",
"[0. 1. 1. 1. 1. 1. 0. 0. 1. 0. 1. 1. 0. 0. 1. 1. 1. 0. 0. 0. 0. 0. 0. 1.\n",
" 0.]\n",
"With an estimated average revenue of: 35.78845412272468\n",
"With an estimated average revenue of: 35.79552031037327\n",
"Totalling 12.0 items in the assortment, which is fine with our limit of 12.\n"
]
}
Expand Down Expand Up @@ -629,7 +629,7 @@
"Our Optimal Assortment is:\n",
"[0. 1. 1. 1. 1. 0. 0. 0. 0. 0. 1. 1. 0. 0. 1. 1. 1. 0. 0. 0. 0. 0. 0. 1.\n",
" 0.]\n",
"With an estimated average revenue of: 35.45812433045049\n",
"With an estimated average revenue of: 35.46667393198682\n",
"Size of our assortment: 10 which is fine with our limit of 12!\n",
"Capacity of our new assortment: 34.0 which is below our limit of 35!\n"
]
Expand Down Expand Up @@ -744,7 +744,7 @@
"metadata": {},
"outputs": [],
"source": [
"from choice_learn.toolbox.assortment_optimizer import LatentClassAssortmentOptimizerWithPricing"
"from choice_learn.toolbox.assortment_optimizer import LatentClassPricingOptimizer"
]
},
{
Expand All @@ -764,13 +764,13 @@
" 0. 0. 59.62222222 56.06666667 57.6 0.\n",
" 0. 53.12222222 0. 0. 0. 56.7\n",
" 0. ]\n",
"With an estimated average revenue of: 41.21036506371539\n",
"With an estimated average revenue of: 41.21468607531388\n",
"Size of our assortment: 12 which is fine with our limit of 12!\n"
]
}
],
"source": [
"opt = LatentClassAssortmentOptimizerWithPricing(\n",
"opt = LatentClassPricingOptimizer(\n",
" class_weights=age_frequencies, # Weights of each class\n",
" class_utilities=item_utilities, # utilities in the shape (n_classes, n_items)\n",
" itemwise_values=prices_grid, # Values to optimize for each item, here price that is used to compute turnover\n",
Expand Down Expand Up @@ -853,14 +853,14 @@
" 0. 0. 59.62222222 56.06666667 57.6 0.\n",
" 0. 53.12222222 0. 0. 0. 56.7\n",
" 0. ]\n",
"With an estimated average revenue of: 41.15936606666387\n",
"With an estimated average revenue of: 41.164100003155916\n",
"Size of our assortment: 11 which is fine with our limit of 12!\n",
"Capacity of our new assortment: 35 which is below our limit of 35!\n"
]
}
],
"source": [
"opt = LatentClassAssortmentOptimizerWithPricing(\n",
"opt = LatentClassPricingOptimizer(\n",
" class_weights=age_frequencies, # Weights of each class\n",
" class_utilities=item_utilities, # utilities in the shape (n_classes, n_items)\n",
" itemwise_values=prices_grid, # Values to optimize for each item, here price that is used to compute turnover\n",
Expand Down Expand Up @@ -921,7 +921,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.18"
"version": "3.11.4"
}
},
"nbformat": 4,
Expand Down

0 comments on commit 0719d28

Please sign in to comment.