{ "cells": [ { "cell_type": "markdown", "metadata": { "pycharm": {} }, "source": [ "```{index} single: application; profit maximization\n", "```\n", "```{index} single: solver; cbc\n", "```\n", "```{index} pandas dataframe\n", "```\n", "```{index} stochastic optimization\n", "```\n", "\n", "# Pop-up shop" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "# install dependencies and select solver\n", "%pip install -q amplpy numpy pandas\n", "\n", "SOLVER = \"cbc\"\n", "\n", "from amplpy import AMPL, ampl_notebook\n", "\n", "ampl = ampl_notebook(\n", " modules=[\"cbc\"], # modules to install\n", " license_uuid=\"default\", # license to use\n", ") # instantiate AMPL object and register magics" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "## The problem: Maximizing the net profit of a pop-up shop\n", "\n", "There is an opportunity to operate a pop-up shop to sell a unique commemorative item for events held at a famous location. The items cost 12 € each when bought from the supplier and will sell for 40 €. Unsold items can be returned to the supplier at a value of only 2 € due to their commemorative nature.\n", "\n", "| Parameter | Symbol | Value |\n", "| :---: | :---: | :---: |\n", "| sales price | $r$ | 40 € |\n", "| unit cost | $c$ | 12 € |\n", "| salvage value | $w$ | 2 € |\n", "\n", "Demand for these items, however, will be high only if the weather is good. Historical data suggests three typical scenarios, namely $S=\\{\\text{sunny skies, good weather, poor weather}\\}$, as detailed in the following table." ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "| Scenario ($s$) | Demand ($d_s$) | Probability ($p_s$) |\n", "| :---: | :-----: | :----------: |\n", "| Sunny Skies | 650 | 0.10 |\n", "| Good Weather | 400 | 0.60 |\n", "| Poor Weather | 200 | 0.30 |\n", "\n", "The problem is to determine how many items to order for the pop-up shop. \n", "\n", "The dilemma is that the weather will not be known until after the order is placed. Ordering enough items to meet demand for a good weather day results in a financial penalty on returned goods if the weather is poor. On the other hand, ordering just enough items to satisfy demand on a poor weather day leaves \"money on the table\" if the weather is good.\n", "\n", "How many items should be ordered for sale?" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "## Expected value for the mean scenario (EVM)\n", " \n", "A naive solution to this problem is to place an order equal to the expected demand, which can be calculated as\n", "\n", "$$\n", "\\begin{align*}\n", "\\mathbb E[D] & = \\sum_{s\\in S} p_s d_s.\n", "\\end{align*}\n", "$$\n", "\n", "Choosing an order size $\\hat{x} = \\mathbb E[D] = 365$ results in an expected profit we call the **expected value of the mean scenario (EVM)**. The resulting expected profit is given by\n", "\n", "$$\n", "\\begin{align*}\n", "\\text{EVM} = \\mathbb E[f] & = \\sum_{s\\in S} p_s f_s,\n", "\\end{align*}\n", "$$\n", "where $f_s$ is the net profit in scenario $s$ assuming that we ordered $\\hat{x}$ items.\n", "\n", "These calculations can be executed using operations on the pandas dataframe. First, we create a pandas DataFrame object to store the scenario data and calculate the expected demand." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
probabilitydemand
sunny skies0.1650.0
good weather0.6400.0
poor weather0.3200.0
\n", "
" ], "text/plain": [ " probability demand\n", "sunny skies 0.1 650.0\n", "good weather 0.6 400.0\n", "poor weather 0.3 200.0" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Expected demand = 365.0\n" ] } ], "source": [ "# price information\n", "r = 40\n", "c = 12\n", "w = 2\n", "\n", "# scenario information\n", "scenarios = {\n", " \"sunny skies\": {\"probability\": 0.10, \"demand\": 650},\n", " \"good weather\": {\"probability\": 0.60, \"demand\": 400},\n", " \"poor weather\": {\"probability\": 0.30, \"demand\": 200},\n", "}\n", "\n", "df = pd.DataFrame.from_dict(scenarios).T\n", "display(df)\n", "\n", "expected_demand = sum(df[\"probability\"] * df[\"demand\"])\n", "print(f\"Expected demand = {expected_demand}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Subsequent calculations to obtain the EVM can be done directly within the pandas dataframe holding the scenario data." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
probabilitydemandordersoldsalvageprofit
sunny skies0.1650.0365.0365.00.010220.0
good weather0.6400.0365.0365.00.010220.0
poor weather0.3200.0365.0200.0165.03950.0
\n", "
" ], "text/plain": [ " probability demand order sold salvage profit\n", "sunny skies 0.1 650.0 365.0 365.0 0.0 10220.0\n", "good weather 0.6 400.0 365.0 365.0 0.0 10220.0\n", "poor weather 0.3 200.0 365.0 200.0 165.0 3950.0" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Expected value of the mean demand (EVM) = 8339.0\n" ] } ], "source": [ "df[\"order\"] = expected_demand\n", "df[\"sold\"] = df[[\"demand\", \"order\"]].min(axis=1)\n", "df[\"salvage\"] = df[\"order\"] - df[\"sold\"]\n", "df[\"profit\"] = r * df[\"sold\"] + w * df[\"salvage\"] - c * df[\"order\"]\n", "\n", "EVM = sum(df[\"probability\"] * df[\"profit\"])\n", "display(df)\n", "print(f\"Expected value of the mean demand (EVM) = {EVM}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "No scenario shows a profit loss, which appears to be a satisfactory outcome. However, can we find an order resulting in a higher expected profit?" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "## Value of the stochastic solution (VSS)\n", "\n", "In order to answer this question, let us formulate the problem in mathematical terms. Let $x$ be a non-negative number representing the number of items that will be ordered, and $y_s$ be the non-negative variable describing the number of items sold in scenario $s$ in the set $S$ comprising all scenarios under consideration. The number $y_s$ of sold items is the lesser of the demand $d_s$ and the order size $x$, that is\n", "\n", "$$\n", "\\begin{align*}\n", "y_s & = \\min(d_s, x) & \\forall s \\in S.\n", "\\end{align*}\n", "$$\n", "\n", "Any unsold inventory $x - y_s$ remaining after the event will be sold at the salvage price $w$. Taking into account the revenue from sales $r y_s$, the salvage value of the unsold inventory $w(x - y_s)$, and the cost of the order $c x$, the profit $f_s$ for scenario $s$ is given by\n", "\n", "$$\n", "\\begin{align*}\n", "f_s & = r y_s + w (x - y_s) - c x & \\forall s \\in S\n", "\\end{align*}\n", "$$\n", "\n", "Using the constants introduced earlier, the profit $f_s$ for scenario $s \\in S$ can then be written as \n", "\n", "$$\n", " f_s = \\underbrace{r y_s}_\\text{sales revenue} + \\underbrace{w (d_s - y_s)}_\\text{salvage value} - \\underbrace{c x}_\\text{order cost}.\n", "$$\n", "\n", "The expected profit is given by $\\mathbb E(F) = \\sum_s p_s f_s$. Operationally, $y_s$ can be no larger the number of items ordered, $x$, or the demand under scenario $s$, $d_s$. \n", "The optimization problem is to find the order size $x$ that maximizes expected profit subject to operational constraints on the decision variables. The variables $x$ and $y_s$ are non-negative integers, while $f_s$ is a real number that can take either positive or negative values. Putting these facts together, the optimization problem to be solved is\n", "\n", "$$\n", "\\begin{align*}\n", " \\text{EV} = \\max \\quad & \\mathbb E(F) = \\sum_{s\\in S} p_s f_s \\\\\n", " \\text{s.t.} \\quad \n", " &f_s = r y_s + w(d_s - y_s) - c x & \\forall s \\in S\\\\\n", " &y_s \\leq x & \\forall s \\in S \\\\\n", " &y_s \\leq d_s & \\forall s \\in S\\\\\n", " &y_s \\in \\mathbb{Z}_+ & \\forall s \\in S\\\\\n", " &x \\in \\mathbb{Z}_+,\n", "\\end{align*}\n", "$$\n", "where $S$ is the set of all scenarios under consideration.\n", "\n", "We can implement this problem in AMPL as follows." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting pop.mod\n" ] } ], "source": [ "%%writefile pop.mod\n", "\n", "param r;\n", "param c;\n", "param w;\n", "\n", "# set of scenarios\n", "set S;\n", "\n", "param p{S};\n", "param d{S};\n", "\n", "# decision variables\n", "var x >= 0;\n", "var y{S} >= 0;\n", "var f{S};\n", "\n", "# objective\n", "maximize EV: sum{s in S} p[s] * f[s];\n", "\n", "# constraints\n", "s.t. profit {s in S}: f[s] == r * y[s] + w * (x - y[s]) - c * x;\n", "s.t. sales_less_than_order {s in S}: y[s] <= x;\n", "s.t. sales_less_than_demand {s in S}: y[s] <= d[s];" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "cbc 2.10.7: cbc 2.10.7: optimal solution; objective 8920\n", "0 simplex iterations\n", "Solver Termination Condition: solved\n", "\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
demandprobabilityordersoldsalvageprofit
sunny skies650.00.1400.04000.011200
good weather400.00.6400.04000.011200
poor weather200.00.3400.0200200.03600
\n", "
" ], "text/plain": [ " demand probability order sold salvage profit\n", "sunny skies 650.0 0.1 400.0 400 0.0 11200\n", "good weather 400.0 0.6 400.0 400 0.0 11200\n", "poor weather 200.0 0.3 400.0 200 200.0 3600" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Expected Profit: 8920.0\n" ] } ], "source": [ "# price and scenario information\n", "r = 40\n", "c = 12\n", "w = 2\n", "\n", "scenarios = {\n", " \"sunny skies\": {\"demand\": 650, \"probability\": 0.1},\n", " \"good weather\": {\"demand\": 400, \"probability\": 0.6},\n", " \"poor weather\": {\"demand\": 200, \"probability\": 0.3},\n", "}\n", "\n", "# create a data frame with the data and rename the columns to match the model\n", "scenarios_df = pd.DataFrame.from_dict(scenarios).T.rename(\n", " columns={\"demand\": \"d\", \"probability\": \"p\"}\n", ")\n", "\n", "# Create AMPL instance and load the model\n", "ampl = AMPL()\n", "ampl.read(\"pop.mod\")\n", "\n", "# load the data\n", "ampl.param[\"r\"] = r\n", "ampl.param[\"c\"] = c\n", "ampl.param[\"w\"] = w\n", "\n", "ampl.set_data(scenarios_df, \"S\")\n", "\n", "# solve the problem\n", "ampl.option[\"solver\"] = SOLVER\n", "ampl.solve()\n", "\n", "print(\"Solver Termination Condition:\", ampl.get_value(\"solve_result\"))\n", "print()\n", "\n", "# display solution using Pandas\n", "df = pd.DataFrame.from_dict(scenarios).T\n", "df[\"order\"] = ampl.var[\"x\"].value()\n", "df[\"sold\"] = ampl.var[\"y\"].get_values().toPandas()\n", "df[\"salvage\"] = df[\"order\"] - df[\"sold\"]\n", "df[\"profit\"] = ampl.var[\"f\"].get_values().toPandas()\n", "\n", "display(df)\n", "print(\"Expected Profit:\", ampl.obj[\"EV\"].value())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Optimizing over all scenarios provides an expected profit of 8,920 €, an increase of 581 € over the naive strategy of simply ordering the expected number of items sold. The new optimal solution places a larger order, that is $x=400$. In poor weather conditions, there will be more returns and lower profit that is more than compensated by the increased profits in good weather conditions. \n", "\n", "The additional value that results from solve of this planning problem is called the **Value of the Stochastic Solution (VSS)**. The value of the stochastic solution is the additional profit compared to ordering to meet the expected demand. In this case,\n", "\n", "$$\\text{VSS} = \\text{EV} - \\text{EVM} = 8,920 - 8,339 = 581.$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Expected value with perfect information (EVPI)\n", "\n", "Maximizing expected profit requires the size of the order be decided before knowing what scenario will unfold. The decision for $x$ has to be made \"here and now\" with probablistic information about the future, but without specific information on which future will actually transpire.\n", "\n", "Nevertheless, we can perform the hypothetical calculation of what profit would be realized if we could know the future. We are still subject to the variability of weather, what is different is we know what the weather will be at the time the order is placed. \n", "\n", "The resulting value for the expected profit is called the **Expected Value of Perfect Information (EVPI)**. The difference EVPI - EV is the extra profit due to having perfect knowledge of the future.\n", "\n", "To compute the expected profit with perfect information, we let the order variable $x$ be indexed by the subsequent scenario that will unfold. Given decision varaible $x_s$, the model for EVPI becomes\n", "\n", "$$\n", "\\begin{align*}\n", "\\text{EVPI} = \\max_{x_s, y_s} \\quad & \\mathbb E[f] = \\sum_{s\\in S} p_s f_s \\\\\n", "\\text{s.t.} \\quad\n", "& f_s = r y_s + w(x_s - y_s) - c x_s & \\forall s \\in S\\\\\n", "& y_s \\leq x_s & \\forall s \\in S \\\\\n", "& y_s \\leq d_s & \\forall s \\in S\n", "\\end{align*}\n", "$$\n", "\n", "The following implementation is a variation of the prior cell." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting pop_evpi.mod\n" ] } ], "source": [ "%%writefile pop_evpi.mod\n", "\n", "param r;\n", "param c;\n", "param w;\n", "\n", "# set of scenarios\n", "set S;\n", "\n", "param p{S};\n", "param d{S};\n", "\n", "# decision variables\n", "var x{S} >= 0;\n", "var y{S} >= 0;\n", "var f{S};\n", "\n", "# objective\n", "maximize EV: sum{s in S} p[s] * f[s];\n", "\n", "# constraints\n", "s.t. profit {s in S}: f[s] == r * y[s] + w * (x[s] - y[s]) - c * x[s];\n", "s.t. sales_less_than_order {s in S}: y[s] <= x[s];\n", "s.t. sales_less_than_demand {s in S}: y[s] <= d[s];" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "cbc 2.10.7: cbc 2.10.7: optimal solution; objective 10220\n", "0 simplex iterations\n", "Solver Termination Condition: solved\n", "\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
demandprobabilityordersoldsalvageprofit
sunny skies650.00.1650.06500.018200
good weather400.00.6400.04000.011200
poor weather200.00.3200.0200-0.05600
\n", "
" ], "text/plain": [ " demand probability order sold salvage profit\n", "sunny skies 650.0 0.1 650.0 650 0.0 18200\n", "good weather 400.0 0.6 400.0 400 0.0 11200\n", "poor weather 200.0 0.3 200.0 200 -0.0 5600" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Expected Profit: 10220.0\n" ] } ], "source": [ "# Create AMPL instance and load the model\n", "ampl = AMPL()\n", "ampl.read(\"pop_evpi.mod\")\n", "\n", "# load the data\n", "ampl.param[\"r\"] = r\n", "ampl.param[\"c\"] = c\n", "ampl.param[\"w\"] = w\n", "\n", "ampl.set_data(scenarios_df, \"S\")\n", "\n", "# solve the problem\n", "ampl.option[\"solver\"] = SOLVER\n", "ampl.solve()\n", "\n", "print(\"Solver Termination Condition:\", ampl.get_value(\"solve_result\"))\n", "print()\n", "\n", "# display solution using Pandas\n", "df = pd.DataFrame.from_dict(scenarios).T\n", "df[\"order\"] = ampl.var[\"x\"].get_values().toPandas()\n", "df[\"sold\"] = ampl.var[\"y\"].get_values().toPandas()\n", "df[\"salvage\"] = df[\"order\"] - df[\"sold\"]\n", "df[\"salvage\"] = df[\"salvage\"].round(2)\n", "df[\"profit\"] = ampl.var[\"f\"].get_values().toPandas()\n", "\n", "display(df)\n", "print(\"Expected Profit:\", ampl.obj[\"EV\"].value())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Summary\n", "\n", "To summarize, have computed three different solutions to the problem of order size:\n", "\n", "* The expected value of the mean solution (EVM) is the expected profit resulting from ordering the number of items expected to sold under all scenarios. \n", "\n", "* The expected value of the stochastic solution (EVSS) is the expected profit found by solving an two-state optimization problem where the order size was the \"here and now\" decision without specific knowledge of which future scenario would transpire.\n", "\n", "* The expected value of perfect information (EVPI) is the result of a hypotherical case where knowledge of the future scenario was somehow available when then order had to be placed. \n", "\n", "For this example we found\n", "\n", "| Solution | Value (€) |\n", "| :------ | ----: |\n", "| Expected Value of the Mean Solution (EVM) | 8,399.0 | \n", "| Expected Value of the Stochastic Solution (EVSS) | 8,920.0 |\n", "| Expected Value of Perfect Information (EVPI) | 10,220.0 |\n", "\n", "These results verify our expectation that\n", "\n", "$$\n", "\\begin{align*}\n", "EVM \\leq EVSS \\leq EVPI\n", "\\end{align*}\n", "$$\n", "\n", "The value of the stochastic solution \n", "\n", "$$\n", "\\begin{align*}\n", "VSS = EVSS - EVM = 581\n", "\\end{align*}\n", "$$\n", "\n", "The value of perfect information\n", "\n", "$$\n", "\\begin{align*}\n", "VPI = EVPI - EVSS = 1,300\n", "\\end{align*}\n", "$$\n", "\n", "\n", "As one might expect, there is a cost that results from lack of knowledge about an uncertain future." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.12" } }, "nbformat": 4, "nbformat_minor": 4 }