diff --git a/jupyter/Cloud Pak for Data v3.5.x/CopyAndSolveScenarios.ipynb b/jupyter/Cloud Pak for Data v3.5.x/CopyAndSolveScenarios.ipynb index f5b41bc..99a4b8a 100644 --- a/jupyter/Cloud Pak for Data v3.5.x/CopyAndSolveScenarios.ipynb +++ b/jupyter/Cloud Pak for Data v3.5.x/CopyAndSolveScenarios.ipynb @@ -1,255 +1,271 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To use this Notebook you have to import the StaffPlanning example (New Decision Optimization Model/From File)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Client\n", - "\n", - "Create a DODS client to connect to initial scenario." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from dd_scenario import *\n", - "\n", - "client = Client()\n", - "decision = client.get_model_builder(name=\"StaffPlanning\")\n", - "scenario = decision.get_scenario(name=\"Scenario 1\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Global parameters\n", - "\n", - "The number of days and number of periods per day:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "N_DAYS = 2\n", - "N_PERIODS_PER_DAY = 24*4\n", - "N_PERIODS = N_DAYS * N_PERIODS_PER_DAY" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Random generator\n", - "\n", - "A method to generate the random demand for the given number of days and periods. " - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "import random\n", - "import numpy as np\n", - "import pandas as pd\n", - "\n", - "def random_demand( b_size ):\n", - " rs = []\n", - " for d in range(N_DAYS):\n", - " # Morning\n", - " p1 = random.uniform(0.2, 0.4)\n", - " s1 = int(random.uniform(b_size*0.5, b_size*1.5))\n", - " rs.append(np.random.binomial(n=N_PERIODS_PER_DAY, p=p1, size=s1) + d*N_PERIODS_PER_DAY)\n", - " # Afternoon\n", - " p2 = random.uniform(0.6, 0.8)\n", - " s2 = int(random.uniform(b_size*0.5, b_size*1.5))\n", - " rs.append(np.random.binomial(n=N_PERIODS_PER_DAY, p=p2, size=s2) + d*N_PERIODS_PER_DAY)\n", - " # Rest of day\n", - " s3 = int(random.uniform(b_size*0.4, b_size*0.7))\n", - " e = np.array([ random.randint(int(d*N_PERIODS_PER_DAY + 0.2*N_PERIODS_PER_DAY), int(d*N_PERIODS_PER_DAY + 0.8*N_PERIODS_PER_DAY)) for i in range(s3) ])\n", - " #print(e)\n", - " rs.append(e)\n", - " #print(rs)\n", - " s = np.concatenate(rs)\n", - " #print(s)\n", - " g_arrivals = pd.DataFrame(data=s, columns=['value'])\n", - " _demands = [0 for i in range(0, N_PERIODS+1)]\n", - " for t in s:\n", - " _demands[t] = _demands[t] +1\n", - " demands = pd.DataFrame(data= [(t, _demands[t]) for t in range(N_PERIODS)], columns = ['period', 'demand'])\n", - " return demands\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The number of scenarios you want to generate and solve:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "N_SCENARIOS = 5" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "When copying the scenario, copy the input data, the model and the solution if any.\n", - "\n", - "Then attach new randomly generated data and solve.\n", - "\n", - "Grab the solution to perform some multi scenario reporting in this notebook." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Copy 01\n", - " Deleting old...\n", - " Copying from original scenario...\n", - " Generating new demand...\n", - " Solving...\n", - "[2019-10-17T14:25:55Z, INFO] Reduced MIP has 0 binaries, 304 generals, 0 SOSs, and 0 indicators.\n", - "[2019-10-17T14:25:55Z, INFO] Presolve time = 0.00 sec. (1.52 ticks)\n", - "[2019-10-17T14:25:55Z, INFO] MIP emphasis: balance optimality and feasibility.\n", - "[2019-10-17T14:25:55Z, INFO] MIP search method: dynamic search.\n", - "[2019-10-17T14:25:55Z, INFO] Parallel mode: none, using 1 thread.\n", - "[2019-10-17T14:25:55Z, INFO] Root relaxation solution time = 0.00 sec. (0.89 ticks)\n", - "[2019-10-17T14:25:55Z, INFO] \n", - "[2019-10-17T14:25:55Z, INFO] Nodes Cuts/\n", - "[2019-10-17T14:25:55Z, INFO] Node Left Objective IInf Best Integer Best Bound ItCnt Gap\n", - "[2019-10-17T14:25:55Z, INFO] * 0+ 0 6460.0000 1000.0000 84.52%\n", - "[2019-10-17T14:25:55Z, INFO] * 0 0 integral 0 5740.0000 5740.0000 79 0.00%\n", - "[2019-10-17T14:25:55Z, INFO] Elapsed time = 0.05 sec. (11.72 ticks, tree = 0.00 MB\n", - "[2019-10-17T14:25:55Z, INFO] , solutions = 2\n", - "[2019-10-17T14:25:55Z, INFO] )\n", - "[2019-10-17T14:25:55Z, INFO] \n", - "[2019-10-17T14:25:55Z, INFO] Root node processing (before b&c):\n", - "[2019-10-17T14:25:55Z, INFO] Real time = 0.05 sec. (11.74 ticks)\n", - "[2019-10-17T14:25:55Z, INFO] Sequential b&c:\n", - "[2019-10-17T14:25:55Z, INFO] Real time = 0.00 sec. (0.00 ticks)\n", - "[2019-10-17T14:25:55Z, INFO] ------------\n", - "[2019-10-17T14:25:55Z, INFO] Total (root+branch&cut) = 0.05 sec. (11.74 ticks)\n", - "[2019-10-17T14:25:55Z, INFO] Feasible 5740\n", - "[2019-10-17T14:25:55Z, INFO] starts.csv\n", - "[2019-10-17T14:25:55Z, INFO] works.csv\n", - "[2019-10-17T14:25:55Z, INFO] nr.csv\n", - " Grabbing solution kpis...\n", - "Copy 02\n", - " Deleting old...\n", - " Copying from original scenario...\n", - " Generating new demand...\n", - " Solving...\n", - " Grabbing solution kpis...\n", - "Copy 03\n", - " Deleting old...\n", - " Copying from original scenario...\n", - " Generating new demand...\n", - " Solving...\n", - " Grabbing solution kpis...\n", - "Copy 04\n", - " Deleting old...\n", - " Copying from original scenario...\n", - " Generating new demand...\n", - " Solving...\n", - " Grabbing solution kpis...\n", - "Copy 05\n", - " Deleting old...\n", - " Copying from original scenario...\n", - " Generating new demand...\n", - " Solving...\n", - " Grabbing solution kpis...\n", - "Done!\n" - ] - } - ], - "source": [ - "all_kpis = pd.DataFrame()\n", - "\n", - "for i in range(1, N_SCENARIOS+1):\n", - " sc_name = \"Copy %02d\" % (i)\n", - " print(sc_name)\n", - " copy = decision.get_scenario(name=sc_name)\n", - " if (copy != None):\n", - " print(\" Deleting old...\")\n", - " decision.delete_container(copy)\n", - " print(\" Copying from original scenario...\") \n", - " copy = scenario.copy(sc_name)\n", - " print(\" Generating new demand...\")\n", - " df_demands = random_demand(200)\n", - " copy.add_table_data(\"demands\", df_demands, category='input')\n", - " print(\" Solving...\")\n", - " copy.solve()\n", - " print(\" Grabbing solution kpis...\")\n", - " kpis = copy.get_table_data('kpis')\n", - " kpis['scenario'] = sc_name\n", - " mk = [[ kpis.iloc[0]['Value'], \"%02d\" % (kpis.iloc[1]['Value']), sc_name, \"%02d\" % (kpis.iloc[2]['Value'])]]\n", - " my_kpis = pd.DataFrame(data=mk, columns=['cost','fix','scenario','temp'])\n", - " copy.add_table_data('my_kpis', data=my_kpis, category='output')\n", - " all_kpis = all_kpis.append(kpis)\n", - " \n", - "print(\"Done!\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3.6", - "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.6.8" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "To use this Notebook you have to import the StaffPlanning example (New Decision Optimization Model/From File)\n" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Client\n", + "\n", + "Create a DODS client to connect to initial scenario." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from dd_scenario import *\n", + "\n", + "client = Client()\n", + "decision = client.get_model_builder(name=\"StaffPlanning\")\n", + "scenario = decision.get_scenario(name=\"Scenario 1\")\n" + ], + "execution_count": null, + "outputs": [] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Global parameters\n", + "\n", + "The number of days and number of periods per day:" + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "N_DAYS = 2\n", + "N_PERIODS_PER_DAY = 24*4\n", + "N_PERIODS = N_DAYS * N_PERIODS_PER_DAY" + ], + "execution_count": null, + "outputs": [] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Random generator\n", + "\n", + "A method to generate the random demand for the given number of days and periods. " + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import random\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "def random_demand( b_size ):\n", + " rs = []\n", + " for d in range(N_DAYS):\n", + " # Morning\n", + " p1 = random.uniform(0.2, 0.4)\n", + " s1 = int(random.uniform(b_size*0.5, b_size*1.5))\n", + " rs.append(np.random.binomial(n=N_PERIODS_PER_DAY, p=p1, size=s1) + d*N_PERIODS_PER_DAY)\n", + " # Afternoon\n", + " p2 = random.uniform(0.6, 0.8)\n", + " s2 = int(random.uniform(b_size*0.5, b_size*1.5))\n", + " rs.append(np.random.binomial(n=N_PERIODS_PER_DAY, p=p2, size=s2) + d*N_PERIODS_PER_DAY)\n", + " # Rest of day\n", + " s3 = int(random.uniform(b_size*0.4, b_size*0.7))\n", + " e = np.array([ random.randint(int(d*N_PERIODS_PER_DAY + 0.2*N_PERIODS_PER_DAY), int(d*N_PERIODS_PER_DAY + 0.8*N_PERIODS_PER_DAY)) for i in range(s3) ])\n", + " #print(e)\n", + " rs.append(e)\n", + " #print(rs)\n", + " s = np.concatenate(rs)\n", + " #print(s)\n", + " g_arrivals = pd.DataFrame(data=s, columns=['value'])\n", + " _demands = [0 for i in range(0, N_PERIODS+1)]\n", + " for t in s:\n", + " _demands[t] = _demands[t] +1\n", + " demands = pd.DataFrame(data= [(t, _demands[t]) for t in range(N_PERIODS)], columns = ['period', 'demand'])\n", + " return demands\n" + ], + "execution_count": null, + "outputs": [] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "The number of scenarios you want to generate and solve:" + }, + { + "metadata": {}, + "cell_type": "code", + "source": "N_SCENARIOS = 5", + "execution_count": null, + "outputs": [] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\n", + "When copying the scenario, copy the input data, the model and the solution if any.\n", + "\n", + "Then attach new randomly generated data and solve.\n", + "\n", + "Grab the solution to perform some multi scenario reporting in this notebook." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "all_kpis = pd.DataFrame()\n", + "\n", + "for i in range(1, N_SCENARIOS+1):\n", + " sc_name = \"Copy %02d\" % (i)\n", + " print(sc_name)\n", + " copy = decision.get_scenario(name=sc_name)\n", + " if (copy != None):\n", + " print(\" Deleting old...\")\n", + " decision.delete_container(copy)\n", + " print(\" Copying from original scenario...\") \n", + " copy = scenario.copy(sc_name)\n", + " print(\" Generating new demand...\")\n", + " df_demands = random_demand(200)\n", + " copy.add_table_data(\"demands\", df_demands, category='input')\n", + " print(\" Solving...\")\n", + " copy.solve()\n", + " print(\" Grabbing solution kpis...\")\n", + " kpis = copy.get_table_data('kpis')\n", + " kpis['scenario'] = sc_name\n", + " mk = [[ kpis.iloc[0]['Value'], \"%02d\" % (kpis.iloc[1]['Value']), sc_name, \"%02d\" % (kpis.iloc[2]['Value'])]]\n", + " my_kpis = pd.DataFrame(data=mk, columns=['cost','fix','scenario','temp'])\n", + " copy.add_table_data('my_kpis', data=my_kpis, category='output')\n", + " all_kpis = all_kpis.append(kpis)\n", + " \n", + "print(\"Done!\")" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Copy 01\n", + " Deleting old...\n", + " Copying from original scenario...\n", + " Generating new demand...\n", + " Solving...\n", + " Grabbing solution kpis...\n", + "Copy 02\n", + " Deleting old...\n", + " Copying from original scenario...\n", + " Generating new demand...\n", + " Solving...\n", + " Grabbing solution kpis...\n", + "Copy 03\n", + " Deleting old...\n", + " Copying from original scenario...\n", + " Generating new demand...\n", + " Solving...\n", + " Grabbing solution kpis...\n", + "Copy 04\n", + " Deleting old...\n", + " Copying from original scenario...\n", + " Generating new demand...\n", + " Solving...\n", + " Grabbing solution kpis...\n", + "Copy 05\n", + " Deleting old...\n", + " Copying from original scenario...\n", + " Generating new demand...\n", + " Solving...\n", + " Grabbing solution kpis...\n", + "Done!\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Reporting\n", + "\n", + "Display multi scenario comparison report." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": "total_cost = all_kpis[all_kpis.Name=='Total Cost']", + "execution_count": null, + "outputs": [] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import matplotlib.pyplot as plt\n", + "import matplotlib.colors as mcolors\n", + "\n", + "my_colors = mcolors.TABLEAU_COLORS\n", + "\n", + "plot = plt.figure(figsize=(20,5))\n", + "\n", + "plot = plt.bar(range(N_SCENARIOS),[total_cost.iloc[i]['Value'] for i in range(N_SCENARIOS)], width = 0.8, color = my_colors)\n", + "plot = plt.xticks(range(N_SCENARIOS),[total_cost.iloc[i]['scenario'] for i in range(N_SCENARIOS)])\n", + "\n", + "labels = list(total_cost.iloc[i]['scenario'] for i in range(N_SCENARIOS))\n", + "handles = [plt.Rectangle((0,0),1,1, color = my_colors[v_color]) for v_color in my_colors]\n", + "plot = plt.legend(handles, labels, title = 'Scenario', loc = 'upper right', bbox_to_anchor=(1.1, 1))" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + } + } + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": "", + "execution_count": null, + "outputs": [] + } + ], + "metadata": { + "kernelspec": { + "name": "python3", + "display_name": "Python 3.7", + "language": "python" + }, + "language_info": { + "name": "python", + "version": "3.7.9", + "mimetype": "text/x-python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "pygments_lexer": "ipython3", + "nbconvert_exporter": "python", + "file_extension": ".py" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} + diff --git a/jupyter/Watson Studio Public/CopyAndSolveScenarios.ipynb b/jupyter/Watson Studio Public/CopyAndSolveScenarios.ipynb index ac07a54..c6cdbe2 100644 --- a/jupyter/Watson Studio Public/CopyAndSolveScenarios.ipynb +++ b/jupyter/Watson Studio Public/CopyAndSolveScenarios.ipynb @@ -1,689 +1,282 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To use this Notebook:\n", - " - import the StaffPlanning example (New Decision Optimization Model/From File)\n", - " - generate the project token code (From the notebook menu, do \"Insert project token\")\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "!pip install --user dd-scenario\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "### Client\n", - "\n", - "Create a DODS client to connect to initial scenario." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "from dd_scenario import *\n", - "\n", - "# In order to use the solve() function, you must provide an API key when creating the client\n", - "client = Client(pc=pc, apikey='IAM_APIKEY')\n", - "decision = client.get_model_builder(name=\"StaffPlanning\")\n", - "scenario = decision.get_scenario(name=\"Scenario 1\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "### Global parameters\n", - "\n", - "The number of days and number of periods per day:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": true, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "N_DAYS = 2\n", - "N_PERIODS_PER_DAY = 24*4\n", - "N_PERIODS = N_DAYS * N_PERIODS_PER_DAY" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "### Random generator\n", - "\n", - "A method to generate the random demand for the given number of days and periods. " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": true, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "import random\n", - "import numpy as np\n", - "import pandas as pd\n", - "\n", - "def random_demand( b_size ):\n", - " rs = []\n", - " for d in range(N_DAYS):\n", - " # Morning\n", - " p1 = random.uniform(0.2, 0.4)\n", - " s1 = int(random.uniform(b_size*0.5, b_size*1.5))\n", - " rs.append(np.random.binomial(n=N_PERIODS_PER_DAY, p=p1, size=s1) + d*N_PERIODS_PER_DAY)\n", - " # Afternoon\n", - " p2 = random.uniform(0.6, 0.8)\n", - " s2 = int(random.uniform(b_size*0.5, b_size*1.5))\n", - " rs.append(np.random.binomial(n=N_PERIODS_PER_DAY, p=p2, size=s2) + d*N_PERIODS_PER_DAY)\n", - " # Rest of day\n", - " s3 = int(random.uniform(b_size*0.4, b_size*0.7))\n", - " e = np.array([ random.randint(int(d*N_PERIODS_PER_DAY + 0.2*N_PERIODS_PER_DAY), int(d*N_PERIODS_PER_DAY + 0.8*N_PERIODS_PER_DAY)) for i in range(s3) ])\n", - " #print(e)\n", - " rs.append(e)\n", - " #print(rs)\n", - " s = np.concatenate(rs)\n", - " #print(s)\n", - " g_arrivals = pd.DataFrame(data=s, columns=['value'])\n", - " _demands = [0 for i in range(0, N_PERIODS+1)]\n", - " for t in s:\n", - " _demands[t] = _demands[t] +1\n", - " demands = pd.DataFrame(data= [(t, _demands[t]) for t in range(N_PERIODS)], columns = ['period', 'demand'])\n", - " return demands\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "The number of scenarios you want to generate and solve:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": true, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "N_SCENARIOS = 5" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "\n", - "When copying the scenario, copy the input data, the model and the solution if any.\n", - "\n", - "Then attach new randomly generated data and solve.\n", - "\n", - "Grab the solution to perform some multi scenario reporting in this notebook." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": false, - "deletable": true, - "editable": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Copy 01\n", - " Deleting old...\n", - " Copying from original scenario...\n", - " Generating new demand...\n", - " Solving...\n", - " Grabbing solution kpis...\n", - "Copy 02\n", - " Deleting old...\n", - " Copying from original scenario...\n", - " Generating new demand...\n", - " Solving...\n", - " Grabbing solution kpis...\n", - "Copy 03\n", - " Deleting old...\n", - " Copying from original scenario...\n", - " Generating new demand...\n", - " Solving...\n", - " Grabbing solution kpis...\n", - "Copy 04\n", - " Deleting old...\n", - " Copying from original scenario...\n", - " Generating new demand...\n", - " Solving...\n", - " Grabbing solution kpis...\n", - "Copy 05\n", - " Deleting old...\n", - " Copying from original scenario...\n", - " Generating new demand...\n", - " Solving...\n", - " Grabbing solution kpis...\n", - "Done!\n" - ] - } - ], - "source": [ - "all_kpis = pd.DataFrame()\n", - "\n", - "for i in range(1, N_SCENARIOS+1):\n", - " sc_name = \"Copy %02d\" % (i)\n", - " print(sc_name)\n", - " copy = decision.get_scenario(name=sc_name)\n", - " if (copy != None):\n", - " print(\" Deleting old...\")\n", - " decision.delete_container(copy)\n", - " print(\" Copying from original scenario...\") \n", - " copy = scenario.copy(sc_name)\n", - " print(\" Generating new demand...\")\n", - " df_demands = random_demand(200)\n", - " copy.add_table_data(\"demands\", df_demands, category='input')\n", - " print(\" Solving...\")\n", - " copy.solve()\n", - " print(\" Grabbing solution kpis...\")\n", - " kpis = copy.get_table_data('kpis')\n", - " kpis['scenario'] = sc_name\n", - " mk = [[ kpis.iloc[0]['Value'], \"%02d\" % (kpis.iloc[1]['Value']), sc_name, \"%02d\" % (kpis.iloc[2]['Value'])]]\n", - " my_kpis = pd.DataFrame(data=mk, columns=['cost','fix','scenario','temp'])\n", - " copy.add_table_data('my_kpis', data=my_kpis, category='output')\n", - " all_kpis = all_kpis.append(kpis)\n", - " \n", - "print(\"Done!\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "### Reporting\n", - "\n", - "Display multi scenario comparison report." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false, - "deletable": true, - "editable": true - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": [ - "/*\n", - " * Copyright (c) 2015 IBM Corporation and others.\n", - " *\n", - " * Licensed under the Apache License, Version 2.0 (the \"License\");\n", - " * You may not use this file except in compliance with the License.\n", - " * You may obtain a copy of the License at\n", - " *\n", - " * http://www.apache.org/licenses/LICENSE-2.0\n", - " *\n", - " * Unless required by applicable law or agreed to in writing, software\n", - " * distributed under the License is distributed on an \"AS IS\" BASIS,\n", - " * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", - " * See the License for the specific language governing permissions and\n", - " * limitations under the License.\n", - " */\n", - "\n", - "require.config({\n", - " waitSeconds: 60,\n", - " paths: {\n", - " 'd3': '//cdnjs.cloudflare.com/ajax/libs/d3/4.2.1/d3.min',\n", - " 'topojson': '//cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min',\n", - " 'brunel' : '/dsx-jupyter/ibmdsxuser-1016/1527263617784/nbextensions/brunel_ext/brunel.2.3.min',\n", - " 'brunelControls' : '/dsx-jupyter/ibmdsxuser-1016/1527263617784/nbextensions/brunel_ext/brunel.controls.2.3.min'\n", - " },\n", - " shim: {\n", - " 'brunel' : {\n", - " exports: 'BrunelD3',\n", - " deps: ['d3', 'topojson'],\n", - " init: function() {\n", - " return {\n", - " BrunelD3 : BrunelD3,\n", - " BrunelData : BrunelData\n", - " }\n", - " }\n", - " },\n", - " 'brunelControls' : {\n", - " exports: 'BrunelEventHandlers',\n", - " init: function() {\n", - " return {\n", - " BrunelEventHandlers: BrunelEventHandlers,\n", - " BrunelJQueryControlFactory: BrunelJQueryControlFactory\n", - " }\n", - " }\n", - " }\n", - "\n", - " }\n", - "\n", - "});\n", - "\n", - "require([\"d3\"], function(d3) {\n", - " require([\"brunel\", \"brunelControls\"], function(brunel, brunelControls) {\n", - " function BrunelVis(visId) {\n", - " \"use strict\"; // strict mode\n", - " var datasets = [], // array of datasets for the original data\n", - " pre = function(d, i) { return d }, // default pre-process does nothing\n", - " post = function(d, i) { return d }, // default post-process does nothing\n", - " transitionTime = 200, // transition time for animations\n", - " charts = [], // the charts in the system\n", - " vis = d3.select('#' + visId).attr('class', 'brunel'); // the SVG container\n", - "\n", - " BrunelD3.addDefinitions(vis); // ensure standard symbols present\n", - "\n", - " // Define chart #1 in the visualization //////////////////////////////////////////////////////////\n", - "\n", - " charts[0] = function(parentNode, filterRows) {\n", - " var geom = BrunelD3.geometry(parentNode || vis.node(), 0, 0, 1, 1, 5, 49, 37, 82),\n", - " elements = []; // array of elements in this chart\n", - "\n", - " // Define groups for the chart parts ///////////////////////////////////////////////////////////\n", - "\n", - " var chart = vis.append('g').attr('class', 'chart1')\n", - " .attr('transform','translate(' + geom.chart_left + ',' + geom.chart_top + ')');\n", - " var overlay = chart.append('g').attr('class', 'element').attr('class', 'overlay');\n", - " var zoom = d3.zoom().scaleExtent([1/3,3]);\n", - " var zoomNode = overlay.append('rect').attr('class', 'overlay')\n", - " .attr('x', geom.inner_left).attr('y', geom.inner_top)\n", - " .attr('width', geom.inner_rawWidth).attr('height', geom.inner_rawHeight)\n", - " .style('cursor', 'move').call(zoom)\n", - " .node();\n", - " zoomNode.__zoom = d3.zoomIdentity;\n", - " chart.append('rect').attr('class', 'background').attr('width', geom.chart_right-geom.chart_left).attr('height', geom.chart_bottom-geom.chart_top);\n", - " var interior = chart.append('g').attr('class', 'interior zoomNone')\n", - " .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')')\n", - " .attr('clip-path', 'url(#clip_visid2e788f9e-6036-11e8-a9f6-b2d2a6210d9d_chart1_inner)');\n", - " interior.append('rect').attr('class', 'inner').attr('width', geom.inner_width).attr('height', geom.inner_height);\n", - " var gridGroup = interior.append('g').attr('class', 'grid');\n", - " var axes = chart.append('g').attr('class', 'axis')\n", - " .attr('transform','translate(' + geom.inner_left + ',' + geom.inner_top + ')');\n", - " var legends = chart.append('g').attr('class', 'legend')\n", - " .attr('transform','translate(' + (geom.chart_right-geom.chart_left - 3) + ',' + 0 + ')');\n", - " vis.append('clipPath').attr('id', 'clip_visid2e788f9e-6036-11e8-a9f6-b2d2a6210d9d_chart1_inner').append('rect')\n", - " .attr('x', 0).attr('y', 0)\n", - " .attr('width', geom.inner_rawWidth+1).attr('height', geom.inner_rawHeight+1);\n", - "\n", - " // Scales //////////////////////////////////////////////////////////////////////////////////////\n", - "\n", - " var scale_x = d3.scalePoint().padding(0.5)\n", - " .domain(['Copy 01', 'Copy 02', 'Copy 03', 'Copy 04', 'Copy 05'])\n", - " .range([0, geom.inner_width]);\n", - " var scale_inner = d3.scaleLinear().domain([0,1])\n", - " .range([-0.5, 0.5]);\n", - " var scale_y = d3.scaleLinear().domain([0, 6000.0006])\n", - " .range([geom.inner_height, 0]);\n", - " var base_scales = [scale_x, scale_y]; // untransformed original scales\n", - "\n", - " // Axes ////////////////////////////////////////////////////////////////////////////////////////\n", - "\n", - " axes.append('g').attr('class', 'x axis')\n", - " .attr('transform','translate(0,' + geom.inner_rawHeight + ')')\n", - " .attr('clip-path', 'url(#clip_visid2e788f9e-6036-11e8-a9f6-b2d2a6210d9d_chart1_haxis)');\n", - " vis.append('clipPath').attr('id', 'clip_visid2e788f9e-6036-11e8-a9f6-b2d2a6210d9d_chart1_haxis').append('polyline')\n", - " .attr('points', '-1,-1000, -1,-1 -5,5, -1000,5, -100,1000, 10000,1000 10000,-1000');\n", - " axes.select('g.axis.x').append('text').attr('class', 'title').text('Scenario').style('text-anchor', 'middle')\n", - " .attr('x',geom.inner_rawWidth/2)\n", - " .attr('y', geom.inner_bottom - 2.0).attr('dy','-0.27em');\n", - " axes.append('g').attr('class', 'y axis')\n", - " .attr('clip-path', 'url(#clip_visid2e788f9e-6036-11e8-a9f6-b2d2a6210d9d_chart1_vaxis)');\n", - " vis.append('clipPath').attr('id', 'clip_visid2e788f9e-6036-11e8-a9f6-b2d2a6210d9d_chart1_vaxis').append('polyline')\n", - " .attr('points', '-1000,-10000, 10000,-10000, 10000,' + (geom.inner_rawHeight+1) + ', -1,' + (geom.inner_rawHeight+1) + ', -1,' + (geom.inner_rawHeight+5) + ', -1000,' + (geom.inner_rawHeight+5) );\n", - " axes.select('g.axis.y').append('text').attr('class', 'title').text('Value').style('text-anchor', 'middle')\n", - " .attr('x',-geom.inner_rawHeight/2)\n", - " .attr('y', 4-geom.inner_left).attr('dy', '0.7em').attr('transform', 'rotate(270)');\n", - "\n", - " var axis_bottom = d3.axisBottom(scale_x).ticks(Math.min(10, Math.round(geom.inner_width / 66.0)));\n", - " var axis_left = d3.axisLeft(scale_y).ticks(Math.min(10, Math.round(geom.inner_width / 20)));\n", - "\n", - " function buildAxes(time) {\n", - " axis_bottom.tickValues(BrunelD3.filterTicks(scale_x))\n", - " var axis_x = axes.select('g.axis.x');\n", - " BrunelD3.transition(axis_x, time).call(axis_bottom.scale(scale_x));\n", - " var axis_y = axes.select('g.axis.y');\n", - " BrunelD3.transition(axis_y, time).call(axis_left.scale(scale_y));\n", - " }\n", - " zoom.on('zoom', function(t, time) {\n", - " t = t ||BrunelD3.restrictZoom(d3.event.transform, geom, this);\n", - " scale_y = t.rescaleY(base_scales[1]);\n", - " zoomNode.__zoom = t;\n", - " interior.attr('class', 'interior ' + BrunelD3.zoomLabel(t.k));;\n", - " build(time || -1);\n", - " });\n", - "\n", - " // Define element #1 ///////////////////////////////////////////////////////////////////////////\n", - "\n", - " elements[0] = function() {\n", - " var original, processed, // data sets passed in and then transformed\n", - " element, data, // brunel element information and brunel data\n", - " selection, merged; // d3 selection and merged selection\n", - " var elementGroup = interior.append('g').attr('class', 'element1'),\n", - " main = elementGroup.append('g').attr('class', 'main'),\n", - " labels = BrunelD3.undoTransform(elementGroup.append('g').attr('class', 'labels').attr('aria-hidden', 'true'), elementGroup);\n", - "\n", - " function makeData() {\n", - " original = datasets[0];\n", - " if (filterRows) original = original.retainRows(filterRows);\n", - " processed = pre(original, 0);\n", - " processed = post(processed, 0);\n", - " var f0 = processed.field('scenario'),\n", - " f1 = processed.field('Value'),\n", - " f2 = processed.field('#row'),\n", - " f3 = processed.field('#selection');\n", - " var keyFunc = function(d) { return f0.value(d) };\n", - " data = {\n", - " scenario: function(d) { return f0.value(d.row) },\n", - " VALUE: function(d) { return f1.value(d.row) },\n", - " $row: function(d) { return f2.value(d.row) },\n", - " $selection: function(d) { return f3.value(d.row) },\n", - " scenario_f: function(d) { return f0.valueFormatted(d.row) },\n", - " VALUE_f: function(d) { return f1.valueFormatted(d.row) },\n", - " $row_f: function(d) { return f2.valueFormatted(d.row) },\n", - " $selection_f: function(d) { return f3.valueFormatted(d.row) },\n", - " _split: function(d) { return f0.value(d.row) },\n", - " _key: keyFunc,\n", - " _rows: BrunelD3.makeRowsWithKeys(keyFunc, processed.rowCount())\n", - " };\n", - " }\n", - " // Aesthetic Functions\n", - " var scale_color = d3.scaleOrdinal()\n", - " .domain(['Copy 01', 'Copy 02', 'Copy 03', 'Copy 04', 'Copy 05'])\n", - " .range([ '#347DAD', '#D43F58', '#F7D84A', '#31A461', '#A66A9C', '#FF954D', \n", - " '#A7978E', '#FFCA4D', '#F99EAF', '#B1C43B', '#7E64A2', '#FFB04D', '#CA5C7C', \n", - " '#DDBC8C', '#FFA28D', '#A5473D', '#8B6141', '#F57357', '#5C6B46']);\n", - " var color = function(d) { return scale_color(data.scenario(d)) };\n", - " legends._legend = legends._legend || { title: ['Scenario'], \n", - " ticks: scale_color.domain()};\n", - " legends._legend.color = scale_color;\n", - "\n", - " // Build element from data ///////////////////////////////////////////////////////////////////\n", - "\n", - " function build(transitionMillis) {\n", - " element = elements[0];\n", - " var w = 0.9 * Math.abs(scale_x(scale_x.domain()[1]) - scale_x(scale_x.domain()[0]) );\n", - " var x = function(d) { return scale_x(data.scenario(d))};\n", - " var h = Math.abs( scale_y(scale_y.domain()[0] + 60.0) - scale_y.range()[0] );\n", - " var y1 = scale_y.range()[0];\n", - " var y2 = function(d) { return scale_y(data.Value(d))};\n", - "\n", - " // Define selection entry operations\n", - " function initialState(selection) {\n", - " selection\n", - " .attr('class', 'element bar filled')\n", - " }\n", - "\n", - " // Define selection update operations on merged data\n", - " function updateState(selection) {\n", - " selection\n", - " .each(function(d) {\n", - " var width = w, left = x(d) - width/2, \n", - " c = y1, d = y2(d), top = Math.min(c,d), height = Math.max(1e-6, Math.abs(c-d));\n", - " this.r = {x:left, y:top, w:width, h:height};\n", - " })\n", - " .attr('x', function(d) { return this.r.x })\n", - " .attr('y', function(d) { return this.r.y })\n", - " .attr('width', function(d) { return this.r.w })\n", - " .attr('height', function(d) { return this.r.h })\n", - " .filter(BrunelD3.hasData) // following only performed for data items\n", - " .style('fill', color);\n", - " }\n", - "\n", - " // Define labeling for the selection\n", - " function label(selection, transitionMillis) {\n", - "\n", - " var tooltipLabeling = {\n", - " index: -1, method: 'box', location: ['center', 'top'], inside: true, align: 'middle', pad: 0, dy: 0.7,\n", - " fit: true, granularity: 0,\n", - " content: function(d) {\n", - " return d.row == null ? null : 'Scenario: '\n", - "\t\t\t+ '' + data.scenario_f(d) + ''\n", - "\t\t\t+ '
'\n", - "\t\t\t+ 'VALUE: '\n", - "\t\t\t+ '' + data.VALUE_f(d) + ''\n", - " }\n", - " };\n", - " BrunelD3.addTooltip(selection, tooltipLabeling, geom);\n", - " }\n", - " // Create selections, set the initial state and transition updates\n", - " selection = main.selectAll('.element').data(data._rows, function(d) { return d.key });\n", - " var added = selection.enter().append('rect');\n", - " merged = selection.merge(added);\n", - " initialState(added);\n", - " selection.filter(BrunelD3.hasData)\n", - " .classed('selected', BrunelD3.isSelected(data))\n", - " .filter(BrunelD3.isSelected(data)).raise();\n", - " updateState(BrunelD3.transition(merged, transitionMillis));\n", - " label(merged, transitionMillis);\n", - "\n", - " BrunelD3.transition(selection.exit(), transitionMillis/3)\n", - " .style('opacity', 0.5).each( function() {\n", - " this.remove(); BrunelD3.removeLabels(this); \n", - " });\n", - " }\n", - "\n", - " return {\n", - " data: function() { return processed },\n", - " original: function() { return original },\n", - " internal: function() { return data },\n", - " selection: function() { return merged },\n", - " makeData: makeData,\n", - " build: build,\n", - " chart: function() { return charts[0] },\n", - " group: function() { return elementGroup },\n", - " fields: {\n", - " x: ['scenario'],\n", - " y: ['Value'],\n", - " key: ['scenario'],\n", - " color: ['scenario']\n", - " }\n", - " };\n", - " }();\n", - "\n", - " function build(time, noData) {\n", - " var first = elements[0].data() == null;\n", - " if (first) time = 0; // no transition for first call\n", - " buildAxes(time);\n", - " if ((first || time > -1) && !noData) {\n", - " elements[0].makeData();\n", - " BrunelD3.addLegend(legends, legends._legend);\n", - " }\n", - " elements[0].build(time);\n", - " }\n", - "\n", - " // Expose the following components of the chart\n", - " return {\n", - " elements : elements,\n", - " interior : interior,\n", - " scales: {x:scale_x, y:scale_y},\n", - " zoom: function(params, time) {\n", - " if (params) zoom.on('zoom').call(zoomNode, params, time);\n", - " return d3.zoomTransform(zoomNode);\n", - " },\n", - " build : build\n", - " };\n", - " }();\n", - "\n", - " function setData(rowData, i) { datasets[i||0] = BrunelD3.makeData(rowData) }\n", - " function updateAll(time) { charts.forEach(function(x) {x.build(time || 0)}) }\n", - " function buildAll() {\n", - " for (var i=0;i" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import brunel \n", - "\n", - "total_cost = all_kpis[all_kpis.Name=='Total Cost']\n", - "\n", - "%brunel data('total_cost') x(scenario) y(value:linear) color(scenario) tooltip(#all) bar :: width=1500, height=300" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python3.6", - "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.6.6" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "To use this Notebook:\n", + " - import the StaffPlanning example (New Decision Optimization Model/From File)\n", + " - generate the project token code (From the notebook menu, do \"Insert project token\")\n", + "\n" + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": "!pip install --user dd-scenario\n", + "execution_count": null, + "outputs": [] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Client\n", + "\n", + "Create a DODS client to connect to initial scenario." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from dd_scenario import *\n", + "\n", + "# In order to use the solve() function, you must provide an API key when creating the client\n", + "client = Client(pc=pc, apikey='IAM_APIKEY')\n", + "decision = client.get_model_builder(name=\"StaffPlanning\")\n", + "scenario = decision.get_scenario(name=\"Scenario 1\")\n" + ], + "execution_count": null, + "outputs": [] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Global parameters\n", + "\n", + "The number of days and number of periods per day:" + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "N_DAYS = 2\n", + "N_PERIODS_PER_DAY = 24*4\n", + "N_PERIODS = N_DAYS * N_PERIODS_PER_DAY" + ], + "execution_count": null, + "outputs": [] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Random generator\n", + "\n", + "A method to generate the random demand for the given number of days and periods. " + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import random\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "def random_demand( b_size ):\n", + " rs = []\n", + " for d in range(N_DAYS):\n", + " # Morning\n", + " p1 = random.uniform(0.2, 0.4)\n", + " s1 = int(random.uniform(b_size*0.5, b_size*1.5))\n", + " rs.append(np.random.binomial(n=N_PERIODS_PER_DAY, p=p1, size=s1) + d*N_PERIODS_PER_DAY)\n", + " # Afternoon\n", + " p2 = random.uniform(0.6, 0.8)\n", + " s2 = int(random.uniform(b_size*0.5, b_size*1.5))\n", + " rs.append(np.random.binomial(n=N_PERIODS_PER_DAY, p=p2, size=s2) + d*N_PERIODS_PER_DAY)\n", + " # Rest of day\n", + " s3 = int(random.uniform(b_size*0.4, b_size*0.7))\n", + " e = np.array([ random.randint(int(d*N_PERIODS_PER_DAY + 0.2*N_PERIODS_PER_DAY), int(d*N_PERIODS_PER_DAY + 0.8*N_PERIODS_PER_DAY)) for i in range(s3) ])\n", + " #print(e)\n", + " rs.append(e)\n", + " #print(rs)\n", + " s = np.concatenate(rs)\n", + " #print(s)\n", + " g_arrivals = pd.DataFrame(data=s, columns=['value'])\n", + " _demands = [0 for i in range(0, N_PERIODS+1)]\n", + " for t in s:\n", + " _demands[t] = _demands[t] +1\n", + " demands = pd.DataFrame(data= [(t, _demands[t]) for t in range(N_PERIODS)], columns = ['period', 'demand'])\n", + " return demands\n" + ], + "execution_count": null, + "outputs": [] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "The number of scenarios you want to generate and solve:" + }, + { + "metadata": {}, + "cell_type": "code", + "source": "N_SCENARIOS = 5", + "execution_count": null, + "outputs": [] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\n", + "When copying the scenario, copy the input data, the model and the solution if any.\n", + "\n", + "Then attach new randomly generated data and solve.\n", + "\n", + "Grab the solution to perform some multi scenario reporting in this notebook." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "all_kpis = pd.DataFrame()\n", + "\n", + "for i in range(1, N_SCENARIOS+1):\n", + " sc_name = \"Copy %02d\" % (i)\n", + " print(sc_name)\n", + " copy = decision.get_scenario(name=sc_name)\n", + " if (copy != None):\n", + " print(\" Deleting old...\")\n", + " decision.delete_container(copy)\n", + " print(\" Copying from original scenario...\") \n", + " copy = scenario.copy(sc_name)\n", + " print(\" Generating new demand...\")\n", + " df_demands = random_demand(200)\n", + " copy.add_table_data(\"demands\", df_demands, category='input')\n", + " print(\" Solving...\")\n", + " copy.solve()\n", + " print(\" Grabbing solution kpis...\")\n", + " kpis = copy.get_table_data('kpis')\n", + " kpis['scenario'] = sc_name\n", + " mk = [[ kpis.iloc[0]['Value'], \"%02d\" % (kpis.iloc[1]['Value']), sc_name, \"%02d\" % (kpis.iloc[2]['Value'])]]\n", + " my_kpis = pd.DataFrame(data=mk, columns=['cost','fix','scenario','temp'])\n", + " copy.add_table_data('my_kpis', data=my_kpis, category='output')\n", + " all_kpis = all_kpis.append(kpis)\n", + " \n", + "print(\"Done!\")" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Copy 01\n", + " Deleting old...\n", + " Copying from original scenario...\n", + " Generating new demand...\n", + " Solving...\n", + " Grabbing solution kpis...\n", + "Copy 02\n", + " Deleting old...\n", + " Copying from original scenario...\n", + " Generating new demand...\n", + " Solving...\n", + " Grabbing solution kpis...\n", + "Copy 03\n", + " Deleting old...\n", + " Copying from original scenario...\n", + " Generating new demand...\n", + " Solving...\n", + " Grabbing solution kpis...\n", + "Copy 04\n", + " Deleting old...\n", + " Copying from original scenario...\n", + " Generating new demand...\n", + " Solving...\n", + " Grabbing solution kpis...\n", + "Copy 05\n", + " Deleting old...\n", + " Copying from original scenario...\n", + " Generating new demand...\n", + " Solving...\n", + " Grabbing solution kpis...\n", + "Done!\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Reporting\n", + "\n", + "Display multi scenario comparison report." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": "total_cost = all_kpis[all_kpis.Name=='Total Cost']", + "execution_count": null, + "outputs": [] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import matplotlib.pyplot as plt\n", + "import matplotlib.colors as mcolors\n", + "\n", + "my_colors = mcolors.TABLEAU_COLORS\n", + "\n", + "plot = plt.figure(figsize=(20,5))\n", + "\n", + "plot = plt.bar(range(N_SCENARIOS),[total_cost.iloc[i]['Value'] for i in range(N_SCENARIOS)], width = 0.8, color = my_colors)\n", + "plot = plt.xticks(range(N_SCENARIOS),[total_cost.iloc[i]['scenario'] for i in range(N_SCENARIOS)])\n", + "\n", + "labels = list(total_cost.iloc[i]['scenario'] for i in range(N_SCENARIOS))\n", + "handles = [plt.Rectangle((0,0),1,1, color = my_colors[v_color]) for v_color in my_colors]\n", + "plot = plt.legend(handles, labels, title = 'Scenario', loc = 'upper right', bbox_to_anchor=(1.1, 1))" + ], + "execution_count": null, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/png": "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\n" + }, + "metadata": { + "needs_background": "light" + } + } + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": "", + "execution_count": null, + "outputs": [] + } + ], + "metadata": { + "kernelspec": { + "name": "python3", + "display_name": "Python 3.7", + "language": "python" + }, + "language_info": { + "name": "python", + "version": "3.7.9", + "mimetype": "text/x-python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "pygments_lexer": "ipython3", + "nbconvert_exporter": "python", + "file_extension": ".py" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} +