{ "cells": [ { "cell_type": "code", "execution_count": 79, "metadata": {}, "outputs": [], "source": [ "# In this example notebook, we forecast 7 days in advance, for an item with:\n", "# - 1-day shelf life\n", "# - periodic daily replenishment\n", "# - Training data between 2017-05-24 and 2017-11-02\n", "# - 4 Forecasts are generated between 2017-11-03 and 2017-11-30, every 7 days\n", "# - The demand that can be satisfied with the replenishment plan over the test period (2017-11-03 to 2017-11-30) is then assessed vs. actual performance\n", "\n", "# We see the following benefits from using WasteNot, versus using an unbuffered demand predictor:\n", "# Improvement in Service Levels (91% -> 98% of demand satisfied)\n", "# Increase in Profit ($117K -> $122K, an improvement of $5K over 28 days, or c.+4%)\n", "\n", "###### Input your access_token here!######\n", "access_token = 'xxxxxxxxxxxxx'\n", "infile = 'https://raw.githubusercontent.com/bluedotthinking/wastenot-documentation/master/example_data/bdt_example_input.csv'\n", "\n", "import requests\n", "import numpy as np\n", "requests.packages.urllib3.disable_warnings() \n", "import json\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "import matplotlib.dates as mdates\n", "\n", "def generate_payload(input_df, input_dict):\n", " r = json.dumps(input_dict)\n", " return r\n", "\n", "def import_data(infile):\n", "# Import the time-series representing your historical demand - example CSV provided here:\n", " input_df = pd.read_csv(infile)\n", " return input_df\n", "\n", "def generate_forecast(access_token, payload):\n", " # Calling the WasteNot API service with the payload, check the response text\n", " url = \"https://api.bluedotthinking.com/forecast\"\n", " headers = {\n", " 'Content-Type': 'application/json',\n", " 'access_token': access_token\n", " }\n", "\n", " response = requests.request(\"POST\", url, headers=headers, data=payload, verify=True)\n", " print ('API response')\n", " print (response.text)\n", " print ('-----')\n", " return response\n", "\n", "# Plotting:\n", "# a) Historical demand for the last 30 days, \n", "# b) Units to be replenished using the replenishment_df - this includes the buffer\n", "def visualise_results(input_df, json_data):\n", "\n", " input_df['timestamp'] = pd.to_datetime(input_df['timestamp']) \n", "\n", " # Load the output payload into pandas dataframes, converting the timestamp columns into datetimes for easier plotting\n", " replenishment_df = pd.DataFrame(data={'timestamp':json_data['replenishment_timestamp'],\n", " 'replenished_units':json_data['replenishment_units']\n", " })\n", " replenishment_df['timestamp'] = pd.to_datetime(replenishment_df['timestamp'])\n", "\n", " forecast_df = pd.DataFrame(data={'timestamp': json_data['forecast_timestamp'],\n", " 'predicted_demand_units': json_data['predicted_demand_units'],\n", " 'optimised_demand_units': json_data['optimised_demand_units'],\n", " })\n", " forecast_df['timestamp'] = pd.to_datetime(forecast_df['timestamp'])\n", " plt.rc('font', size=12)\n", " fig, ax = plt.subplots(figsize=(20, 6))\n", "\n", " # Specify how our lines should look\n", " ax.plot(input_df.timestamp[-30:], input_df.demand_units[-30:], color='black', \n", " linestyle='-', label='Historical Data: Demand Units')\n", " ax.plot(forecast_df.timestamp, forecast_df.predicted_demand_units, color='blue', \n", " marker=\"o\", linestyle='--', label='Forecast: Predicted Demand Units')\n", " ax.bar(replenishment_df.timestamp, replenishment_df.replenished_units, color='green', \n", " # marker=\"o\",linestyle='--', \n", " label='Optimised Forecast: Buffered Units to Deliver')\n", "\n", " # For a less busy graph, only show the beginning of each week (Monday) as a major label\n", " ax.xaxis.set_major_locator(mdates.WeekdayLocator(byweekday=mdates.MO))\n", " ax.xaxis.set_major_formatter(mdates.DateFormatter('%d/%m'))\n", " ax.xaxis.set_minor_locator(mdates.DayLocator())\n", "\n", " ax.set_xlabel('Datetime')\n", " ax.set_ylabel('Units')\n", " ax.set_title('Historical & Optimised Forecast Demand using WasteNot')\n", "\n", " ax.grid(True)\n", " ax.legend(loc='upper left');\n", "\n", " # Visualising the tables containing the delivered units (optimised), by day\n", " print ('Replenishment Schedule & Amounts')\n", " print (replenishment_df)\n", " print ('-----')\n", " # Visualising the tables containing the forecasted demand units (unoptimised and optimised), by day\n", " print ('Forecast Dates & Amounts')\n", " print (forecast_df)\n", " print ('-----')\n" ] }, { "cell_type": "code", "execution_count": 80, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " timestamp demand_units\n", "0 2017-05-24T00:00:00 559\n", "1 2017-05-25T00:00:00 1407\n", "2 2017-05-26T00:00:00 1727\n", "3 2017-05-27T00:00:00 930\n", "4 2017-05-28T00:00:00 642\n", ".. ... ...\n", "186 2017-11-26T00:00:00 1033\n", "187 2017-11-27T00:00:00 1166\n", "188 2017-11-28T00:00:00 1078\n", "189 2017-11-29T00:00:00 724\n", "190 2017-11-30T00:00:00 568\n", "\n", "[191 rows x 2 columns]\n", "0 2017-11-02T00:00:00\n", "API response\n", "{\"status\":\"success\",\"forecast_timestamp\":[\"2017-11-03\",\"2017-11-04\",\"2017-11-05\",\"2017-11-06\",\"2017-11-07\",\"2017-11-08\",\"2017-11-09\"],\"predicted_demand_units\":[1794.0,1085.0,757.0,970.0,1071.0,1060.0,1255.0],\"optimised_demand_units\":[2070.0,1252.0,874.0,1119.0,1236.0,1223.0,1449.0],\"replenishment_timestamp\":[\"2017-11-03\",\"2017-11-04\",\"2017-11-05\",\"2017-11-06\",\"2017-11-07\",\"2017-11-08\",\"2017-11-09\"],\"replenishment_units\":[2070.0,1252.0,874.0,1119.0,1236.0,1223.0,1449.0]}\n", "-----\n", "1 2017-11-09T00:00:00\n", "API response\n", "{\"status\":\"success\",\"forecast_timestamp\":[\"2017-11-10\",\"2017-11-11\",\"2017-11-12\",\"2017-11-13\",\"2017-11-14\",\"2017-11-15\",\"2017-11-16\"],\"predicted_demand_units\":[1787.0,1080.0,758.0,966.0,1054.0,1055.0,1258.0],\"optimised_demand_units\":[2057.0,1243.0,872.0,1112.0,1213.0,1214.0,1448.0],\"replenishment_timestamp\":[\"2017-11-10\",\"2017-11-11\",\"2017-11-12\",\"2017-11-13\",\"2017-11-14\",\"2017-11-15\",\"2017-11-16\"],\"replenishment_units\":[2057.0,1243.0,872.0,1112.0,1213.0,1214.0,1448.0]}\n", "-----\n", "2 2017-11-16T00:00:00\n", "API response\n", "{\"status\":\"success\",\"forecast_timestamp\":[\"2017-11-17\",\"2017-11-18\",\"2017-11-19\",\"2017-11-20\",\"2017-11-21\",\"2017-11-22\",\"2017-11-23\"],\"predicted_demand_units\":[1797.0,1099.0,783.0,992.0,1074.0,1074.0,1271.0],\"optimised_demand_units\":[2074.0,1268.0,903.0,1145.0,1240.0,1240.0,1467.0],\"replenishment_timestamp\":[\"2017-11-17\",\"2017-11-18\",\"2017-11-19\",\"2017-11-20\",\"2017-11-21\",\"2017-11-22\",\"2017-11-23\"],\"replenishment_units\":[2074.0,1268.0,903.0,1145.0,1240.0,1240.0,1467.0]}\n", "-----\n", "3 2017-11-23T00:00:00\n", "API response\n", "{\"status\":\"success\",\"forecast_timestamp\":[\"2017-11-24\",\"2017-11-25\",\"2017-11-26\",\"2017-11-27\",\"2017-11-28\",\"2017-11-29\",\"2017-11-30\"],\"predicted_demand_units\":[1806.0,1089.0,803.0,1006.0,1074.0,1086.0,1282.0],\"optimised_demand_units\":[2081.0,1255.0,926.0,1159.0,1238.0,1251.0,1477.0],\"replenishment_timestamp\":[\"2017-11-24\",\"2017-11-25\",\"2017-11-26\",\"2017-11-27\",\"2017-11-28\",\"2017-11-29\",\"2017-11-30\"],\"replenishment_units\":[2081.0,1255.0,926.0,1159.0,1238.0,1251.0,1477.0]}\n", "-----\n" ] } ], "source": [ "input_df = pd.read_csv(infile)\n", "\n", "input_df = input_df.loc[(input_df['timestamp']<='2017-12-01')\n", " ]\n", "print (input_df)\n", "\n", "replenishment_df_list = []\n", "forecast_df_list = []\n", "\n", "n_sims = 4\n", "\n", "for n in range(n_sims):\n", " train_df = input_df[:-(7*(n_sims-n))]\n", " print (n, train_df['timestamp'].max())\n", " unit_cost =1.\n", " unit_sale_price=5.\n", " shelf_life_seconds=86400\n", " forecast_start_datetime = (pd.to_datetime(train_df['timestamp']).max() + pd.Timedelta(1, unit='d')).strftime(\"%Y-%m-%d\")\n", " forecast_end_datetime = (pd.to_datetime(train_df['timestamp']).max() + pd.Timedelta(7, unit='d')).strftime(\"%Y-%m-%d\")\n", " input_dict = {'timestamp':list(train_df['timestamp'].values),\n", " 'demand':[int(x) for x in train_df['demand_units'].values],\n", " \"cost\": unit_cost, \"sale_price\": unit_sale_price, \n", " \"shelf_life_seconds\": shelf_life_seconds,\n", " \"replenishment_schedule\": 'periodic',\n", " \"replenishment_dayofweek\":[0,1,2,3,4,5,6],\n", " \"forecast_start_datetime\":forecast_start_datetime,\n", " \"forecast_end_datetime\":forecast_end_datetime,\n", " }\n", " payload = generate_payload(train_df, input_dict)\n", " \n", " # Running forecast\n", " response = generate_forecast(access_token, payload)\n", "\n", " # Loading the response into JSON format\n", " json_data = json.loads(response.text)\n", " replenishment_df = pd.DataFrame(data={'timestamp':json_data['replenishment_timestamp'],\n", " 'replenished_units':json_data['replenishment_units']\n", " })\n", " replenishment_df['timestamp'] = pd.to_datetime(replenishment_df['timestamp'])\n", "\n", " forecast_df = pd.DataFrame(data={'timestamp': json_data['forecast_timestamp'],\n", " 'predicted_demand_units': json_data['predicted_demand_units'],\n", " 'optimised_demand_units': json_data['optimised_demand_units'],\n", " })\n", " forecast_df['timestamp'] = pd.to_datetime(forecast_df['timestamp']) \n", " replenishment_df_list.append(replenishment_df)\n", " forecast_df_list.append(forecast_df) \n", "\n", "final_forecast_df = pd.concat(forecast_df_list)\n", "final_replenishment_df = pd.concat(replenishment_df_list)" ] }, { "cell_type": "code", "execution_count": 81, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "----Actual----\n", "Actual Demand units: 32529\n", "\n", "----Unbuffered----\n", "Revenue: 149475.0\n", "Cost: 32186.0\n", "Profit: 117289.0\n", "Service Level: 91.90260997878816 %\n", "Satisfied Units: 29895\n", "\n", "----Buffered for Max Profit----\n", "Revenue: 159550.0\n", "Cost: 37106.0\n", "Profit: 122444.0\n", "Service Level: 98.0970826032156 %\n", "Satisfied Units: 31910\n", "\n", "----Buffered for 100% Service Level----\n", "Revenue: 162645\n", "Cost: 46025\n", "Profit: 116620\n", "Service Level: 100.0 %\n", "Satisfied Units: 32529\n", "\n" ] } ], "source": [ "\n", "\n", "\n", "\n", "final_replenishment_df['timestamp'] = pd.to_datetime(final_replenishment_df['timestamp']) \n", "final_forecast_df['timestamp'] = pd.to_datetime(final_forecast_df['timestamp']) \n", "input_df['timestamp'] = pd.to_datetime(input_df['timestamp']) \n", "\n", "comparison_df = pd.merge(final_forecast_df, input_df, on='timestamp', how='inner')\n", "\n", "unit_cost = 1.\n", "unit_sale_price = 5.\n", "\n", "\n", "\n", "comparison_df['unbuffered_satisfied_units'] = np.minimum(comparison_df['predicted_demand_units'], \n", " comparison_df['demand_units'])\n", "\n", "comparison_df['buffered_satisfied_units'] = np.minimum(comparison_df['optimised_demand_units'], \n", " comparison_df['demand_units'])\n", "\n", "human_buffer_multiplier = 1.43\n", "comparison_df['max_human_buffered_satisfied_units'] = np.minimum(comparison_df['predicted_demand_units']*human_buffer_multiplier, \n", " comparison_df['demand_units'])\n", "\n", "\n", "\n", "print ('----Actual----')\n", "actual_demand_units = comparison_df['demand_units'].sum()\n", "print ('Actual Demand units:',actual_demand_units)\n", "print ('')\n", "print ('----Unbuffered----')\n", "print ('Revenue:',comparison_df['unbuffered_satisfied_units'].sum()*unit_sale_price)\n", "print ('Cost:',comparison_df['predicted_demand_units'].sum()*unit_cost)\n", "print ('Profit:',(comparison_df['unbuffered_satisfied_units'].sum()*unit_sale_price) - (comparison_df['predicted_demand_units'].sum()*unit_cost))\n", "print ('Service Level:',100*comparison_df['unbuffered_satisfied_units'].sum()/actual_demand_units,'%')\n", "print ('Satisfied Units:',int(comparison_df['unbuffered_satisfied_units'].sum()))\n", "print ('')\n", "\n", "print ('----Buffered for Max Profit----')\n", "print ('Revenue:',comparison_df['buffered_satisfied_units'].sum()*unit_sale_price)\n", "print ('Cost:',comparison_df['optimised_demand_units'].sum()*unit_cost)\n", "print ('Profit:',(comparison_df['buffered_satisfied_units'].sum()*unit_sale_price) - (comparison_df['optimised_demand_units'].sum()*unit_cost))\n", "print ('Service Level:',100.*comparison_df['buffered_satisfied_units'].sum()/actual_demand_units,'%')\n", "print ('Satisfied Units:',int(comparison_df['buffered_satisfied_units'].sum()))\n", "print ('')\n", "\n", "\n", "print ('----Buffered for 100% Service Level----')\n", "\n", "print ('Revenue:',int(comparison_df['max_human_buffered_satisfied_units'].sum()*unit_sale_price))\n", "print ('Cost:',int(human_buffer_multiplier*comparison_df['predicted_demand_units'].sum()*unit_cost))\n", "print ('Profit:',(int(comparison_df['max_human_buffered_satisfied_units'].sum()*unit_sale_price)) - int(human_buffer_multiplier*comparison_df['predicted_demand_units'].sum()*unit_cost))\n", "print ('Service Level:',100*comparison_df['max_human_buffered_satisfied_units'].sum()/actual_demand_units,'%')\n", "print ('Satisfied Units:',int(comparison_df['max_human_buffered_satisfied_units'].sum()))\n", "print ('')" ] }, { "cell_type": "code", "execution_count": 82, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
| \n", " | Buffer Multiplier Value | \n", "Profit | \n", "Revenue | \n", "Cost of Goods | \n", "Service Level % | \n", "
|---|---|---|---|---|---|
| 0 | \n", "1.00 | \n", "117289.00 | \n", "149475.00 | \n", "32186.00 | \n", "91.90 | \n", "
| 1 | \n", "1.01 | \n", "118020.19 | \n", "150528.05 | \n", "32507.86 | \n", "92.55 | \n", "
| 2 | \n", "1.02 | \n", "118712.88 | \n", "151542.60 | \n", "32829.72 | \n", "93.17 | \n", "
| 3 | \n", "1.03 | \n", "119335.27 | \n", "152486.85 | \n", "33151.58 | \n", "93.75 | \n", "
| 4 | \n", "1.04 | \n", "119875.36 | \n", "153348.80 | \n", "33473.44 | \n", "94.28 | \n", "
| 5 | \n", "1.05 | \n", "120325.70 | \n", "154121.00 | \n", "33795.30 | \n", "94.76 | \n", "
| 6 | \n", "1.06 | \n", "120733.04 | \n", "154850.20 | \n", "34117.16 | \n", "95.21 | \n", "
| 7 | \n", "1.07 | \n", "121095.88 | \n", "155534.90 | \n", "34439.02 | \n", "95.63 | \n", "
| 8 | \n", "1.08 | \n", "121359.12 | \n", "156120.00 | \n", "34760.88 | \n", "95.99 | \n", "
| 9 | \n", "1.09 | \n", "121547.26 | \n", "156630.00 | \n", "35082.74 | \n", "96.30 | \n", "
| 10 | \n", "1.10 | \n", "121733.40 | \n", "157138.00 | \n", "35404.60 | \n", "96.61 | \n", "
| 11 | \n", "1.11 | \n", "121867.84 | \n", "157594.30 | \n", "35726.46 | \n", "96.89 | \n", "
| 12 | \n", "1.12 | \n", "122002.28 | \n", "158050.60 | \n", "36048.32 | \n", "97.18 | \n", "
| 13 | \n", "1.13 | \n", "122136.72 | \n", "158506.90 | \n", "36370.18 | \n", "97.46 | \n", "
| 14 | \n", "1.14 | \n", "122271.16 | \n", "158963.20 | \n", "36692.04 | \n", "97.74 | \n", "
| 15 | \n", "1.15 | \n", "122405.60 | \n", "159419.50 | \n", "37013.90 | \n", "98.02 | \n", "
| 16 | \n", "1.16 | \n", "122535.24 | \n", "159871.00 | \n", "37335.76 | \n", "98.29 | \n", "
| 17 | \n", "1.17 | \n", "122619.38 | \n", "160277.00 | \n", "37657.62 | \n", "98.54 | \n", "
| 18 | \n", "1.18 | \n", "122610.32 | \n", "160589.80 | \n", "37979.48 | \n", "98.74 | \n", "
| 19 | \n", "1.19 | \n", "122502.31 | \n", "160803.65 | \n", "38301.34 | \n", "98.87 | \n", "
| 20 | \n", "1.20 | \n", "122383.80 | \n", "161007.00 | \n", "38623.20 | \n", "98.99 | \n", "
| 21 | \n", "1.21 | \n", "122265.29 | \n", "161210.35 | \n", "38945.06 | \n", "99.12 | \n", "
| 22 | \n", "1.22 | \n", "122146.78 | \n", "161413.70 | \n", "39266.92 | \n", "99.24 | \n", "
| 23 | \n", "1.23 | \n", "121976.82 | \n", "161565.60 | \n", "39588.78 | \n", "99.34 | \n", "
| 24 | \n", "1.24 | \n", "121772.16 | \n", "161682.80 | \n", "39910.64 | \n", "99.41 | \n", "
| 25 | \n", "1.25 | \n", "121565.00 | \n", "161797.50 | \n", "40232.50 | \n", "99.48 | \n", "
| 26 | \n", "1.26 | \n", "121322.44 | \n", "161876.80 | \n", "40554.36 | \n", "99.53 | \n", "
| 27 | \n", "1.27 | \n", "121079.88 | \n", "161956.10 | \n", "40876.22 | \n", "99.58 | \n", "
| 28 | \n", "1.28 | \n", "120837.32 | \n", "162035.40 | \n", "41198.08 | \n", "99.63 | \n", "
| 29 | \n", "1.29 | \n", "120580.41 | \n", "162100.35 | \n", "41519.94 | \n", "99.67 | \n", "
| 30 | \n", "1.30 | \n", "120297.70 | \n", "162139.50 | \n", "41841.80 | \n", "99.69 | \n", "
| 31 | \n", "1.31 | \n", "120014.99 | \n", "162178.65 | \n", "42163.66 | \n", "99.71 | \n", "
| 32 | \n", "1.32 | \n", "119732.28 | \n", "162217.80 | \n", "42485.52 | \n", "99.74 | \n", "
| 33 | \n", "1.33 | \n", "119449.57 | \n", "162256.95 | \n", "42807.38 | \n", "99.76 | \n", "
| 34 | \n", "1.34 | \n", "119166.86 | \n", "162296.10 | \n", "43129.24 | \n", "99.79 | \n", "
| 35 | \n", "1.35 | \n", "118884.15 | \n", "162335.25 | \n", "43451.10 | \n", "99.81 | \n", "
| 36 | \n", "1.36 | \n", "118601.44 | \n", "162374.40 | \n", "43772.96 | \n", "99.83 | \n", "
| 37 | \n", "1.37 | \n", "118318.73 | \n", "162413.55 | \n", "44094.82 | \n", "99.86 | \n", "
| 38 | \n", "1.38 | \n", "118036.02 | \n", "162452.70 | \n", "44416.68 | \n", "99.88 | \n", "
| 39 | \n", "1.39 | \n", "117753.31 | \n", "162491.85 | \n", "44738.54 | \n", "99.91 | \n", "
| 40 | \n", "1.40 | \n", "117470.60 | \n", "162531.00 | \n", "45060.40 | \n", "99.93 | \n", "
| 41 | \n", "1.41 | \n", "117187.89 | \n", "162570.15 | \n", "45382.26 | \n", "99.95 | \n", "
| 42 | \n", "1.42 | \n", "116905.18 | \n", "162609.30 | \n", "45704.12 | \n", "99.98 | \n", "
| 43 | \n", "1.43 | \n", "116619.02 | \n", "162645.00 | \n", "46025.98 | \n", "100.00 | \n", "
| 44 | \n", "1.44 | \n", "116297.16 | \n", "162645.00 | \n", "46347.84 | \n", "100.00 | \n", "
| 45 | \n", "1.45 | \n", "115975.30 | \n", "162645.00 | \n", "46669.70 | \n", "100.00 | \n", "
| 46 | \n", "1.46 | \n", "115653.44 | \n", "162645.00 | \n", "46991.56 | \n", "100.00 | \n", "
| 47 | \n", "1.47 | \n", "115331.58 | \n", "162645.00 | \n", "47313.42 | \n", "100.00 | \n", "
| 48 | \n", "1.48 | \n", "115009.72 | \n", "162645.00 | \n", "47635.28 | \n", "100.00 | \n", "
| 49 | \n", "1.49 | \n", "114687.86 | \n", "162645.00 | \n", "47957.14 | \n", "100.00 | \n", "
| 50 | \n", "1.50 | \n", "114366.00 | \n", "162645.00 | \n", "48279.00 | \n", "100.00 | \n", "