{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Vacation Cabin Rental Revenue Management Example" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Introduction\n", "\n", "Revenue management (RM) synchronizes market segmentation, optimal pricing, and optimal capacity usage to maximize revenues attainable through existing capacity. In this notebook we will demonstrate the impact of optimizing capacity allocation in a cabin rental context. In this scenario, we have several luxury properties at a mountain lake resort that we rent out: \n", "\n", "* 3 Small Cabins\n", "* 5 Medium Cabins\n", "* 3 Large Cabins. \n", "\n", "We will assume that we have already determined the best prices and have an accurate demand forecast for the upcoming weekend. We will compare three distinct capacity allocation policies against one another to see the impact on revenues:\n", "\n", "* **Optimization:** Optimal demand fulfillment according to the demand forecast\n", "\n", "* **First Come, First Served:** A human-like policy that fulfills requests as they come in, rather than optimally matching capacity to forecasted demand ahead of time.\n", "\n", "As a preliminary, we import the necessary libraries:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "# Import necessary libraries.\n", "from pulp import *\n", "import pandas as pd\n", "import numpy as np\n", "import random as rnd\n", "import functools as ft\n", "import matplotlib.pyplot as plt\n", "import pprint" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Step 1: Load/Specify and View Data\n", "Next we load our demand and price data, define our current capacity of each cabin type, and view the data:" ] }, { "cell_type": "code", "execution_count": 2, "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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Rental PeriodSmallMediumLarge
0Fri-Sat120
1Fri-Sun221
2Fri-Mon010
3Sat-Sun100
4Sat-Mon300
5Sun-Mon201
6Mon-Wed201
\n", "
" ], "text/plain": [ " Rental Period Small Medium Large\n", "0 Fri-Sat 1 2 0\n", "1 Fri-Sun 2 2 1\n", "2 Fri-Mon 0 1 0\n", "3 Sat-Sun 1 0 0\n", "4 Sat-Mon 3 0 0\n", "5 Sun-Mon 2 0 1\n", "6 Mon-Wed 2 0 1" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Load/set up the data\n", "demand_data = pd.read_csv('DemandData.csv')\n", "price_data = pd.read_csv('PriceData.csv')\n", "available_units = {'Small':3, 'Medium':5, 'Large':3}\n", "\n", "# View the demand data\n", "demand_data" ] }, { "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", " \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", "
Rental PeriodSmallMediumLarge
0Fri-Sat607.181195.982531.82
1Fri-Sun956.971884.973990.37
2Fri-Mon1214.362391.965063.64
3Sat-Sun607.181195.982531.82
4Sat-Mon956.971884.973990.37
5Sun-Mon607.181195.982531.82
6Mon-Wed607.181195.982531.82
\n", "
" ], "text/plain": [ " Rental Period Small Medium Large\n", "0 Fri-Sat 607.18 1195.98 2531.82\n", "1 Fri-Sun 956.97 1884.97 3990.37\n", "2 Fri-Mon 1214.36 2391.96 5063.64\n", "3 Sat-Sun 607.18 1195.98 2531.82\n", "4 Sat-Mon 956.97 1884.97 3990.37\n", "5 Sun-Mon 607.18 1195.98 2531.82\n", "6 Mon-Wed 607.18 1195.98 2531.82" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# View the price data\n", "price_data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Step 2: Implement the Optimization Model and Solve\n", "To solve this problem optimally, we will formulate and solve an integer programming (IP) model. We use the PuLP framework for doing this. The function below will construct an IP model instance from the given input data." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "# This function constructs a PuLP integer programming model instance from the model inputs.\n", "# @param T: A list of distinct resource types\n", "# @param S: A list of distinct rental demand slots\n", "# @param r: A dict of form {:}\n", "# @param d: A dict of form {:}\n", "# @param c: A dict of form {(, ): 0 | 1} where 1 designates that k overlaps j's start day \n", "# @param u: A dict of form {(, ): 0 | 1} where 1 = resource can satisfy demand slot; 0 otherwise\n", "# @param A: A dict of form {:}\n", "# @param big_M: A large constant\n", "# @returns: A PuLP Problem instance\n", "def build_model(T, S, r, d, c, u, A, big_M):\n", " # Decision Variables:\n", " x = LpVariable.dicts(\"x\", (T, S), 0, None, LpInteger)\n", " \n", " # Model declaration and objective function:\n", " prob = LpProblem(\"RentalRevMgmt\", LpMaximize)\n", " prob += lpSum([r[j] * x[i][j] for i in T for j in S]), 'TotalRevenue'\n", " \n", " # Constraints:\n", " for j in S:\n", " # Don't assign more resources than demand to any slot:\n", " prob += lpSum([x[i][j] for i in T]) <= d[j] \n", " for i in T:\n", " # Don't overbook any resources:\n", " prob += x[i][j] + lpSum([c[j,k] * x[i][k] for k in set(S).difference([j])]) <= A[i] \n", " prob += x[i][j] <= big_M * u[i,j] # Don't assign any resource type to slots it cannot satisfy\n", " return prob " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we have the model formulated, we need to solve it and obtain the optimal sales plan and corresponding revenue attained. We will create an optimize function to do this. As a pre-requisite, we first need to connstruct a few helper functions to transform the raw data into the format needed by the model" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "# This utility function constructs a start day overlap matrix between every possible pair of distinct rental periods.\n", "# @param distinct_rental_periods: A list of possible rental periods. Each period is of the form \n", "# \"-\" or \"-: \"\n", "# @returns: A dict of form {(, ): 0 | 1}, where 1 designates that k overlaps j's start day\n", "def build_start_day_overlap_matrix(distinct_rental_periods):\n", " week = ['Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat']\n", " day_inds = lambda days: dict([(day, i) for i, day in enumerate(days)])\n", " week_starting_on = lambda day: week[day_inds(week)[day]:] + week[:day_inds(week)[day]]\n", " interval = lambda s, e: week_starting_on(s)[:day_inds(week_starting_on(s))[e] + 1]\n", " matrix = {}\n", " days = lambda p: [p[:3], p[3:6]]\n", " for start_1, end_1, period_1 in [days(x) + [x] for x in distinct_rental_periods]:\n", " for start_2, end_2, period_2 in [days(x) + [x] for x in distinct_rental_periods]:\n", " matrix[(period_1, period_2)] = 1 if start_1 in interval(start_2, end_2) else 0\n", " return matrix\n", "\n", "# This function builds the optimization model inputs from the raw data.\n", "# @param demand_data: A pandas DataFrame containing the demand data in the spreadsheet format\n", "# @param price_data: : A pandas DataFrame containing the price data in the spreadsheet format\n", "# @param resource_availabilities: A dict of form {:}\n", "# @param valid_substitutions: A list of form [(, ), ...] where A can sub for B\n", "# @param big_M: A large constant\n", "# @returns: a tuple of inputs of form (T, S, r, d, c, u, A, big_M)\n", "def build_model_inputs(demand_data, price_data, resource_availabities, \n", " valid_substitutions, big_M):\n", " T = list(demand_data.columns)[1:]\n", " rental_periods = [x.replace('-', '') for x in list(demand_data.iloc[0:len(demand_data), 0])]\n", " resource_col_ind = {'Small':1, 'Medium':2, 'Large':3}\n", " period_row_ind = {period:ind for ind, period in enumerate(rental_periods)}\n", " S = [str(period) + str(resource) for resource in T for period in rental_periods]\n", " d = {str(p) + str(r): demand_data.iloc[period_row_ind[p], resource_col_ind[r]] \n", " for r in T for p in rental_periods}\n", " r = {str(p) + str(r):price_data.iloc[period_row_ind[p], resource_col_ind[r]] \n", " for r in T for p in rental_periods}\n", " A = resource_availabities\n", " c = build_start_day_overlap_matrix(S)\n", " subs = valid_substitutions + [(r, r) for r in T]\n", " u = {(r, str(p) + str(rp)): 1 if (r, rp) in subs else 0 \n", " for r in T for rp in T for p in rental_periods}\n", " return T, S, r, d, c, u, A, big_M\n", " \n", "# This function uses the model to optimize the allocation of resources available to demands. Uses the\n", "# CBC solver that comes with PuLP to solve the model.\n", "# @param demand_data: A pandas DataFrame containing the demand data in the spreadsheet format\n", "# @param price_data: : A pandas DataFrame containing the price data in the spreadsheet format\n", "# @param resource_availabilities: A dict of form {:}\n", "# @param valid_substitutions: A list of form [(, ), ...] where A can sub for B\n", "# @returns: A tuple of form (, ). The data frame is the capacity allocation/sales plan.\n", "def optimize(demand_data, price_data, resource_availabities, valid_substitutions):\n", " big_M = 1 + sum(resource_availabities.values())\n", " model_inputs = build_model_inputs(demand_data, price_data, resource_availabities, \n", " valid_substitutions, big_M)\n", " model = build_model(*model_inputs)\n", " model.solve() \n", " resources, slots, assignments = [], [], []\n", " for v in model.variables():\n", " if v.name.startswith('x'):\n", " var, i, j, val = v.name.split('_') + [v.varValue]\n", " if val > 0:\n", " resources.append(i)\n", " slots.append(j)\n", " assignments.append(val)\n", " return pd.DataFrame({'Resource':resources, 'Demand Slot':slots, \n", " 'Units Allocated':assignments}), value(model.objective) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can use this to get an optimal sales plan and see the resulting revenues:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Total Revenue Attained: $24,471.65\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", " \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", "
ResourceDemand SlotUnits Allocated
0LargeFriSatMedium1.0
1LargeFriSunLarge1.0
2LargeFriSunSmall1.0
3LargeMonWedLarge1.0
4LargeSunMonLarge1.0
5MediumFriMonMedium1.0
6MediumFriSatMedium1.0
7MediumFriSatSmall1.0
8MediumFriSunMedium2.0
9MediumMonWedSmall2.0
10MediumSunMonSmall2.0
11SmallSatMonSmall3.0
\n", "
" ], "text/plain": [ " Resource Demand Slot Units Allocated\n", "0 Large FriSatMedium 1.0\n", "1 Large FriSunLarge 1.0\n", "2 Large FriSunSmall 1.0\n", "3 Large MonWedLarge 1.0\n", "4 Large SunMonLarge 1.0\n", "5 Medium FriMonMedium 1.0\n", "6 Medium FriSatMedium 1.0\n", "7 Medium FriSatSmall 1.0\n", "8 Medium FriSunMedium 2.0\n", "9 Medium MonWedSmall 2.0\n", "10 Medium SunMonSmall 2.0\n", "11 Small SatMonSmall 3.0" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "valid_substitutions = [('Large', 'Small'), ('Large', 'Medium'), ('Medium', 'Small')]\n", "plan_OPT, revenue_OPT = optimize(demand_data, price_data, available_units, valid_substitutions)\n", "print(\"Total Revenue Attained: ${:,}\".format(round(revenue_OPT, 2)))\n", "plan_OPT" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Step 3: Solving the Human Way - First Come, First Served\n", "\n", "To provide comparison, we will implement a first-come-first-served (FCFS) algorithm to mimic the way a human agent not using RM would most likely allocate the capacity. In FCFS, booking requests are served as they come in if there is available capacity to meet them. Substituting larger cabins for smaller is not used since, without a demand forecast, we would not know whether the next request might be for a larger, higher-paying cabin.\n", "\n", "In the code below, we also include a data pre-processing function to transform the raw data into the form needed by the FCFS algorithm. The pre-processing function randomly orders the booking requests in order to simulate the way they come in the real world." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "# This function builds formatted inputs for the FCFS heuristic from the raw data. It simulates the real world\n", "# by randomly ordering the requests for rental units.\n", "# @param demand_data: A pandas DataFrame containing the demand data in the spreadsheet format\n", "# @param price_data: : A pandas DataFrame containing the price data in the spreadsheet format\n", "# @param resource_availabilities: A dict of form {:}\n", "# @returns: A tuple of required inputs in form (T, S, r, d, c, A)\n", "def build_FCFS_inputs(demand_data, price_data, resource_availabities):\n", " T, S, r, d, c, _, A, _ = build_model_inputs(demand_data, price_data, \n", " resource_availabities, [], 0)\n", " formatted_d = ft.reduce(lambda x, y: x + y, [[s for i in range(dmd)] for s, dmd in d.items()])\n", " rnd.shuffle(formatted_d)\n", " conflict_matrix = {(j, k): 1 if c[j,k] + c[k,j] > 0 else 0 for (j, k) in c.keys()}\n", " return T, S, r, formatted_d, conflict_matrix, A\n", " \n", "# This heuristic approximates the way a human might allocate the available units to the demand. It fulfills demand \n", "# on a first-come-first-served basis with exactly the requested resource (versus the requested or better). The\n", "# fulfillment plan and resulting reveue is largely dependent on the order in which the slots are requested.\n", "# @param T: A list of distinct resource types\n", "# @param S: A list of distinct rental demand slots. \n", "# @param r: A dict of form {:}\n", "# @param d: A list of rental demand slots. Each demand slot corresponds to one unit of demand. \n", "# The same demand slot can occur multiple times, corresponding to multiple demand requests.\n", "# @param c: A dict of form {(, ): 0 | 1} where 1 = conflict between slots; 0 = no conflict\n", "# @param A: A dict of form {:}\n", "# @returns: A tuple of form (, ). The data frame is the capacity allocation/sales plan.\n", "def solve_FCFS(T, S, r, d, c, A):\n", " resource = lambda j: [i for i in T if j.endswith(i)][0]\n", " slot_resources_available = {j:A[resource(j)] for j in S}\n", " revenue = 0\n", " plan = {j:0 for j in S}\n", " for j in d:\n", " if slot_resources_available[j] > 0:\n", " keys = [(x, y) for (x, y) in c.keys() \n", " if j == x and y in slot_resources_available and y.endswith(resource(j))]\n", " for _, k in keys:\n", " if c[j, k] == 1:\n", " slot_resources_available[k] -= 1\n", " plan[j] += 1\n", " revenue += r[j]\n", " plan = {j:plan[j] for j in plan.keys() if plan[j] > 0}\n", " resources, slots, assignments = [], [], []\n", " for slot, allocation in plan.items():\n", " resources.append(slot[6:])\n", " slots.append(slot)\n", " assignments.append(allocation)\n", " return pd.DataFrame({'Resource':resources, 'Demand Slot':slots, \n", " 'Units Allocated':assignments}), revenue " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Step 4: Compare Optimization with FCFS\n", "\n", "Below we will compare two variations of optimization (one with no substitutions, and one that allows larger cabins to sub for smaller ones) against FCFS to see the impact on our revenues. We will run FCFS 100 times, randomly ordering the booking requests in each case, and compare the worst, median (i.e. typical), and best cases to the two optimization results." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "image/png": "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RWADAAAAAItilciH3Pnz5x90E/AQ8fLyMv9GGJAb3DP4PbhvkFfcM8ir+3nPlCpV6r7U82dhhA0AAAAALIrABgAAAAAWRWADAAAAAIsisAEAAACARRHYAAAAAMCiCGwAAAAAYFEENgAAAACwKAIbAAAAAFgUgQ0AAAAALIrABgAAAAAWRWADAAAAAIsisAEAAACARTk96Abgj5nq4/OgmwAAAADkWa9z5x50Ex4KjLABAAAAgEUR2AAAAADAoghsAAAAAGBRBDYAAAAAsCgCGwAAAABYFIENAAAAACyKwAYAAAAAFkVgAwAAAACLIrABAAAAgEUR2AAAAADAoghsAAAAAGBRBDYAAAAAsCgCGwAAAABYFIENAAAAACyKwAYAAAAAFkVgAwAAAACLIrABAAAAgEUR2AAAAABYxpdffqlZs2bZbRs2bJiCgoLk6+urRo0aacGCBVmO++CDD+Tj4yN/f/97nqN///7y8fFRixYtJEmnTp2SzWbL8nOnHTNnzpS/v79sNpvc3d3vWvfNmzc1YMAAFStWTG5ubqpVq5aSk5MlSc2aNbOrv3Dhwvdsq9M9S/zFkpKSNH36dJ09e1aGYah27drq2rWrnJxybur169e1detWtWrVKlfnuHr1qnr37q2XXnpJjz32WI7lxo8fry5duqh48eL67rvvtHLlStlsNhmGobCwMNWrVy/HY6OiohQTE6Pw8PBctenXvv32W7m4uKh58+Z5PhYAAAB4GMXExKhnz546duyYpNufxadMmaLg4GDt379fHTp0kJeXlyZMmKDw8HBt2bJF5cqVkyT99NNPmjx5slxdXe95nk2bNmn16tV227y9ve1C4JgxY3To0CHVqlVLkpSamqp27dpp1qxZunnz5l3rHzp0qD799FP16NFDjRo10vfff69bt26Z+wMCAvSf//xHkuTs7HzP9loqsBmGoQ8//FCPP/64hgwZoszMTEVGRmrBggXq2rVrjsddv35da9euzXVg2759u/z9/RUdHZ1jYDtz5owyMzNVvHhxJSUl6auvvtLYsWPl5uam1NRUXb169XddY240b95cw4cPJ7ABAADgHyMyMlJxcXF64YUX5ObmplKlSsnFxUWStGzZMjPcnD59WlOmTNGJEydUrlw5ZWZmatCgQercubPWrFmjS5cu5XiOlJQUvfHGGxo8eLBGjRplbi9Q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CmwiIiIiIiJWSoFNRERERETESimwiYiIiIiIWCkFNhERERERESulwCYiIiIiImKlFNhERERERESslAKbiIiIiIiIlbKt6w7Ij/NyVlZdd0EeIR4eHuTl5dV1N+QRopqRB6G6kfulmhGpnUbYRERERERErJQCm4iIiIiIiJVSYBMREREREbFSCmwiIiIiIiJWSoFNRERERETESimwiYiIiIiIWCkFNhERERERESulwCYiIiIiImKlFNhERERERESslAKbiIiIiIiIlVJgExERERERsVIKbCIiIiIiIlbKtq47ID/OumbN6roLIiIiIj/ay1lZdd0FEaukETYRERERERErpcAmIiIiIiJipRTYRERERERErJQCm4iIiIiIiJVSYBMREREREbFSCmwiIiIiIiJWSoFNRERERETESimwiYiIiIiIWCkFNhERERERESulwCYiIiIiImKlFNhERERERESslAKbiIiIiIiIlVJgExERERERsVIKbCIiIiIiIlZKgU1ERERERMRKKbCJiIiIiIhYKQU2EREREbFamzZt4v3336/y3qxZs+jVqxetW7emX79+bNu2zbLs6tWrhIeH07ZtW7p06cLq1atr3fbFixd57rnnaN26NT179uRvf/ubZdlbb71Fx44dadWqFX369OGDDz6wLDMMg/fee4/u3bvTunVrBg0aRFpaWo37CAsLo1mzZpb/2rdvb1n28ccfExQURJs2bfjnf/5nzpw5c7+nRx4DCmwiIiIiYnXS0tIICgoiMjKSWbNm0alTJxITEwH46quv+N3vfsdbb73FlStXiIiIICMjA4AFCxawY8cOXnnlFbp3787s2bP55JNPatzH66+/zuHDh5k+fTrNmzcnIiKCs2fPAtCsWTMiIyP585//zM2bN5k5cyY5OTkAxMTEsGjRIjp37szcuXMZNGgQZWVltR6Lj48PK1euZOXKlSxevBiA7Oxsxo8fD0BUVBTp6emEh4c/nJMnvyi2D7riyJEj8fb2tryeNm0aXl5epKamEhsbS2FhISaTCT8/P8aOHUtiYiKxsbG4ubkB0KJFCyZOnMjnn39OXFwchmFQXl5OaGgoTz31VLX9HTt2jMzMTH7zm98wadIkoqOjMZlMnDlzhjfeeINVq1bh7u7O9evXmTBhAtHR0ZjND55Ht23bxogRI2pcVlJSwsaNGzl58iR2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bA/+l6V/ZsOHD6e0tLSuu/GzKC4urjLsLyIiIiIij6eHOiXyp9SwYUMCAwPruhs/i8dhFFFERERERO7ukRlhExERERERedwosImIiIiIiFgpBTYRERERERErpcAmIiIiIiJipRTYRERERERErJQCm4iIiIiIiJVSYBMREREREbFSCmwiIiIiIiJWSoFNRERERETESimwiYiIiIiIWCkFNhERERERESulwCYiIiIiImKlFNhERERERESslAKbiIiIiIiIlVJgExERERERsVIKbCIiIiIiIlZKgU1ERERERMRKKbCJiIiIiIhYKQU2ERERERERK2Vb1x2QH+flrKy67oI8Qjw8PMjLy6vrbsgjRDUjD0J1I/dLNSNSO42wiYiIiIiIWCkFNhERERERESulwCYiIiIiImKlFNhERERERESslAKbiIiIiIiIlVJgExERERERsVIKbCIiIiIiIlZKgU1ERERERMRKKbCJiIiIiIhYKQU2ERERERERK6XAJiIiIiIiYqUU2ERERERERKyUApuIiIiIiIiVUmATERERERGxUibDMIy67oSIiIiIiIhUpxG2R9iMGTPqugvyiFHNyP1SzciDUN3I/VLNyP16nGpGgU1ERERERMRKKbCJiIiIiIhYKZuoqKiouu6EPLjWrVvXdRfkEaOakfulmpEHobqR+6Wakfv1uNSMHjoiIiIiIiJipTQlUkRERERExErZ1nUH5MF8+eWXbNiwgYqKCoKDg3nmmWfquktSRyZMmICjoyNmsxkbGxvmz5/P1atXee+997h06RKenp788Y9/xNnZGYDt27fzj3/8A7PZzNixY+nSpQsA6enprFixgtLSUrp27crYsWMxmUx1eWjyEK1cuZKkpCQaNGjA4sWLAR5qnZSVlfGXv/yF9PR0XFxciIiIwMvLq86OV368mmpmy5YtJCQk4OrqCsCoUaPo1q0boJoRyMvLY8WKFRQWFmIymQgJCSE0NFTXGqlVbTWja80PGPLIuXXrljFx4kTj4sWLRllZmTF16lTj/Pnzdd0tqSPh4eFGUVFRlfdiY2ON7du3G4ZhGNu3bzdiY2MNwzCM8+fPG1OnTjVKS0uN7777zpg4caJx69YtwzAMY8aMGcbp06eNiooKY86cOUZSUtLPeyDyk0pOTjbS0tKMKVOmWN57mHWyZ88eY82aNYZhGMYnn3xi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\n", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Policy 1: Optimization A (substituting larger cabins for smaller ones allowed)\n", "valid_substitutions = [('Large', 'Small'), ('Large', 'Medium'), ('Medium', 'Small')]\n", "_, revenue_OPT_subs = optimize(demand_data, price_data, \n", " available_units, valid_substitutions)\n", " \n", "# Policy 2: Optimization B (no cabin substitutions)\n", "no_subs = []\n", "_, revenue_OPT_no_subs = optimize(demand_data, price_data, available_units, no_subs)\n", "\n", "# Policy 3: FCFS - run 100 times using randomly-ordered data and collect the results\n", "rnd.seed(2159) # This will allow the randomized results to be reproduced.\n", "FCFS_results = []\n", "for i in range(100):\n", " Fi = build_FCFS_inputs(demand_data, price_data, available_units)\n", " FCFS_results.append(solve_FCFS(*Fi)[1])\n", "\n", "# Construct a bar chart of the results:\n", "plt.style.use('ggplot')\n", "\n", "x = ['FCFS (Worst Case)', 'FCFS (Typical Case)', 'FCFS (Best Case)',\n", " 'Opt. B (No Subs.)', 'Opt. A (Subs.)']\n", "y = [min(FCFS_results), np.median(FCFS_results), max(FCFS_results), \n", " revenue_OPT_no_subs, revenue_OPT_subs]\n", "\n", "fig, ax = plt.subplots(figsize = (13, 6)) \n", "width = 0.8 # the width of the bars \n", "ind = np.arange(len(y)) # the x locations for the groups\n", "ax.barh(ind, y, width, color=\"maroon\", align='center')\n", "ax.set_yticks(ind+width/20)\n", "ax.set_yticklabels(x, minor=False)\n", "for i, v in enumerate(y):\n", " ax.text(v, i, str(\" ${:,}\".format(round(v, 2))), \n", " color='black', fontweight='bold')\n", "plt.title('Impact of Capacity Allocation Policy on Revenue')\n", "plt.xlabel('Revenue')\n", "plt.ylabel('') \n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see that FCFS will typically leave revenue on the table - and if we get unlucky, this can be a significant amount. Only in the best case will FCFS reach the maximum revenue possible under the assumption of no substitutions allowed (which is always reached by the Optimization B policy). By allowing substitutions and optimizing, however, we attain a big boost in revenues - amounting to an increase of more than 22% over the worst-case (or well over $4,000) for the weekend. Moreover, this extra revenue is earned using existing capacity without incurring any additional costs, so it is entirely profit. This comparison demonstrates the impact RM can make." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.8.3" } }, "nbformat": 4, "nbformat_minor": 4 }