diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 830855e..962e4f0 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -23,12 +23,6 @@ jobs: cache: 'pip' - run: pip install -r requirements.txt - - name: Lint model code + - name: Lint run: | - flake8 ./simulation - pylint ./simulation - - - name: Lint tests - run: | - flake8 ./tests - pylint ./tests + bash lint.sh \ No newline at end of file diff --git a/README.md b/README.md index 6aa5a16..a36968e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ This repository provides a template for building discrete-event simulation (DES) For clarity, changes from the DES book in this template are explained in `docs/hsma_changes.md`. -✨ **Style:** The coding style is based on the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html). Linting is implemented using `flake8` and `pylint` for `.py` files, and `pycodestyle` for `.ipynb` files. +✨ **Style:** The coding style is based on the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html). Linting is implemented using `pylint` (with `nbqa` to enable it to run on jupyter notebooks). 🧱 **Package structure:** In Python, a package can simply be defined as a directory with an `__init__.py` file in it. In this repository, the scripts for model (within `simulation/`) are treated as a little local package. This keeps the code model code isolated from our experiments and analysis. It is installed as an editable (`-e`) local import - with `-e` meaning it will update with changes to the local files in `simulation/`. As it is installed in our environment, it can then easily be used anywhere else in the directory - here, in `notebooks/` and `tests/` - without needing any additional code (e.g. no need to modify `sys.path`, or have additional `__init__.py` files). @@ -162,18 +162,22 @@ If you have changed the model behaviour, you may wish to amend, remove or write 🔎 **Linting** -You can lint the `.py` files by running either of this commands from the terminal: +You can lint the `.py` files by running: ``` -flake8 simulation/model.py pylint simulation/model.py ``` -The first commands in the `.ipynb` files will lint the notebooks using `pycodestyle` when executed: +You can lint the `.ipynb` by adding `nbqa` to the start of the command - e.g.: ``` -%load_ext pycodestyle_magic -%pycodestyle_on +nbqa pylint notebooks/analysis.ipynb +``` + +A bash script has been provided which can be used to lint all files in succession by running: + +``` +bash lint.sh ```
@@ -225,6 +229,7 @@ repo/ ├── CONTRIBUTING.md # Contribution instructions ├── environment.yaml # Conda environment (includes Python version) ├── LICENSE # Licence file +├── lint.sh # Bash script to lint all .py and .ipynb files at once ├── pyproject.toml # Metadata for local `simulation/` package ├── README.md # This file! Describes the repository └── requirements.txt # Virtual environment (used by GitHub actions) diff --git a/docs/time_weighted_averages.ipynb b/docs/time_weighted_averages.ipynb index 9894ae1..cbac3af 100644 --- a/docs/time_weighted_averages.ipynb +++ b/docs/time_weighted_averages.ipynb @@ -15,9 +15,7 @@ "metadata": {}, "outputs": [], "source": [ - "%load_ext pycodestyle_magic\n", - "%pycodestyle_on\n", - "\n", + "# pylint: disable=missing-module-docstring\n", "import plotly.express as px\n", "import plotly.io as pio\n", "\n", @@ -57,7 +55,7 @@ { "data": { "image/svg+xml": [ - "02468101214160123TimeQueue size" + "02468101214160123TimeQueue size" ] }, "metadata": {}, @@ -171,6 +169,7 @@ " queue[i] * (time[i+1] - time[i]) for i in range(len(time)-1))\n", "\n", "# Total time duration\n", + "# pylint:disable=invalid-name\n", "total_time = time[-1] - time[0]\n", "\n", "# Compute time-weighted average\n", @@ -227,7 +226,7 @@ { "data": { "image/svg+xml": [ - "024681012141600.51TimeNurse utilisation" + "024681012141600.51TimeNurse utilisation" ] }, "metadata": {}, @@ -351,7 +350,7 @@ ], "metadata": { "kernelspec": { - "display_name": "template-des", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -365,7 +364,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.0" + "version": "3.13.1" } }, "nbformat": 4, diff --git a/environment.yaml b/environment.yaml index f34ad17..7f3fe20 100644 --- a/environment.yaml +++ b/environment.yaml @@ -2,24 +2,20 @@ name: template-des channels: - conda-forge dependencies: - - black - - flake8=7.1.1 - ipykernel=6.29.5 - jinja2=3.1.5 - joblib=1.4.2 - nbformat=5.10.4 - - nbqa - - numpy=2.1.3 + - nbqa=1.9.0 + - numpy=2.2.2 - pandas=2.2.3 - - pip=24.3.1 + - pip=25.0 - plotly_express=0.4.1 - - pycodestyle=2.12.1 - - pylint=3.3.3 + - pylint=3.3.4 - pytest=8.3.4 - pytest-xdist=3.6.1 - - python=3.13.0 + - python=3.13.1 - simpy=4.1.1 - pip: - kaleido==0.2.1 - - pycodestyle_magic==0.5 - -e .[dev] diff --git a/lint.sh b/lint.sh new file mode 100644 index 0000000..e558a8e --- /dev/null +++ b/lint.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +echo "Linting model code..." +pylint ./simulation + +echo "Linting tests..." +pylint ./tests + +echo "Linting notebooks..." +nbqa pylint ./notebooks + +echo "Linting time-weighted averages notebook..." +nbqa pylint ./docs/time_weighted_averages.ipynb \ No newline at end of file diff --git a/notebooks/analysis.ipynb b/notebooks/analysis.ipynb index d7a2e7f..46ef9c0 100644 --- a/notebooks/analysis.ipynb +++ b/notebooks/analysis.ipynb @@ -16,178 +16,15 @@ "\n", "Credit:\n", "\n", - "* Analysis of the spread of replication results was adapted from Tom Monks (2024) HPDM097 - Making a difference with health data (https://github.com/health-data-science-OR/stochastic_systems) (MIT Licence)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "jp-MarkdownHeadingCollapsed": true - }, - "source": [ - "## Plotly \"allow a user to select KPI\" histogram\n", - "\n", - "The function `create_user_controlled_hist` creates a plotly chart that allows a user to choose which of the KPIs to view. It has optional parameter to map the simulation result variable names to a display \"friendly name\" and \"units\". Users pick a KPI from a drop down list. An optional parameter can be used to exclude certain results if they are not relevant to the modeller. " - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import plotly.graph_objects as go" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "def create_user_controlled_hist(\n", - " results, exclude_columns=None, name_mappings=None, include_instruct=True\n", - "):\n", - " \"\"\"\n", - " Create a plotly histogram that includes a drop down list that allows a user\n", - " to select which KPI is displayed as a histogram\n", - "\n", - " Params:\n", - " -------\n", - " results: pd.DataFrame\n", - " rows = replications, cols = KPIs\n", - " exclude_columns: list, optional\n", - " List of column numbers to exclude from the dropdown list\n", - " name_mappings: dict, optional\n", - " Nested dictionary mapping column names to friendly names and units\n", - " Format: {column_name: {'friendly_name': str, 'units': str}}#\n", - " include_instruct: bool, optional\n", - " Including the instruction \"Select KPI from drop down list\" above\n", - " plot. Useful for interactive applications.\n", - "\n", - " Returns:\n", - " -------\n", - " plotly.figure\n", - "\n", - " Source:\n", - " ------\n", - " The code in this function was adapted from:\n", - " https://stackoverflow.com/questions/59406167/plotly-how-to-filter-a-pandas-\n", - " dataframe-using-a-dropdown-menu\n", - "\n", - " and\n", - "\n", - " Monks T and Harper A. Improving the usability of open health service \n", - " delivery simulation models using Python and web apps \n", - " [version 2; peer review: 3 approved]. NIHR Open Res 2023, 3:48 #\n", - " https://doi.org/10.3310/nihropenres.13467.2\n", - " \"\"\"\n", - "\n", - " # create a figure\n", - " fig = go.Figure()\n", - "\n", - " # Filter out excluded columns\n", - " if exclude_columns is None:\n", - " exclude_columns = []\n", - " included_columns = [\n", - " col\n", - " for i, col in enumerate(results.columns)\n", - " if i not in exclude_columns\n", - " ]\n", - "\n", - " # set up ONE trace\n", - " first_col = included_columns[0]\n", - " first_friendly_name = (\n", - " name_mappings[first_col]['friendly_name']\n", - " if name_mappings and first_col in name_mappings\n", - " else first_col\n", - " )\n", - " first_units = (\n", - " name_mappings[first_col]['units']\n", - " if name_mappings and first_col in name_mappings\n", - " else ''\n", - " )\n", - " first_x_title = (\n", - " f'{first_friendly_name} ({first_units})'\n", - " if first_units\n", - " else first_friendly_name\n", - " )\n", - "\n", - " fig.add_trace(go.Histogram(x=results[first_col]))\n", - " fig.update_xaxes(title_text=first_x_title)\n", - "\n", - " buttons = []\n", - "\n", - " # create list of drop down items - KPIs\n", - " for col in included_columns:\n", - " if name_mappings and col in name_mappings:\n", - " friendly_name = name_mappings[col]['friendly_name']\n", - " units = name_mappings[col]['units']\n", - " x_title = f'{friendly_name} ({units})' if units else friendly_name\n", - " else:\n", - " friendly_name = col\n", - " x_title = col\n", - "\n", - " buttons.append(\n", - " dict(\n", - " method='update',\n", - " label=friendly_name,\n", - " args=[{'x': [results[col]]}, {'xaxis': {'title': x_title}}],\n", - " )\n", - " )\n", - "\n", - " # create update menu and parameters\n", - " updatemenu = []\n", - " your_menu = dict()\n", - " updatemenu.append(your_menu)\n", - "\n", - " updatemenu[0]['buttons'] = buttons\n", - " updatemenu[0]['direction'] = 'down'\n", - " updatemenu[0]['showactive'] = True\n", - "\n", - " # add dropdown menus to the figure\n", - " fig.update_layout(showlegend=False, updatemenus=updatemenu)\n", - "\n", - " # Add annotation as instruction\n", - " if include_instruct:\n", - " fig.add_annotation(\n", - " text='Select a KPI from the drop down list',\n", - " xref='paper',\n", - " yref='paper',\n", - " x=0.0,\n", - " y=1.1, # Positions the text above the plot\n", - " showarrow=False,\n", - " font=dict(size=12),\n", - " )\n", - "\n", - " return fig" + "* Analysis of the spread of replication results was adapted from Tom Monks (2024) HPDM097 - Making a difference with health data (https://github.com/health-data-science-OR/stochastic_systems) (MIT Licence).\n", + "* The function `create_user_controlled_hist()` was adapted from [stack overflow](https://stackoverflow.com/questions/59406167/plotly-how-to-filter-a-pandas-dataframe-using-a-dropdown-menu) and Monks T and Harper A. [Improving the usability of open health service delivery simulation models using Python and web apps](https://doi.org/10.3310/nihropenres.13467.2) [version 2; peer review: 3 approved]. NIHR Open Res 2023, 3:48." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Set-up\n", - "\n", - "Load notebook linters." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "# %load_ext pycodestyle_magic" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "# %pycodestyle_on" + "## Set-up" ] }, { @@ -199,10 +36,11 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ + "# pylint: disable=missing-module-docstring\n", "# To ensure any updates to `simulation/` are fetched without needing to restart\n", "# the notebook environment, reload `simulation/` before execution of each cell\n", "%load_ext autoreload\n", @@ -212,15 +50,18 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ + "# pylint: disable=wrong-import-position\n", "import os\n", + "import time\n", + "from IPython.display import display\n", "import pandas as pd\n", "import plotly.express as px\n", + "import plotly.graph_objects as go\n", "import plotly.io as pio\n", - "import time\n", "\n", "from simulation.logging import SimLogger\n", "from simulation.model import (\n", @@ -231,48 +72,58 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Display plotly express figures as non-interactive figures. This means they will be visible when browsing the notebooks on GitHub. To switch these back to interactive figures, simply remove this line." + "Start timer." ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ - "pio.renderers.default = 'svg'" + "start_time = time.time()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Start timer." + "Define path to outputs folder." ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ - "start_time = time.time()" + "OUTPUT_DIR = '../outputs/'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Define path to outputs folder." + "Define labels for variables in the dataset." ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ - "output_dir = '../outputs/'" + "LABELS = {\n", + " 'arrivals': 'Patient arrivals (n)',\n", + " 'mean_q_time_nurse': 'Mean wait time for nurse (minutes)',\n", + " 'mean_n_consult_time': 'Mean consultation time with nurse (minutes)',\n", + " 'mean_time_with_nurse': 'Mean consultation time with nurse (minutes)',\n", + " 'mean_nurse_utilisation': 'Mean nurse utilisation',\n", + " 'mean_nurse_utilisation_tw': 'Time-weighted mean nurse utilisation',\n", + " 'mean_nurse_q_length': 'Time-weighted mean queue length for nurse (n)',\n", + " 'patient_inter': 'Patient inter-arrival time',\n", + " 'number_of_nurses': 'Number of nurses'\n", + "}" ] }, { @@ -291,7 +142,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -311,7 +162,7 @@ "```python\n", "# Save file\n", "experiment.patient_results_df.to_csv(\n", - " os.path.join(output_dir, 'example_patient.csv.gz'),\n", + " os.path.join(OUTPUT_DIR, 'example_patient.csv.gz'),\n", " index=False, compression='gzip')\n", "\n", "# Load file\n", @@ -323,7 +174,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -418,7 +269,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -535,12 +386,12 @@ "source": [ "display(experiment.run_results_df.head())\n", "experiment.run_results_df.to_csv(\n", - " os.path.join(output_dir, 'example_run.csv'), index=False)" + " os.path.join(OUTPUT_DIR, 'example_run.csv'), index=False)" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -645,12 +496,12 @@ "source": [ "display(experiment.interval_audit_df.head())\n", "experiment.interval_audit_df.to_csv(\n", - " os.path.join(output_dir, 'example_interval_audit.csv'), index=False)" + " os.path.join(OUTPUT_DIR, 'example_interval_audit.csv'), index=False)" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -750,83 +601,1374 @@ "source": [ "display(experiment.overall_results_df.head())\n", "experiment.overall_results_df.to_csv(\n", - " os.path.join(output_dir, 'example_overall.csv'), index=False)" + " os.path.join(OUTPUT_DIR, 'example_overall.csv'), index=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## View spread of results across replications\n", + "## Interactive plots that show the spread of results across replications\n", "\n", - "> TM Note: I have kept code as before, but added the additional user controlled histogram plot to select KPIs" + "In this plot, modeller select the desired metric to view from a drop down list.\n", + "\n", + "Note: These will not be rendered if viewing the notebook on GitHub - instead, scroll down to view non-interactive version of plots." ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "pio.renderers.default = 'plotly_mimetype'" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "def create_user_controlled_hist(\n", + " results, exclude_columns=None, name_mappings=None, include_instruct=True\n", + "):\n", + " \"\"\"\n", + " Create a plotly histogram that includes a drop down list that allows a user\n", + " to select which key performance indicator (KPIs) are displayed.\n", + "\n", + " It has the optional parameter to map the simulation result variable names\n", + " to display a \"friendly name\" and \"units\". Users pick a KPI from a drop\n", + " down list. An optional parameter can be used to exclude certain results if\n", + " they are not relevant to the modeller.\n", + "\n", + " Arguments:\n", + " results (pd.DataFrame)\n", + " Dataframe where rows are replications and columns are KPIs.\n", + " exclude_columns (list, optional)\n", + " List of column numbers to exclude from the dropdown list.\n", + " name_mappings (dict, optional)\n", + " Dictionary mapping column names to labels. If not provided,\n", + " function will default to variable names.\n", + " include_instruct (bool, optional)\n", + " Including the instruction \"Select KPI from drop down list\" above\n", + " plot. Useful for interactive applications.\n", + "\n", + " Returns:\n", + " plotly.figure\n", + " \"\"\"\n", + " # Set as empty dictionary if mappings not provided. The function will later\n", + " # search for a label and find no match, so use the variable name.\n", + " if name_mappings is None:\n", + " name_mappings = {}\n", + "\n", + " # Initialise figure\n", + " fig = go.Figure()\n", + "\n", + " # Determine included columns (filter out excluded ones)\n", + " exclude_columns = exclude_columns or []\n", + " included_columns = [col for i, col in enumerate(results.columns)\n", + " if i not in exclude_columns]\n", + "\n", + " # Add initial histogram trace, with first column, for initial display\n", + " initial_column = included_columns[0]\n", + " fig.add_trace(go.Histogram(x=results[initial_column]))\n", + " fig.update_layout(\n", + " # If name_mappings is provided, use it; otherwise, use the column name\n", + " xaxis_title=name_mappings.get(initial_column, initial_column),\n", + " yaxis_title='Replications')\n", + "\n", + " # Generate dropdown buttons for KPI selection\n", + " buttons = [\n", + " {\n", + " 'method': 'update',\n", + " 'label': name_mappings.get(col, col),\n", + " 'args': [\n", + " {'x': [results[col]]},\n", + " {'xaxis': {'title': name_mappings.get(col, col)}}\n", + " ],\n", + " }\n", + " for col in included_columns\n", + " ]\n", + "\n", + " # Configure dropdown menu\n", + " fig.update_layout(\n", + " showlegend=False,\n", + " updatemenus=[{'buttons': buttons,\n", + " 'direction': 'down',\n", + " 'showactive': True}]\n", + " )\n", + "\n", + " # Optionally add an instruction annotation\n", + " if include_instruct:\n", + " fig.add_annotation(\n", + " text='Select a KPI from the drop down list',\n", + " xref='paper',\n", + " yref='paper',\n", + " x=0.0,\n", + " y=1.1, # Position text above the plot\n", + " showarrow=False,\n", + " font={'size': 12},\n", + " )\n", + "\n", + " return fig" + ] + }, + { + "cell_type": "code", + "execution_count": 13, "metadata": {}, "outputs": [ { "data": { - "image/svg+xml": [ - "10.6k10.8k11k024681012Patient Arrivals (n)Select a KPI from the drop down listPatient Arrivals" - ] + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "type": "histogram", + "x": [ + 10972, + 10784, + 10854, + 10831, + 10720, + 10772, + 10831, + 10781, + 10772, + 10705, + 10927, + 10688, + 11092, + 10640, + 10904, + 10849, + 10719, + 10713, + 10568, + 10707, + 10845, + 10726, + 10618, + 10914, + 10660, + 10685, + 10806, + 10748, + 10589, + 10863, + 10796 + ] + } + ], + "layout": { + "annotations": [ + { + "font": { + "size": 12 + }, + "showarrow": false, + "text": "Select a KPI from the drop down list", + "x": 0, + "xref": "paper", + "y": 1.1, + "yref": "paper" + } + ], + "showlegend": false, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "updatemenus": [ + { + "buttons": [ + { + "args": [ + { + "x": [ + [ + 10972, + 10784, + 10854, + 10831, + 10720, + 10772, + 10831, + 10781, + 10772, + 10705, + 10927, + 10688, + 11092, + 10640, + 10904, + 10849, + 10719, + 10713, + 10568, + 10707, + 10845, + 10726, + 10618, + 10914, + 10660, + 10685, + 10806, + 10748, + 10589, + 10863, + 10796 + ] + ] + }, + { + "xaxis": { + "title": "Patient arrivals (n)" + } + } + ], + "label": "Patient arrivals (n)", + "method": "update" + }, + { + "args": [ + { + "x": [ + [ + 0.504541081338615, + 0.514150649003393, + 0.5232349226016817, + 0.4791488631810612, + 0.46145745726579823, + 0.3882646868128185, + 0.4669381081669874, + 0.625888360901447, + 0.4684971326393017, + 0.5634349862001105, + 0.562585969190057, + 0.39781182403365434, + 0.44530362984867583, + 0.4397605749351992, + 0.6107822771635987, + 0.47670998312351787, + 0.428336230071242, + 0.505574486075843, + 0.3615821082986753, + 0.4191589720392036, + 0.6120142008002236, + 0.5173212645251098, + 0.5316566599495316, + 0.5270546197660896, + 0.5029697553047842, + 0.6270646900660448, + 0.5061794260886525, + 0.4925537687951073, + 0.4577189216542841, + 0.5422405738480466, + 0.5102078888697092 + ] + ] + }, + { + "xaxis": { + "title": "Mean wait time for nurse (minutes)" + } + } + ], + "label": "Mean wait time for nurse (minutes)", + "method": "update" + }, + { + "args": [ + { + "x": [ + [ + 9.84226781662332, + 10.060480983450425, + 9.925024519746302, + 9.9370571543943, + 10.015904147971671, + 9.884995942861282, + 10.041799654744745, + 10.086979128063648, + 10.202270228377186, + 10.09260227901333, + 10.083553615426284, + 9.893334037740962, + 9.810905063859671, + 9.933879963761742, + 10.167683587926042, + 9.975581276412813, + 9.809804967118032, + 9.989154604862323, + 9.800182515281657, + 9.9193906381964, + 9.813774247048583, + 9.858186468091487, + 9.969891296475984, + 9.925291785145637, + 9.96454608027881, + 10.236338445072258, + 10.039653295082593, + 10.050997027592315, + 10.044180668559221, + 10.034383892923174, + 9.922063713004379 + ] + ] + }, + { + "xaxis": { + "title": "Mean consultation time with nurse (minutes)" + } + } + ], + "label": "Mean consultation time with nurse (minutes)", + "method": "update" + }, + { + "args": [ + { + "x": [ + [ + 0.4996386466177992, + 0.5019905433327594, + 0.4981297032213408, + 0.49822049332297624, + 0.4968703372055205, + 0.492904233058074, + 0.503356251584998, + 0.5034489381047406, + 0.508790994907774, + 0.49997178325917613, + 0.5098373896503802, + 0.4895233535105571, + 0.5034276977164731, + 0.48925457544062534, + 0.513130373051619, + 0.5009629889699926, + 0.4866947365248423, + 0.4954087210793937, + 0.4793715775153063, + 0.4915291568180775, + 0.49261944375555833, + 0.48940978932654106, + 0.4892835610032433, + 0.5013818263178943, + 0.49160360886911897, + 0.5060501169430693, + 0.5022615440123254, + 0.4999546777219452, + 0.4923573694687436, + 0.5046064549261675, + 0.49578999982774674 + ] + ] + }, + { + "xaxis": { + "title": "Mean nurse utilisation" + } + } + ], + "label": "Mean nurse utilisation", + "method": "update" + }, + { + "args": [ + { + "x": [ + [ + 0.49974045251278504, + 0.5020690100987935, + 0.49826045142699366, + 0.49826895028941315, + 0.4971055007588991, + 0.4930559758823056, + 0.5034005361957363, + 0.5037422085180931, + 0.5089130015618397, + 0.5002278075946733, + 0.5099889270180521, + 0.489627107280519, + 0.503459064398387, + 0.48926772880832675, + 0.5135294499218969, + 0.5010455334069701, + 0.48682744992243954, + 0.4957697619501464, + 0.4795652084277076, + 0.49176351271863106, + 0.4926720149388125, + 0.4895612849605331, + 0.4893719186906069, + 0.5014338001561723, + 0.49162164437257705, + 0.5061407672693301, + 0.5023039700989324, + 0.5001183558252174, + 0.49236726450892265, + 0.5047331518468752, + 0.49597325153194544 + ] + ] + }, + { + "xaxis": { + "title": "Time-weighted mean nurse utilisation" + } + } + ], + "label": "Time-weighted mean nurse utilisation", + "method": "update" + }, + { + "args": [ + { + "x": [ + [ + 0.12814409130665008, + 0.1283472360845507, + 0.1326692082859329, + 0.12013104947023319, + 0.11450981346966105, + 0.09681451866545558, + 0.11706959836936667, + 0.15619681525181714, + 0.11682062761089254, + 0.1399966447950717, + 0.14230039086434612, + 0.09842159202017818, + 0.11437196007956935, + 0.10831140086366944, + 0.1541659710692565, + 0.11971820849321864, + 0.10628092708642692, + 0.1253754506789469, + 0.08845369723380557, + 0.10388738688943873, + 0.15364106499255614, + 0.1284441639651928, + 0.13067431517000294, + 0.1331544935214607, + 0.12411244424881943, + 0.15515572552368928, + 0.126615159220231, + 0.12254555340300495, + 0.11219411253234293, + 0.13635091096554006, + 0.12767905794112022 + ] + ] + }, + { + "xaxis": { + "title": "Time-weighted mean queue length for nurse (n)" + } + } + ], + "label": "Time-weighted mean queue length for nurse (n)", + "method": "update" + } + ], + "direction": "down", + "showactive": true + } + ], + "xaxis": { + "title": { + "text": "Patient arrivals (n)" + } + }, + "yaxis": { + "title": { + "text": "Replications" + } + } + } + } }, "metadata": {}, "output_type": "display_data" } ], "source": [ - "# optional name mappings to help user. if excluded then column names used.\n", - "name_mappings = {\n", - " 'arrivals': {'friendly_name': 'Patient Arrivals', 'units': 'n'},\n", - " 'mean_q_time_nurse': {\n", - " 'friendly_name': 'Nurse waiting time',\n", - " 'units': 'minutes',\n", - " },\n", - " 'mean_time_with_nurse': {\n", - " 'friendly_name': 'Patient contact time',\n", - " 'units': 'minutes',\n", - " },\n", - " 'mean_nurse_utilisation': {\n", - " 'friendly_name': 'Nurse Utilization',\n", - " 'units': '%',\n", - " },\n", - "}\n", - "\n", "create_user_controlled_hist(\n", " experiment.run_results_df,\n", " exclude_columns=[0, 1],\n", - " name_mappings=name_mappings,\n", + " name_mappings=LABELS,\n", " include_instruct=True,\n", ")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Static plots that show the spread of results across replications\n", + "\n", + "Display subsequent plotly express figures as non-interactive figures. This means they will be visible when browsing the notebooks on GitHub. To switch these back to interactive figures, simply remove this line." + ] + }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "pio.renderers.default = 'svg'" + ] + }, + { + "cell_type": "code", + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ - "def plot_results_spread(rep_results, column, x_label, file):\n", + "def plot_results_spread(\n", + " rep_results, column, file, name_mappings=None, path=OUTPUT_DIR\n", + "):\n", " \"\"\"\n", " Plot spread of results from across replications, for chosen column.\n", " Show figure and save under specified file name.\n", "\n", " Arguments:\n", " rep_results (pandas.DataFrame)\n", - " The dataframe of replication results. \n", + " The dataframe of replication results.\n", " column (str):\n", " Name of column to plot.\n", - " x_label (str):\n", - " X axis label.\n", " file (str):\n", " Filename to save figure to.\n", + " name_mappings (dict, optional)\n", + " Dictionary mapping column names to labels. If not provided,\n", + " function will default to variable names.\n", + " path (str):\n", + " Path to save the file to (excluding filename).\n", " \"\"\"\n", + " # Set as empty dictionary if mappings not provided. The function will later\n", + " # search for a label and find no match, so use the variable name.\n", + " if name_mappings is None:\n", + " name_mappings = {}\n", + "\n", + " # Create plot\n", " fig = px.histogram(rep_results[column])\n", " fig.update_layout(\n", - " xaxis_title=x_label,\n", + " xaxis_title=name_mappings.get(column, column),\n", " yaxis_title='Frequency',\n", " template='plotly_white',\n", " showlegend=False)\n", @@ -835,18 +1977,18 @@ " fig.show()\n", "\n", " # Save figure\n", - " fig.write_image(os.path.join(output_dir, file))" + " fig.write_image(os.path.join(path, file))" ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ - "10.5k10.6k10.7k10.8k10.9k11k024681012ArrivalsFrequency" + "10.5k10.6k10.7k10.8k10.9k11k024681012Patient arrivals (n)Frequency" ] }, "metadata": {}, @@ -855,7 +1997,7 @@ { "data": { "image/svg+xml": [ - "0.350.40.450.50.550.60.650246810Mean wait time for nurseFrequency" + "0.350.40.450.50.550.60.650246810Mean wait time for nurse (minutes)Frequency" ] }, "metadata": {}, @@ -864,7 +2006,7 @@ { "data": { "image/svg+xml": [ - "9.89.91010.110.20123456789Mean length of nurse consultationFrequency" + "9.89.91010.110.20123456789Mean consultation time with nurse (minutes)Frequency" ] }, "metadata": {}, @@ -873,7 +2015,7 @@ { "data": { "image/svg+xml": [ - "0.480.490.50.51012345678Mean nurse utilisationFrequency" + "0.480.490.50.51012345678Mean nurse utilisationFrequency" ] }, "metadata": {}, @@ -883,30 +2025,30 @@ "source": [ "plot_results_spread(\n", " rep_results=experiment.run_results_df,\n", - " column='arrivals', \n", - " x_label='Arrivals', \n", - " file='spread_arrivals.png'\n", + " column='arrivals',\n", + " file='spread_arrivals.png',\n", + " name_mappings=LABELS\n", ")\n", "\n", "plot_results_spread(\n", " rep_results=experiment.run_results_df,\n", " column='mean_q_time_nurse',\n", - " x_label='Mean wait time for nurse',\n", " file='spread_nurse_wait.png',\n", + " name_mappings=LABELS\n", ")\n", "\n", "plot_results_spread(\n", " rep_results=experiment.run_results_df,\n", " column='mean_time_with_nurse',\n", - " x_label='Mean length of nurse consultation',\n", " file='spread_nurse_time.png',\n", + " name_mappings=LABELS\n", ")\n", "\n", "plot_results_spread(\n", " rep_results=experiment.run_results_df,\n", " column='mean_nurse_utilisation',\n", - " x_label='Mean nurse utilisation',\n", " file='spread_nurse_util.png',\n", + " name_mappings=LABELS\n", ")" ] }, @@ -921,7 +2063,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -961,7 +2103,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 18, "metadata": {}, "outputs": [ { @@ -1090,7 +2232,7 @@ "4 3 5 " ] }, - "execution_count": 19, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -1108,12 +2250,11 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ - "def plot_scenario(results, x_var, result_var, colour_var, xaxis_title,\n", - " yaxis_title, legend_title):\n", + "def plot_scenario(results, x_var, result_var, colour_var, name_mappings):\n", " \"\"\"\n", " Plot results from different model scenarios.\n", "\n", @@ -1126,12 +2267,9 @@ " Name of variable with results, to plot on Y axis.\n", " colour_var (str|None):\n", " Name of variable to colour lines with (or set to None).\n", - " xaxis_title (str):\n", - " Title for X axis.\n", - " yaxis_title (str):\n", - " Title for Y axis.\n", - " legend_title (str):\n", - " Title for figure legend.\n", + " name_mappings (dict, optional)\n", + " Dictionary mapping column names to labels. If not provided,\n", + " function will default to variable names.\n", " \"\"\"\n", " # If x_var and colour_var are provided, combine both in a list to use\n", " # as grouping variables when calculating average results\n", @@ -1153,9 +2291,9 @@ " # Plot mean line\n", " fig = px.line(df, x=x_var, y='mean', color=colour_var)\n", " fig.update_layout(\n", - " xaxis_title=xaxis_title,\n", - " yaxis_title=yaxis_title,\n", - " legend_title_text=legend_title,\n", + " xaxis_title=name_mappings.get(x_var, x_var),\n", + " yaxis_title=name_mappings.get(result_var, result_var),\n", + " legend_title_text=name_mappings.get(colour_var, colour_var),\n", " template='plotly_white'\n", " )\n", "\n", @@ -1179,13 +2317,13 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ - "3456700.511.52Nurses5678Patient inter-arrival timeMean wait time for nurse (minutes)" + "3456700.511.52Number of nurses5678Patient inter-arrival timeMean wait time for nurse (minutes)" ] }, "metadata": {}, @@ -1193,18 +2331,16 @@ } ], "source": [ - "result, fig = plot_scenario(\n", + "result, plot = plot_scenario(\n", " results=scenario_results,\n", " x_var='patient_inter',\n", " result_var='mean_q_time_nurse',\n", " colour_var='number_of_nurses',\n", - " xaxis_title='Patient inter-arrival time',\n", - " yaxis_title='Mean wait time for nurse (minutes)',\n", - " legend_title='Nurses')\n", + " name_mappings=LABELS)\n", "\n", - "fig.show()\n", + "plot.show()\n", "\n", - "fig.write_image(os.path.join(output_dir, 'scenario_nurse_wait.png'))" + "plot.write_image(os.path.join(OUTPUT_DIR, 'scenario_nurse_wait.png'))" ] }, { @@ -1216,13 +2352,13 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ - "345670.20.30.40.50.6Nurses5678Patient inter-arrival timeMean nurse utilisation" + "345670.20.30.40.50.6Number of nurses5678Patient inter-arrival timeMean nurse utilisation" ] }, "metadata": {}, @@ -1230,18 +2366,16 @@ } ], "source": [ - "result, fig = plot_scenario(\n", + "result, plot = plot_scenario(\n", " results=scenario_results,\n", " x_var='patient_inter',\n", " result_var='mean_nurse_utilisation',\n", " colour_var='number_of_nurses',\n", - " xaxis_title='Patient inter-arrival time',\n", - " yaxis_title='Mean nurse utilisation',\n", - " legend_title='Nurses')\n", + " name_mappings=LABELS)\n", "\n", - "fig.show()\n", + "plot.show()\n", "\n", - "fig.write_image(os.path.join(output_dir, 'scenario_nurse_util.png'))" + "plot.write_image(os.path.join(OUTPUT_DIR, 'scenario_nurse_util.png'))" ] }, { @@ -1253,7 +2387,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 22, "metadata": {}, "outputs": [ { @@ -1296,7 +2430,8 @@ "# Convert to latex, display and save\n", "table_latex = table.to_latex()\n", "print(table_latex)\n", - "with open(os.path.join(output_dir, 'scenario_nurse_util.tex'), 'w') as tf:\n", + "with open(os.path.join(OUTPUT_DIR, 'scenario_nurse_util.tex'),\n", + " 'w', encoding='utf-8') as tf:\n", " tf.write(table_latex)" ] }, @@ -1316,7 +2451,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 23, "metadata": {}, "outputs": [ { @@ -1344,13 +2479,13 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ - "89101112131415012345Mean nurse consultation time (minutes)Mean wait time for nurse (minutes)" + "89101112131415012345Mean consultation time with nurse (minutes)Mean wait time for nurse (minutes)" ] }, "metadata": {}, @@ -1358,24 +2493,22 @@ } ], "source": [ - "result, fig = plot_scenario(\n", + "result, plot = plot_scenario(\n", " results=sensitivity_consult,\n", " x_var='mean_n_consult_time',\n", " result_var='mean_q_time_nurse',\n", " colour_var=None,\n", - " xaxis_title='Mean nurse consultation time (minutes)',\n", - " yaxis_title='Mean wait time for nurse (minutes)',\n", - " legend_title='Nurses',\n", + " name_mappings=LABELS\n", ")\n", "\n", - "fig.show()\n", + "plot.show()\n", "\n", - "fig.write_image(os.path.join(output_dir, 'sensitivity_consult_time.png'))" + "plot.write_image(os.path.join(OUTPUT_DIR, 'sensitivity_consult_time.png'))" ] }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 25, "metadata": {}, "outputs": [ { @@ -1419,7 +2552,8 @@ "# Convert to latex, display and save\n", "table_latex = table.to_latex()\n", "print(table_latex)\n", - "with open(os.path.join(output_dir, 'sensitivity_consult_time.tex'), 'w') as tf:\n", + "with open(os.path.join(OUTPUT_DIR, 'sensitivity_consult_time.tex'),\n", + " 'w', encoding='utf-8') as tf:\n", " tf.write(table_latex)" ] }, @@ -1434,7 +2568,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 26, "metadata": {}, "outputs": [ { @@ -1519,7 +2653,7 @@ "667690 21590 61919.845349 NaN NaN 30" ] }, - "execution_count": 27, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -1547,80 +2681,80 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 27, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "2025-01-29 13:39:29,576 - INFO - logging.py:log():128 - Initialised model: {'param': , 'run_number': 0, 'env': , 'nurse': , 'patients': [], 'nurse_time_used': 0, 'nurse_consult_count': 0, 'running_mean_nurse_wait': 0, 'audit_list': [], 'results_list': [], 'patient_inter_arrival_dist': , 'nurse_consult_time_dist': }\n", - "2025-01-29 13:39:29,577 - INFO - logging.py:log():128 - Parameters: {'_initialising': False, 'patient_inter': 4, 'mean_n_consult_time': 10, 'number_of_nurses': 5, 'warm_up_period': 50, 'data_collection_period': 100, 'number_of_runs': 1, 'audit_interval': 120, 'scenario_name': 0, 'cores': 0, 'logger': }\n", - "2025-01-29 13:39:29,577 - INFO - logging.py:log():128 - Patient 1 arrives at: 0.000\n", - "2025-01-29 13:39:29,577 - INFO - logging.py:log():128 - Patient 1 is seen by nurse after 0.000 minutes. Consultation length: 8.031 minutes.\n", - "2025-01-29 13:39:29,578 - INFO - logging.py:log():128 - Patient 2 arrives at: 13.174\n", - "2025-01-29 13:39:29,578 - INFO - logging.py:log():128 - Patient 2 is seen by nurse after 13.174 minutes. Consultation length: 3.820 minutes.\n", - "2025-01-29 13:39:29,578 - INFO - logging.py:log():128 - Patient 3 arrives at: 16.227\n", - "2025-01-29 13:39:29,578 - INFO - logging.py:log():128 - Patient 3 is seen by nurse after 16.227 minutes. Consultation length: 3.642 minutes.\n", - "2025-01-29 13:39:29,578 - INFO - logging.py:log():128 - Patient 4 arrives at: 21.236\n", - "2025-01-29 13:39:29,579 - INFO - logging.py:log():128 - Patient 4 is seen by nurse after 21.236 minutes. Consultation length: 5.295 minutes.\n", - "2025-01-29 13:39:29,579 - INFO - logging.py:log():128 - Patient 5 arrives at: 22.140\n", - "2025-01-29 13:39:29,579 - INFO - logging.py:log():128 - Patient 5 is seen by nurse after 22.140 minutes. Consultation length: 27.884 minutes.\n", - "2025-01-29 13:39:29,579 - INFO - logging.py:log():128 - Patient 6 arrives at: 23.023\n", - "2025-01-29 13:39:29,579 - INFO - logging.py:log():128 - Patient 6 is seen by nurse after 23.023 minutes. Consultation length: 19.610 minutes.\n", - "2025-01-29 13:39:29,579 - INFO - logging.py:log():128 - Patient 7 arrives at: 30.223\n", - "2025-01-29 13:39:29,580 - INFO - logging.py:log():128 - Patient 7 is seen by nurse after 30.223 minutes. Consultation length: 9.490 minutes.\n", - "2025-01-29 13:39:29,580 - INFO - logging.py:log():128 - Patient 8 arrives at: 30.487\n", - "2025-01-29 13:39:29,580 - INFO - logging.py:log():128 - Patient 8 is seen by nurse after 30.487 minutes. Consultation length: 41.665 minutes.\n", - "2025-01-29 13:39:29,580 - INFO - logging.py:log():128 - Patient 9 arrives at: 34.089\n", - "2025-01-29 13:39:29,580 - INFO - logging.py:log():128 - Patient 9 is seen by nurse after 34.089 minutes. Consultation length: 5.874 minutes.\n", - "2025-01-29 13:39:29,581 - INFO - logging.py:log():128 - Patient 10 arrives at: 35.270\n", - "2025-01-29 13:39:29,581 - INFO - logging.py:log():128 - Patient 10 is seen by nurse after 35.270 minutes. Consultation length: 27.882 minutes.\n", - "2025-01-29 13:39:29,581 - INFO - logging.py:log():128 - Patient 11 arrives at: 44.470\n", - "2025-01-29 13:39:29,581 - INFO - logging.py:log():128 - Patient 11 is seen by nurse after 44.470 minutes. Consultation length: 24.915 minutes.\n", - "2025-01-29 13:39:29,582 - INFO - logging.py:log():128 - ──────────\n", - "2025-01-29 13:39:29,582 - INFO - logging.py:log():128 - 50.00: Warm up complete.\n", - "2025-01-29 13:39:29,582 - INFO - logging.py:log():128 - ──────────\n", - "2025-01-29 13:39:29,582 - INFO - logging.py:log():128 - Patient 1 arrives at: 51.904\n", - "2025-01-29 13:39:29,582 - INFO - logging.py:log():128 - Patient 1 is seen by nurse after 51.904 minutes. Consultation length: 18.079 minutes.\n", - "2025-01-29 13:39:29,583 - INFO - logging.py:log():128 - Patient 2 arrives at: 51.963\n", - "2025-01-29 13:39:29,583 - INFO - logging.py:log():128 - Patient 2 is seen by nurse after 51.963 minutes. Consultation length: 3.102 minutes.\n", - "2025-01-29 13:39:29,583 - INFO - logging.py:log():128 - Patient 3 arrives at: 74.349\n", - "2025-01-29 13:39:29,583 - INFO - logging.py:log():128 - Patient 3 is seen by nurse after 74.349 minutes. Consultation length: 26.745 minutes.\n", - "2025-01-29 13:39:29,583 - INFO - logging.py:log():128 - Patient 4 arrives at: 77.534\n", - "2025-01-29 13:39:29,584 - INFO - logging.py:log():128 - Patient 4 is seen by nurse after 77.534 minutes. Consultation length: 0.748 minutes.\n", - "2025-01-29 13:39:29,584 - INFO - logging.py:log():128 - Patient 5 arrives at: 78.932\n", - "2025-01-29 13:39:29,584 - INFO - logging.py:log():128 - Patient 5 is seen by nurse after 78.932 minutes. Consultation length: 0.528 minutes.\n", - "2025-01-29 13:39:29,584 - INFO - logging.py:log():128 - Patient 6 arrives at: 86.815\n", - "2025-01-29 13:39:29,584 - INFO - logging.py:log():128 - Patient 6 is seen by nurse after 86.815 minutes. Consultation length: 2.435 minutes.\n", - "2025-01-29 13:39:29,585 - INFO - logging.py:log():128 - Patient 7 arrives at: 89.783\n", - "2025-01-29 13:39:29,585 - INFO - logging.py:log():128 - Patient 7 is seen by nurse after 89.783 minutes. Consultation length: 9.666 minutes.\n", - "2025-01-29 13:39:29,585 - INFO - logging.py:log():128 - Patient 8 arrives at: 89.807\n", - "2025-01-29 13:39:29,585 - INFO - logging.py:log():128 - Patient 8 is seen by nurse after 89.807 minutes. Consultation length: 7.005 minutes.\n", - "2025-01-29 13:39:29,585 - INFO - logging.py:log():128 - Patient 9 arrives at: 93.118\n", - "2025-01-29 13:39:29,586 - INFO - logging.py:log():128 - Patient 9 is seen by nurse after 93.118 minutes. Consultation length: 20.185 minutes.\n", - "2025-01-29 13:39:29,586 - INFO - logging.py:log():128 - Patient 10 arrives at: 95.598\n", - "2025-01-29 13:39:29,586 - INFO - logging.py:log():128 - Patient 10 is seen by nurse after 95.598 minutes. Consultation length: 7.651 minutes.\n", - "2025-01-29 13:39:29,587 - INFO - logging.py:log():128 - Patient 11 arrives at: 98.019\n", - "2025-01-29 13:39:29,587 - INFO - logging.py:log():128 - Patient 11 is seen by nurse after 98.019 minutes. Consultation length: 27.908 minutes.\n", - "2025-01-29 13:39:29,587 - INFO - logging.py:log():128 - Patient 12 arrives at: 109.379\n", - "2025-01-29 13:39:29,587 - INFO - logging.py:log():128 - Patient 12 is seen by nurse after 109.379 minutes. Consultation length: 16.811 minutes.\n", - "2025-01-29 13:39:29,587 - INFO - logging.py:log():128 - Patient 13 arrives at: 110.322\n", - "2025-01-29 13:39:29,588 - INFO - logging.py:log():128 - Patient 13 is seen by nurse after 110.322 minutes. Consultation length: 24.157 minutes.\n", - "2025-01-29 13:39:29,588 - INFO - logging.py:log():128 - Patient 14 arrives at: 120.342\n", - "2025-01-29 13:39:29,588 - INFO - logging.py:log():128 - Patient 14 is seen by nurse after 120.342 minutes. Consultation length: 1.451 minutes.\n", - "2025-01-29 13:39:29,588 - INFO - logging.py:log():128 - Patient 15 arrives at: 121.643\n", - "2025-01-29 13:39:29,588 - INFO - logging.py:log():128 - Patient 15 is seen by nurse after 121.643 minutes. Consultation length: 1.343 minutes.\n", - "2025-01-29 13:39:29,589 - INFO - logging.py:log():128 - Patient 16 arrives at: 127.827\n", - "2025-01-29 13:39:29,589 - INFO - logging.py:log():128 - Patient 16 is seen by nurse after 127.827 minutes. Consultation length: 1.554 minutes.\n", - "2025-01-29 13:39:29,589 - INFO - logging.py:log():128 - Patient 17 arrives at: 132.055\n", - "2025-01-29 13:39:29,589 - INFO - logging.py:log():128 - Patient 17 is seen by nurse after 132.055 minutes. Consultation length: 15.623 minutes.\n", - "2025-01-29 13:39:29,589 - INFO - logging.py:log():128 - Patient 18 arrives at: 133.617\n", - "2025-01-29 13:39:29,590 - INFO - logging.py:log():128 - Patient 18 is seen by nurse after 133.617 minutes. Consultation length: 4.688 minutes.\n", - "2025-01-29 13:39:29,590 - INFO - logging.py:log():128 - Patient 19 arrives at: 136.346\n", - "2025-01-29 13:39:29,590 - INFO - logging.py:log():128 - Patient 19 is seen by nurse after 136.346 minutes. Consultation length: 2.612 minutes.\n", - "2025-01-29 13:39:29,590 - INFO - logging.py:log():128 - Patient 20 arrives at: 144.142\n", - "2025-01-29 13:39:29,590 - INFO - logging.py:log():128 - Patient 20 is seen by nurse after 144.142 minutes. Consultation length: 6.348 minutes.\n" + "2025-01-30 13:21:27,913 - INFO - logging.py:log():128 - Initialised model: {'param': , 'run_number': 0, 'env': , 'nurse': , 'patients': [], 'nurse_time_used': 0, 'nurse_consult_count': 0, 'running_mean_nurse_wait': 0, 'audit_list': [], 'results_list': [], 'patient_inter_arrival_dist': , 'nurse_consult_time_dist': }\n", + "2025-01-30 13:21:27,913 - INFO - logging.py:log():128 - Parameters: {'_initialising': False, 'patient_inter': 4, 'mean_n_consult_time': 10, 'number_of_nurses': 5, 'warm_up_period': 50, 'data_collection_period': 100, 'number_of_runs': 1, 'audit_interval': 120, 'scenario_name': 0, 'cores': 0, 'logger': }\n", + "2025-01-30 13:21:27,914 - INFO - logging.py:log():128 - Patient 1 arrives at: 0.000\n", + "2025-01-30 13:21:27,914 - INFO - logging.py:log():128 - Patient 1 is seen by nurse after 0.000 minutes. Consultation length: 8.031 minutes.\n", + "2025-01-30 13:21:27,914 - INFO - logging.py:log():128 - Patient 2 arrives at: 13.174\n", + "2025-01-30 13:21:27,914 - INFO - logging.py:log():128 - Patient 2 is seen by nurse after 0.000 minutes. Consultation length: 3.820 minutes.\n", + "2025-01-30 13:21:27,915 - INFO - logging.py:log():128 - Patient 3 arrives at: 16.227\n", + "2025-01-30 13:21:27,915 - INFO - logging.py:log():128 - Patient 3 is seen by nurse after 0.000 minutes. Consultation length: 3.642 minutes.\n", + "2025-01-30 13:21:27,915 - INFO - logging.py:log():128 - Patient 4 arrives at: 21.236\n", + "2025-01-30 13:21:27,915 - INFO - logging.py:log():128 - Patient 4 is seen by nurse after 0.000 minutes. Consultation length: 5.295 minutes.\n", + "2025-01-30 13:21:27,916 - INFO - logging.py:log():128 - Patient 5 arrives at: 22.140\n", + "2025-01-30 13:21:27,916 - INFO - logging.py:log():128 - Patient 5 is seen by nurse after 0.000 minutes. Consultation length: 27.884 minutes.\n", + "2025-01-30 13:21:27,916 - INFO - logging.py:log():128 - Patient 6 arrives at: 23.023\n", + "2025-01-30 13:21:27,916 - INFO - logging.py:log():128 - Patient 6 is seen by nurse after 0.000 minutes. Consultation length: 19.610 minutes.\n", + "2025-01-30 13:21:27,916 - INFO - logging.py:log():128 - Patient 7 arrives at: 30.223\n", + "2025-01-30 13:21:27,917 - INFO - logging.py:log():128 - Patient 7 is seen by nurse after 0.000 minutes. Consultation length: 9.490 minutes.\n", + "2025-01-30 13:21:27,917 - INFO - logging.py:log():128 - Patient 8 arrives at: 30.487\n", + "2025-01-30 13:21:27,917 - INFO - logging.py:log():128 - Patient 8 is seen by nurse after 0.000 minutes. Consultation length: 41.665 minutes.\n", + "2025-01-30 13:21:27,917 - INFO - logging.py:log():128 - Patient 9 arrives at: 34.089\n", + "2025-01-30 13:21:27,917 - INFO - logging.py:log():128 - Patient 9 is seen by nurse after 0.000 minutes. Consultation length: 5.874 minutes.\n", + "2025-01-30 13:21:27,918 - INFO - logging.py:log():128 - Patient 10 arrives at: 35.270\n", + "2025-01-30 13:21:27,918 - INFO - logging.py:log():128 - Patient 10 is seen by nurse after 4.442 minutes. Consultation length: 27.882 minutes.\n", + "2025-01-30 13:21:27,918 - INFO - logging.py:log():128 - Patient 11 arrives at: 44.470\n", + "2025-01-30 13:21:27,918 - INFO - logging.py:log():128 - Patient 11 is seen by nurse after 0.000 minutes. Consultation length: 24.915 minutes.\n", + "2025-01-30 13:21:27,919 - INFO - logging.py:log():128 - ──────────\n", + "2025-01-30 13:21:27,919 - INFO - logging.py:log():128 - 50.00: Warm up complete.\n", + "2025-01-30 13:21:27,919 - INFO - logging.py:log():128 - ──────────\n", + "2025-01-30 13:21:27,919 - INFO - logging.py:log():128 - Patient 1 arrives at: 51.904\n", + "2025-01-30 13:21:27,920 - INFO - logging.py:log():128 - Patient 1 is seen by nurse after 0.000 minutes. Consultation length: 18.079 minutes.\n", + "2025-01-30 13:21:27,920 - INFO - logging.py:log():128 - Patient 2 arrives at: 51.963\n", + "2025-01-30 13:21:27,920 - INFO - logging.py:log():128 - Patient 2 is seen by nurse after 0.000 minutes. Consultation length: 3.102 minutes.\n", + "2025-01-30 13:21:27,920 - INFO - logging.py:log():128 - Patient 3 arrives at: 74.349\n", + "2025-01-30 13:21:27,920 - INFO - logging.py:log():128 - Patient 3 is seen by nurse after 0.000 minutes. Consultation length: 26.745 minutes.\n", + "2025-01-30 13:21:27,921 - INFO - logging.py:log():128 - Patient 4 arrives at: 77.534\n", + "2025-01-30 13:21:27,921 - INFO - logging.py:log():128 - Patient 4 is seen by nurse after 0.000 minutes. Consultation length: 0.748 minutes.\n", + "2025-01-30 13:21:27,921 - INFO - logging.py:log():128 - Patient 5 arrives at: 78.932\n", + "2025-01-30 13:21:27,921 - INFO - logging.py:log():128 - Patient 5 is seen by nurse after 0.000 minutes. Consultation length: 0.528 minutes.\n", + "2025-01-30 13:21:27,921 - INFO - logging.py:log():128 - Patient 6 arrives at: 86.815\n", + "2025-01-30 13:21:27,922 - INFO - logging.py:log():128 - Patient 6 is seen by nurse after 0.000 minutes. Consultation length: 2.435 minutes.\n", + "2025-01-30 13:21:27,922 - INFO - logging.py:log():128 - Patient 7 arrives at: 89.783\n", + "2025-01-30 13:21:27,922 - INFO - logging.py:log():128 - Patient 7 is seen by nurse after 0.000 minutes. Consultation length: 9.666 minutes.\n", + "2025-01-30 13:21:27,923 - INFO - logging.py:log():128 - Patient 8 arrives at: 89.807\n", + "2025-01-30 13:21:27,923 - INFO - logging.py:log():128 - Patient 8 is seen by nurse after 0.000 minutes. Consultation length: 7.005 minutes.\n", + "2025-01-30 13:21:27,923 - INFO - logging.py:log():128 - Patient 9 arrives at: 93.118\n", + "2025-01-30 13:21:27,923 - INFO - logging.py:log():128 - Patient 9 is seen by nurse after 0.000 minutes. Consultation length: 20.185 minutes.\n", + "2025-01-30 13:21:27,924 - INFO - logging.py:log():128 - Patient 10 arrives at: 95.598\n", + "2025-01-30 13:21:27,924 - INFO - logging.py:log():128 - Patient 10 is seen by nurse after 0.000 minutes. Consultation length: 7.651 minutes.\n", + "2025-01-30 13:21:27,924 - INFO - logging.py:log():128 - Patient 11 arrives at: 98.019\n", + "2025-01-30 13:21:27,924 - INFO - logging.py:log():128 - Patient 11 is seen by nurse after 0.000 minutes. Consultation length: 27.908 minutes.\n", + "2025-01-30 13:21:27,925 - INFO - logging.py:log():128 - Patient 12 arrives at: 109.379\n", + "2025-01-30 13:21:27,925 - INFO - logging.py:log():128 - Patient 12 is seen by nurse after 0.000 minutes. Consultation length: 16.811 minutes.\n", + "2025-01-30 13:21:27,925 - INFO - logging.py:log():128 - Patient 13 arrives at: 110.322\n", + "2025-01-30 13:21:27,925 - INFO - logging.py:log():128 - Patient 13 is seen by nurse after 0.000 minutes. Consultation length: 24.157 minutes.\n", + "2025-01-30 13:21:27,925 - INFO - logging.py:log():128 - Patient 14 arrives at: 120.342\n", + "2025-01-30 13:21:27,926 - INFO - logging.py:log():128 - Patient 14 is seen by nurse after 0.000 minutes. Consultation length: 1.451 minutes.\n", + "2025-01-30 13:21:27,926 - INFO - logging.py:log():128 - Patient 15 arrives at: 121.643\n", + "2025-01-30 13:21:27,926 - INFO - logging.py:log():128 - Patient 15 is seen by nurse after 0.000 minutes. Consultation length: 1.343 minutes.\n", + "2025-01-30 13:21:27,926 - INFO - logging.py:log():128 - Patient 16 arrives at: 127.827\n", + "2025-01-30 13:21:27,927 - INFO - logging.py:log():128 - Patient 16 is seen by nurse after 0.000 minutes. Consultation length: 1.554 minutes.\n", + "2025-01-30 13:21:27,927 - INFO - logging.py:log():128 - Patient 17 arrives at: 132.055\n", + "2025-01-30 13:21:27,927 - INFO - logging.py:log():128 - Patient 17 is seen by nurse after 0.000 minutes. Consultation length: 15.623 minutes.\n", + "2025-01-30 13:21:27,927 - INFO - logging.py:log():128 - Patient 18 arrives at: 133.617\n", + "2025-01-30 13:21:27,927 - INFO - logging.py:log():128 - Patient 18 is seen by nurse after 0.000 minutes. Consultation length: 4.688 minutes.\n", + "2025-01-30 13:21:27,927 - INFO - logging.py:log():128 - Patient 19 arrives at: 136.346\n", + "2025-01-30 13:21:27,928 - INFO - logging.py:log():128 - Patient 19 is seen by nurse after 0.000 minutes. Consultation length: 2.612 minutes.\n", + "2025-01-30 13:21:27,928 - INFO - logging.py:log():128 - Patient 20 arrives at: 144.142\n", + "2025-01-30 13:21:27,928 - INFO - logging.py:log():128 - Patient 20 is seen by nurse after 0.000 minutes. Consultation length: 6.348 minutes.\n" ] } ], @@ -1646,7 +2780,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -1734,14 +2868,13 @@ " 'time_with_nurse': 6.348067933102151}]" ] }, - "execution_count": 29, "metadata": {}, - "output_type": "execute_result" + "output_type": "display_data" } ], "source": [ "# Compare to patient-level results\n", - "model.results_list" + "display(model.results_list)" ] }, { @@ -1753,14 +2886,14 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 29, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Notebook run time: 0m 27s\n" + "Notebook run time: 0m 28s\n" ] } ], @@ -1790,7 +2923,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.0" + "version": "3.13.1" } }, "nbformat": 4, diff --git a/notebooks/choosing_parameters.ipynb b/notebooks/choosing_parameters.ipynb index 23e89c6..0f912de 100644 --- a/notebooks/choosing_parameters.ipynb +++ b/notebooks/choosing_parameters.ipynb @@ -31,27 +31,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Set-up\n", - "\n", - "Load notebook linters." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext pycodestyle_magic" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "%pycodestyle_on" + "## Set-up" ] }, { @@ -63,10 +43,11 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ + "# pylint: disable=missing-module-docstring\n", "# To ensure any updates to `simulation/` are fetched without needing to restart\n", "# the notebook environment, reload `simulation/` before execution of each cell\n", "%load_ext autoreload\n", @@ -76,21 +57,23 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ - "from simulation.model import Defaults, Runner, summary_stats\n", + "# pylint: disable=wrong-import-position\n", + "import os\n", + "import time\n", + "import warnings\n", "\n", "from IPython.display import display\n", - "import os\n", "import pandas as pd\n", "import plotly.express as px\n", "import plotly.io as pio\n", "import plotly.graph_objects as go\n", "import plotly.subplots as sp\n", - "import time\n", - "import warnings" + "\n", + "from simulation.model import Defaults, Runner, summary_stats" ] }, { @@ -102,7 +85,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -118,7 +101,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -134,11 +117,41 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "OUTPUT_DIR = '../outputs/'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Define labels for variables in the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ - "output_dir = '../outputs/'" + "LABELS = {\n", + " 'arrivals': 'Patient arrivals (n)',\n", + " 'mean_q_time_nurse': 'Mean wait time for nurse (minutes)',\n", + " 'mean_n_consult_time': 'Mean consultation time with nurse (minutes)',\n", + " 'mean_time_with_nurse': 'Mean consultation time with nurse (minutes)',\n", + " 'mean_nurse_utilisation': 'Mean nurse utilisation',\n", + " 'adj_mean_nurse_utilisation': 'Mean nurse utilisation (*100 - %)',\n", + " 'adj_mean_q_time_nurse': 'Mean wait time for nurse (*100) (minutes)',\n", + " 'mean_nurse_utilisation_tw': 'Time-weighted mean nurse utilisation',\n", + " 'mean_nurse_q_length': 'Time-weighted mean queue length for nurse (n)',\n", + " 'patient_inter': 'Patient inter-arrival time',\n", + " 'number_of_nurses': 'Number of nurses',\n", + " 'utilisation': 'Utilisation',\n", + " 'running_mean_wait_time': 'Running mean nurse wait time (minutes)'\n", + "}" ] }, { @@ -156,11 +169,13 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ - "def time_series_inspection(file, data_collection_period, warm_up=None):\n", + "def time_series_inspection(\n", + " file, data_collection_period, warm_up=None, path=OUTPUT_DIR, labels=None\n", + "):\n", " \"\"\"\n", " Time series inspection method for determining length of warm-up.\n", "\n", @@ -172,6 +187,11 @@ " warm_up (float, optional):\n", " Location on X axis to plot vertical red line indicating the chosen\n", " warm-up period. Defaults to None, which will not plot a line.\n", + " path (str):\n", + " Path to save file to (exc. filename)\n", + " labels (dict):\n", + " Contains mappings from variable names to full labels. If none\n", + " provided, will default to using variable names.\n", " \"\"\"\n", " # Use default parameters, but with no warm-up and specified run length,\n", " # and with no replications\n", @@ -190,16 +210,13 @@ " choose_warmup.interval_audit_df['resource_name'] == 'nurse']\n", "\n", " # Define columns to analyse\n", - " plot = {\n", - " 'utilisation': 'Cumulative mean nurse utilisation',\n", - " 'running_mean_wait_time': 'Running mean nurse wait time'\n", - " }\n", + " plot = ['utilisation', 'running_mean_wait_time']\n", "\n", " # Create 1x2 subplot\n", " full_figure = sp.make_subplots(rows=2, cols=1, shared_xaxes=True)\n", "\n", - " i = 1\n", - " for var, label in plot.items():\n", + " counter = 1\n", + " for var in plot:\n", " # Reformat so index is simulation time and columns are each run\n", " reformat = (\n", " nurse[['simulation_time', var, 'run']]\n", @@ -213,6 +230,9 @@ " cumulative = reformat.expanding().mean()\n", " elif var == 'running_mean_wait_time':\n", " cumulative = reformat.copy()\n", + " else:\n", + " print('Expected var to be utilisation or running_mean_wait_time.')\n", + " break\n", "\n", " # Create plot (using go.Scatter instead of px.express, for sub-plots)\n", " full_figure.add_trace(\n", @@ -220,13 +240,14 @@ " x=cumulative.index,\n", " y=cumulative[0],\n", " mode='lines',\n", - " line=dict(color='royalblue')\n", + " line={'color': 'royalblue'}\n", " ),\n", - " row=i, col=1\n", + " row=counter, col=1\n", " )\n", "\n", " # Add y axis label\n", - " full_figure.update_yaxes(title_text=label, row=i, col=1)\n", + " full_figure.update_yaxes(title_text=labels.get(var, var),\n", + " row=counter, col=1)\n", "\n", " # Add vertical line for warm-up period specified\n", " if warm_up is not None:\n", @@ -235,7 +256,7 @@ " annotation_text='Suggested warm-up length',\n", " annotation_position='top left',\n", " annotation_font_color='red')\n", - " i += 1\n", + " counter += 1\n", "\n", " # Add x axis title\n", " full_figure.update_xaxes(title_text='Run time (minutes)')\n", @@ -252,7 +273,7 @@ " full_figure.show()\n", "\n", " # Save figure\n", - " full_figure.write_image(os.path.join(output_dir, file))" + " full_figure.write_image(os.path.join(path, file))" ] }, { @@ -264,13 +285,13 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ - "0.30.350.40.450.50.550.650010001500200025003000350040000.10.20.30.40.50.60.70.8Run time (minutes)Run time (minutes)Cumulative mean nurse utilisationRunning mean nurse wait timeSuggested warm-up lengthSuggested warm-up lengthSuggested warm-up length" + "00.10.20.30.40500100015002000250030003500400000.20.40.60.8Run time (minutes)Run time (minutes)UtilisationRunning mean nurse wait time (minutes)Suggested warm-up lengthSuggested warm-up lengthSuggested warm-up length" ] }, "metadata": {}, @@ -278,8 +299,10 @@ } ], "source": [ - "time_series_inspection(file='choose_param_time_series_1.png',\n", - " data_collection_period=1440*3, warm_up=2520)" + "time_series_inspection(\n", + " file='choose_param_time_series_1.png',\n", + " data_collection_period=1440*3, warm_up=2520,\n", + " labels=LABELS)" ] }, { @@ -291,13 +314,13 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ - "0.30.350.40.450.50.550.610k20k30k40k50k0.10.20.30.40.50.60.70.8Run time (minutes)Run time (minutes)Cumulative mean nurse utilisationRunning mean nurse wait timeSuggested warm-up lengthSuggested warm-up lengthSuggested warm-up length" + "00.10.20.30.40.5010k20k30k40k50k00.20.40.60.8Run time (minutes)Run time (minutes)UtilisationRunning mean nurse wait time (minutes)Suggested warm-up lengthSuggested warm-up lengthSuggested warm-up length" ] }, "metadata": {}, @@ -305,8 +328,10 @@ } ], "source": [ - "time_series_inspection(file='choose_param_time_series_2.png',\n", - " data_collection_period=1440*40, warm_up=1440*13)" + "time_series_inspection(\n", + " file='choose_param_time_series_2.png',\n", + " data_collection_period=1440*40, warm_up=1440*13,\n", + " labels=LABELS)" ] }, { @@ -326,12 +351,15 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ - "def confidence_interval_method(file, replications, metric, desired_precision,\n", - " yaxis_title, min_rep=None):\n", + "# pylint: disable=too-many-arguments,too-many-positional-arguments\n", + "def confidence_interval_method(\n", + " file, replications, metric, desired_precision,\n", + " min_rep=None, path=OUTPUT_DIR, labels=None\n", + "):\n", " \"\"\"\n", " Use the confidence interval method to select the number of replications.\n", "\n", @@ -344,10 +372,14 @@ " Name of performance metric to assess.\n", " desired_precision (float):\n", " Desired mean deviation from confidence interval.\n", - " yaxis_title (str):\n", - " Label for y axis.\n", " min_rep (int):\n", - " A suggested minimum number of replications.\n", + " A suggested minimum number of replications, using to draw vertical\n", + " line on plot. If none provided, will not add a line.\n", + " path (str):\n", + " Path to save file to (exc. filename).\n", + " labels (dict):\n", + " Contains mappings from variable names to full labels. If none\n", + " provided, will default to using variable names.\n", " \"\"\"\n", " param = Defaults()\n", " param.number_of_runs = replications\n", @@ -355,29 +387,26 @@ " choose_rep.run_reps()\n", "\n", " # If mean of metric is less than 1, multiply by 100\n", - " if choose_rep.run_results_df[metric].mean() < 1:\n", - " choose_rep.run_results_df[f'adj_{metric}'] = (\n", - " choose_rep.run_results_df[metric]*100)\n", + " df = choose_rep.run_results_df\n", + " if df[metric].mean() < 1:\n", + " df[f'adj_{metric}'] = df[metric] * 100\n", " metric = f'adj_{metric}'\n", "\n", - " # Initialise list to store the results\n", - " cumulative_list = []\n", - "\n", - " # For each row in the dataframe, filter to rows up to the i-th replication\n", - " # then perform calculations\n", - " for i in range(1, replications+1):\n", - " mean, std_dev, ci_lower, ci_upper = summary_stats(\n", - " choose_rep.run_results_df[metric].iloc[:i])\n", - " deviation = ((ci_upper-mean)/mean)*100\n", - " cumulative_list.append({\n", + " # Compute cumulative statistics\n", + " cumulative = pd.DataFrame([\n", + " {\n", " 'replications': i,\n", - " 'cumulative_mean': mean,\n", - " 'cumulative_std': std_dev,\n", - " 'lower_ci': ci_lower,\n", - " 'upper_ci': ci_upper,\n", - " 'perc_deviation': deviation\n", - " })\n", - " cumulative = pd.DataFrame(cumulative_list)\n", + " 'cumulative_mean': stats[0],\n", + " 'cumulative_std': stats[1],\n", + " 'lower_ci': stats[2],\n", + " 'upper_ci': stats[3],\n", + " 'perc_deviation': ((stats[3] - stats[0]) / stats[0]) * 100\n", + " }\n", + " for i, stats in enumerate(\n", + " (summary_stats(df[metric].iloc[:i])\n", + " for i in range(1, replications + 1))\n", + " )\n", + " ])\n", " display(cumulative)\n", "\n", " # Get minimum number of replications where deviation is less than target\n", @@ -391,27 +420,27 @@ " f'desired precision ({desired_precision}).')\n", "\n", " # Plot the cumulative mean and confidence interval\n", - " fig = px.line(cumulative,\n", - " x='replications',\n", - " y=['cumulative_mean', 'lower_ci', 'upper_ci'])\n", - " fig.update_layout(\n", + " figure = px.line(cumulative,\n", + " x='replications',\n", + " y=['cumulative_mean', 'lower_ci', 'upper_ci'])\n", + " figure.update_layout(\n", " xaxis_title='Number of replications',\n", - " yaxis_title=yaxis_title,\n", + " yaxis_title=labels.get(metric, metric),\n", " template='plotly_white'\n", " )\n", " if min_rep is not None:\n", - " fig.add_vline(x=min_rep, line_color='red', line_dash='dash')\n", + " figure.add_vline(x=min_rep, line_color='red', line_dash='dash')\n", "\n", " # Show figure\n", - " fig.show()\n", + " figure.show()\n", "\n", " # Save figure\n", - " fig.write_image(os.path.join(output_dir, file))" + " figure.write_image(os.path.join(path, file))" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -446,7 +475,7 @@ " \n", " \n", " 0\n", - " 1\n", + " 0\n", " 9.842268\n", " NaN\n", " NaN\n", @@ -455,7 +484,7 @@ " \n", " \n", " 1\n", - " 2\n", + " 1\n", " 9.951374\n", " 0.154300\n", " 8.565044\n", @@ -464,7 +493,7 @@ " \n", " \n", " 2\n", - " 3\n", + " 2\n", " 9.942591\n", " 0.110162\n", " 9.668933\n", @@ -473,7 +502,7 @@ " \n", " \n", " 3\n", - " 4\n", + " 3\n", " 9.941208\n", " 0.089990\n", " 9.798014\n", @@ -482,7 +511,7 @@ " \n", " \n", " 4\n", - " 5\n", + " 4\n", " 9.956147\n", " 0.084791\n", " 9.850865\n", @@ -491,7 +520,7 @@ " \n", " \n", " 5\n", - " 6\n", + " 5\n", " 9.944288\n", " 0.081212\n", " 9.859062\n", @@ -500,7 +529,7 @@ " \n", " \n", " 6\n", - " 7\n", + " 6\n", " 9.958219\n", " 0.082792\n", " 9.881649\n", @@ -509,7 +538,7 @@ " \n", " \n", " 7\n", - " 8\n", + " 7\n", " 9.974314\n", " 0.089150\n", " 9.899783\n", @@ -518,7 +547,7 @@ " \n", " \n", " 8\n", - " 9\n", + " 8\n", " 9.999642\n", " 0.112818\n", " 9.912922\n", @@ -527,7 +556,7 @@ " \n", " \n", " 9\n", - " 10\n", + " 9\n", " 10.008938\n", " 0.110354\n", " 9.929996\n", @@ -536,7 +565,7 @@ " \n", " \n", " 10\n", - " 11\n", + " 10\n", " 10.015721\n", " 0.107081\n", " 9.943784\n", @@ -545,7 +574,7 @@ " \n", " \n", " 11\n", - " 12\n", + " 11\n", " 10.005522\n", " 0.108038\n", " 9.936879\n", @@ -554,7 +583,7 @@ " \n", " \n", " 12\n", - " 13\n", + " 12\n", " 9.990552\n", " 0.116675\n", " 9.920046\n", @@ -563,7 +592,7 @@ " \n", " \n", " 13\n", - " 14\n", + " 13\n", " 9.986504\n", " 0.113116\n", " 9.921193\n", @@ -572,7 +601,7 @@ " \n", " \n", " 14\n", - " 15\n", + " 14\n", " 9.998583\n", " 0.118616\n", " 9.932895\n", @@ -581,7 +610,7 @@ " \n", " \n", " 15\n", - " 16\n", + " 15\n", " 9.997145\n", " 0.114738\n", " 9.936005\n", @@ -590,7 +619,7 @@ " \n", " \n", " 16\n", - " 17\n", + " 16\n", " 9.986125\n", " 0.120027\n", " 9.924413\n", @@ -599,7 +628,7 @@ " \n", " \n", " 17\n", - " 18\n", + " 17\n", " 9.986293\n", " 0.116445\n", " 9.928386\n", @@ -608,7 +637,7 @@ " \n", " \n", " 18\n", - " 19\n", + " 18\n", " 9.976498\n", " 0.120951\n", " 9.918201\n", @@ -617,7 +646,7 @@ " \n", " \n", " 19\n", - " 20\n", + " 19\n", " 9.973643\n", " 0.118416\n", " 9.918222\n", @@ -630,26 +659,26 @@ ], "text/plain": [ " replications cumulative_mean cumulative_std lower_ci upper_ci \\\n", - "0 1 9.842268 NaN NaN NaN \n", - "1 2 9.951374 0.154300 8.565044 11.337705 \n", - "2 3 9.942591 0.110162 9.668933 10.216249 \n", - "3 4 9.941208 0.089990 9.798014 10.084401 \n", - "4 5 9.956147 0.084791 9.850865 10.061429 \n", - "5 6 9.944288 0.081212 9.859062 10.029515 \n", - "6 7 9.958219 0.082792 9.881649 10.034788 \n", - "7 8 9.974314 0.089150 9.899783 10.048845 \n", - "8 9 9.999642 0.112818 9.912922 10.086362 \n", - "9 10 10.008938 0.110354 9.929996 10.087880 \n", - "10 11 10.015721 0.107081 9.943784 10.087659 \n", - "11 12 10.005522 0.108038 9.936879 10.074166 \n", - "12 13 9.990552 0.116675 9.920046 10.061058 \n", - "13 14 9.986504 0.113116 9.921193 10.051815 \n", - "14 15 9.998583 0.118616 9.932895 10.064270 \n", - "15 16 9.997145 0.114738 9.936005 10.058284 \n", - "16 17 9.986125 0.120027 9.924413 10.047837 \n", - "17 18 9.986293 0.116445 9.928386 10.044200 \n", - "18 19 9.976498 0.120951 9.918201 10.034795 \n", - "19 20 9.973643 0.118416 9.918222 10.029063 \n", + "0 0 9.842268 NaN NaN NaN \n", + "1 1 9.951374 0.154300 8.565044 11.337705 \n", + "2 2 9.942591 0.110162 9.668933 10.216249 \n", + "3 3 9.941208 0.089990 9.798014 10.084401 \n", + "4 4 9.956147 0.084791 9.850865 10.061429 \n", + "5 5 9.944288 0.081212 9.859062 10.029515 \n", + "6 6 9.958219 0.082792 9.881649 10.034788 \n", + "7 7 9.974314 0.089150 9.899783 10.048845 \n", + "8 8 9.999642 0.112818 9.912922 10.086362 \n", + "9 9 10.008938 0.110354 9.929996 10.087880 \n", + "10 10 10.015721 0.107081 9.943784 10.087659 \n", + "11 11 10.005522 0.108038 9.936879 10.074166 \n", + "12 12 9.990552 0.116675 9.920046 10.061058 \n", + "13 13 9.986504 0.113116 9.921193 10.051815 \n", + "14 14 9.998583 0.118616 9.932895 10.064270 \n", + "15 15 9.997145 0.114738 9.936005 10.058284 \n", + "16 16 9.986125 0.120027 9.924413 10.047837 \n", + "17 17 9.986293 0.116445 9.928386 10.044200 \n", + "18 18 9.976498 0.120951 9.918201 10.034795 \n", + "19 19 9.973643 0.118416 9.918222 10.029063 \n", "\n", " perc_deviation \n", "0 NaN \n", @@ -687,7 +716,7 @@ { "data": { "image/svg+xml": [ - "51015208.599.51010.511variablecumulative_meanlower_ciupper_ciNumber of replicationsMean time with nurse" + "0510158.599.51010.511variablecumulative_meanlower_ciupper_ciNumber of replicationsMean consultation time with nurse (minutes)" ] }, "metadata": {}, @@ -700,8 +729,8 @@ " replications=20,\n", " metric='mean_time_with_nurse',\n", " desired_precision=0.05,\n", - " yaxis_title='Mean time with nurse',\n", - " min_rep=3\n", + " min_rep=3,\n", + " labels=LABELS\n", ")" ] }, @@ -714,7 +743,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -749,7 +778,7 @@ " \n", " \n", " 0\n", - " 1\n", + " 0\n", " 50.454108\n", " NaN\n", " NaN\n", @@ -758,7 +787,7 @@ " \n", " \n", " 1\n", - " 2\n", + " 1\n", " 50.934587\n", " 0.679499\n", " 44.829530\n", @@ -767,7 +796,7 @@ " \n", " \n", " 2\n", - " 3\n", + " 2\n", " 51.397555\n", " 0.934815\n", " 49.075346\n", @@ -776,7 +805,7 @@ " \n", " \n", " 3\n", - " 4\n", + " 3\n", " 50.526888\n", " 1.901271\n", " 47.501541\n", @@ -785,7 +814,7 @@ " \n", " \n", " 4\n", - " 5\n", + " 4\n", " 49.650659\n", " 2.559298\n", " 46.472872\n", @@ -794,7 +823,7 @@ " \n", " \n", " 5\n", - " 6\n", + " 5\n", " 47.846628\n", " 4.976664\n", " 42.623939\n", @@ -803,7 +832,7 @@ " \n", " \n", " 6\n", - " 7\n", + " 6\n", " 47.681940\n", " 4.563900\n", " 43.461036\n", @@ -812,7 +841,7 @@ " \n", " \n", " 7\n", - " 8\n", + " 7\n", " 49.545302\n", " 6.755037\n", " 43.897949\n", @@ -821,7 +850,7 @@ " \n", " \n", " 8\n", - " 9\n", + " 8\n", " 49.245792\n", " 6.382325\n", " 44.339903\n", @@ -830,7 +859,7 @@ " \n", " \n", " 9\n", - " 10\n", + " 9\n", " 49.955562\n", " 6.422290\n", " 45.361333\n", @@ -839,7 +868,7 @@ " \n", " \n", " 10\n", - " 11\n", + " 10\n", " 50.528566\n", " 6.382232\n", " 46.240924\n", @@ -848,7 +877,7 @@ " \n", " \n", " 11\n", - " 12\n", + " 11\n", " 49.632950\n", " 6.830477\n", " 45.293072\n", @@ -857,7 +886,7 @@ " \n", " \n", " 12\n", - " 13\n", + " 12\n", " 49.240444\n", " 6.691058\n", " 45.197078\n", @@ -866,7 +895,7 @@ " \n", " \n", " 13\n", - " 14\n", + " 13\n", " 48.864416\n", " 6.580726\n", " 45.064818\n", @@ -875,7 +904,7 @@ " \n", " \n", " 14\n", - " 15\n", + " 14\n", " 49.678670\n", " 7.082218\n", " 45.756668\n", @@ -884,7 +913,7 @@ " \n", " \n", " 15\n", - " 16\n", + " 15\n", " 49.553191\n", " 6.860458\n", " 45.897511\n", @@ -893,7 +922,7 @@ " \n", " \n", " 16\n", - " 17\n", + " 16\n", " 49.157922\n", " 6.839612\n", " 45.641318\n", @@ -902,7 +931,7 @@ " \n", " \n", " 17\n", - " 18\n", + " 17\n", " 49.235673\n", " 6.643593\n", " 45.931892\n", @@ -911,7 +940,7 @@ " \n", " \n", " 18\n", - " 19\n", + " 18\n", " 48.547386\n", " 7.119432\n", " 45.115930\n", @@ -920,7 +949,7 @@ " \n", " \n", " 19\n", - " 20\n", + " 19\n", " 48.215812\n", " 7.086427\n", " 44.899262\n", @@ -929,7 +958,7 @@ " \n", " \n", " 20\n", - " 21\n", + " 20\n", " 48.834174\n", " 7.465680\n", " 45.435837\n", @@ -938,7 +967,7 @@ " \n", " \n", " 21\n", - " 22\n", + " 21\n", " 48.965899\n", " 7.311908\n", " 45.723980\n", @@ -947,7 +976,7 @@ " \n", " \n", " 22\n", - " 23\n", + " 22\n", " 49.148498\n", " 7.197270\n", " 46.036165\n", @@ -956,7 +985,7 @@ " \n", " \n", " 23\n", - " 24\n", + " 23\n", " 49.296704\n", " 7.076415\n", " 46.308596\n", @@ -965,7 +994,7 @@ " \n", " \n", " 24\n", - " 25\n", + " 24\n", " 49.336715\n", " 6.930310\n", " 46.476024\n", @@ -974,7 +1003,7 @@ " \n", " \n", " 25\n", - " 26\n", + " 25\n", " 49.850937\n", " 7.278945\n", " 46.910907\n", @@ -983,7 +1012,7 @@ " \n", " \n", " 26\n", - " 27\n", + " 26\n", " 49.879344\n", " 7.139119\n", " 47.055203\n", @@ -992,7 +1021,7 @@ " \n", " \n", " 27\n", - " 28\n", + " 27\n", " 49.857060\n", " 7.006658\n", " 47.140161\n", @@ -1001,7 +1030,7 @@ " \n", " \n", " 28\n", - " 29\n", + " 28\n", " 49.716192\n", " 6.922094\n", " 47.083168\n", @@ -1010,7 +1039,7 @@ " \n", " \n", " 29\n", - " 30\n", + " 29\n", " 49.866454\n", " 6.851314\n", " 47.308131\n", @@ -1019,7 +1048,7 @@ " \n", " \n", " 30\n", - " 31\n", + " 30\n", " 49.903691\n", " 6.739347\n", " 47.431678\n", @@ -1028,7 +1057,7 @@ " \n", " \n", " 31\n", - " 32\n", + " 31\n", " 49.850411\n", " 6.636604\n", " 47.457660\n", @@ -1037,7 +1066,7 @@ " \n", " \n", " 32\n", - " 33\n", + " 32\n", " 49.955695\n", " 6.560024\n", " 47.629612\n", @@ -1046,7 +1075,7 @@ " \n", " \n", " 33\n", - " 34\n", + " 33\n", " 50.078240\n", " 6.499265\n", " 47.810540\n", @@ -1055,7 +1084,7 @@ " \n", " \n", " 34\n", - " 35\n", + " 34\n", " 50.002865\n", " 6.418484\n", " 47.798038\n", @@ -1064,7 +1093,7 @@ " \n", " \n", " 35\n", - " 36\n", + " 35\n", " 49.888926\n", " 6.362958\n", " 47.736010\n", @@ -1073,7 +1102,7 @@ " \n", " \n", " 36\n", - " 37\n", + " 36\n", " 50.308907\n", " 6.774128\n", " 48.050300\n", @@ -1082,7 +1111,7 @@ " \n", " \n", " 37\n", - " 38\n", + " 37\n", " 50.617933\n", " 6.948198\n", " 48.334117\n", @@ -1091,7 +1120,7 @@ " \n", " \n", " 38\n", - " 39\n", + " 38\n", " 50.726526\n", " 6.889623\n", " 48.493168\n", @@ -1100,7 +1129,7 @@ " \n", " \n", " 39\n", - " 40\n", + " 39\n", " 51.107307\n", " 7.214539\n", " 48.799985\n", @@ -1109,7 +1138,7 @@ " \n", " \n", " 40\n", - " 41\n", + " 40\n", " 51.253305\n", " 7.184864\n", " 48.985482\n", @@ -1118,7 +1147,7 @@ " \n", " \n", " 41\n", - " 42\n", + " 41\n", " 51.158101\n", " 7.123473\n", " 48.938271\n", @@ -1127,7 +1156,7 @@ " \n", " \n", " 42\n", - " 43\n", + " 42\n", " 51.309010\n", " 7.107386\n", " 49.121680\n", @@ -1136,7 +1165,7 @@ " \n", " \n", " 43\n", - " 44\n", + " 43\n", " 51.432981\n", " 7.072227\n", " 49.282828\n", @@ -1145,7 +1174,7 @@ " \n", " \n", " 44\n", - " 45\n", + " 44\n", " 51.762158\n", " 7.331830\n", " 49.559432\n", @@ -1154,7 +1183,7 @@ " \n", " \n", " 45\n", - " 46\n", + " 45\n", " 52.120721\n", " 7.646912\n", " 49.849868\n", @@ -1163,7 +1192,7 @@ " \n", " \n", " 46\n", - " 47\n", + " 46\n", " 51.999456\n", " 7.608890\n", " 49.765402\n", @@ -1172,7 +1201,7 @@ " \n", " \n", " 47\n", - " 48\n", + " 47\n", " 52.091068\n", " 7.554220\n", " 49.897551\n", @@ -1181,7 +1210,7 @@ " \n", " \n", " 48\n", - " 49\n", + " 48\n", " 52.011852\n", " 7.495655\n", " 49.858849\n", @@ -1190,7 +1219,7 @@ " \n", " \n", " 49\n", - " 50\n", + " 49\n", " 51.955719\n", " 7.429385\n", " 49.844311\n", @@ -1203,56 +1232,56 @@ ], "text/plain": [ " replications cumulative_mean cumulative_std lower_ci upper_ci \\\n", - "0 1 50.454108 NaN NaN NaN \n", - "1 2 50.934587 0.679499 44.829530 57.039643 \n", - "2 3 51.397555 0.934815 49.075346 53.719764 \n", - "3 4 50.526888 1.901271 47.501541 53.552234 \n", - "4 5 49.650659 2.559298 46.472872 52.828447 \n", - "5 6 47.846628 4.976664 42.623939 53.069317 \n", - "6 7 47.681940 4.563900 43.461036 51.902843 \n", - "7 8 49.545302 6.755037 43.897949 55.192654 \n", - "8 9 49.245792 6.382325 44.339903 54.151681 \n", - "9 10 49.955562 6.422290 45.361333 54.549792 \n", - "10 11 50.528566 6.382232 46.240924 54.816207 \n", - "11 12 49.632950 6.830477 45.293072 53.972828 \n", - "12 13 49.240444 6.691058 45.197078 53.283810 \n", - "13 14 48.864416 6.580726 45.064818 52.664014 \n", - "14 15 49.678670 7.082218 45.756668 53.600672 \n", - "15 16 49.553191 6.860458 45.897511 53.208871 \n", - "16 17 49.157922 6.839612 45.641318 52.674526 \n", - "17 18 49.235673 6.643593 45.931892 52.539455 \n", - "18 19 48.547386 7.119432 45.115930 51.978842 \n", - "19 20 48.215812 7.086427 44.899262 51.532361 \n", - "20 21 48.834174 7.465680 45.435837 52.232511 \n", - "21 22 48.965899 7.311908 45.723980 52.207817 \n", - "22 23 49.148498 7.197270 46.036165 52.260830 \n", - "23 24 49.296704 7.076415 46.308596 52.284812 \n", - "24 25 49.336715 6.930310 46.476024 52.197406 \n", - "25 26 49.850937 7.278945 46.910907 52.790966 \n", - "26 27 49.879344 7.139119 47.055203 52.703485 \n", - "27 28 49.857060 7.006658 47.140161 52.573958 \n", - "28 29 49.716192 6.922094 47.083168 52.349216 \n", - "29 30 49.866454 6.851314 47.308131 52.424776 \n", - "30 31 49.903691 6.739347 47.431678 52.375703 \n", - "31 32 49.850411 6.636604 47.457660 52.243162 \n", - "32 33 49.955695 6.560024 47.629612 52.281778 \n", - "33 34 50.078240 6.499265 47.810540 52.345941 \n", - "34 35 50.002865 6.418484 47.798038 52.207691 \n", - "35 36 49.888926 6.362958 47.736010 52.041841 \n", - "36 37 50.308907 6.774128 48.050300 52.567514 \n", - "37 38 50.617933 6.948198 48.334117 52.901749 \n", - "38 39 50.726526 6.889623 48.493168 52.959883 \n", - "39 40 51.107307 7.214539 48.799985 53.414629 \n", - "40 41 51.253305 7.184864 48.985482 53.521127 \n", - "41 42 51.158101 7.123473 48.938271 53.377932 \n", - "42 43 51.309010 7.107386 49.121680 53.496341 \n", - "43 44 51.432981 7.072227 49.282828 53.583135 \n", - "44 45 51.762158 7.331830 49.559432 53.964883 \n", - "45 46 52.120721 7.646912 49.849868 54.391573 \n", - "46 47 51.999456 7.608890 49.765402 54.233510 \n", - "47 48 52.091068 7.554220 49.897551 54.284585 \n", - "48 49 52.011852 7.495655 49.858849 54.164856 \n", - "49 50 51.955719 7.429385 49.844311 54.067127 \n", + "0 0 50.454108 NaN NaN NaN \n", + "1 1 50.934587 0.679499 44.829530 57.039643 \n", + "2 2 51.397555 0.934815 49.075346 53.719764 \n", + "3 3 50.526888 1.901271 47.501541 53.552234 \n", + "4 4 49.650659 2.559298 46.472872 52.828447 \n", + "5 5 47.846628 4.976664 42.623939 53.069317 \n", + "6 6 47.681940 4.563900 43.461036 51.902843 \n", + "7 7 49.545302 6.755037 43.897949 55.192654 \n", + "8 8 49.245792 6.382325 44.339903 54.151681 \n", + "9 9 49.955562 6.422290 45.361333 54.549792 \n", + "10 10 50.528566 6.382232 46.240924 54.816207 \n", + "11 11 49.632950 6.830477 45.293072 53.972828 \n", + "12 12 49.240444 6.691058 45.197078 53.283810 \n", + "13 13 48.864416 6.580726 45.064818 52.664014 \n", + "14 14 49.678670 7.082218 45.756668 53.600672 \n", + "15 15 49.553191 6.860458 45.897511 53.208871 \n", + "16 16 49.157922 6.839612 45.641318 52.674526 \n", + "17 17 49.235673 6.643593 45.931892 52.539455 \n", + "18 18 48.547386 7.119432 45.115930 51.978842 \n", + "19 19 48.215812 7.086427 44.899262 51.532361 \n", + "20 20 48.834174 7.465680 45.435837 52.232511 \n", + "21 21 48.965899 7.311908 45.723980 52.207817 \n", + "22 22 49.148498 7.197270 46.036165 52.260830 \n", + "23 23 49.296704 7.076415 46.308596 52.284812 \n", + "24 24 49.336715 6.930310 46.476024 52.197406 \n", + "25 25 49.850937 7.278945 46.910907 52.790966 \n", + "26 26 49.879344 7.139119 47.055203 52.703485 \n", + "27 27 49.857060 7.006658 47.140161 52.573958 \n", + "28 28 49.716192 6.922094 47.083168 52.349216 \n", + "29 29 49.866454 6.851314 47.308131 52.424776 \n", + "30 30 49.903691 6.739347 47.431678 52.375703 \n", + "31 31 49.850411 6.636604 47.457660 52.243162 \n", + "32 32 49.955695 6.560024 47.629612 52.281778 \n", + "33 33 50.078240 6.499265 47.810540 52.345941 \n", + "34 34 50.002865 6.418484 47.798038 52.207691 \n", + "35 35 49.888926 6.362958 47.736010 52.041841 \n", + "36 36 50.308907 6.774128 48.050300 52.567514 \n", + "37 37 50.617933 6.948198 48.334117 52.901749 \n", + "38 38 50.726526 6.889623 48.493168 52.959883 \n", + "39 39 51.107307 7.214539 48.799985 53.414629 \n", + "40 40 51.253305 7.184864 48.985482 53.521127 \n", + "41 41 51.158101 7.123473 48.938271 53.377932 \n", + "42 42 51.309010 7.107386 49.121680 53.496341 \n", + "43 43 51.432981 7.072227 49.282828 53.583135 \n", + "44 44 51.762158 7.331830 49.559432 53.964883 \n", + "45 45 52.120721 7.646912 49.849868 54.391573 \n", + "46 46 51.999456 7.608890 49.765402 54.233510 \n", + "47 47 52.091068 7.554220 49.897551 54.284585 \n", + "48 48 52.011852 7.495655 49.858849 54.164856 \n", + "49 49 51.955719 7.429385 49.844311 54.067127 \n", "\n", " perc_deviation \n", "0 NaN \n", @@ -1320,7 +1349,7 @@ { "data": { "image/svg+xml": [ - "10203040504244464850525456variablecumulative_meanlower_ciupper_ciNumber of replicationsMean wait time for nurse" + "0102030404244464850525456variablecumulative_meanlower_ciupper_ciNumber of replicationsMean wait time for nurse (*100) (minutes)" ] }, "metadata": {}, @@ -1333,14 +1362,14 @@ " replications=50,\n", " metric='mean_q_time_nurse',\n", " desired_precision=0.05,\n", - " yaxis_title='Mean wait time for nurse',\n", - " min_rep=31\n", + " min_rep=31,\n", + " labels=LABELS\n", ")" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -1375,7 +1404,7 @@ " \n", " \n", " 0\n", - " 1\n", + " 0\n", " 49.963865\n", " NaN\n", " NaN\n", @@ -1384,7 +1413,7 @@ " \n", " \n", " 1\n", - " 2\n", + " 1\n", " 50.081459\n", " 0.166304\n", " 48.587275\n", @@ -1393,7 +1422,7 @@ " \n", " \n", " 2\n", - " 3\n", + " 2\n", " 49.991963\n", " 0.194570\n", " 49.508625\n", @@ -1402,7 +1431,7 @@ " \n", " \n", " 3\n", - " 4\n", + " 3\n", " 49.949485\n", " 0.180155\n", " 49.662817\n", @@ -1411,7 +1440,7 @@ " \n", " \n", " 4\n", - " 5\n", + " 4\n", " 49.896994\n", " 0.195238\n", " 49.654574\n", @@ -1420,7 +1449,7 @@ " \n", " \n", " 5\n", - " 6\n", + " 5\n", " 49.795899\n", " 0.303011\n", " 49.477908\n", @@ -1429,7 +1458,7 @@ " \n", " \n", " 6\n", - " 7\n", + " 6\n", " 49.873003\n", " 0.343698\n", " 49.555136\n", @@ -1438,7 +1467,7 @@ " \n", " \n", " 7\n", - " 8\n", + " 7\n", " 49.931989\n", " 0.359288\n", " 49.631617\n", @@ -1447,7 +1476,7 @@ " \n", " \n", " 8\n", - " 9\n", + " 8\n", " 50.037224\n", " 0.461108\n", " 49.682785\n", @@ -1456,7 +1485,7 @@ " \n", " \n", " 9\n", - " 10\n", + " 9\n", " 50.033219\n", " 0.434921\n", " 49.722095\n", @@ -1465,7 +1494,7 @@ " \n", " \n", " 10\n", - " 11\n", + " 10\n", " 50.119630\n", " 0.502370\n", " 49.782133\n", @@ -1474,7 +1503,7 @@ " \n", " \n", " 11\n", - " 12\n", + " 11\n", " 50.022356\n", " 0.585646\n", " 49.650254\n", @@ -1483,7 +1512,7 @@ " \n", " \n", " 12\n", - " 13\n", + " 12\n", " 50.047003\n", " 0.567711\n", " 49.703938\n", @@ -1492,7 +1521,7 @@ " \n", " \n", " 13\n", - " 14\n", + " 13\n", " 49.966892\n", " 0.622376\n", " 49.607543\n", @@ -1501,7 +1530,7 @@ " \n", " \n", " 14\n", - " 15\n", + " 14\n", " 50.056635\n", " 0.693174\n", " 49.672768\n", @@ -1510,7 +1539,7 @@ " \n", " \n", " 15\n", - " 16\n", + " 15\n", " 50.059114\n", " 0.669744\n", " 49.702233\n", @@ -1519,7 +1548,7 @@ " \n", " \n", " 16\n", - " 17\n", + " 16\n", " 49.977371\n", " 0.730832\n", " 49.601612\n", @@ -1528,7 +1557,7 @@ " \n", " \n", " 17\n", - " 18\n", + " 17\n", " 49.953121\n", " 0.716437\n", " 49.596845\n", @@ -1537,7 +1566,7 @@ " \n", " \n", " 18\n", - " 19\n", + " 18\n", " 49.847018\n", " 0.835863\n", " 49.444144\n", @@ -1546,7 +1575,7 @@ " \n", " \n", " 19\n", - " 20\n", + " 19\n", " 49.812312\n", " 0.828241\n", " 49.424684\n", @@ -1559,26 +1588,26 @@ ], "text/plain": [ " replications cumulative_mean cumulative_std lower_ci upper_ci \\\n", - "0 1 49.963865 NaN NaN NaN \n", - "1 2 50.081459 0.166304 48.587275 51.575644 \n", - "2 3 49.991963 0.194570 49.508625 50.475301 \n", - "3 4 49.949485 0.180155 49.662817 50.236152 \n", - "4 5 49.896994 0.195238 49.654574 50.139415 \n", - "5 6 49.795899 0.303011 49.477908 50.113890 \n", - "6 7 49.873003 0.343698 49.555136 50.190870 \n", - "7 8 49.931989 0.359288 49.631617 50.232361 \n", - "8 9 50.037224 0.461108 49.682785 50.391663 \n", - "9 10 50.033219 0.434921 49.722095 50.344343 \n", - "10 11 50.119630 0.502370 49.782133 50.457127 \n", - "11 12 50.022356 0.585646 49.650254 50.394457 \n", - "12 13 50.047003 0.567711 49.703938 50.390067 \n", - "13 14 49.966892 0.622376 49.607543 50.326242 \n", - "14 15 50.056635 0.693174 49.672768 50.440503 \n", - "15 16 50.059114 0.669744 49.702233 50.415996 \n", - "16 17 49.977371 0.730832 49.601612 50.353130 \n", - "17 18 49.953121 0.716437 49.596845 50.309397 \n", - "18 19 49.847018 0.835863 49.444144 50.249891 \n", - "19 20 49.812312 0.828241 49.424684 50.199941 \n", + "0 0 49.963865 NaN NaN NaN \n", + "1 1 50.081459 0.166304 48.587275 51.575644 \n", + "2 2 49.991963 0.194570 49.508625 50.475301 \n", + "3 3 49.949485 0.180155 49.662817 50.236152 \n", + "4 4 49.896994 0.195238 49.654574 50.139415 \n", + "5 5 49.795899 0.303011 49.477908 50.113890 \n", + "6 6 49.873003 0.343698 49.555136 50.190870 \n", + "7 7 49.931989 0.359288 49.631617 50.232361 \n", + "8 8 50.037224 0.461108 49.682785 50.391663 \n", + "9 9 50.033219 0.434921 49.722095 50.344343 \n", + "10 10 50.119630 0.502370 49.782133 50.457127 \n", + "11 11 50.022356 0.585646 49.650254 50.394457 \n", + "12 12 50.047003 0.567711 49.703938 50.390067 \n", + "13 13 49.966892 0.622376 49.607543 50.326242 \n", + "14 14 50.056635 0.693174 49.672768 50.440503 \n", + "15 15 50.059114 0.669744 49.702233 50.415996 \n", + "16 16 49.977371 0.730832 49.601612 50.353130 \n", + "17 17 49.953121 0.716437 49.596845 50.309397 \n", + "18 18 49.847018 0.835863 49.444144 50.249891 \n", + "19 19 49.812312 0.828241 49.424684 50.199941 \n", "\n", " perc_deviation \n", "0 NaN \n", @@ -1616,7 +1645,7 @@ { "data": { "image/svg+xml": [ - "510152048.54949.55050.55151.5variablecumulative_meanlower_ciupper_ciNumber of replicationsMean nurse utilisation" + "05101548.54949.55050.55151.5variablecumulative_meanlower_ciupper_ciNumber of replicationsMean nurse utilisation (*100 - %)" ] }, "metadata": {}, @@ -1629,8 +1658,8 @@ " replications=20,\n", " metric='mean_nurse_utilisation',\n", " desired_precision=0.05,\n", - " yaxis_title='Mean nurse utilisation',\n", - " min_rep=3\n", + " min_rep=3,\n", + " labels=LABELS\n", ")" ] }, @@ -1645,7 +1674,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -1674,7 +1703,7 @@ { "data": { "image/svg+xml": [ - "1234567811.522.533.544.55Number of coresRun time (rounded to nearest .5 seconds)" + "1234567811.522.533.544.55Number of coresRun time (rounded to nearest .5 seconds)" ] }, "metadata": {}, @@ -1688,9 +1717,9 @@ " print(f'Running with cores: {i}.')\n", " start_time = time.time()\n", "\n", - " param = Defaults()\n", - " param.cores = i\n", - " experiment = Runner(param)\n", + " run_param = Defaults()\n", + " run_param.cores = i\n", + " experiment = Runner(run_param)\n", " experiment.run_reps()\n", "\n", " # Round time to nearest .5 seconds\n", @@ -1700,17 +1729,17 @@ "# Plot time by number of cores\n", "timing_results = pd.DataFrame(speed)\n", "print(timing_results)\n", - "fig = px.line(timing_results, x='cores', y='run_time')\n", - "fig.update_layout(\n", + "cores_fig = px.line(timing_results, x='cores', y='run_time')\n", + "cores_fig.update_layout(\n", " xaxis_title='Number of cores',\n", " yaxis_title='Run time (rounded to nearest .5 seconds)',\n", " template='plotly_white'\n", ")\n", "\n", "# Display and save figure\n", - "fig.show()\n", - "fig.write_image(os.path.join(output_dir, 'choose_param_cores.png'),\n", - " width=800, height=600)" + "cores_fig.show()\n", + "cores_fig.write_image(os.path.join(OUTPUT_DIR, 'choose_param_cores.png'),\n", + " width=800, height=600)" ] }, { @@ -1722,7 +1751,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -1759,7 +1788,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.0" + "version": "3.13.1" } }, "nbformat": 4, diff --git a/notebooks/generate_exp_results.ipynb b/notebooks/generate_exp_results.ipynb index 1ae6846..1c1a4aa 100644 --- a/notebooks/generate_exp_results.ipynb +++ b/notebooks/generate_exp_results.ipynb @@ -17,27 +17,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Set-up\n", - "\n", - "Load notebook linters." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext pycodestyle_magic" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "%pycodestyle_on" + "## Set-up" ] }, { @@ -49,10 +29,11 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ + "# pylint: disable=missing-module-docstring\n", "# To ensure any updates to `simulation/` are fetched without needing to restart\n", "# the notebook environment, reload `simulation/` before execution of each cell\n", "%load_ext autoreload\n", @@ -62,14 +43,17 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ + "# pylint: disable=wrong-import-position\n", "# Import required packages\n", - "from simulation.model import Defaults, Runner\n", "import os\n", - "import time" + "import time\n", + "from IPython.display import display\n", + "\n", + "from simulation.model import Defaults, Runner" ] }, { @@ -81,7 +65,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -98,12 +82,12 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "# Define path to folder for expected results for tests\n", - "tests = '../tests/exp_results/'" + "TESTS = '../tests/exp_results/'" ] }, { @@ -115,7 +99,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -138,7 +122,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -288,12 +272,12 @@ "# Patient-level results\n", "display(experiment.patient_results_df)\n", "experiment.patient_results_df.to_csv(\n", - " os.path.join(tests, 'patient.csv'), index=False)" + " os.path.join(TESTS, 'patient.csv'), index=False)" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -411,12 +395,12 @@ "# Run results\n", "display(experiment.run_results_df)\n", "experiment.run_results_df.to_csv(\n", - " os.path.join(tests, 'run.csv'), index=False)" + " os.path.join(TESTS, 'run.csv'), index=False)" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -591,12 +575,12 @@ "# Interval audit results\n", "display(experiment.interval_audit_df)\n", "experiment.interval_audit_df.to_csv(\n", - " os.path.join(tests, 'interval.csv'), index=False)" + " os.path.join(TESTS, 'interval.csv'), index=False)" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -697,12 +681,12 @@ "# Overall results\n", "display(experiment.overall_results_df)\n", "experiment.overall_results_df.to_csv(\n", - " os.path.join(tests, 'overall.csv'), index=True)" + " os.path.join(TESTS, 'overall.csv'), index=True)" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -739,7 +723,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.0" + "version": "3.13.1" } }, "nbformat": 4, diff --git a/outputs/choose_param_conf_int_1.png b/outputs/choose_param_conf_int_1.png index 1df7b26..fadfa6a 100644 Binary files a/outputs/choose_param_conf_int_1.png and b/outputs/choose_param_conf_int_1.png differ diff --git a/outputs/choose_param_conf_int_2.png b/outputs/choose_param_conf_int_2.png index 1aae52e..7e248d7 100644 Binary files a/outputs/choose_param_conf_int_2.png and b/outputs/choose_param_conf_int_2.png differ diff --git a/outputs/choose_param_conf_int_3.png b/outputs/choose_param_conf_int_3.png index 26c64e3..18e1ceb 100644 Binary files a/outputs/choose_param_conf_int_3.png and b/outputs/choose_param_conf_int_3.png differ diff --git a/outputs/choose_param_time_series_1.png b/outputs/choose_param_time_series_1.png index cae4d17..ae39346 100644 Binary files a/outputs/choose_param_time_series_1.png and b/outputs/choose_param_time_series_1.png differ diff --git a/outputs/choose_param_time_series_2.png b/outputs/choose_param_time_series_2.png index 8eba128..8b5c23f 100644 Binary files a/outputs/choose_param_time_series_2.png and b/outputs/choose_param_time_series_2.png differ diff --git a/outputs/example_overall.csv b/outputs/example_overall.csv index 8deedfd..c7bf801 100644 --- a/outputs/example_overall.csv +++ b/outputs/example_overall.csv @@ -1,5 +1,5 @@ arrivals,mean_q_time_nurse,mean_time_with_nurse,mean_nurse_utilisation,mean_nurse_utilisation_tw,mean_nurse_q_length -10776.741935483871,0.499036905566402,9.978456743390534,0.49767035119563796,0.497804034286856,0.12459850290556283 -115.80327218806197,0.06739346957542013,0.11513823236530764,0.007524465631261531,0.0075405891452702024,0.017212719807337778 -10734.264952313013,0.47431678135150124,9.936223698796068,0.4949103549196282,0.4950381238581607,0.11828482630760263 -10819.21891865473,0.5237570297813028,10.020689787985,0.5004303474716477,0.5005699447155513,0.13091217950352302 +10776.741935483871,0.499036905566402,9.978456743390534,0.49767035119563796,0.49780403428685593,0.12459850290556283 +115.80327218806197,0.06739346957542013,0.11513823236530764,0.007524465631261531,0.007540589145270748,0.017212719807337778 +10734.264952313013,0.47431678135150124,9.936223698796068,0.4949103549196282,0.4950381238581605,0.11828482630760263 +10819.21891865473,0.5237570297813028,10.020689787985,0.5004303474716477,0.5005699447155514,0.13091217950352302 diff --git a/outputs/example_run.csv b/outputs/example_run.csv index cdec45f..fb80c78 100644 --- a/outputs/example_run.csv +++ b/outputs/example_run.csv @@ -1,32 +1,32 @@ run_number,scenario,arrivals,mean_q_time_nurse,mean_time_with_nurse,mean_nurse_utilisation,mean_nurse_utilisation_tw,mean_nurse_q_length -0,0,10972,0.504541081338615,9.84226781662332,0.4996386466177992,0.4997404525127849,0.12814409130665008 -1,0,10784,0.514150649003393,10.060480983450425,0.5019905433327594,0.502069010098797,0.1283472360845507 -2,0,10854,0.5232349226016817,9.925024519746302,0.4981297032213408,0.4982604514269931,0.1326692082859329 -3,0,10831,0.4791488631810612,9.9370571543943,0.49822049332297624,0.4982689502894135,0.12013104947023319 -4,0,10720,0.46145745726579823,10.015904147971671,0.4968703372055205,0.4971055007588973,0.11450981346966105 -5,0,10772,0.3882646868128185,9.884995942861282,0.492904233058074,0.4930559758823053,0.09681451866545558 -6,0,10831,0.4669381081669874,10.041799654744745,0.503356251584998,0.5034005361957391,0.11706959836936667 +0,0,10972,0.504541081338615,9.84226781662332,0.4996386466177992,0.49974045251278504,0.12814409130665008 +1,0,10784,0.514150649003393,10.060480983450425,0.5019905433327594,0.5020690100987935,0.1283472360845507 +2,0,10854,0.5232349226016817,9.925024519746302,0.4981297032213408,0.49826045142699366,0.1326692082859329 +3,0,10831,0.4791488631810612,9.9370571543943,0.49822049332297624,0.49826895028941315,0.12013104947023319 +4,0,10720,0.46145745726579823,10.015904147971671,0.4968703372055205,0.4971055007588991,0.11450981346966105 +5,0,10772,0.3882646868128185,9.884995942861282,0.492904233058074,0.4930559758823056,0.09681451866545558 +6,0,10831,0.4669381081669874,10.041799654744745,0.503356251584998,0.5034005361957363,0.11706959836936667 7,0,10781,0.625888360901447,10.086979128063648,0.5034489381047406,0.5037422085180931,0.15619681525181714 -8,0,10772,0.4684971326393017,10.202270228377186,0.508790994907774,0.5089130015618383,0.11682062761089254 -9,0,10705,0.5634349862001105,10.09260227901333,0.49997178325917613,0.5002278075946729,0.1399966447950717 -10,0,10927,0.562585969190057,10.083553615426284,0.5098373896503802,0.5099889270180503,0.14230039086434612 -11,0,10688,0.39781182403365434,9.893334037740962,0.4895233535105571,0.48962710728052056,0.09842159202017818 -12,0,11092,0.44530362984867583,9.810905063859671,0.5034276977164731,0.5034590643983867,0.11437196007956935 -13,0,10640,0.4397605749351992,9.933879963761742,0.48925457544062534,0.4892677288083271,0.10831140086366944 -14,0,10904,0.6107822771635987,10.167683587926042,0.513130373051619,0.5135294499218972,0.1541659710692565 -15,0,10849,0.47670998312351787,9.975581276412813,0.5009629889699926,0.5010455334069686,0.11971820849321864 -16,0,10719,0.428336230071242,9.809804967118032,0.4866947365248423,0.4868274499224406,0.10628092708642692 -17,0,10713,0.505574486075843,9.989154604862323,0.4954087210793937,0.4957697619501451,0.1253754506789469 -18,0,10568,0.3615821082986753,9.800182515281657,0.4793715775153063,0.4795652084277097,0.08845369723380557 -19,0,10707,0.4191589720392036,9.9193906381964,0.4915291568180775,0.4917635127186296,0.10388738688943873 -20,0,10845,0.6120142008002236,9.813774247048583,0.49261944375555833,0.49267201493881163,0.15364106499255614 -21,0,10726,0.5173212645251098,9.858186468091487,0.48940978932654106,0.4895612849605359,0.1284441639651928 -22,0,10618,0.5316566599495316,9.969891296475984,0.4892835610032433,0.4893719186906082,0.13067431517000294 -23,0,10914,0.5270546197660896,9.925291785145637,0.5013818263178943,0.501433800156172,0.1331544935214607 -24,0,10660,0.5029697553047842,9.96454608027881,0.49160360886911897,0.49162164437257694,0.12411244424881943 -25,0,10685,0.6270646900660448,10.236338445072258,0.5060501169430693,0.5061407672693311,0.15515572552368928 -26,0,10806,0.5061794260886525,10.039653295082593,0.5022615440123254,0.5023039700989318,0.126615159220231 -27,0,10748,0.4925537687951073,10.050997027592315,0.4999546777219452,0.5001183558252156,0.12254555340300495 -28,0,10589,0.4577189216542841,10.044180668559221,0.4923573694687436,0.4923672645089264,0.11219411253234293 -29,0,10863,0.5422405738480466,10.034383892923174,0.5046064549261675,0.5047331518468727,0.13635091096554006 -30,0,10796,0.5102078888697092,9.922063713004379,0.49578999982774674,0.49597325153194355,0.12767905794112022 +8,0,10772,0.4684971326393017,10.202270228377186,0.508790994907774,0.5089130015618397,0.11682062761089254 +9,0,10705,0.5634349862001105,10.09260227901333,0.49997178325917613,0.5002278075946733,0.1399966447950717 +10,0,10927,0.562585969190057,10.083553615426284,0.5098373896503802,0.5099889270180521,0.14230039086434612 +11,0,10688,0.39781182403365434,9.893334037740962,0.4895233535105571,0.489627107280519,0.09842159202017818 +12,0,11092,0.44530362984867583,9.810905063859671,0.5034276977164731,0.503459064398387,0.11437196007956935 +13,0,10640,0.4397605749351992,9.933879963761742,0.48925457544062534,0.48926772880832675,0.10831140086366944 +14,0,10904,0.6107822771635987,10.167683587926042,0.513130373051619,0.5135294499218969,0.1541659710692565 +15,0,10849,0.47670998312351787,9.975581276412813,0.5009629889699926,0.5010455334069701,0.11971820849321864 +16,0,10719,0.428336230071242,9.809804967118032,0.4866947365248423,0.48682744992243954,0.10628092708642692 +17,0,10713,0.505574486075843,9.989154604862323,0.4954087210793937,0.4957697619501464,0.1253754506789469 +18,0,10568,0.3615821082986753,9.800182515281657,0.4793715775153063,0.4795652084277076,0.08845369723380557 +19,0,10707,0.4191589720392036,9.9193906381964,0.4915291568180775,0.49176351271863106,0.10388738688943873 +20,0,10845,0.6120142008002236,9.813774247048583,0.49261944375555833,0.4926720149388125,0.15364106499255614 +21,0,10726,0.5173212645251098,9.858186468091487,0.48940978932654106,0.4895612849605331,0.1284441639651928 +22,0,10618,0.5316566599495316,9.969891296475984,0.4892835610032433,0.4893719186906069,0.13067431517000294 +23,0,10914,0.5270546197660896,9.925291785145637,0.5013818263178943,0.5014338001561723,0.1331544935214607 +24,0,10660,0.5029697553047842,9.96454608027881,0.49160360886911897,0.49162164437257705,0.12411244424881943 +25,0,10685,0.6270646900660448,10.236338445072258,0.5060501169430693,0.5061407672693301,0.15515572552368928 +26,0,10806,0.5061794260886525,10.039653295082593,0.5022615440123254,0.5023039700989324,0.126615159220231 +27,0,10748,0.4925537687951073,10.050997027592315,0.4999546777219452,0.5001183558252174,0.12254555340300495 +28,0,10589,0.4577189216542841,10.044180668559221,0.4923573694687436,0.49236726450892265,0.11219411253234293 +29,0,10863,0.5422405738480466,10.034383892923174,0.5046064549261675,0.5047331518468752,0.13635091096554006 +30,0,10796,0.5102078888697092,9.922063713004379,0.49578999982774674,0.49597325153194544,0.12767905794112022 diff --git a/outputs/scenario_nurse_util.png b/outputs/scenario_nurse_util.png index cc3822e..0e449ea 100644 Binary files a/outputs/scenario_nurse_util.png and b/outputs/scenario_nurse_util.png differ diff --git a/outputs/scenario_nurse_wait.png b/outputs/scenario_nurse_wait.png index f768431..17fadfb 100644 Binary files a/outputs/scenario_nurse_wait.png and b/outputs/scenario_nurse_wait.png differ diff --git a/outputs/sensitivity_consult_time.png b/outputs/sensitivity_consult_time.png index 241803b..00de371 100644 Binary files a/outputs/sensitivity_consult_time.png and b/outputs/sensitivity_consult_time.png differ diff --git a/outputs/spread_arrivals.png b/outputs/spread_arrivals.png index 4e08329..8bc1a42 100644 Binary files a/outputs/spread_arrivals.png and b/outputs/spread_arrivals.png differ diff --git a/outputs/spread_nurse_time.png b/outputs/spread_nurse_time.png index bc6a988..5ad4f50 100644 Binary files a/outputs/spread_nurse_time.png and b/outputs/spread_nurse_time.png differ diff --git a/outputs/spread_nurse_wait.png b/outputs/spread_nurse_wait.png index d864b5f..9c291a4 100644 Binary files a/outputs/spread_nurse_wait.png and b/outputs/spread_nurse_wait.png differ diff --git a/requirements.txt b/requirements.txt index 2a6123d..2d6ec97 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,14 @@ -flake8==7.1.1 ipykernel==6.29.5 -jinja2==3.1.4 +jinja2==3.1.5 joblib==1.4.2 kaleido==0.2.1 nbformat==5.10.4 -numpy==2.1.3 +nbqa==1.9.0 +numpy==2.2.2 pandas==2.2.3 -pip==24.3.1 +pip==25.0 plotly_express==0.4.1 -pycodestyle==2.12.1 -pycodestyle_magic==0.5 -pylint==3.3.3 +pylint==3.3.4 pytest==8.3.4 pytest-xdist==3.6.1 simpy==4.1.1 diff --git a/simulation/model.py b/simulation/model.py index 2a37de8..68a7b86 100644 --- a/simulation/model.py +++ b/simulation/model.py @@ -198,14 +198,14 @@ class MonitoredResource(simpy.Resource): simpy.Resource, which is referred to as the superclass or parent class. Attributes: - time_last_event (float): + time_last_event (list): Time of last resource request or release. - area_n_in_queue (float): - Total time that patients have spent queueing for the resource + area_n_in_queue (list): + Time that patients have spent queueing for the resource (i.e. sum of the times each patient spent waiting). Used to calculate the average queue length. - area_resource_busy (float): - Total time that resources have been in use during the simulation + area_resource_busy (list): + Time that resources have been in use during the simulation (i.e. sum of the times each individual resource was busy). Used to calculated utilisation. """ @@ -229,9 +229,9 @@ def init_results(self): """ Resets monitoring attributes to initial values. """ - self.time_last_event = self._env.now - self.area_n_in_queue = 0.0 - self.area_resource_busy = 0.0 + self.time_last_event = [self._env.now] + self.area_n_in_queue = [0.0] + self.area_resource_busy = [0.0] def request(self, *args, **kwargs): """ @@ -289,16 +289,18 @@ def update_time_weighted_stats(self): persist over time. """ # Calculate time since last event - time_since_last_event = self._env.now - self.time_last_event + time_since_last_event = self._env.now - self.time_last_event[-1] - # Update self.time_last_event to current time - self.time_last_event = self._env.now + # Add record of current time + self.time_last_event.append(self._env.now) - # Update the statistics + # Add "area under curve" of people in queue # len(self.queue) is the number of requests queued - self.area_n_in_queue += len(self.queue) * time_since_last_event + self.area_n_in_queue.append(len(self.queue) * time_since_last_event) + + # Add "area under curve" of resources in use # self.count is the number of resources in use - self.area_resource_busy += self.count * time_since_last_event + self.area_resource_busy.append(self.count * time_since_last_event) class Exponential: @@ -499,7 +501,7 @@ def attend_clinic(self, patient): # Log wait time and time spent with nurse self.param.logger.log( f'Patient {patient.patient_id} is seen by nurse after ' + - f'{patient.arrival_time:.3f} minutes. Consultation length: ' + + f'{patient.q_time_nurse:.3f} minutes. Consultation length: ' + f'{patient.time_with_nurse:.3f} minutes.' ) @@ -552,20 +554,22 @@ def init_results_variables(self): def warm_up_complete(self): """ - Resets all results collection variables once warm-up period has passed. + If there is a warm-up period, then reset all results collection + variables once warm-up period has passed. """ - # Delay process until warm-up period has completed - yield self.env.timeout(self.param.warm_up_period) + if self.param.warm_up_period > 0: + # Delay process until warm-up period has completed + yield self.env.timeout(self.param.warm_up_period) - # Reset results collection variables - self.init_results_variables() + # Reset results collection variables + self.init_results_variables() - # If there was a warm-up period, log that this time has passed so we - # can distinguish between patients before and after warm-up in logs - if self.param.warm_up_period > 0: - self.param.logger.log('─' * 10) - self.param.logger.log(f'{self.env.now:.2f}: Warm up complete.') - self.param.logger.log('─' * 10) + # If there was a warm-up period, log that this time has passed so + # can distinguish between patients before and after warm-up in logs + if self.param.warm_up_period > 0: + self.param.logger.log('─' * 10) + self.param.logger.log(f'{self.env.now:.2f}: Warm up complete.') + self.param.logger.log('─' * 10) def run(self): """ @@ -576,6 +580,7 @@ def run(self): self.param.data_collection_period) # Schedule process which will reset results when warm-up period ends + # (or does nothing if these is no warm-up) self.env.process(self.warm_up_complete()) # Schedule patient generator to run during simulation @@ -590,7 +595,7 @@ def run(self): # If the simulation ends while resources are still in use or requests # are still in the queue, the time between the last recorded event and - # the simulation end will not have been accounted for. Hence, we call + # the simulation end will not have been accounted for. Hence, we call # update_time_weighted_stats() to run for last event --> end. self.nurse.update_time_weighted_stats() @@ -672,10 +677,10 @@ def run_single(self, run): 'mean_nurse_utilisation': (model.nurse_time_used / (self.param.number_of_nurses * self.param.data_collection_period)), - 'mean_nurse_utilisation_tw': (model.nurse.area_resource_busy / + 'mean_nurse_utilisation_tw': (sum(model.nurse.area_resource_busy) / (self.param.number_of_nurses * self.param.data_collection_period)), - 'mean_nurse_q_length': (model.nurse.area_n_in_queue / + 'mean_nurse_q_length': (sum(model.nurse.area_n_in_queue) / self.param.data_collection_period) } diff --git a/tests/exp_results/overall.csv b/tests/exp_results/overall.csv index dbf57c7..4a48725 100644 --- a/tests/exp_results/overall.csv +++ b/tests/exp_results/overall.csv @@ -1,5 +1,5 @@ ,arrivals,mean_q_time_nurse,mean_time_with_nurse,mean_nurse_utilisation,mean_nurse_utilisation_tw,mean_nurse_q_length mean,378.0,2.1667325673589564,10.159320371752361,0.6362147508884318,0.6397764054245421,0.5445692746656905 -std_dev,15.198684153570664,0.7844583147114635,0.4786676748781607,0.02383218108751965,0.024277980144559095,0.19009766156834382 -lower_95_ci,359.12834106644124,1.192698919890132,9.564975952752244,0.606623189633386,0.6096313115299372,0.3085318521535542 -upper_95_ci,396.87165893355876,3.1407662148277806,10.75366479075248,0.6658063121434776,0.6699214993191469,0.7806066971778267 +std_dev,15.198684153570664,0.7844583147114635,0.4786676748781607,0.02383218108751965,0.024277980144559032,0.19009766156834385 +lower_95_ci,359.12834106644124,1.192698919890132,9.564975952752244,0.606623189633386,0.6096313115299372,0.30853185215355416 +upper_95_ci,396.87165893355876,3.1407662148277806,10.75366479075248,0.6658063121434776,0.6699214993191469,0.7806066971778268 diff --git a/tests/exp_results/run.csv b/tests/exp_results/run.csv index b7c840d..531d0e5 100644 --- a/tests/exp_results/run.csv +++ b/tests/exp_results/run.csv @@ -1,6 +1,6 @@ run_number,scenario,arrivals,mean_q_time_nurse,mean_time_with_nurse,mean_nurse_utilisation,mean_nurse_utilisation_tw,mean_nurse_q_length -0,0,403,2.065395937787463,9.622970542788952,0.6430060048077357,0.6430060048077361,0.5549030419522317 -1,0,382,1.3967545762785822,10.18623477209793,0.6456396558371599,0.6476900423541169,0.35570683209227894 -2,0,369,3.4842840540548505,10.733822459914006,0.6607899582685668,0.6683185188812463,0.8582185938875108 -3,0,367,2.0505219928782124,10.511446608041043,0.6345411517895263,0.6384068134475295,0.5021881615506957 -4,0,369,1.8367062757956718,9.742127475919874,0.5970969837391709,0.6014606476320816,0.45182974384573527 +0,0,403,2.065395937787463,9.622970542788952,0.6430060048077357,0.643006004807736,0.5549030419522317 +1,0,382,1.3967545762785822,10.18623477209793,0.6456396558371599,0.6476900423541168,0.35570683209227894 +2,0,369,3.4842840540548505,10.733822459914006,0.6607899582685668,0.6683185188812463,0.8582185938875109 +3,0,367,2.0505219928782124,10.511446608041043,0.6345411517895263,0.6384068134475298,0.5021881615506957 +4,0,369,1.8367062757956718,9.742127475919874,0.5970969837391709,0.6014606476320817,0.45182974384573527 diff --git a/tests/test_unittest_model.py b/tests/test_unittest_model.py index 3e64e51..22783c8 100644 --- a/tests/test_unittest_model.py +++ b/tests/test_unittest_model.py @@ -210,12 +210,38 @@ def helper_warmup(warm_up_period): f'high arrival rate, but got {first_warmup["q_time_nurse"]}.' ) - # Check that, in model without warm-up, first patient has 0 queue time + # Check that model without warm-up has arrival and queue time of 0 + assert first_none['arrival_time'] == 0, ( + 'Expect first patient to arrive at time 0 when model is run ' + + f'without warm-up, but got {first_none["arrival_time"]}.' + ) assert first_none['q_time_nurse'] == 0, ( 'Expect first patient to have no wait time in model without warm-up ' + f'but got {first_none["q_time_nurse"]}.' ) + # Check that the first interval audit entry with no warm-up has time and + # results of 0 + first_interval = results_none['interval_audit'].iloc[0] + assert first_interval['simulation_time'] == 0, ( + 'With no warm-up, expect first entry in interval audit to be ' + + f'at time 0, but it was at time {first_interval['simulation_time']}.' + ) + assert first_interval['utilisation'] == 0, ( + 'With no warm-up, expect first entry in interval audit to ' + + f'have 0 utilisation, but it was {first_interval['utilisation']}.' + ) + assert first_interval['queue_length'] == 0, ( + 'With no warm-up, expect first entry in interval audit to ' + + 'have no queue, but there was queue length of ' + + f'{first_interval['queue_length']}.' + ) + assert first_interval['running_mean_wait_time'] == 0, ( + 'With no warm-up, expect first entry in interval audit to ' + + 'have running mean wait time of 0 but it was ' + + f'{first_interval['running_mean_wait_time']}.' + ) + def test_arrivals(): """ @@ -497,11 +523,11 @@ def process_task(env, resource, duration): expected_busy_time = 12.0 # Run assertions - assert resource.area_n_in_queue == expected_queue_time, ( + assert sum(resource.area_n_in_queue) == expected_queue_time, ( f'Expected queue time {expected_queue_time} but ' + f'observed {resource.area_n_in_queue}.' ) - assert resource.area_resource_busy == expected_busy_time, ( + assert sum(resource.area_resource_busy) == expected_busy_time, ( f'Expected queue time {expected_busy_time} but ' + f'observed {resource.area_resource_busy}.' )