diff --git a/CHANGELOG.md b/CHANGELOG.md index 374cdf0..9bc927a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Added +* `07a_exercise.ipynb`and `07a_solutions.ipynb`: simple exercises to complement learning in `07_experiments.ipynb` * `10_multiple_arrival_processes.ipynb`: simulating multiple arrivals classes each with their own distribution * `11_blocking.ipynb`: simulating the blocking of one resource while queuing for another. * `12_arrival_classes.ipynb`: simulate unique processes for different classes of arrival to the model. * `distributions.py`: module containing some distributions to reduce code in notebooks. +* `basic_model.py`: contains a single activity version of the call centre model to use with `07_exercise.ipynb` ## [v0.2.0 - 11/02/2024](https://github.com/pythonhealthdatascience/intro-open-sim/releases/tag/v0.2.0) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.14849934.svg)](https://doi.org/10.5281/zenodo.14849934) diff --git a/README.md b/README.md index 6c20db6..0dc04d2 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ We then move on to some more advanced concepts, and create a full process model: * `05_basic_results.ipynb`: Collecting results from a single run by storing process metrics during a run and performing calculations at the end * `06a_basic_results_exercise.ipynb`: An exercise to practice collecting results from a `simpy` model. * `07_experiments.ipynb`: our approach to setting up a model for multiple replications, experimentation, and common random numbers +* `07a_exercise.ipynb`: An exercise to practice using an `Experiment` class with a model +* `07b_solutions.ipynb` Solutions to the `Experiment` exercises. * `08_full_model.ipynb`: an extended version of the 111 call centre process. We also introduce a warm-up period * `09_time_weighted_calcs.ipynb`: An alternative approach to collects results for queue length and resource utilisation. diff --git a/content/07_experiments.ipynb b/content/07_experiments.ipynb index 9a6b5b0..03c71f3 100644 --- a/content/07_experiments.ipynb +++ b/content/07_experiments.ipynb @@ -32,7 +32,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "09f761a3-c100-4dc8-a3c9-30a5cd101c0d", "metadata": {}, "outputs": [], @@ -55,7 +55,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "a3e7b498-8019-4d38-9192-c3d483361885", "metadata": {}, "outputs": [], @@ -94,7 +94,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "14e8f47f-e90d-40cd-b5fd-63e8975eba83", "metadata": {}, "outputs": [], @@ -148,7 +148,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "56dd64c1-adfa-417b-ad23-43a07672c7cd", "metadata": {}, "outputs": [], @@ -204,7 +204,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "b5110385-e293-47c8-8038-e0ddfa3d6b10", "metadata": {}, "outputs": [], @@ -323,7 +323,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "cda27204-a567-4a83-9d21-aa63646c3a08", "metadata": {}, "outputs": [], @@ -342,42 +342,20 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "a5a90a1f-b6e2-42c2-8799-d56c37c5569d", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "1.9761166744858965" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "default_experiment.arrival_dist.sample()" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "0b88a91c-b502-41bb-a0f6-608db60afea6", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.6" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "default_experiment.mean_iat" ] @@ -394,7 +372,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "ba139ce8-1d5b-437e-a606-d40e9db88d7d", "metadata": {}, "outputs": [], @@ -405,21 +383,10 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "b74b4b3d-32e5-4176-8549-6b0215aace9e", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "14" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "extra_server.n_operators" ] @@ -438,7 +405,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "96c43528-2390-4006-aa31-ec5aa7d19ba9", "metadata": {}, "outputs": [], @@ -458,7 +425,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "564db16d-b2b1-4e2f-b1f3-6ef4b42a59f7", "metadata": {}, "outputs": [], @@ -523,7 +490,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "838b63ac-8add-442a-b25b-ed9c7e5fdfd9", "metadata": {}, "outputs": [], @@ -570,7 +537,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "1bde92ab-c010-4fa9-90cf-ebb715537ae9", "metadata": {}, "outputs": [], @@ -624,19 +591,10 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "50fcc6ef-9e7d-4caf-8f62-fbe4e4e1d061", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Mean waiting time: 3.87 mins \n", - "Operator Utilisation 92.90%\n" - ] - } - ], + "outputs": [], "source": [ "TRACE = False\n", "default_scenario = Experiment()\n", @@ -657,7 +615,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "9729b480-4499-44be-91e7-77ce2f64dce9", "metadata": {}, "outputs": [], @@ -697,85 +655,10 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "id": "8263448d-a1d1-488e-8985-55ba07037786", "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", - "
01_mean_waiting_time02_operator_util
rep
13.87263092.904245
23.19623393.642054
33.52599094.224088
41.53615990.900402
53.06592793.531723
\n", - "
" - ], - "text/plain": [ - " 01_mean_waiting_time 02_operator_util\n", - "rep \n", - "1 3.872630 92.904245\n", - "2 3.196233 93.642054\n", - "3 3.525990 94.224088\n", - "4 1.536159 90.900402\n", - "5 3.065927 93.531723" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "TRACE = False\n", "default_scenario = Experiment()\n", @@ -799,7 +682,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "id": "a06787e1-d024-40c9-9f38-7e4f01a51166", "metadata": {}, "outputs": [], @@ -829,7 +712,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "id": "2957de2b-c0cf-4eab-a639-0058c8c56326", "metadata": {}, "outputs": [], @@ -869,25 +752,10 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "id": "1fa0335d-18c6-463b-92f9-f8f83ef535c3", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Model experiments:\n", - "No. experiments to execute = 2\n", - "\n", - "Running base => done.\n", - "\n", - "Running operators+1 => done.\n", - "\n", - "All experiments are complete.\n" - ] - } - ], + "outputs": [], "source": [ "# get the experiments\n", "experiments = get_experiments()\n", @@ -898,92 +766,17 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "id": "63aad945-4817-459f-8d80-51adf8b0c18b", "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", - "
01_mean_waiting_time02_operator_util
rep
11.17509086.268227
21.07963387.104060
31.19560387.742674
40.60700084.407517
51.15987787.013792
\n", - "
" - ], - "text/plain": [ - " 01_mean_waiting_time 02_operator_util\n", - "rep \n", - "1 1.175090 86.268227\n", - "2 1.079633 87.104060\n", - "3 1.195603 87.742674\n", - "4 0.607000 84.407517\n", - "5 1.159877 87.013792" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "experiment_results[\"operators+1\"]" ] }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "id": "b9ec8071-6359-4664-8a3a-3aa145256542", "metadata": {}, "outputs": [], @@ -1014,61 +807,10 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "id": "cd9cf15a-a8cf-4e96-8328-7be4e37a00b2", "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", - "
baseoperators+1
01_mean_waiting_time3.041.04
02_operator_util93.0486.51
\n", - "
" - ], - "text/plain": [ - " base operators+1\n", - "01_mean_waiting_time 3.04 1.04\n", - "02_operator_util 93.04 86.51" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# as well as rounding you may want to rename the cols/rows to\n", "# more readable alternatives.\n", @@ -1103,7 +845,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "id": "7cb70cd7-339f-4701-8b70-c67e93ff4292", "metadata": {}, "outputs": [], @@ -1137,85 +879,10 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "id": "d687df09-0d17-4371-8cca-bb0e0e120241", "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", - "
experimentn_operatorsmean_iat
id
0base130.60
1op+1140.60
2high_demand130.55
3combination140.55
\n", - "
" - ], - "text/plain": [ - " experiment n_operators mean_iat\n", - "id \n", - "0 base 13 0.60\n", - "1 op+1 14 0.60\n", - "2 high_demand 13 0.55\n", - "3 combination 14 0.55" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "create_example_csv()\n", "\n", @@ -1241,7 +908,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "id": "bc15c677-458d-4cd0-8e83-94053662d834", "metadata": {}, "outputs": [], @@ -1276,19 +943,10 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "id": "d8b095d2-c0f3-4875-902c-10e4ddc4f723", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "14.0\n" - ] - } - ], + "outputs": [], "source": [ "# test of the function\n", "\n", @@ -1314,85 +972,10 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "id": "859a8bb3-1ba8-4516-8e35-3e6b84342582", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Model experiments:\n", - "No. experiments to execute = 4\n", - "\n", - "Running base => done.\n", - "\n", - "Running op+1 => done.\n", - "\n", - "Running high_demand => done.\n", - "\n", - "Running combination => done.\n", - "\n", - "All experiments are complete.\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", - "
01_mean_waiting_time02_operator_util
rep
13.87263092.904245
23.19623393.642054
\n", - "
" - ], - "text/plain": [ - " 01_mean_waiting_time 02_operator_util\n", - "rep \n", - "1 3.872630 92.904245\n", - "2 3.196233 93.642054" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "results = run_all_experiments(experiments_to_run)\n", "\n", @@ -1402,134 +985,20 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "id": "a070a8bd-8ec8-45b8-a712-c2e87705542f", "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", - "
01_mean_waiting_time02_operator_util
rep
127.10138998.357059
220.13914098.748543
\n", - "
" - ], - "text/plain": [ - " 01_mean_waiting_time 02_operator_util\n", - "rep \n", - "1 27.101389 98.357059\n", - "2 20.139140 98.748543" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "results[\"high_demand\"].head(2)" ] }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "id": "1ecc0d91-d50f-4271-b2bf-0895044633e4", "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", - "
baseop+1high_demandcombination
01_mean_waiting_time3.041.0417.983.57
02_operator_util93.0486.5198.6794.36
\n", - "
" - ], - "text/plain": [ - " base op+1 high_demand combination\n", - "01_mean_waiting_time 3.04 1.04 17.98 3.57\n", - "02_operator_util 93.04 86.51 98.67 94.36" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# show results\n", "# further adaptions might include adding units for figures.\n", @@ -1538,73 +1007,10 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "id": "4ae51707-a06c-4661-b9cf-36ee4424b8e2", "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", - "
01_mean_waiting_time02_operator_util
base3.0493.04
op+11.0486.51
high_demand17.9898.67
combination3.5794.36
\n", - "
" - ], - "text/plain": [ - " 01_mean_waiting_time 02_operator_util\n", - "base 3.04 93.04\n", - "op+1 1.04 86.51\n", - "high_demand 17.98 98.67\n", - "combination 3.57 94.36" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "experiment_summary_frame(results).round(2).T" ] diff --git a/content/07a_exercise.ipynb b/content/07a_exercise.ipynb new file mode 100644 index 0000000..77e2663 --- /dev/null +++ b/content/07a_exercise.ipynb @@ -0,0 +1,378 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e27ef2a4-58f2-426e-9db5-7b31bdc6c9a8", + "metadata": {}, + "source": [ + "# Call Centre Optimization Exercise ๐Ÿ“ž\n", + "\n", + "๐Ÿง For the solutions, please see the [experimentation exercise notebook](./07b_solutions.ipynb)\n", + "\n", + "**Objective**: Apply your knowledge of the `Experiment` class to analyze call centre performance under different scenarios.\n", + "\n", + "## Learning Goals\n", + "- Create `Experiment` objects with different parameter configurations\n", + "- Run single experiments and interpret results \n", + "- Conduct multiple replications for statistical reliability\n", + "- Compare scenarios and make data-driven recommendations\n", + "\n", + "## Exercise Overview\n", + "You will analyze a call centre's performance by testing different staffing levels and demand scenarios. The exercise is divided into 4 tasks." + ] + }, + { + "cell_type": "markdown", + "id": "1ca24e08-df89-479f-8ed5-42a25a1a362b", + "metadata": {}, + "source": [ + "## 1. Imports " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f9c1cc0-bbbb-4247-a695-fa93f4bbb116", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import simpy\n", + "import itertools\n", + "\n", + "# Set display options for 2dp in pandas\n", + "pd.set_option('display.precision', 2)" + ] + }, + { + "cell_type": "markdown", + "id": "331ab9b1-a15a-4656-b88f-f03a6e3a866c", + "metadata": {}, + "source": [ + "## 2. Simulation model imports\n", + "\n", + "For convenience the model is stored in a python module called `basic_model`. We need to import:\n", + "\n", + "* The `Experiment` class - to set parameters,\n", + "* Functions that wrap the model and allow us to run it. I.e. `single_run` and `multiple_replications`.\n", + "* A function to toggle simulation debug info on and off `set_trace`\n", + "* A function that will summarise the results of multiple experiments `create_summary_table`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99acde15-aabc-4117-bdb5-9fd98a717191", + "metadata": {}, + "outputs": [], + "source": [ + "from basic_model import (\n", + " Experiment, \n", + " single_run, \n", + " multiple_replications, \n", + " create_summary_table,\n", + " set_trace,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "85cb4b91-72c6-4155-a6b5-522ed6c70e40", + "metadata": {}, + "source": [ + "## 4. Exercises" + ] + }, + { + "cell_type": "markdown", + "id": "36a424f3-419d-445c-9bf5-af50356713b5", + "metadata": {}, + "source": [ + "### 4.1 Baseline Analysis\n", + "\n", + "First, let's understand the current system performance.\n", + "\n", + "**Instructions**:\n", + "1. Create a default experiment using the `Experiment()` class\n", + "2. Run it **once** to see the simulation events. Toggle trace on and off.\n", + "3. Record the mean waiting time and operator utilization.\n", + "4. Now run 10 multiple replications of the model and view results.\n", + "6. Answer the reflection question below\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fdb072c7-ea95-4f56-889b-adb10e7a8364", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: Create a default experiment\n", + "# baseline_experiment = ...\n", + "\n", + "# TODO: toggle trace to False/True\n", + "set_trace(trace_on=False)\n", + "\n", + "# TODO: Run the experiment once and store results\n", + "# baseline_results = ...\n", + "\n", + "# Display results\n", + "print(\"=== BASELINE SCENARIO RESULTS ===\")\n", + "# print(f\"Mean waiting time: {baseline_results['01_mean_waiting_time']:.2f} minutes\")\n", + "# print(f\"Operator utilization: {baseline_results['02_operator_util']:.2f}%\")" + ] + }, + { + "cell_type": "markdown", + "id": "8180f39d-0e3a-4ec9-b2b9-3ca71f7550f9", + "metadata": {}, + "source": [ + "We will now run multiple replications of the model. The function returns a `pandas` dataframe containing a a replication per row and a column per KPI." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b09fbbd-8fd7-433c-8506-1e496d722b51", + "metadata": {}, + "outputs": [], + "source": [ + "# Run multiple replications (trace off!)\n", + "set_trace(trace_on=False)\n", + "\n", + "# TODO: Run the experiment once and store results\n", + "# baseline_reps = multiple_...\n", + "\n", + "# show results\n", + "# baseline_reps" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "057a944e-a65a-466e-bc77-3c600167e308", + "metadata": {}, + "outputs": [], + "source": [ + "# summary statistics\n", + "# baseline_reps.describe()" + ] + }, + { + "cell_type": "markdown", + "id": "75bb3f56-13b4-497e-a873-4414eb60e1ba", + "metadata": {}, + "source": [ + "๐Ÿ’ก๐Ÿค” **Reflection Question**: Based on the baseline results, what do you observe about the system performance? Is the utilization high or low? What might this suggest about the current staffing level?" + ] + }, + { + "cell_type": "markdown", + "id": "a439d818-e14e-40f5-9b77-2447111f11a4", + "metadata": {}, + "source": [ + "**Your Answer**: [Write your analysis here]" + ] + }, + { + "cell_type": "markdown", + "id": "03ca3af9-621b-4ef4-b4c5-17a4b9bf57a0", + "metadata": {}, + "source": [ + "### 4.2 Staffing Scenarios Analysis\n", + "\n", + "Now test three different staffing scenarios using multiple replications for statistical reliability.\n", + "\n", + "**Scenarios to test**:\n", + "1. **Reduced Staffing**: 11 operators\n", + "2. **Current Staffing**: 13 operators (baseline) \n", + "3. **Increased Staffing**: 15 operators\n", + "\n", + "**Requirements**:\n", + "- Run each scenario with **10 replications**\n", + "- Calculate mean and standard deviation for both KPIs\n", + "- Create a summary comparison table\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "924d6ab1-c240-4832-8737-8adf4390fc33", + "metadata": {}, + "outputs": [], + "source": [ + "# Task 2: Staffing Scenarios\n", + "# Turn off tracing for cleaner output\n", + "set_trace(trace_on=False)\n", + "\n", + "# TODO: Create three experiments with different staffing levels\n", + "# reduced_staffing = Experiment(n_operators=11)\n", + "# current_staffing = ...\n", + "# increased_staffing = ...\n", + "\n", + "# TODO: Run 10 replications for each scenario\n", + "print(\"Running reduced staffing scenario (11 operators)...\")\n", + "# reduced_results = ...\n", + "\n", + "print(\"Running current staffing scenario (13 operators)...\")\n", + "# current_results = ...\n", + "\n", + "print(\"Running increased staffing scenario (15 operators)...\")\n", + "# increased_results = ...\n", + "\n", + "print(\"All scenarios completed!\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01f4d22d-bddf-4a2b-a231-aeb979acdd70", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: Calculate summary statistics for each scenario (uncomment and run)\n", + "\n", + "# Create results dictionary\n", + "# scenario_results = {\n", + "# 'Reduced (11 ops)': reduced_results,\n", + "# 'Current (13 ops)': current_results,\n", + "# 'Increased (15 ops)': increased_results\n", + "# }\n", + "\n", + "# # Create summary table\n", + "# scenarios_summary = create_summary_table(\n", + "# results_dict=scenario_results,\n", + "# label_key='Scenario',\n", + "# label_order=['Reduced (11 ops)', 'Current (13 ops)', 'Increased (15 ops)']\n", + "# )\n", + "\n", + "# print(\"=== STAFFING SCENARIOS SUMMARY ===\")\n", + "# scenarios_summary" + ] + }, + { + "cell_type": "markdown", + "id": "be8beaf1-b274-4a9a-af0c-54a41a644ef8", + "metadata": {}, + "source": [ + "### 4.3 Demand Variation Analysis\n", + "\n", + "Test how the baseline staffing (13 operators) performs under different demand levels by varying the mean inter-arrival time.\n", + "\n", + "**Demand Scenarios**:\n", + "1. **Low Demand**: `mean_iat=0.8` (calls arrive less frequently)\n", + "2. **Current Demand**: `mean_iat=0.6` (baseline)\n", + "3. **High Demand**: `mean_iat=0.4` (calls arrive more frequently)\n", + "\n", + "**Requirements**:\n", + "- Use 10 replications for each demand scenario\n", + "- Keep staffing at 13 operators for all tests\n", + "- Compare results to identify potential problems\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bbf3fd54-0b66-4db7-b98c-c4268f65f69e", + "metadata": {}, + "outputs": [], + "source": [ + "# Task 3: Demand Variation Analysis\n", + "# TODO: Create three experiments with different demand levels (mean_iat)\n", + "# low_demand = Experiment(n_operators=13, mean_iat=0.8)\n", + "# current_demand = ...\n", + "# high_demand = ...\n", + "\n", + "# TODO: Run 10 replications for each demand scenario\n", + "print(\"Running low demand scenario (mean_iat=0.8)...\")\n", + "# low_demand_results = multiple_replications(low_demand, n_reps=10)\n", + "\n", + "print(\"Running current demand scenario (mean_iat=0.6)...\")\n", + "# current_demand_results = multiple_replications(current_demand, n_reps=10)\n", + "\n", + "print(\"Running high demand scenario (mean_iat=0.4)...\")\n", + "# high_demand_results = multiple_replications(high_demand, n_reps=10)\n", + "\n", + "print(\"All demand scenarios completed!\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b900ef13-9ec6-41d8-8bf3-49193c3b0ed7", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: Create summary table for demand scenarios (uncomment and run)\n", + "\n", + "# # Create results dictionary\n", + "# scenario_results = {\n", + "# 'Low (IAT=0.8)': low_demand_results,\n", + "# 'Current (IAT=0.6)': current_demand_results,\n", + "# 'High (IAT=0.4)': high_demand_results\n", + "# }\n", + "\n", + "# # Create summary table\n", + "# demand_summary = create_summary_table(\n", + "# results_dict=scenario_results,\n", + "# label_key='Scenario',\n", + "# label_order=['Low (IAT=0.8)', 'Current (IAT=0.6)', 'High (IAT=0.4)']\n", + "# )\n", + "\n", + "# print(\"=== STAFFING SCENARIOS SUMMARY ===\")\n", + "# demand_summary" + ] + }, + { + "cell_type": "markdown", + "id": "46cdd911-0124-4ff6-8bad-0646149b8ae8", + "metadata": {}, + "source": [ + "### 4.4 Recommendations\n", + "\n", + "Based on your analysis from the exercises above, provide data-driven recommendations.\n", + "\n", + "**Questions to address**:\n", + "\n", + "1. **Optimal Staffing**: What is the \"optimal\" number of operators for current demand levels? Consider both waiting times and utilization efficiency.\n", + "\n", + "2. **High Demand Response**: What happens to the system under high demand conditions? What would you recommend to management?\n", + "\n", + "3. **Trade-offs**: Explain the trade-off between operator utilization and customer waiting times that you observed.\n", + "\n", + "**Your Recommendations**:\n", + "\n", + "### Question 1 - Optimal Staffing\n", + "[Provide your analysis and recommendation here]\n", + "\n", + "### Question 2 - High Demand Response \n", + "[Describe what happens under high demand and your recommendations]\n", + "\n", + "### Question 3 - Trade-offs\n", + "[Explain the utilization vs. waiting time trade-off you observed]\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/content/07b_solutions.ipynb b/content/07b_solutions.ipynb new file mode 100644 index 0000000..3546919 --- /dev/null +++ b/content/07b_solutions.ipynb @@ -0,0 +1,764 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e27ef2a4-58f2-426e-9db5-7b31bdc6c9a8", + "metadata": {}, + "source": [ + "# Call Centre Optimization Exercise - SOLUTIONS ๐Ÿ“ž\n", + "\n", + "โš ๏ธ **SOLUTIONS:** This notebook contains example solutions for the [experiments exercise](./07a_exercise.ipynb)\n", + "\n", + "**Objective**: Apply your knowledge of the `Experiment` class to analyze call centre performance under different scenarios.\n", + "\n", + "## Learning Goals\n", + "- Create `Experiment` objects with different parameter configurations\n", + "- Run single experiments and interpret results \n", + "- Conduct multiple replications for statistical reliability\n", + "- Compare scenarios and make data-driven recommendations\n", + "\n", + "## Exercise Overview\n", + "You will analyze a call centre's performance by testing different staffing levels and demand scenarios. The exercise is divided into 4 tasks." + ] + }, + { + "cell_type": "markdown", + "id": "1ca24e08-df89-479f-8ed5-42a25a1a362b", + "metadata": {}, + "source": [ + "## 1. Imports " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "0f9c1cc0-bbbb-4247-a695-fa93f4bbb116", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import simpy\n", + "import itertools\n", + "\n", + "# Set display options for 2dp in pandas\n", + "pd.set_option('display.precision', 2)" + ] + }, + { + "cell_type": "markdown", + "id": "331ab9b1-a15a-4656-b88f-f03a6e3a866c", + "metadata": {}, + "source": [ + "## 2. Simulation model imports\n", + "\n", + "For convenience the model is stored in a python module called `basic_model`. We need to import:\n", + "\n", + "* The `Experiment` class - to set parameters,\n", + "* Functions that wrap the model and allow us to run it. I.e. `single_run` and `multiple_replications`.\n", + "* A function to toggle simulation debug info on and off `set_trace`\n", + "* A function that will summarise the results of multiple experiments `create_summary_table`" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "99acde15-aabc-4117-bdb5-9fd98a717191", + "metadata": {}, + "outputs": [], + "source": [ + "from basic_model import (\n", + " Experiment, \n", + " single_run, \n", + " multiple_replications, \n", + " create_summary_table,\n", + " set_trace,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "85cb4b91-72c6-4155-a6b5-522ed6c70e40", + "metadata": {}, + "source": [ + "## 4. Exercises" + ] + }, + { + "cell_type": "markdown", + "id": "36a424f3-419d-445c-9bf5-af50356713b5", + "metadata": {}, + "source": [ + "### 4.1 Baseline Analysis\n", + "\n", + "First, let's understand the current system performance.\n", + "\n", + "**Instructions**:\n", + "1. Create a default experiment using the `Experiment()` class\n", + "2. Run it **once** to see the simulation events. Toggle trace on and off.\n", + "3. Record the mean waiting time and operator utilization.\n", + "4. Now run 10 multiple replications of the model and view results.\n", + "6. Answer the reflection question below\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "fdb072c7-ea95-4f56-889b-adb10e7a8364", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== BASELINE SCENARIO RESULTS ===\n", + "Mean waiting time: 3.87 minutes\n", + "Operator utilization: 92.90%\n" + ] + } + ], + "source": [ + "# TODO: Create a default experiment\n", + "baseline_experiment = Experiment()\n", + "\n", + "# TODO: toggle trace to False/True\n", + "set_trace(trace_on=False)\n", + "\n", + "# TODO: Run the experiment once and store results\n", + "baseline_results = single_run(baseline_experiment)\n", + "\n", + "# Display results\n", + "print(\"=== BASELINE SCENARIO RESULTS ===\")\n", + "print(f\"Mean waiting time: {baseline_results['01_mean_waiting_time']:.2f} minutes\")\n", + "print(f\"Operator utilization: {baseline_results['02_operator_util']:.2f}%\")" + ] + }, + { + "cell_type": "markdown", + "id": "8180f39d-0e3a-4ec9-b2b9-3ca71f7550f9", + "metadata": {}, + "source": [ + "We will now run multiple replications of the model. The function returns a `pandas` dataframe containing a a replication per row and a column per KPI." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6b09fbbd-8fd7-433c-8506-1e496d722b51", + "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", + " \n", + " \n", + " \n", + " \n", + "
01_mean_waiting_time02_operator_util
rep
13.8792.90
23.2093.64
33.5394.22
41.5490.90
53.0793.53
62.0893.60
72.0191.18
81.8288.85
92.2593.23
103.5894.85
\n", + "
" + ], + "text/plain": [ + " 01_mean_waiting_time 02_operator_util\n", + "rep \n", + "1 3.87 92.90\n", + "2 3.20 93.64\n", + "3 3.53 94.22\n", + "4 1.54 90.90\n", + "5 3.07 93.53\n", + "6 2.08 93.60\n", + "7 2.01 91.18\n", + "8 1.82 88.85\n", + "9 2.25 93.23\n", + "10 3.58 94.85" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Run multiple replications (trace off!)\n", + "set_trace(trace_on=False)\n", + "\n", + "# TODO: Run the experiment once and store results\n", + "baseline_reps = multiple_replications(baseline_experiment, n_reps=10)\n", + "\n", + "# show results\n", + "baseline_reps" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "057a944e-a65a-466e-bc77-3c600167e308", + "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", + "
01_mean_waiting_time02_operator_util
count10.0010.00
mean2.6992.69
std0.841.83
min1.5488.85
25%2.0391.61
50%2.6693.38
75%3.4493.63
max3.8794.85
\n", + "
" + ], + "text/plain": [ + " 01_mean_waiting_time 02_operator_util\n", + "count 10.00 10.00\n", + "mean 2.69 92.69\n", + "std 0.84 1.83\n", + "min 1.54 88.85\n", + "25% 2.03 91.61\n", + "50% 2.66 93.38\n", + "75% 3.44 93.63\n", + "max 3.87 94.85" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# summary statistics\n", + "baseline_reps.describe()" + ] + }, + { + "cell_type": "markdown", + "id": "75bb3f56-13b4-497e-a873-4414eb60e1ba", + "metadata": {}, + "source": [ + "๐Ÿ’ก๐Ÿค” **Reflection Question**: Based on the baseline results, what do you observe about the system performance? Is the utilization high or low? What might this suggest about the current staffing level?" + ] + }, + { + "cell_type": "markdown", + "id": "a439d818-e14e-40f5-9b77-2447111f11a4", + "metadata": {}, + "source": [ + "**Your Answer**: [Write your analysis here]" + ] + }, + { + "cell_type": "markdown", + "id": "03ca3af9-621b-4ef4-b4c5-17a4b9bf57a0", + "metadata": {}, + "source": [ + "### 4.2 Staffing Scenarios Analysis\n", + "\n", + "Now test three different staffing scenarios using multiple replications for statistical reliability.\n", + "\n", + "**Scenarios to test**:\n", + "1. **Reduced Staffing**: 11 operators\n", + "2. **Current Staffing**: 13 operators (baseline) \n", + "3. **Increased Staffing**: 15 operators\n", + "\n", + "**Requirements**:\n", + "- Run each scenario with **10 replications**\n", + "- Calculate mean and standard deviation for both KPIs\n", + "- Create a summary comparison table\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "924d6ab1-c240-4832-8737-8adf4390fc33", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running reduced staffing scenario (11 operators)...\n", + "Running current staffing scenario (13 operators)...\n", + "Running increased staffing scenario (15 operators)...\n", + "All scenarios completed!\n" + ] + } + ], + "source": [ + "# Task 2: Staffing Scenarios\n", + "# Turn off tracing for cleaner output\n", + "set_trace(trace_on=False)\n", + "\n", + "# TODO: Create three experiments with different staffing levels\n", + "reduced_staffing = Experiment(n_operators=11)\n", + "current_staffing = Experiment(n_operators=13) # baseline\n", + "increased_staffing = Experiment(n_operators=15)\n", + "\n", + "# TODO: Run 10 replications for each scenario\n", + "print(\"Running reduced staffing scenario (11 operators)...\")\n", + "reduced_results = multiple_replications(reduced_staffing, n_reps=10)\n", + "\n", + "print(\"Running current staffing scenario (13 operators)...\")\n", + "current_results = multiple_replications(current_staffing, n_reps=10)\n", + "\n", + "print(\"Running increased staffing scenario (15 operators)...\")\n", + "increased_results = multiple_replications(increased_staffing, n_reps=10)\n", + "\n", + "print(\"All scenarios completed!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "01f4d22d-bddf-4a2b-a231-aeb979acdd70", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== STAFFING SCENARIOS SUMMARY ===\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", + "
ScenarioMean_Waiting_TimeStd_Waiting_TimeMean_UtilizationStd_Utilization
0Reduced (11 ops)50.1011.6198.950.58
1Current (13 ops)2.690.8492.691.83
2Increased (15 ops)0.470.1080.571.61
\n", + "
" + ], + "text/plain": [ + " Scenario Mean_Waiting_Time Std_Waiting_Time Mean_Utilization \\\n", + "0 Reduced (11 ops) 50.10 11.61 98.95 \n", + "1 Current (13 ops) 2.69 0.84 92.69 \n", + "2 Increased (15 ops) 0.47 0.10 80.57 \n", + "\n", + " Std_Utilization \n", + "0 0.58 \n", + "1 1.83 \n", + "2 1.61 " + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# TODO: Calculate summary statistics for each scenario\n", + "\n", + "# Create results dictionary\n", + "scenario_results = {\n", + " 'Reduced (11 ops)': reduced_results,\n", + " 'Current (13 ops)': current_results,\n", + " 'Increased (15 ops)': increased_results\n", + "}\n", + "\n", + "# Create summary table\n", + "scenarios_summary = create_summary_table(\n", + " results_dict=scenario_results,\n", + " label_key='Scenario',\n", + " label_order=['Reduced (11 ops)', 'Current (13 ops)', 'Increased (15 ops)']\n", + ")\n", + "\n", + "print(\"=== STAFFING SCENARIOS SUMMARY ===\")\n", + "scenarios_summary" + ] + }, + { + "cell_type": "markdown", + "id": "be8beaf1-b274-4a9a-af0c-54a41a644ef8", + "metadata": {}, + "source": [ + "### 4.3 Demand Variation Analysis\n", + "\n", + "Test how the baseline staffing (13 operators) performs under different demand levels by varying the mean inter-arrival time.\n", + "\n", + "**Demand Scenarios**:\n", + "1. **Low Demand**: `mean_iat=0.8` (calls arrive less frequently)\n", + "2. **Current Demand**: `mean_iat=0.6` (baseline)\n", + "3. **High Demand**: `mean_iat=0.4` (calls arrive more frequently)\n", + "\n", + "**Requirements**:\n", + "- Use 10 replications for each demand scenario\n", + "- Keep staffing at 13 operators for all tests\n", + "- Compare results to identify potential problems\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "bbf3fd54-0b66-4db7-b98c-c4268f65f69e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running low demand scenario (mean_iat=0.8)...\n", + "Running current demand scenario (mean_iat=0.6)...\n", + "Running high demand scenario (mean_iat=0.4)...\n", + "All demand scenarios completed!\n" + ] + } + ], + "source": [ + "# Task 3: Demand Variation Analysis\n", + "# TODO: Create three experiments with different demand levels (mean_iat)\n", + "low_demand = Experiment(n_operators=13, mean_iat=0.8)\n", + "current_demand = Experiment(n_operators=13, mean_iat=0.6) # baseline\n", + "high_demand = Experiment(n_operators=13, mean_iat=0.4)\n", + "\n", + "# TODO: Run 10 replications for each demand scenario\n", + "print(\"Running low demand scenario (mean_iat=0.8)...\")\n", + "low_demand_results = multiple_replications(low_demand, n_reps=10)\n", + "\n", + "print(\"Running current demand scenario (mean_iat=0.6)...\")\n", + "current_demand_results = multiple_replications(current_demand, n_reps=10)\n", + "\n", + "print(\"Running high demand scenario (mean_iat=0.4)...\")\n", + "high_demand_results = multiple_replications(high_demand, n_reps=10)\n", + "\n", + "print(\"All demand scenarios completed!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "b900ef13-9ec6-41d8-8bf3-49193c3b0ed7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== STAFFING SCENARIOS SUMMARY ===\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", + "
ScenarioMean_Waiting_TimeStd_Waiting_TimeMean_UtilizationStd_Utilization
0Low (IAT=0.8)0.190.0569.441.96
1Current (IAT=0.6)2.690.8492.691.83
2High (IAT=0.4)143.349.9199.280.15
\n", + "
" + ], + "text/plain": [ + " Scenario Mean_Waiting_Time Std_Waiting_Time Mean_Utilization \\\n", + "0 Low (IAT=0.8) 0.19 0.05 69.44 \n", + "1 Current (IAT=0.6) 2.69 0.84 92.69 \n", + "2 High (IAT=0.4) 143.34 9.91 99.28 \n", + "\n", + " Std_Utilization \n", + "0 1.96 \n", + "1 1.83 \n", + "2 0.15 " + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# TODO: Create summary table for demand scenarios\n", + "\n", + "# Create results dictionary\n", + "scenario_results = {\n", + " 'Low (IAT=0.8)': low_demand_results,\n", + " 'Current (IAT=0.6)': current_demand_results,\n", + " 'High (IAT=0.4)': high_demand_results\n", + "}\n", + "\n", + "# Create summary table\n", + "demand_summary = create_summary_table(\n", + " results_dict=scenario_results,\n", + " label_key='Scenario',\n", + " label_order=['Low (IAT=0.8)', 'Current (IAT=0.6)', 'High (IAT=0.4)']\n", + ")\n", + "\n", + "print(\"=== STAFFING SCENARIOS SUMMARY ===\")\n", + "demand_summary" + ] + }, + { + "cell_type": "markdown", + "id": "46cdd911-0124-4ff6-8bad-0646149b8ae8", + "metadata": {}, + "source": [ + "### 4.4 Recommendations\n", + "\n", + "Based on your analysis from the exercises above, provide data-driven recommendations.\n", + "\n", + "**Questions to address**:\n", + "\n", + "1. **Optimal Staffing**: What is the \"optimal\" number of operators for current demand levels? Consider both waiting times and utilization efficiency.\n", + "\n", + "2. **High Demand Response**: What happens to the system under high demand conditions? What would you recommend to management?\n", + "\n", + "3. **Trade-offs**: Explain the trade-off between operator utilization and customer waiting times that you observed.\n", + "\n", + "**Your Recommendations**:\n", + "\n", + "### Question 1 - Optimal Staffing\n", + "[Provide your analysis and recommendation here]\n", + "\n", + "### Question 2 - High Demand Response \n", + "[Describe what happens under high demand and your recommendations]\n", + "\n", + "### Question 3 - Trade-offs\n", + "[Explain the utilization vs. waiting time trade-off you observed]\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/content/basic_model.py b/content/basic_model.py new file mode 100644 index 0000000..16a6536 --- /dev/null +++ b/content/basic_model.py @@ -0,0 +1,547 @@ +""" +Call Centre Simulation Model + +A discrete event simulation model of a call centre using SimPy. +Contains experiment management, distribution classes, and simulation logic. + +To be used with 07a_experiments_exercise.ipynb + +Author: Tom Monks +""" + +import numpy as np +import pandas as pd +import simpy +import itertools + +# ============================================================================= +# CONSTANTS AND DEFAULT VALUES +# ============================================================================= + +# Default resources +N_OPERATORS = 13 + +# Default mean inter-arrival time (exp) +MEAN_IAT = 60 / 100 + +# Default service time parameters (triangular) +CALL_LOW = 5.0 +CALL_MODE = 7.0 +CALL_HIGH = 10.0 + +# Sampling settings +N_STREAMS = 2 +DEFAULT_RND_SET = 0 + +# Boolean switch to display simulation results as the model runs +TRACE = False + +# Run variables +RESULTS_COLLECTION_PERIOD = 1000 + + +# ============================================================================= +# DISTRIBUTION CLASSES +# ============================================================================= + +class Triangular: + """ + Convenience class for the triangular distribution. + Packages up distribution parameters, seed and random generator. + """ + + def __init__(self, low, mode, high, random_seed=None): + """ + Constructor. Accepts and stores parameters of the triangular dist + and a random seed. + + Params: + ------ + low: float + The smallest values that can be sampled + + mode: float + The most frequently sample value + + high: float + The highest value that can be sampled + + random_seed: int | SeedSequence, optional (default=None) + Used with params to create a series of repeatable samples. + """ + self.rand = np.random.default_rng(seed=random_seed) + self.low = low + self.high = high + self.mode = mode + + def sample(self, size=None): + """ + Generate one or more samples from the triangular distribution + + Params: + -------- + size: int + the number of samples to return. If size=None then a single + sample is returned. + + Returns: + ------- + float or np.ndarray (if size >=1) + """ + return self.rand.triangular(self.low, self.mode, self.high, size=size) + + +class Exponential: + """ + Convenience class for the exponential distribution. + Packages up distribution parameters, seed and random generator. + """ + + def __init__(self, mean, random_seed=None): + """ + Constructor + + Params: + ------ + mean: float + The mean of the exponential distribution + + random_seed: int| SeedSequence, optional (default=None) + A random seed to reproduce samples. If set to none then a unique + sample is created. + """ + self.rand = np.random.default_rng(seed=random_seed) + self.mean = mean + + def sample(self, size=None): + """ + Generate a sample from the exponential distribution + + Params: + ------- + size: int, optional (default=None) + the number of samples to return. If size=None then a single + sample is returned. + + Returns: + ------- + float or np.ndarray (if size >=1) + """ + return self.rand.exponential(self.mean, size=size) + + +# ============================================================================= +# EXPERIMENT CLASS +# ============================================================================= + +class Experiment: + """ + Encapsulates the concept of an experiment ๐Ÿงช with the urgent care + call centre simulation model. + + An Experiment: + 1. Contains a list of parameters that can be left as defaults or varied + 2. Provides a place for the experimentor to record results of a run + 3. Controls the set & streams of psuedo random numbers used in a run. + """ + + def __init__( + self, + random_number_set=DEFAULT_RND_SET, + n_operators=N_OPERATORS, + mean_iat=MEAN_IAT, + call_low=CALL_LOW, + call_mode=CALL_MODE, + call_high=CALL_HIGH, + n_streams=N_STREAMS, + ): + """ + The init method sets up our defaults. + """ + # sampling + self.random_number_set = random_number_set + self.n_streams = n_streams + + # store parameters for the run of the model + self.n_operators = n_operators + self.mean_iat = mean_iat + self.call_low = call_low + self.call_mode = call_mode + self.call_high = call_high + + # resources: we must init resources after an Environment is created. + # But we will store a placeholder for transparency + self.operators = None + + # initialise results to zero + self.init_results_variables() + + # initialise sampling objects + self.init_sampling() + + def set_random_no_set(self, random_number_set): + """ + Controls the random sampling + Parameters: + ---------- + random_number_set: int + Used to control the set of pseudo random numbers used by + the distributions in the simulation. + """ + self.random_number_set = random_number_set + self.init_sampling() + + def init_sampling(self): + """ + Create the distributions used by the model and initialise + the random seeds of each. + """ + # produce n non-overlapping streams + seed_sequence = np.random.SeedSequence(self.random_number_set) + self.seeds = seed_sequence.spawn(self.n_streams) + + # create distributions + + # call inter-arrival times + self.arrival_dist = Exponential( + self.mean_iat, random_seed=self.seeds[0] + ) + + # duration of call triage + self.call_dist = Triangular( + self.call_low, + self.call_mode, + self.call_high, + random_seed=self.seeds[1], + ) + + def init_results_variables(self): + """ + Initialise all of the experiment variables used in results + collection. This method is called at the start of each run + of the model + """ + # variable used to store results of experiment + self.results = {} + self.results["waiting_times"] = [] + + # total operator usage time for utilisation calculation. + self.results["total_call_duration"] = 0.0 + + +# ============================================================================= +# UTILITY FUNCTIONS +# ============================================================================= + +def trace(msg): + """ + Turning printing of events on and off. + + Params: + ------- + msg: str + string to print to screen. + """ + if TRACE: + print(msg) + + +# ============================================================================= +# MODEL LOGIC +# ============================================================================= + +def service(identifier, env, args): + """ + Simulates the service process for a call operator + + 1. request and wait for a call operator + 2. phone triage (triangular) + 3. exit system + + Params: + ------ + identifier: int + A unique identifer for this caller + + env: simpy.Environment + The current environent the simulation is running in + We use this to pause and restart the process after a delay. + + args: Experiment + The settings and input parameters for the current experiment + """ + + # record the time that call entered the queue + start_wait = env.now + + # request an operator - stored in the Experiment + with args.operators.request() as req: + yield req + + # record the waiting time for call to be answered + waiting_time = env.now - start_wait + + # store the results for an experiment + args.results["waiting_times"].append(waiting_time) + + trace(f"operator answered call {identifier} at {env.now:.3f}") + + # the sample distribution is defined by the experiment + call_duration = args.call_dist.sample() + + # schedule process to begin again after call_duration + yield env.timeout(call_duration) + + # update the total call_duration + args.results["total_call_duration"] += call_duration + + # print out information for patient. + trace( + f"call {identifier} ended {env.now:.3f}; " + + f"waiting time was {waiting_time:.3f}" + ) + + +def arrivals_generator(env, args): + """ + IAT is exponentially distributed + + Parameters: + ------ + env: simpy.Environment + The simpy environment for the simulation + + args: Experiment + The settings and input parameters for the simulation. + """ + # use itertools as it provides an infinite loop + # with a counter variable that we can use for unique Ids + for caller_count in itertools.count(start=1): + + # the sample distribution is defined by the experiment + inter_arrival_time = args.arrival_dist.sample() + + yield env.timeout(inter_arrival_time) + + trace(f"call arrives at: {env.now:.3f}") + + # we pass the experiment to the service function + env.process(service(caller_count, env, args)) + + +# ============================================================================= +# EXPERIMENT EXECUTION FUNCTIONS +# ============================================================================= + +def single_run(experiment, rep=0, rc_period=RESULTS_COLLECTION_PERIOD): + """ + Perform a single run of the model and return the results + + Parameters: + ----------- + experiment: Experiment + The experiment/paramaters to use with model + + rep: int, optional (default=0) + The replication number for random seed control + + rc_period: float, optional (default=RESULTS_COLLECTION_PERIOD) + Results collection period - how long to run the simulation + + Returns: + -------- + dict: Dictionary containing the key performance indicators + """ + + # results dictionary. Each KPI is a new entry. + run_results = {} + + # reset all result collection variables + experiment.init_results_variables() + + # set random number set to the replication no. + # this controls sampling for the run. + experiment.set_random_no_set(rep) + + # environment is (re)created inside single run + env = simpy.Environment() + + # we create simpy resource here - this has to be after we + # create the environment object. + experiment.operators = simpy.Resource(env, capacity=experiment.n_operators) + + # we pass the experiment to the arrivals generator + env.process(arrivals_generator(env, experiment)) + env.run(until=rc_period) + + # end of run results: calculate mean waiting time + run_results["01_mean_waiting_time"] = np.mean( + experiment.results["waiting_times"] + ) + + # end of run results: calculate mean operator utilisation + run_results["02_operator_util"] = ( + experiment.results["total_call_duration"] + / (rc_period * experiment.n_operators) + ) * 100.0 + + # return the results from the run of the model + return run_results + + +def multiple_replications( + experiment, rc_period=RESULTS_COLLECTION_PERIOD, n_reps=5 +): + """ + Perform multiple replications of the model. + + Params: + ------ + experiment: Experiment + The experiment/paramaters to use with model + + rc_period: float, optional (default=RESULTS_COLLECTION_PERIOD) + results collection period. + the number of minutes to run the model to collect results + + n_reps: int, optional (default=5) + Number of independent replications to run. + + Returns: + -------- + pandas.DataFrame + DataFrame containing results from all replications + """ + + # loop over single run to generate results dicts in a python list. + results = [single_run(experiment, rep, rc_period) for rep in range(n_reps)] + + # format and return results in a dataframe + df_results = pd.DataFrame(results) + df_results.index = np.arange(1, len(df_results) + 1) + df_results.index.name = "rep" + return df_results + + +# ============================================================================= +# CONVENIENCE FUNCTIONS +# ============================================================================= + +def set_trace(trace_on=True): + """ + Convenience function to turn tracing on/off globally. + + Its not ideal to use global variables like this, but + it is included for simplicity. + + Params: + ------- + trace_on: bool, optional (default=True) + Whether to turn tracing on or off + """ + global TRACE + TRACE = trace_on + + +def summary_stats(results_df): + """ + Calculate summary statistics for a results DataFrame + + Params: + ------- + results_df: pandas.DataFrame + DataFrame from multiple_replications function + + Returns: + -------- + pandas.DataFrame: Summary statistics (mean, std, min, max) + """ + return results_df.describe() + + +def compare_experiments(experiments_dict, n_reps=10, rc_period=RESULTS_COLLECTION_PERIOD): + """ + Compare multiple experiments and return summary results + + Params: + ------- + experiments_dict: dict + Dictionary where keys are experiment names and values are Experiment objects + n_reps: int, optional (default=10) + Number of replications for each experiment + rc_period: float, optional (default=RESULTS_COLLECTION_PERIOD) + Results collection period + + Returns: + -------- + pandas.DataFrame: Comparison of mean results across experiments + """ + comparison_results = {} + + for name, experiment in experiments_dict.items(): + results = multiple_replications(experiment, rc_period, n_reps) + comparison_results[name] = results.mean() + + return pd.DataFrame(comparison_results).T + + +def create_summary_table(results_dict, label_key, label_order): + """ + Create a summary DataFrame for multiple scenarios or demand levels. + + Parameters: + ----------- + results_dict: dict + Dictionary where keys are labels (e.g., scenario or demand level names) and values are + pandas DataFrames containing replication results with columns '01_mean_waiting_time' and '02_operator_util'. + + label_key: str + The name of the column for the labels in the summary table (e.g., 'Scenario' or 'Demand_Level'). + + label_order: list + The order of labels to appear in the summary table. + + Returns: + -------- + pandas.DataFrame + Summary table with mean and std for waiting time and utilization. + """ + summary_data = { + label_key: [], + 'Mean_Waiting_Time': [], + 'Std_Waiting_Time': [], + 'Mean_Utilization': [], + 'Std_Utilization': [] + } + + for label in label_order: + df = results_dict[label] + summary_data[label_key].append(label) + summary_data['Mean_Waiting_Time'].append(df['01_mean_waiting_time'].mean()) + summary_data['Std_Waiting_Time'].append(df['01_mean_waiting_time'].std()) + summary_data['Mean_Utilization'].append(df['02_operator_util'].mean()) + summary_data['Std_Utilization'].append(df['02_operator_util'].std()) + + return pd.DataFrame(summary_data) + + + +# ============================================================================= +# MODULE INFORMATION +# ============================================================================= + +__version__ = "1.0.0" +__author__ = "Tom Monks" +__all__ = [ + # Classes + 'Experiment', 'Triangular', 'Exponential', + # Main functions + 'single_run', 'multiple_replications', + # Model functions + 'service', 'arrivals_generator', + # Utility functions + 'trace', 'set_trace', 'summary_stats', 'compare_experiments', "create_summary_table", + # Constants + 'N_OPERATORS', 'MEAN_IAT', 'CALL_LOW', 'CALL_MODE', 'CALL_HIGH', + 'RESULTS_COLLECTION_PERIOD', 'TRACE' +]