{ "nbformat": 4, "nbformat_minor": 0, "metadata": { "colab": { "provenance": [] }, "kernelspec": { "name": "python3", "display_name": "Python 3" }, "language_info": { "name": "python" } }, "cells": [ { "cell_type": "markdown", "source": [ "# Case 2 - Student Notebook" ], "metadata": { "id": "oHWpkTqMeBMq" } }, { "cell_type": "markdown", "source": [ "## Imports and data loading" ], "metadata": { "id": "BhR7Z_UqeEgm" } }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "HQwlyagxfXRU" }, "outputs": [], "source": [ "import io\n", "import pandas as pd\n", "import numpy as np\n", "import seaborn as sns\n", "from google.colab import files\n", "from datetime import datetime, timedelta" ] }, { "cell_type": "code", "source": [ "# This avoids scientific notation on millions and larger\n", "pd.set_option('display.float_format', lambda x: '%.2f' % x)" ], "metadata": { "id": "wsv3gEB9qmp6" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "# Upload files from your computer here\n", "# Run the cell and click the \"Browse\" button to upload the provided CSV \n", "# files\n", "uploaded = files.upload()" ], "metadata": { "id": "4psao7htcAwr" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "# Read the files as pandas dataframes and print them so you can check that the\n", "# process went fine\n", "\n", "served_orders = pd.read_csv(io.BytesIO(uploaded['served_orders.csv']))\n", "sourcing_events = pd.read_csv(io.BytesIO(uploaded['sourcing_events.csv']))\n", "\n", "for table in (served_orders, sourcing_events):\n", " print(table.head())" ], "metadata": { "id": "X8N0PZ4qcOls" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "## Analysis" ], "metadata": { "id": "GKty74ZfuBEG" } }, { "cell_type": "code", "source": [ "# You can use this space to analyse the provided data as you see fit." ], "metadata": { "id": "CuYSBC2auDIG" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "## Provided code\n" ], "metadata": { "id": "KM5HVJiIfYPC" } }, { "cell_type": "code", "source": [ "# This cell includes provided code to run simulations.\n", "# You do not need to understand the internals of this code. Feel free to just\n", "# run it and move forward.\n", "# The next section explains how you can call this code to run your simulations.\n", "\n", "base = datetime(2022,1,1)\n", "dates_in_2022 = [base + timedelta(days=x) for x in range(365)]\n", "\n", "class SimulationResult:\n", "\n", " def __init__(self, stock_states, demand_by_day, sourcing_events):\n", " self.stock_states = stock_states\n", " self.demand_by_day = demand_by_day\n", " self.sourcing_events = sourcing_events\n", "\n", " def plot_stock_history(self):\n", " sns.lineplot(x=dates_in_2022, y=self.stock_states)\n", "\n", " def plot_stock_distribution(self):\n", " sns.histplot(x=self.stock_states, kde=True)\n", "\n", " def service_level(self):\n", " return (self.stock_states > 0 ).astype(int).mean()\n", "\n", " def stock_level_summary(self):\n", " print(\n", " pd.DataFrame(self.stock_states).describe()\n", " ) \n", " \n", " def mean_stock_level(self):\n", " return self.stock_states.mean()\n", "\n", " def median_stock_level(self):\n", " return np.median(self.stock_states)\n", "\n", " def stdev_stock_level(self):\n", " return self.stock_states.std()\n", "\n", " def mean_demand(self):\n", " return self.demand_by_day.mean()\n", " \n", " def number_of_purchase_orders_placed(self):\n", " return len(self.sourcing_events)\n", "\n", "\n", "class SimulationConfig:\n", "\n", " def __init__(\n", " self, \n", " starting_stock_raw_beans,\n", " starting_stock_roasted_beans,\n", " starting_stock_decaff_beans, \n", " demand_generator_raw_beans,\n", " demand_generator_roasted_beans,\n", " demand_generator_decaff_beans,\n", " lead_time_generator_raw_beans, \n", " purchaser,\n", " production_line_switcher,\n", " roasted_beans_daily_production,\n", " decaff_beans_daily_production\n", " ):\n", " self.starting_stock_raw_beans = starting_stock_raw_beans\n", " self.starting_stock_roasted_beans = starting_stock_roasted_beans\n", " self.starting_stock_decaff_beans = starting_stock_decaff_beans\n", " self.demand_generator_raw_beans = demand_generator_raw_beans\n", " self.demand_generator_roasted_beans = demand_generator_roasted_beans\n", " self.demand_generator_decaff_beans = demand_generator_decaff_beans\n", " self.lead_time_generator_raw_beans = lead_time_generator_raw_beans\n", " self.purchaser = purchaser\n", " self.production_line_switcher = production_line_switcher\n", " self.roasted_beans_daily_production = roasted_beans_daily_production\n", " self.decaff_beans_daily_production = decaff_beans_daily_production\n", "\n", "class PurchaseOrder:\n", " \n", " def __init__(self, amount, request_date, delivery_date):\n", " self.amount = amount\n", " self.request_date = request_date\n", " self.delivery_date = delivery_date\n", "\n", " def __repr__(self):\n", " return f\"Order of {self.amount:.0f}, requested on {self.request_date}, delivery on {self.delivery_date}.\"\n", "\n", "class ProductionLine:\n", "\n", " def __init__(self, products_and_rates, starting_product=None):\n", " self.products_and_rates = products_and_rates\n", " self.on_the_line = starting_product\n", " self.next_on_the_line = None\n", " self.days_on_current_batch = 0\n", "\n", " def tick(self):\n", " if self.on_the_line in self.products_and_rates:\n", " self.days_on_current_batch += 1\n", " return self.products_and_rates[self.on_the_line]\n", "\n", " if self.on_the_line is None and self.next_on_the_line is None:\n", " self.days_on_current_batch += 1\n", " return 0\n", " if self.on_the_line is None:\n", " self.on_the_line = self.next_on_the_line\n", " self.next_on_the_line = None\n", " self.days_on_current_batch = 0\n", " return 0\n", " \n", " def switch_to_product(self, next_product):\n", " self.on_the_line = None\n", " self.next_on_the_line = next_product\n", " self.days_on_current_batch = -1\n", "\n", "\n", "class Simulation:\n", " \n", " def __init__(self, config: SimulationConfig, verbose=False):\n", " self._config = config\n", " self.verbose = verbose\n", "\n", " def run(self):\n", "\n", " stock_raw_beans = np.array([self._config.starting_stock_raw_beans])\n", " stock_roasted_beans = np.array([self._config.starting_stock_roasted_beans])\n", " stock_decaff_beans = np.array([self._config.starting_stock_decaff_beans])\n", "\n", " opened_orders = []\n", " ongoing_orders = {}\n", "\n", " demand_by_day_raw_beans = np.array(list())\n", " demand_by_day_roasted_beans = np.array(list())\n", " demand_by_day_decaff_beans = np.array(list())\n", "\n", " production_line = ProductionLine(\n", " products_and_rates={\n", " \"roasted_beans\": self._config.roasted_beans_daily_production,\n", " \"decaff_beans\": self._config.decaff_beans_daily_production\n", " },\n", " starting_product=\"roasted_beans\"\n", " )\n", "\n", " production_line_switcher = self._config.production_line_switcher\n", " \n", " for day in dates_in_2022:\n", "\n", " # General\n", " if self.verbose:\n", " print(\"-------------------------\")\n", " print(f\"Simulating day: {day}\")\n", " current_stock_raw_beans = stock_raw_beans[-1]\n", " current_stock_roasted_beans = stock_roasted_beans[-1]\n", " current_stock_decaff_beans = stock_decaff_beans[-1]\n", "\n", " # Generate demand\n", " if self.verbose:\n", " print(f\"Starting stock raw beans: {current_stock_raw_beans:.0f}\")\n", " print(f\"Starting stock roasted beans: {current_stock_roasted_beans:.0f}\")\n", " print(f\"Starting stock decaff beans: {current_stock_decaff_beans:.0f}\")\n", " demand_for_this_day_raw_beans = self._config.demand_generator_raw_beans()\n", " demand_for_this_day_roasted_beans = self._config.demand_generator_roasted_beans()\n", " demand_for_this_day_decaff_beans = self._config.demand_generator_decaff_beans()\n", " if self.verbose:\n", " print(f\"Generated raw beans demand for today: {demand_for_this_day_raw_beans:.0f}\")\n", " print(f\"Generated roasted beans demand for today: {demand_for_this_day_roasted_beans:.0f}\")\n", " print(f\"Generated decaff beans demand for today: {demand_for_this_day_decaff_beans:.0f}\")\n", " demand_by_day_raw_beans = np.append(demand_by_day_raw_beans, [demand_for_this_day_raw_beans])\n", " demand_by_day_roasted_beans = np.append(demand_by_day_roasted_beans, [demand_for_this_day_roasted_beans])\n", " demand_by_day_decaff_beans = np.append(demand_by_day_decaff_beans, [demand_for_this_day_decaff_beans])\n", "\n", "\n", " # Receive orders and place orders\n", " raw_beans_received_this_day = 0\n", " if day in ongoing_orders:\n", " order_delivered_today = ongoing_orders.pop(day)\n", " raw_beans_received_this_day = order_delivered_today.amount\n", " if self.verbose:\n", " print(f\"Raw beans received today: {raw_beans_received_this_day:.0f}\")\n", " \n", " order_to_make = self._config.purchaser(\n", " day, \n", " current_stock_raw_beans, \n", " ongoing_orders,\n", " self._config.lead_time_generator_raw_beans\n", " )\n", " if order_to_make:\n", " if self.verbose:\n", " print(f\"Placing a new order: {order_to_make}\")\n", " opened_orders.append(order_to_make)\n", " ongoing_orders[order_to_make.delivery_date] = order_to_make\n", "\n", "\n", " # Decide on production today and produce whatever gets produced or wait during the changeover\n", "\n", " print(f\"Product on the line: {production_line.on_the_line}\")\n", "\n", " production_line_switcher(\n", " production_line,\n", " {\n", " \"raw_beans_stock\": current_stock_raw_beans,\n", " \"roasted_beans_stock\": current_stock_roasted_beans,\n", " \"decaff_beans_stock\": current_stock_decaff_beans\n", " }\n", " )\n", "\n", " if production_line.on_the_line == \"roasted_beans\":\n", " roasted_beans_produced_this_day = production_line.tick()\n", " decaff_beans_produced_this_day = 0\n", " if production_line.on_the_line == \"decaff_beans\":\n", " roasted_beans_produced_this_day = 0\n", " decaff_beans_produced_this_day = production_line.tick()\n", " if production_line.on_the_line not in (\"roasted_beans\", \"decaff_beans\"):\n", " production_line.tick()\n", " roasted_beans_produced_this_day = 0\n", " decaff_beans_produced_this_day = 0\n", "\n", " raw_beans_consumed_in_production = roasted_beans_produced_this_day + decaff_beans_produced_this_day\n", " \n", " if self.verbose:\n", " print(f\"Roasted beans produced today: {roasted_beans_produced_this_day}\")\n", " print(f\"Decaff beans produced today: {decaff_beans_produced_this_day}\")\n", " print(f\"Product {production_line.on_the_line} has been on the line for {production_line.days_on_current_batch} days.\")\n", "\n", " # Update stocks with the changes of the day\n", "\n", " current_stock_raw_beans = (\n", " current_stock_raw_beans + \n", " raw_beans_received_this_day - \n", " demand_for_this_day_raw_beans -\n", " raw_beans_consumed_in_production\n", " )\n", " stock_raw_beans = np.append(stock_raw_beans, [current_stock_raw_beans])\n", " current_stock_roasted_beans = current_stock_roasted_beans + roasted_beans_produced_this_day - demand_for_this_day_roasted_beans\n", " stock_roasted_beans = np.append(stock_roasted_beans, [current_stock_roasted_beans])\n", " current_stock_decaff_beans = current_stock_decaff_beans + decaff_beans_produced_this_day - demand_for_this_day_decaff_beans\n", " stock_decaff_beans = np.append(stock_decaff_beans, [current_stock_decaff_beans])\n", " \n", " # Remove starting stock\n", " stock_raw_beans = np.delete(stock_raw_beans, 0) \n", " stock_roasted_beans = np.delete(stock_roasted_beans, 0)\n", " stock_decaff_beans = np.delete(stock_decaff_beans, 0)\n", " \n", " raw_beans_results = SimulationResult(\n", " stock_states=stock_raw_beans, \n", " demand_by_day=demand_by_day_raw_beans, \n", " sourcing_events=opened_orders\n", " )\n", " roasted_beans_results = SimulationResult(\n", " stock_states=stock_roasted_beans, \n", " demand_by_day=demand_by_day_roasted_beans, \n", " sourcing_events=None\n", " )\n", " decaff_beans_results = SimulationResult(\n", " stock_states=stock_decaff_beans, \n", " demand_by_day=demand_by_day_decaff_beans, \n", " sourcing_events=opened_orders\n", " )\n", "\n", "\n", " return raw_beans_results, roasted_beans_results, decaff_beans_results\n", " \n", "\n", "\n" ], "metadata": { "id": "cGcEzAIDfa8s" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "## Usage Example" ], "metadata": { "id": "Xul3y3LpYKiY" } }, { "cell_type": "code", "source": [ "# Read this block carefully to understand how to prepare parameters,\n", "# run simulations and fetch the results.\n", "\n", "# These are the steps we will follow:\n", "# 1. Prepare a purchaser function\n", "# 2. Prepare a production line management function\n", "# 3. Assemble a simulation configuration with your parameters and assumptions\n", "# 4. Run a simulation\n", "# 5. Fetch results\n", "\n", "# AN IMPORTANT NOTE: all numbers in this example are random. You will most \n", "# surely have to modify them and fit them to the info and data that has been\n", "# provided to you.\n", "\n", "###\n", "# 1. Prepare a purchaser function\n", "###\n", "\n", "# The purchase function handles the decisions of whether to buy more raw coffee\n", "# beans to send to Diemen, and how much to buy. It gets called once per simulated\n", "# day, so you have an oportunity to place orders each day.\n", "\n", "\n", "# You can name your function whatever you like, but the arguments should have\n", "# the same names and order as show below.\n", "def a_simple_purchaser(\n", " day, # The current day\n", " current_stock, # The level of raw beans stock on that day\n", " ongoing_orders, # A dictionary with the open purchase orders\n", " lead_time_generator # The same lead time generator you pass to the Simulation Config\n", " ):\n", " # Your code goes here. You can make any logic you want. Just make sure to return\n", " # None if you don't want to place an order and to return a PurchaseOrder when\n", " # you want to buy. The policies below are a simple example to inspire you: you\n", " # definitely want to modify the numbers and/or followed logic.\n", " \n", " if ongoing_orders or current_stock > 15_000_000:\n", " # If we are already waiting for an order to arrive or we have enough stock\n", " # we don't request more goods.\n", " return None\n", "\n", " if current_stock <= 15_000_000:\n", " # If the stock is going low, we request more.\n", " return PurchaseOrder(\n", " amount=15_000_000, # The amount to order. This is the only bit you change.\n", " request_date=day, # Always copy paste this.\n", " delivery_date=day + timedelta(days=lead_time_generator()) # Always copy paste this.\n", " )\n", "\n", "\n", "###\n", "# 2. Prepare a production line management function\n", "### \n", "\n", "# The line manager function handles the decision of whether the production line\n", "# should change to a different product (or no product at all). It gets called \n", "# once per day.\n", "\n", "# You can name your function whatever you like, but the arguments should have\n", "# the same names and order as show below.\n", "def a_simple_line_manager(\n", " production_line, # Details about the production line\n", " stock_by_product # A summary of the stock that updates each day\n", "):\n", " # Your code goes here. You can make any logic you want. Just make sure to \n", " # switch to None if you don't want to change the product on the line. If you want\n", " # to switch the product on the line, call production_line.switch_to_product(\"product name\"). \n", " # The policies below are a simple example to inspire you: you definitely want \n", " # to modify the numbers and/or followed logic.\n", " \n", " # If the current product has been less than 21 days on the line, we don't \n", " # change anything.\n", " if production_line.days_on_current_batch < 14:\n", " return\n", " \n", " if (\n", " stock_by_product[\"roasted_beans_stock\"] > 2_000_000 and \n", " stock_by_product[\"decaff_beans_stock\"] > 2_000_000 and\n", " production_line.on_the_line is not None\n", " ):\n", " # If we have plenty of stock and we are still producing, we stop the line\n", " # by switching to None.\n", " production_line.switch_to_product(None)\n", " print(\"Too much inventory. I'm switching to None!\")\n", " return\n", "\n", " if (\n", " stock_by_product[\"roasted_beans_stock\"] > 2_000_000 and \n", " stock_by_product[\"decaff_beans_stock\"] > 2_000_000 and\n", " production_line.on_the_line is None\n", " ):\n", " # If we have plenty of stock and we are stopped, we remain stopped.\n", " print(\"Too much inventory. Staying in None!\")\n", " return\n", "\n", " \n", " if (\n", " (\n", " max(stock_by_product[\"roasted_beans_stock\"], 1) / max(stock_by_product[\"decaff_beans_stock\"], 1) < 2\n", " ) and (\n", " production_line.on_the_line != \"roasted_beans\"\n", " )\n", " ):\n", " # If we are not producing roasted beans, and there is less than 2kg of roasted beans\n", " # for each kg of decaff beans in stock, we switch to roasted beans.\n", " production_line.switch_to_product(\"roasted_beans\")\n", " print(\"I'm switching to roasted!\")\n", " return\n", "\n", " if (\n", " (\n", " max(stock_by_product[\"roasted_beans_stock\"], 1) / max(stock_by_product[\"decaff_beans_stock\"], 1) > 2\n", " ) and (\n", " production_line.on_the_line != \"decaff_beans\"\n", " )\n", " ):\n", " # If we are not producing decaff beans, and there is more than 2kg of roasted beans\n", " # for each kg of decaff beans in stock, we switch to decaff beans.\n", " production_line.switch_to_product(\"decaff_beans\")\n", " print(\"I'm switching to decaff!\")\n", " return\n", "\n", "\n", "###\n", "# 3. Assemble a simulation configuration\n", "### \n", "\n", "# In order to run as Simulation, you must prepare a config. The config allows \n", "# you to pass in your policies as well as to modify different parts of the \n", "# simulation so you can recreate reality accurately. You can find each argument\n", "# explained below.\n", "\n", "an_example_config = SimulationConfig(\n", " starting_stock_raw_beans=20_000, \n", " # ^ How many kgs of raw coffee beans does the warehouse start with.\n", " starting_stock_roasted_beans=1_000,\n", " # ^ How many kgs of roasted coffee beans does the warehouse start with. \n", " starting_stock_decaff_beans=500,\n", " # ^ How many kgs of decaff coffee beans does the warehouse start with.\n", " demand_generator_raw_beans=lambda: np.random.poisson(12/7) * np.random.normal(300, 50),\n", " # ^ A function that generates demand for raw beans. This gets called daily.\n", " # The return units should be kilograms.\n", " demand_generator_roasted_beans=lambda: np.random.poisson(2/7) * np.random.triangular(200, 250, 300),\n", " # ^ Same as above but for roasted beans.\n", " demand_generator_decaff_beans=lambda: np.random.poisson(2/7) * np.random.triangular(200, 250, 300),\n", " # ^ Same as above but for decaff beans.\n", " lead_time_generator_raw_beans=lambda: int(np.random.normal(10, 1)), \n", " # ^ A function that generates the lead times for ships going from Latin America\n", " # to Diemen. This gets called everytime you place an order to get more raw\n", " # beans. Should return an integer number of days. \n", " purchaser=a_simple_purchaser,\n", " # ^ Here you pass your purchasing policy function.\n", " production_line_switcher=a_simple_line_manager,\n", " # ^ Here you pass your production line management policy.\n", " roasted_beans_daily_production=400,\n", " # ^ The capacity of normal bean roasting, in kgs per day.\n", " decaff_beans_daily_production=400\n", " # ^ The capacity of decaff bean roasting, in kgs per day.\n", ")\n", "\n", "###\n", "# 4. Run a simulation\n", "###\n", "\n", "# The Simulation class is the code that actually runs a simulation. It takes a \n", "# SimulationConfig as an input, and returns a SimulationResult as an output.\n", "\n", "example_simulation = Simulation(\n", " config=an_example_config, # The config you build goes here\n", " verbose=True # This shows daily details. Turn to False if you don't want to see them.\n", ")\n", "\n", "# Let's run the simulation and store the results\n", "raw_results, roasted_results, decaff_results = example_simulation.run()\n", "\n", "\n", "###\n", "# 5. Fetch results\n", "###\n", "\n", "# The simulation will provide you back with three SimulationResult variables, \n", "# one for each type of coffee bean. You can use this objects to make some quick\n", "# plots, and also to access the raw data about the stock and demand throughout\n", "# the simulation for each product.\n", "\n", "# In the next cells, you will find a few examples on how to explore these \n", "# results\n" ], "metadata": { "id": "cYHFWRrm3-ns" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "raw_results.plot_stock_history()\n", "roasted_results.plot_stock_history()\n", "decaff_results.plot_stock_history()" ], "metadata": { "id": "iXeAFhIp7848" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "raw_results.plot_stock_distribution()\n", "roasted_results.plot_stock_distribution()\n", "decaff_results.plot_stock_distribution()" ], "metadata": { "id": "RLRHLyVOiysH" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "for product, result in ((\"raw\", raw_results), (\"roasted\", roasted_results), (\"decaff\", decaff_results)):\n", " print(f\"{product} beans service level: {result.service_level()}\")\n", " print(f\"{product} beans mean stock: {result.mean_stock_level()}\")\n", " print(f\"{product} beans median stock: {result.median_stock_level()}\")\n", " print(f\"{product} beans stdev stock: {result.stdev_stock_level()}\")\n", " print(f\"{product} beans mean demand: {result.mean_demand()}\")" ], "metadata": { "id": "Cr-Txnk9jFJK" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "for product, result in ((\"raw\", raw_results), (\"roasted\", roasted_results), (\"decaff\", decaff_results)):\n", " print(f\"Daily stock distribution summary for {product} beans:\")\n", " print(result.stock_level_summary())" ], "metadata": { "id": "z1Ixv0bHqD65" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "# Finally, you can access the raw data with the following attributes\n", "print(roasted_results.stock_states)\n", "print(roasted_results.demand_by_day)" ], "metadata": { "id": "-pw38uGpm-Aa" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "# Your turn\n", "\n", "Run the previous cells in order to load the required packages and code. Once you\n", "have done that, you can start building your own code below." ], "metadata": { "id": "qSOiFi9OmgUR" } }, { "cell_type": "code", "source": [], "metadata": { "id": "fOZ7KhC5rgYc" }, "execution_count": null, "outputs": [] } ] }