These should be available from environment setup in the “Test yourself” section of Environments.
import osfrom IPython.display import HTMLfrom itables import to_html_datatableimport matplotlib.colors as mcolorsimport numpy as npimport pandas as pdimport plotly.express as pximport plotly.graph_objects as goimport scipy.stats as st
When reporting results from a simulation, you will usually include tables and figures. To make your work reproducible, you should:
1. Make your tables and figures using code.
2. Share that code alongside your results.
This is just as important as sharing model and scenario code. In Heather et al. (2025), only one study included the code needed to generate all its reported results. Without this code for the others, recreating outputs was time-consuming and challenging. It required guesswork about which results to extract, how to clean them, and what processing steps were taken when producing the published tables and figures.
This page will show some examples of creating tables and figures, but there are many different ways you could describe and visualise your simulation results. The key takeaway from the page is simple: write code and share it!
This function finds the average results from each experiment.
It uses the summary_stats() function introduced on the Replications page.
NoteView summary_stats()
def summary_stats(data):""" Calculate mean, standard deviation and 95% confidence interval (CI). Parameters ---------- data : pd.Series Data to use in calculation. Returns ------- tuple (mean, standard deviation, CI lower, CI upper). """# Remove any NaN from the series data = data.dropna()# Find number of observations count =len(data)# If there are no observations, then set all to NaNif count ==0: mean, std_dev, ci_lower, ci_upper = np.nan, np.nan, np.nan, np.nan# If there is only one or two observations, can do mean but not otherselif count <3: mean = data.mean() std_dev, ci_lower, ci_upper = np.nan, np.nan, np.nan# With more than one observation, can calculate all...else: mean = data.mean() std_dev = data.std()# Special case for CI if variance is 0if np.var(data) ==0: ci_lower, ci_upper = mean, meanelse:# Calculation of CI uses t-distribution, which is suitable for# smaller sample sizes (n<30) ci_lower, ci_upper = st.t.interval( confidence=0.95, df=count-1, loc=mean, scale=st.sem(data))return mean, std_dev, ci_lower, ci_upper
def summarise_scenarios(results, groups, result_vars, path_prefix=None):""" Find the average results from each scenario for multiple metrics. Parameters ---------- results : pd.DataFrame Run-level results from every scenario. groups : list List of columns to group by (provided as strings). result_vars : list List of performance measures to get results on (provided as strings). path_prefix : str, optional Path prefix to save tables to. Each metric will be saved as {path_prefix}_{metric}.csv Returns ------- summary_tables : dict Dictionary with metric names as keys and summary DataFrames as values. """ summary_tables = {}for result_var in result_vars: summary_table = ( results.groupby(groups)[result_var] .apply(summary_stats) .apply(pd.Series) .reset_index() ) summary_table.columns = (list(summary_table.columns[:-4]) + ["mean", "std_dev", "ci_lower", "ci_upper"])# Add column to identify which metric this is summary_table["metric"] = result_var summary_tables[result_var] = summary_table# Save if path providedif path_prefix: output_path =f"{path_prefix}_{result_var}.csv" summary_table.to_csv(output_path, index=False)return summary_tables
Loading ITables v2.5.2 from the internet...
(need help?)
#' Summarise multiple performance measures at once#'#' @param results Dataframe with results from each replication of scenarios.#' @param groups Vector of grouping columns (as strings).#' @param result_vars Vector of performance measures to summarise (as strings).#' @param path_prefix Optional path prefix to save each table to.#'#' @return A named list of summary data frames.summarise_scenarios <-function(results, groups, result_vars, path_prefix =NULL) { summary_tables <-list()for (result_var in result_vars) { summary_table <- results |>group_by(across(all_of(groups))) |>summarise(mean =mean(.data[[result_var]], na.rm =TRUE),std_dev =sd(.data[[result_var]], na.rm =TRUE),ci_lower =t.test(.data[[result_var]])$conf.int[1],ci_upper =t.test(.data[[result_var]])$conf.int[2],.groups ="drop" ) |>mutate(metric = result_var)# Save table if prefix specifiedif (!is.null(path_prefix)) { output_path <-paste0(path_prefix, "_", result_var, ".csv")write.csv(summary_table, output_path, row.names =FALSE) }# Store in list summary_tables[[result_var]] <- summary_table }return(summary_tables)}
These functions are used to plot the results from the scenarios and sensitivity analysis.
def pale_colour(colour, alpha=0.2):""" Make pale version of a colour. Parameters ---------- colour : str or tuple The colour to pale, as a Plotly-compatible name ('blue', 'red'), hex string (e.g., "#1f77b4"), or RGB tuple (e.g., (0.1, 0.3, 0.8)). Accepts any format readable by matplotlib.colors.to_rgb. alpha : float, optional Opacity for the pale colour (default = 0.2). Should be between 0 and 1. Returns ------- str Colour in 'rgba(r, g, b, a)' string format (e.g., "rgba(31,119,180,0.2)"). Acknowledgements ---------------- This function was generated by Perplexity. """# Convert any accepted colour format to an RGB tuple rgb = mcolors.to_rgb(colour)# Scale values to 0-255 for plotly compatability r, g, b = [int(x *255) for x in rgb]# Return as RGB tuple with added alpha parameter (which makes it paler)returnf"rgba({r},{g},{b},{alpha})"
def plot_metrics( summary_tables, colour_var=None, name_mappings=None, path_prefix=None):""" Plot results from different model scenarios. Parameters ---------- summary_tables : dict Dictionary with metric names as keys and summary DataFrames as values. colour_var : str, optional Name of variable to colour lines with. name_mappings : dict, optional Dictionary mapping column names to labels. If not provided, function will default to variable names. path : str Path to save figure to. Returns ------- figs : dict Dictionary with metric names as keys and plotly figures as values. """ figs = {}for metric_name, summary_table in summary_tables.items():# Initialise figure fig = go.Figure()# Handling color mappings (optional)if colour_var: unique_groups = summary_table[colour_var].unique() colours = px.colors.qualitative.Plotly colour_map = { group: colours[i %len(colours)]for i, group inenumerate(unique_groups) }else: colour_map = {None: "blue"}# Plot each scenario line and its shaded confidence intervalif colour_var: groups = summary_table.groupby(colour_var)else: groups = [(None, summary_table)]for group_name, group_data in groups: x = group_data["interarrival_time"].values mean = group_data["mean"].values ci_upper = group_data["ci_upper"].values ci_lower = group_data["ci_lower"].values main_colour = colour_map[group_name] shade_colour = pale_colour(main_colour, alpha=0.2)# Filled confidence interval region fig.add_trace(go.Scatter( x=np.concatenate([x, x[::-1]]), y=np.concatenate([ci_upper, ci_lower[::-1]]), fill="toself", fillcolor=shade_colour, line={"color": "rgba(255,255,255,0)"}, showlegend=False, name="95% CI", hoverinfo="name+y" ))# Mean line fig.add_trace(go.Scatter( x=x, y=mean, mode="lines", line={"color": main_colour, "width": 2}, name=group_name, hoverinfo="y" ))# Set labels and layout fig.update_layout( xaxis_title=( name_mappings.get("interarrival_time", "interarrival_time")if name_mappings else"interarrival_time" ), yaxis_title=( name_mappings.get(metric_name, metric_name)if name_mappings else metric_name ), legend_title_text=( name_mappings.get(colour_var, colour_var)if colour_var and name_mappingselse (colour_var if colour_var elseNone) ), template="plotly_white" )ifnot colour_var: fig.update_layout(showlegend=False) figs[metric_name] = fig# Save if path providedif path_prefix: output_path =f"{path_prefix}_{metric_name}.png" fig.write_image(output_path)return figs
Scenario analysis
name_mappings = {"interarrival_time": "Patient inter-arrival time","number_of_doctors": "Number of doctors","mean_wait_time": "Mean wait time for the doctor","mean_utilisation_tw": "Mean utilisation","mean_queue_length": "Mean queue length","mean_time_in_system": "Mean patient time in the system","mean_patients_in_system": "Mean number of patients in the system"}
This function is used to plot the results from the scenarios and sensitivity analysis.
#' Plot multiple performance measures at once#'#' @param summary_tables Named list of summary tables, one per metric (like output from summarise_scenarios()).#' @param x_var Name of variable to plot on x axis.#' @param colour_var Name of variable to colour lines with (can be NULL).#' @param name_mappings Optional named list for prettier axis/legend labels.#' @param path_prefix Optional path prefix to save figures.#' @return Named list of ggplot objectsplot_metrics <-function( summary_tables, x_var, colour_var =NULL,name_mappings =NULL, path_prefix =NULL) { plot_list <-list()for (metric_name innames(summary_tables)) { summary_table <- summary_tables[[metric_name]] y_var <-"mean" ci_lower <-"ci_lower" ci_upper <-"ci_upper" xaxis_title <-if (!is.null(name_mappings[[x_var]])) name_mappings[[x_var]] else x_var yaxis_title <-if (!is.null(name_mappings[[metric_name]])) name_mappings[[metric_name]] else metric_name legend_title <-if (!is.null(colour_var) &&!is.null(name_mappings[[colour_var]])) name_mappings[[colour_var]] else colour_varif (!is.null(colour_var)) { summary_table[[colour_var]] <-as.factor(summary_table[[colour_var]]) p <-ggplot(summary_table, aes_string(x = x_var, y = y_var, group = colour_var, color = colour_var, fill = colour_var)) +geom_line() +geom_ribbon(aes_string(ymin = ci_lower, ymax = ci_upper), alpha =0.1) +labs(x = xaxis_title, y = yaxis_title, color = legend_title, fill = legend_title) +theme_minimal() } else { p <-ggplot(summary_table, aes_string(x = x_var, y = y_var)) +geom_line() +geom_ribbon(aes_string(ymin = ci_lower, ymax = ci_upper), alpha =0.1, show.legend =FALSE) +labs(x = xaxis_title, y = yaxis_title) +theme_minimal() }# Save plot if prefix suppliedif (!is.null(path_prefix)) { output_path <-paste0(path_prefix, "_", metric_name, ".png")ggsave(filename = output_path, plot = p, width =6.5, height =4, bg ="white") } plot_list[[metric_name]] <- p }return(plot_list)}
Scenario analysis
name_mappings <-list(interarrival_time ="Patient inter-arrival time",number_of_doctors ="Number of doctors",mean_wait_time_doctor ="Mean wait time for the doctor",utilisation_doctor ="Mean utilisation",mean_queue_length_doctor ="Mean queue length",mean_time_in_system ="Mean patient time in the system",mean_patients_in_system ="Mean number of patients in the system")
Warning: `aes_string()` was deprecated in ggplot2 3.0.0.
ℹ Please use tidy evaluation idioms with `aes()`.
ℹ See also `vignette("ggplot2-in-packages")` for more information.
Warning: Removed 8 rows containing missing values or values outside the scale range
(`geom_ribbon()`).
Warning: Removed 11 rows containing missing values or values outside the scale range
(`geom_ribbon()`).
Warning: Removed 8 rows containing missing values or values outside the scale range
(`geom_ribbon()`).
scenario_plots[["utilisation_doctor"]]
scenario_plots[["mean_queue_length_doctor"]]
Warning: Removed 11 rows containing missing values or values outside the scale range
(`geom_ribbon()`).
scenario_plots[["mean_time_in_system"]]
scenario_plots[["mean_patients_in_system"]]
These plots look a little funny simple due to the wide confidence intervals - with very short run times and few replications in our illustrative example, the confidence intervals are very wide!
Create at least one table and figure from your simulation results.
If you don’t have any to hand, feel free to download ours:
Make sure to write and save code that generates your outputs, and that you save the outputs to files (e.g., csv for tables, png for figures).
For example, create a Jupyter notebook for the analysis, and then push that to your GitHub repository.
For example, create an Rmarkdown file for the analysis, and then push that to your GitHub repository.
References
Heather, Amy, Thomas Monks, Alison Harper, Navonil Mustafee, and Andrew Mayne. 2025. “On the Reproducibility of Discrete-Event Simulation Studies in Health Research: An Empirical Study Using Open Models.”Journal of Simulation 0 (0): 1–25. https://doi.org/10.1080/17477778.2025.2552177.