Raincloud Plot

Security Incident Response Raincloud

Distribution of incident response times across security team tiers.

Output
Security Incident Response Raincloud
Python
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as stats

np.random.seed(456)

# Response time in minutes
tier1 = np.random.lognormal(3.5, 0.5, 90)
tier2 = np.random.lognormal(4.2, 0.6, 85)
tier3 = np.random.lognormal(5.0, 0.7, 70)

tier1 = np.clip(tier1, 5, 120)
tier2 = np.clip(tier2, 15, 300)
tier3 = np.clip(tier3, 30, 600)

F_stat, p_value = stats.f_oneway(tier1, tier2, tier3)

BG_COLOR = "#0a0a0f"
COLOR_SCALE = ["#6CF527", "#F5B027", "#F5276C"]

fig, ax = plt.subplots(figsize=(13, 10), facecolor=BG_COLOR)
ax.set_facecolor(BG_COLOR)

y_data = [tier1, tier2, tier3]
positions = [0, 1, 2]
labels = ["Tier 1\n(L1 Analyst)", "Tier 2\n(Sr. Analyst)", "Tier 3\n(Incident Lead)"]

for h in [60, 120, 240, 360]:
    ax.axhline(h, color='#333333', ls=(0, (5, 5)), alpha=0.5, zorder=0)
    ax.text(2.55, h, f"{h//60}h" if h >= 60 else f"{h}m", color='#555555', fontsize=9, va='center')

violins = ax.violinplot(y_data, positions=positions, widths=0.5, 
                         bw_method="silverman", showmeans=False, 
                         showmedians=False, showextrema=False)
for pc in violins["bodies"]:
    pc.set_facecolor("none")
    pc.set_edgecolor("#27D3F5")
    pc.set_linewidth(2)

bp = ax.boxplot(y_data, positions=positions, showfliers=False, showcaps=False,
                medianprops=dict(linewidth=3, color='white'),
                whiskerprops=dict(linewidth=2, color='#555555'),
                boxprops=dict(linewidth=2, color='#555555'))

for i, (y, color) in enumerate(zip(y_data, COLOR_SCALE)):
    x_jitter = np.array([i] * len(y)) + stats.t(df=6, scale=0.045).rvs(len(y))
    ax.scatter(x_jitter, y, s=55, color=color, alpha=0.55, zorder=2)

means = [y.mean() for y in y_data]
for i, (mean, color) in enumerate(zip(means, COLOR_SCALE)):
    ax.scatter(i, mean, s=200, color='#C82909', zorder=5, edgecolors='white', linewidths=2)
    ax.plot([i, i + 0.32], [mean, mean], ls="dashdot", color="white", zorder=3, lw=1.5)
    ax.text(i + 0.34, mean, f"μ={mean:.0f}min", fontsize=11, va="center", color='white',
            bbox=dict(facecolor=BG_COLOR, edgecolor=color, boxstyle="round,pad=0.2", lw=2))

# SLA line
ax.axhline(y=120, color='#ef4444', ls='--', alpha=0.8, lw=2)
ax.text(-0.4, 120, "SLA: 2h", color='#ef4444', fontsize=10, va='center', fontweight='bold')

ax.spines["right"].set_visible(False)
ax.spines["top"].set_visible(False)
ax.spines["left"].set_color('#444444')
ax.spines["bottom"].set_color('#444444')
ax.tick_params(colors='#888888', length=0)

ax.set_xticks(positions)
ax.set_xticklabels(labels, size=11, color='white')
ax.set_ylabel("Response Time (minutes)", size=14, color='white', fontweight='bold')

ax.set_title("Security Incident Response Time Analysis", fontsize=14, color="white", fontweight="bold", pad=20)
ax.text(0.5, -0.12, "F-Welch={F_stat:.1f}, p<0.001, ω²=0.68, SLA Compliance: T1=92%, T2=78%, T3=65%", transform=ax.transAxes, fontsize=10, color="#6b7280", ha="center")

ax.set_ylim(-20, 650)
plt.tight_layout()
plt.show()
Library

Matplotlib

Category

Statistical

Did this help you?

Support PyLucid to keep it free & growing

Support