SimPy with matplotlib: Visualising Simulation Results

Numbers tell the story. Visualisations make people believe it.

Essential Plots

Every simulation needs: 1. Queue length over time 2. Wait time distribution 3. Utilisation chart 4. Throughput curve

Setup

import simpy
import random
import matplotlib.pyplot as plt
import numpy as np

plt.style.use('seaborn-v0_8-whitegrid')
plt.rcParams['figure.figsize'] = (10, 6)
plt.rcParams['font.size'] = 12

Queue Length Over Time

def plot_queue_length(time_series):
    """Step plot of queue length over simulation time."""
    times = [r['time'] for r in time_series]
    queues = [r['queue'] for r in time_series]

    fig, ax = plt.subplots()
    ax.step(times, queues, where='post', linewidth=1, color='steelblue')
    ax.fill_between(times, queues, step='post', alpha=0.3)

    ax.set_xlabel('Time')
    ax.set_ylabel('Queue Length')
    ax.set_title('Queue Length Over Time')

    plt.tight_layout()
    plt.savefig('queue_length.png', dpi=150)
    plt.show()

Wait Time Distribution

def plot_wait_histogram(wait_times):
    """Histogram with key statistics marked."""
    fig, ax = plt.subplots()

    ax.hist(wait_times, bins=30, edgecolor='white', alpha=0.7, color='steelblue')

    # Mark statistics
    mean_wait = np.mean(wait_times)
    median_wait = np.median(wait_times)
    p95 = np.percentile(wait_times, 95)

    ax.axvline(mean_wait, color='red', linestyle='--', linewidth=2,
               label=f'Mean: {mean_wait:.1f}')
    ax.axvline(median_wait, color='green', linestyle='--', linewidth=2,
               label=f'Median: {median_wait:.1f}')
    ax.axvline(p95, color='orange', linestyle='--', linewidth=2,
               label=f'95th pct: {p95:.1f}')

    ax.set_xlabel('Wait Time')
    ax.set_ylabel('Frequency')
    ax.set_title('Distribution of Wait Times')
    ax.legend()

    plt.tight_layout()
    plt.savefig('wait_distribution.png', dpi=150)
    plt.show()

CDF Plot

def plot_cdf(data, label='Wait Time'):
    """Cumulative distribution function."""
    sorted_data = np.sort(data)
    cdf = np.arange(1, len(sorted_data) + 1) / len(sorted_data)

    fig, ax = plt.subplots()
    ax.plot(sorted_data, cdf, linewidth=2, color='steelblue')

    # Mark percentiles
    for p in [50, 90, 95, 99]:
        val = np.percentile(data, p)
        ax.axhline(p/100, color='gray', linestyle=':', alpha=0.5)
        ax.axvline(val, color='gray', linestyle=':', alpha=0.5)
        ax.annotate(f'{p}%: {val:.1f}', xy=(val, p/100),
                   xytext=(val + 0.5, p/100 - 0.05), fontsize=10)

    ax.set_xlabel(label)
    ax.set_ylabel('Cumulative Probability')
    ax.set_title(f'CDF of {label}')
    ax.set_ylim(0, 1.05)

    plt.tight_layout()
    plt.savefig('cdf.png', dpi=150)
    plt.show()

Utilisation Over Time

def plot_utilisation(time_series, capacity):
    """Resource utilisation over time."""
    times = [r['time'] for r in time_series]
    busy = [r['busy'] for r in time_series]
    utilisation = [b / capacity for b in busy]

    fig, ax = plt.subplots()
    ax.plot(times, utilisation, linewidth=1, color='steelblue')
    ax.fill_between(times, utilisation, alpha=0.3)

    # Average line
    avg_util = np.mean(utilisation)
    ax.axhline(avg_util, color='red', linestyle='--',
               label=f'Average: {avg_util:.1%}')

    ax.set_xlabel('Time')
    ax.set_ylabel('Utilisation')
    ax.set_ylim(0, 1.1)
    ax.set_title('Resource Utilisation Over Time')
    ax.legend()

    plt.tight_layout()
    plt.savefig('utilisation.png', dpi=150)
    plt.show()

Scenario Comparison Box Plot

def plot_scenario_comparison(results_dict):
    """Box plot comparing scenarios."""
    fig, ax = plt.subplots()

    labels = list(results_dict.keys())
    data = list(results_dict.values())

    bp = ax.boxplot(data, labels=labels, patch_artist=True)

    # Color boxes
    for patch in bp['boxes']:
        patch.set_facecolor('steelblue')
        patch.set_alpha(0.7)

    ax.set_ylabel('Wait Time')
    ax.set_title('Wait Time Comparison Across Scenarios')

    plt.tight_layout()
    plt.savefig('comparison.png', dpi=150)
    plt.show()

Multi-Panel Dashboard

def simulation_dashboard(entity_data, time_series, capacity):
    """Create a multi-panel dashboard."""
    fig, axes = plt.subplots(2, 2, figsize=(14, 10))

    # Panel 1: Queue length
    ax1 = axes[0, 0]
    times = [r['time'] for r in time_series]
    queues = [r['queue'] for r in time_series]
    ax1.step(times, queues, where='post', color='steelblue')
    ax1.fill_between(times, queues, step='post', alpha=0.3)
    ax1.set_xlabel('Time')
    ax1.set_ylabel('Queue Length')
    ax1.set_title('Queue Length Over Time')

    # Panel 2: Wait distribution
    ax2 = axes[0, 1]
    waits = [r['wait'] for r in entity_data]
    ax2.hist(waits, bins=30, edgecolor='white', color='steelblue', alpha=0.7)
    ax2.axvline(np.mean(waits), color='red', linestyle='--',
                label=f'Mean: {np.mean(waits):.1f}')
    ax2.set_xlabel('Wait Time')
    ax2.set_ylabel('Frequency')
    ax2.set_title('Wait Time Distribution')
    ax2.legend()

    # Panel 3: Utilisation
    ax3 = axes[1, 0]
    util = [r['busy'] / capacity for r in time_series]
    ax3.plot(times, util, color='steelblue')
    ax3.fill_between(times, util, alpha=0.3)
    ax3.axhline(np.mean(util), color='red', linestyle='--')
    ax3.set_xlabel('Time')
    ax3.set_ylabel('Utilisation')
    ax3.set_ylim(0, 1.1)
    ax3.set_title('Resource Utilisation')

    # Panel 4: Throughput
    ax4 = axes[1, 1]
    departures = sorted([r['departure'] for r in entity_data])
    cumulative = range(1, len(departures) + 1)
    ax4.plot(departures, cumulative, color='steelblue')
    ax4.set_xlabel('Time')
    ax4.set_ylabel('Cumulative Departures')
    ax4.set_title('Throughput')

    plt.tight_layout()
    plt.savefig('dashboard.png', dpi=150)
    plt.show()

Confidence Interval Plot

def plot_confidence_intervals(scenarios_results):
    """Bar chart with confidence intervals."""
    from scipy import stats

    fig, ax = plt.subplots()

    names = list(scenarios_results.keys())
    means = []
    errors = []

    for name, data in scenarios_results.items():
        mean = np.mean(data)
        se = stats.sem(data)
        ci = 1.96 * se
        means.append(mean)
        errors.append(ci)

    x = range(len(names))
    ax.bar(x, means, yerr=errors, capsize=10, color='steelblue', alpha=0.7)
    ax.set_xticks(x)
    ax.set_xticklabels(names)
    ax.set_ylabel('Mean Wait Time')
    ax.set_title('Scenario Comparison with 95% Confidence Intervals')

    plt.tight_layout()
    plt.savefig('confidence.png', dpi=150)
    plt.show()

Heatmap

def plot_heatmap(df, x_col, y_col, value_col):
    """Heatmap of values by two dimensions."""
    pivot = df.pivot_table(values=value_col, index=y_col, columns=x_col, aggfunc='mean')

    fig, ax = plt.subplots(figsize=(12, 8))
    im = ax.imshow(pivot.values, aspect='auto', cmap='YlOrRd')

    ax.set_xticks(range(len(pivot.columns)))
    ax.set_xticklabels(pivot.columns)
    ax.set_yticks(range(len(pivot.index)))
    ax.set_yticklabels(pivot.index)

    plt.colorbar(im, label=value_col)
    ax.set_xlabel(x_col)
    ax.set_ylabel(y_col)
    ax.set_title(f'{value_col} by {x_col} and {y_col}')

    plt.tight_layout()
    plt.savefig('heatmap.png', dpi=150)
    plt.show()

Animation (Basic)

from matplotlib.animation import FuncAnimation

def animate_queue(time_series, interval=100):
    """Animate queue length over time."""
    fig, ax = plt.subplots()
    ax.set_xlim(0, time_series[-1]['time'])
    ax.set_ylim(0, max(r['queue'] for r in time_series) + 1)
    ax.set_xlabel('Time')
    ax.set_ylabel('Queue Length')

    line, = ax.plot([], [], lw=2)

    def init():
        line.set_data([], [])
        return line,

    def update(frame):
        data = time_series[:frame+1]
        times = [r['time'] for r in data]
        queues = [r['queue'] for r in data]
        line.set_data(times, queues)
        return line,

    ani = FuncAnimation(fig, update, frames=len(time_series),
                       init_func=init, blit=True, interval=interval)

    ani.save('queue_animation.gif', writer='pillow')
    plt.show()

Complete Visualisation Suite

class SimulationVisualiser:
    def __init__(self, entity_data, time_series, config):
        self.entity_data = entity_data
        self.time_series = time_series
        self.config = config

    def all_plots(self, save_dir='.'):
        """Generate all standard plots."""
        import os

        # Ensure directory exists
        os.makedirs(save_dir, exist_ok=True)

        # Queue length
        self._plot_queue(f'{save_dir}/queue.png')

        # Wait distribution
        self._plot_wait_dist(f'{save_dir}/wait_dist.png')

        # CDF
        self._plot_cdf(f'{save_dir}/wait_cdf.png')

        # Utilisation
        self._plot_util(f'{save_dir}/utilisation.png')

        # Dashboard
        self._plot_dashboard(f'{save_dir}/dashboard.png')

        print(f"All plots saved to {save_dir}/")

    def _plot_queue(self, filename):
        # Implementation as above
        pass

    def _plot_wait_dist(self, filename):
        # Implementation as above
        pass

    # ... etc

Summary

Visualisation essentials: - Queue length over time (step plot) - Wait time distribution (histogram + CDF) - Utilisation (line + fill) - Scenario comparison (box plots) - Confidence intervals (error bars)

Show the data. Make it clear. Win the argument.

Next Steps


Discover the Power of Simulation

Want to become a go-to expert in simulation with Python? The Complete Simulation Bootcamp will show you how simulation can transform your career and your projects.

Explore the Bootcamp