Framing Effects in Risk Communication#
Part 0 introduced framing as a cognitive bias at a surface level: “97% of phishing emails blocked” versus “3% got through”, as same data, different feeling.
This notebook goes deeper. The way security data is presented determines which projects get funded, which risks get ignored, and which teams get blamed. Every board deck, risk register, and vulnerability report is a framing exercise, whether the author knows it or not.
We cover five mechanisms through which framing manipulates security decisions:
Loss vs gain framing — prospect theory applied to security investment decisions
Denominator neglect — how vulnerability counts mislead without rates and context
Risk matrix distortion — identical scores hiding radically different loss distributions
Anchoring in budget presentations — how the reference point shifts the outcome
Constructive risk communication — choosing the frame deliberately
Setup#
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from decision_security.synth import make_rng, sample
from decision_security.montecarlo import simulate_aggregate_losses, make_lognormal_severity, var_es
rng = make_rng(42)
plt.rcParams.update({
"font.family": "serif",
"font.size": 10,
"axes.labelsize": 11,
"axes.titlesize": 12,
"xtick.labelsize": 9,
"ytick.labelsize": 9,
"legend.fontsize": 9,
"figure.dpi": 150,
"axes.spines.top": False,
"axes.spines.right": False,
})
PRIMARY = "#1A1A1A"
ACCENT = "#E74C3C"
DARK_BG = "#34495E"
LIGHT_GRAY = "#95A5A6"
1. Loss Frame vs Gain Frame#
Kahneman and Tversky’s prospect theory predicts that people are risk-averse in the domain of gains and risk-seeking in the domain of losses (for moderate-to-high probabilities; for low-probability losses the fourfold pattern predicts risk aversion (Tversky and Kahneman, 1992), so the direction for a rare breach is not settled). The security investment decision is the same either way:
Gain frame: “Deploying EDR will prevent
$2.4Min expected losses.”Loss frame: “Without EDR, we face
$2.4Min expected losses.”
Under the gain frame, the investment feels like locking in a sure gain and decision-makers approve it when they are risk-averse for gains. Under the loss frame, the situation feels like an existing gamble, decision-makers become risk-seeking and are more willing to gamble on avoiding the loss without spending, so fewer approve.
The canonical demonstration is Tversky and Kahneman’s (1981) “Asian disease” problem: framing identical outcomes as lives saved versus lives lost flipped the majority choice — about 72% chose the sure option under the gain frame, but only about 22% under the loss frame. The 60% and 45% approval rates used below are illustrative stand-ins for that effect, not measured security data; they assume a direction rather than test it.
n_respondents = 200
# Stylized, not measured: stand-ins for the Asian-disease effect at a plausible magnitude.
gain_approval_rate = 0.60 # risk-averse: lock in the sure gain
gain_responses = sample("binomial", n_respondents, rng=rng, n=1, p=gain_approval_rate)
loss_approval_rate = 0.45 # risk-seeking: gamble on avoiding the loss
loss_responses = sample("binomial", n_respondents, rng=rng, n=1, p=loss_approval_rate)
gain_obs = gain_responses.mean()
loss_obs = loss_responses.mean()
gain_se = np.sqrt(gain_obs * (1 - gain_obs) / n_respondents)
loss_se = np.sqrt(loss_obs * (1 - loss_obs) / n_respondents)
print(f"Gain frame approval: {gain_obs:.1%} (SE: {gain_se:.1%})")
print(f"Loss frame approval: {loss_obs:.1%} (SE: {loss_se:.1%})")
print(f"Difference: {gain_obs - loss_obs:.1%}")
Gain frame approval: 61.5% (SE: 3.4%)
Loss frame approval: 46.0% (SE: 3.5%)
Difference: 15.5%
fig, ax = plt.subplots(figsize=(5, 4))
labels = ["Gain frame\n\"EDR prevents \\$2.4M\nin losses\"", "Loss frame\n\"Without EDR, we\nface \\$2.4M in losses\""]
rates = [gain_obs, loss_obs]
errors = [1.96 * gain_se, 1.96 * loss_se]
colors = [DARK_BG, ACCENT]
bars = ax.bar(labels, rates, yerr=errors, capsize=5, color=colors, ecolor=PRIMARY,
edgecolor="white", linewidth=0.8, width=0.5)
# Place labels above the error bar, not on it
for bar, rate, err in zip(bars, rates, errors):
ax.text(bar.get_x() + bar.get_width() / 2, rate + err + 0.02,
f"{rate:.0%}", ha="center", va="bottom", fontsize=11, fontweight="bold", color=PRIMARY)
ax.set_ylabel("Approval rate")
ax.set_title("Same investment, different frame")
ax.set_ylim(0, 0.95)
ax.axhline(0.5, color=LIGHT_GRAY, linestyle="--", linewidth=0.7, label="50% threshold")
ax.legend(loc="upper right")
plt.tight_layout()
plt.show()
The expected value of the investment is identical in both framings. Under these stylized rates, a CISO who frames the investment as losses prevented rather than losses faced would get more approvals — the direction prospect theory suggests, not a measured effect.
2. Denominator Neglect in Vulnerability Reporting#
“We have 85,000 critical vulnerabilities” is a statement designed to produce panic. It works because people evaluate the numerator (85,000 — big number!) without considering the denominator (out of how many assets? what fraction is actually reachable, unmitigated, and exploitable?).
This is denominator neglect, and vulnerability management is its natural habitat. We generate a vulnerability landscape and show how four presentations of the same data — raw count, rate per asset, risk-adjusted count, and peer comparison — tell four different stories.
# Synthetic vulnerability landscape
total_assets = 50_000
total_vuln_instances = 3_300_000 # scanner findings
critical_vulns = 85_000 # CVSS >= 9.0: the headline count
critical_network_exposed = 22_000 # critical + reachable from network
critical_exposed_unmitigated = 10_000 # ... + no compensating control
# Illustratively, assume ~30% of these carry high exploitability (EPSS > 0.1).
risk_adjusted = round(0.30 * critical_exposed_unmitigated)
# Illustrative peer median, a stand-in for a peer-cohort benchmark (e.g. a Cyentia-style report).
# Counts are vulnerability instances, not affected assets, so rates are per asset, not % of assets.
peer_per_100 = 0.08 # risk-adjusted criticals per 100 assets
our_per_100 = risk_adjusted / total_assets * 100
print("Views of the same vulnerability data:\n")
print(f" Headline critical count: {critical_vulns:,} (CVSS >= 9.0)")
print(f" Actionable subset: {critical_exposed_unmitigated:,} (critical + exposed + unmitigated)")
print(f" Per asset: {critical_exposed_unmitigated / total_assets:.2f} critical exposed unmitigated vulns per asset")
print(f" Risk-adjusted count: {risk_adjusted:,} (exploitable + exposed)")
print(f" Per 100 assets: {our_per_100:.2f} risk-adjusted criticals vs {peer_per_100:.2f} peer median "
f"({our_per_100 / peer_per_100:.1f}x)")
Views of the same vulnerability data:
Headline critical count: 85,000 (CVSS >= 9.0)
Actionable subset: 10,000 (critical + exposed + unmitigated)
Per asset: 0.20 critical exposed unmitigated vulns per asset
Risk-adjusted count: 3,000 (exploitable + exposed)
Per 100 assets: 6.00 risk-adjusted criticals vs 0.08 peer median (75.0x)
million_fmt = lambda x, _: f"{x/1e6:.1f}M" if x >= 1e6 else f"{x/1e3:.0f}K"
thousand_fmt = lambda x, _: f"{x/1e3:.0f}K"
def barh_panel(ax, categories, values, colors, title, xlabel,
formatter=None, value_labels=None, label_offset=0.0):
bars = ax.barh(categories, values, color=colors, edgecolor="white", linewidth=0.5)
ax.set_xlabel(xlabel)
ax.set_title(title, fontweight="bold")
if formatter is not None:
ax.xaxis.set_major_formatter(plt.FuncFormatter(formatter))
if value_labels is not None:
for bar, lab in zip(bars, value_labels):
ax.text(bar.get_width() + label_offset,
bar.get_y() + bar.get_height() / 2, lab, va="center", fontsize=9, color=PRIMARY)
return bars
fig, axes = plt.subplots(1, 4, figsize=(14, 4))
# Panel 1: Raw count (Info is the remainder, so bars sum to total_vuln_instances)
low, medium, high = 900_000, 450_000, 140_000
info = total_vuln_instances - (low + medium + high + critical_vulns)
barh_panel(
axes[0],
["Info", "Low", "Medium", "High", "Critical"],
[info, low, medium, high, critical_vulns],
[LIGHT_GRAY, LIGHT_GRAY, LIGHT_GRAY, DARK_BG, ACCENT],
"Raw counts", "Vulnerability instances", formatter=million_fmt,
)
# Panel 2: Per asset
barh_panel(
axes[1],
["Any vuln", "Critical", "Critical +\nnetwork", "Critical +\nnetwork +\nunmitigated"],
[total_vuln_instances / total_assets, critical_vulns / total_assets,
critical_network_exposed / total_assets, critical_exposed_unmitigated / total_assets],
[LIGHT_GRAY, LIGHT_GRAY, DARK_BG, ACCENT],
"Per asset", "Vulns per asset",
)
# Panel 3: Risk-adjusted
barh_panel(
axes[2],
["Critical\n(all)", "Critical +\nexposed", "Critical +\nexposed +\nunmitigated", "Risk-\nadjusted"],
[critical_vulns, critical_network_exposed, critical_exposed_unmitigated, risk_adjusted],
[LIGHT_GRAY, LIGHT_GRAY, DARK_BG, ACCENT],
"Risk-adjusted", "Count", formatter=thousand_fmt,
)
# Panel 4: Industry benchmark
barh_panel(
axes[3],
["Peer median", "Our org"],
[peer_per_100, our_per_100],
[DARK_BG, ACCENT],
"Industry context", "Risk-adjusted criticals\nper 100 assets",
value_labels=[f"{peer_per_100:.2f}", f"{our_per_100:.2f}"],
label_offset=0.1,
)
axes[3].set_xlim(0, our_per_100 * 1.25)
fig.suptitle("Same data, four stories", fontsize=13, fontweight="bold", y=1.02)
plt.tight_layout()
plt.show()
The raw count screams “85,000 critical vulnerabilities.” Narrow to what is exposed and unmitigated and it is 10,000. The rate says “0.2 per asset.” The risk-adjusted count says “3,000 that actually matter.” The industry comparison says “75x the peer median.” Each is factually correct. The person who chooses the denominator chooses the narrative.
3. The Risk Register Framing Problem#
The classic 5x5 risk matrix assigns a score by multiplying likelihood and impact ratings. This imposes a false equivalence: Risk A (likelihood=2, impact=4, score=8) and Risk B (likelihood=4, impact=2, score=8) receive the same score. They get the same priority.
But their loss distributions are completely different. Risk A is a low-frequency, high-severity event — it rarely happens, but when it does, the damage is large and unpredictable. Risk B is a high-frequency, low-severity event — it happens often but the losses are tightly concentrated and predictable.
We simulate both to see what the “same score” hides.
n_sim = 10_000
# Risk A: low-frequency, high-severity (likelihood=2, impact=4).
# Per-event mean exp(13.5 + 1.8**2 / 2) ~= $3.69M, so ~$1.84M expected annual loss at lam=0.5.
sev_a = make_lognormal_severity(meanlog=13.5, sdlog=1.8)
losses_a = simulate_aggregate_losses(n_sim, lam=0.5, severity_sampler=sev_a, rng=make_rng(42))
# Risk B: high-frequency, low-severity (likelihood=4, impact=2).
# Per-event mean exp(10.1 + 0.5**2 / 2) ~= $28K, so ~$140K expected annual loss at lam=5.
sev_b = make_lognormal_severity(meanlog=10.1, sdlog=0.5)
losses_b = simulate_aggregate_losses(n_sim, lam=5.0, severity_sampler=sev_b, rng=make_rng(43))
var_a, es_a = var_es(losses_a, alpha=0.95)
var_b, es_b = var_es(losses_b, alpha=0.95)
print("Both risks score 8 on a 5x5 matrix.\n")
print(f"Risk A (low-freq, high-sev): Mean = ${np.mean(losses_a):,.0f} | VaR95 = ${var_a:,.0f} | ES95 = ${es_a:,.0f}")
print(f"Risk B (high-freq, low-sev): Mean = ${np.mean(losses_b):,.0f} | VaR95 = ${var_b:,.0f} | ES95 = ${es_b:,.0f}")
print(f"\nTail ratio (ES_A / ES_B): {es_a / es_b:.1f}x")
print(f"VaR95 ratio (VaR_A / VaR_B): {var_a / var_b:.1f}x")
Both risks score 8 on a 5x5 matrix.
Risk A (low-freq, high-sev): Mean = $1,815,601 | VaR95 = $7,520,839 | ES95 = $25,958,335
Risk B (high-freq, low-sev): Mean = $137,554 | VaR95 = $263,179 | ES95 = $303,964
Tail ratio (ES_A / ES_B): 85.4x
VaR95 ratio (VaR_A / VaR_B): 28.6x
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
risks = [
(losses_a, var_a, es_a, "Risk A: Low-frequency, high-severity", "Likelihood=2, Impact=4, Score=8", 1, "upper right"),
(losses_b, var_b, es_b, "Risk B: High-frequency, low-severity", "Likelihood=4, Impact=2, Score=8", 2, "upper left"),
]
for ax, (losses, var, es, name, score, dec, loc) in zip(axes, risks):
nonzero = losses[losses > 0]
zero_share = 1 - nonzero.size / losses.size
# Clip at P99.5 so the bulk and the tail are both visible
x_max = np.quantile(nonzero, 0.995)
ax.hist(nonzero, bins=80, range=(0, x_max), color=DARK_BG, edgecolor="white", linewidth=0.3, alpha=0.85)
ax.axvline(var, color=ACCENT, linestyle="--", linewidth=1.5, label=f"VaR$_{{95}}$ = \\${var/1e6:.{dec}f}M")
ax.axvline(es, color=ACCENT, linestyle="-", linewidth=2, label=f"ES$_{{95}}$ = \\${es/1e6:.{dec}f}M")
ax.set_xlabel("Annual aggregate loss (\\$, axis clipped at P99.5)")
ax.set_ylabel("Simulated years")
ax.set_title(f"{name}\n({score})\nNon-zero years only; {zero_share:.0%} of years have no loss", fontsize=10)
ax.xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"\\${x/1e6:.0f}M" if x >= 1e6 else f"\\${x/1e3:.0f}K"))
ax.legend(loc=loc, fontsize=8)
fig.suptitle("\"Same risk score\" hides different tail risk", fontsize=13, fontweight="bold", y=1.04)
plt.tight_layout()
plt.show()
The risk matrix says these two risks are equivalent. The loss distributions say otherwise. Risk A has a fat right tail — the 95th percentile loss is multiples of Risk B’s.
4. Anchoring in Board Presentations#
A CISO wants an $800K security budget. How it lands depends heavily on the
anchor — the first number the board hears.
Strategy 1: Open with the average breach cost (
$4.99M, IBM 2026), then present the$800Kask. The budget looks like a bargain — 16% of the cost of a single average breach, not of the expected annual loss.Strategy 2: Open with last year’s budget (
$500K), then present the$800Kask. The budget looks like a 60% increase.
The request is identical. The anchor shapes whether the board perceives it as a bargain or an overreach.
Note: the approval model below is stylized. It maps the anchor-to-ask ratio through a logistic curve, so it assumes the anchoring effect it then displays. Read it as an illustration of the mechanism’s direction and rough magnitude, not as a measurement.
budget_ask = 800_000
anchor_breach = 4_990_000 # IBM Cost of a Data Breach 2026: average cost per breach, not expected annual loss
anchor_last_year = 500_000 # last year's budget
# Stylized: the logistic curve builds in the anchoring effect by construction.
n_board_sims = 1000
def simulate_approval(anchor, ask, n_sims, rng):
"""Simulate approval probability based on anchor-to-ask ratio."""
ratio = anchor / ask
# Log-ratio compresses the scale so extreme anchors don't saturate
log_ratio = np.log(ratio)
base_prob = 1.0 / (1.0 + np.exp(-1.8 * log_ratio))
# Clamp to [0.15, 0.85] — stylized: no framing guarantees or precludes approval
base_prob = np.clip(base_prob, 0.15, 0.85)
votes = sample("binomial", n_sims, rng=rng, n=1, p=float(base_prob))
return votes.mean(), float(base_prob)
rng_anchor = make_rng(99)
approval_1, prob_1 = simulate_approval(anchor_breach, budget_ask, n_board_sims, rng_anchor)
approval_2, prob_2 = simulate_approval(anchor_last_year, budget_ask, n_board_sims, make_rng(100))
print(f"Same budget request: ${budget_ask/1e3:.0f}K\n")
print(f"Strategy 1 — Anchor on breach cost (${anchor_breach/1e6:.2f}M):")
print(f" Ratio: ask is {budget_ask/anchor_breach:.0%} of anchor")
print(f" Approval rate: {approval_1:.0%}\n")
print(f"Strategy 2 — Anchor on last year's budget (${anchor_last_year/1e3:.0f}K):")
print(f" Ratio: ask is {budget_ask/anchor_last_year:.0%} of anchor")
print(f" Approval rate: {approval_2:.0%}")
Same budget request: $800K
Strategy 1 — Anchor on breach cost ($4.99M):
Ratio: ask is 16% of anchor
Approval rate: 83%
Strategy 2 — Anchor on last year's budget ($500K):
Ratio: ask is 160% of anchor
Approval rate: 33%
fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))
# Left: the two strategies
ax = axes[0]
strategies = ["Strategy 1:\nAnchor on\nbreach cost", "Strategy 2:\nAnchor on\nlast year's budget"]
approvals = [approval_1, approval_2]
colors_strat = [DARK_BG, ACCENT]
bars = ax.bar(strategies, approvals, color=colors_strat, edgecolor="white",
linewidth=0.8, width=0.45)
for bar, rate in zip(bars, approvals):
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.02,
f"{rate:.0%}", ha="center", va="bottom", fontsize=11, fontweight="bold", color=PRIMARY)
ax.set_ylabel("Board approval rate")
ax.set_title(f"Same \\${budget_ask/1e3:.0f}K ask, different anchor")
ax.set_ylim(0, 1.0)
ax.axhline(0.5, color=LIGHT_GRAY, linestyle="--", linewidth=0.7)
# Right: approval across a range of anchors
ax = axes[1]
anchor_range = np.linspace(200_000, 6_000_000, 200)
ratio_range = anchor_range / budget_ask
log_ratio_range = np.log(ratio_range)
prob_range = 1.0 / (1.0 + np.exp(-1.8 * log_ratio_range))
prob_range = np.clip(prob_range, 0.15, 0.85)
ax.plot(anchor_range / 1e6, prob_range, color=DARK_BG, linewidth=2)
ax.axvline(anchor_last_year / 1e6, color=ACCENT, linestyle="--", linewidth=1.2,
label=f"Last year's budget (\\${anchor_last_year/1e6:.1f}M)")
ax.axvline(anchor_breach / 1e6, color=DARK_BG, linestyle="--", linewidth=1.2,
label=f"Breach cost (\\${anchor_breach/1e6:.2f}M)")
ax.scatter([anchor_last_year / 1e6], [prob_2], color=ACCENT, s=60, zorder=5)
ax.scatter([anchor_breach / 1e6], [prob_1], color=DARK_BG, s=60, zorder=5)
ax.set_xlabel("Anchor value (\\$M)")
ax.set_ylabel("Approval probability")
ax.set_title("The anchor sets the reference point")
ax.legend(loc="lower right", fontsize=8)
plt.tight_layout()
plt.show()
A CISO who opens with the breach cost is not being manipulative — they are providing a reference point, though a per-breach average is not the expected loss the spend would reduce. A CFO who opens with last year’s budget is also providing a reference point. The question is which anchor produces better decisions, not which is more “neutral” — there is no neutral.
5. Constructive Risk Communication#
If framing is inescapable, the ethical response is to choose frames deliberately, transparently, and in service of better decisions. The same Monte Carlo output can be communicated three ways, each suited to a different audience:
(a) Quantile table — dry but precise, suited for risk analysts
(b) Loss exceedance curve — visual and intuitive for technical audiences
(c) 1-in-N probability statement — “There is a 1-in-10 chance we lose more than USD X” — accessible to executives
sev_comm = make_lognormal_severity(meanlog=12.5, sdlog=1.2)
losses_comm = simulate_aggregate_losses(10_000, lam=2.0, severity_sampler=sev_comm, rng=make_rng(77))
# inverted_cdf = the empirical order statistic var_es uses, so P95 equals VaR95
quantiles = [0.50, 0.75, 0.90, 0.95, 0.99]
quantile_values = np.quantile(losses_comm, quantiles, method="inverted_cdf")
p90_val, p95_val, p99_val = quantile_values[2:]
var_95, es_95 = var_es(losses_comm, alpha=0.95)
print("=" * 50)
print("(a) Annual Loss Quantile Table")
print("=" * 50)
for q, v in zip(quantiles, quantile_values):
print(f" P{int(q*100):>2d}: ${v:>12,.0f}" + (" (= VaR95)" if q == 0.95 else ""))
print(f"\n ES(95%): ${es_95:>12,.0f}\n")
print("(c) 1-in-N Probability Statements")
print("=" * 50)
print(f" There is a 1-in-10 chance we lose more than ${p90_val:,.0f} this year.")
print(f" There is a 1-in-20 chance we lose more than ${p95_val:,.0f} this year.")
print(f" There is a 1-in-100 chance we lose more than ${p99_val:,.0f} this year.")
==================================================
(a) Annual Loss Quantile Table
==================================================
P50: $ 601,429
P75: $ 1,433,155
P90: $ 2,654,176
P95: $ 3,781,639 (= VaR95)
P99: $ 7,366,197
ES(95%): $ 6,242,168
(c) 1-in-N Probability Statements
==================================================
There is a 1-in-10 chance we lose more than $2,654,176 this year.
There is a 1-in-20 chance we lose more than $3,781,639 this year.
There is a 1-in-100 chance we lose more than $7,366,197 this year.
fig, axes = plt.subplots(1, 3, figsize=(15, 4.5))
# (a) Quantile table
ax = axes[0]
q_labels = [f"P{int(q*100)}" for q in quantiles]
colors_q = [DARK_BG if q < 0.95 else ACCENT for q in quantiles]
bars = ax.barh(q_labels, quantile_values / 1e6, color=colors_q,
edgecolor="white", linewidth=0.5)
for bar, val in zip(bars, quantile_values):
ax.text(bar.get_width() + 0.02, bar.get_y() + bar.get_height() / 2,
f"\\${val/1e6:.2f}M", va="center", fontsize=8, color=PRIMARY)
ax.set_xlabel("Annual loss (\\$M)")
ax.set_title("(a) Quantile table\nFor risk analysts", fontsize=10)
# (b) Loss exceedance curve
ax = axes[1]
sorted_losses = np.sort(losses_comm)
exceedance_prob = 1.0 - np.arange(1, len(sorted_losses) + 1) / len(sorted_losses)
ax.plot(sorted_losses / 1e6, exceedance_prob, color=DARK_BG, linewidth=1.5)
ax.axhline(0.10, color=ACCENT, linestyle="--", linewidth=1, alpha=0.7)
ax.axhline(0.05, color=ACCENT, linestyle="--", linewidth=1, alpha=0.7)
ax.text(ax.get_xlim()[1] * 0.7, 0.105, "10% exceedance", fontsize=8, color=ACCENT)
ax.text(ax.get_xlim()[1] * 0.7, 0.055, "5% exceedance", fontsize=8, color=ACCENT)
ax.set_xlabel("Annual loss (\\$M)")
ax.set_ylabel("Probability of exceeding")
ax.set_title("(b) Loss exceedance curve\nFor technical audiences", fontsize=10)
ax.set_ylim(0, 1.0)
# (c) 1-in-N statements
ax = axes[2]
freq_labels = ["1-in-10\n(this year)", "1-in-20\n(this year)", "1-in-100\n(this year)"]
freq_values = [p90_val / 1e6, p95_val / 1e6, p99_val / 1e6]
colors_freq = [DARK_BG, DARK_BG, ACCENT]
bars = ax.bar(freq_labels, freq_values, color=colors_freq, edgecolor="white",
linewidth=0.8, width=0.5)
for bar, val in zip(bars, freq_values):
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.05,
f"\\${val:.2f}M", ha="center", va="bottom", fontsize=10, fontweight="bold", color=PRIMARY)
ax.set_ylabel("Loss (\\$M)")
ax.set_title('(c) "How often, how much?"\nFor executives', fontsize=10)
fig.suptitle("Same simulation, three audiences", fontsize=13, fontweight="bold", y=1.02)
plt.tight_layout()
plt.show()
The quantile table is
precise but opaque to a non-technical board. The exceedance curve is visual but
requires statistical literacy. The 1-in-N probability statement — “a 1-in-10
chance of losing more than $X” — is the most accessible of the three, though
the 1-in-X format can itself inflate perceived likelihood (Pighin et al., 2011).
None of these is more “correct” than the others. The communicator’s job is to
match the frame to the audience.
6. Pitfalls#
Gain/loss framing reverses preferences with no new information. The same expected value produces different approval rates depending on whether the investment is presented as preventing loss or facing loss. This is not a failure of rationality to be fixed — it is a stable feature of how people decide that must be anticipated.
Denominators are chosen strategically. Absolute numbers alarm; context reassures. A headline of “85,000 critical vulnerabilities” triggers a different response than “3,000 are actually exploitable and exposed.” Both are true. Neither is complete. Anyone presenting one without the other has made a framing choice.
Risk matrices impose false precision on different distributions. A single score (likelihood x impact) collapses the entire loss distribution into one number. Two risks with the same score can have radically different tail behavior.
Anchors set the negotiation range before the conversation begins. The first number in a budget presentation is not context — it is the reference point against which everything else is judged. Choosing the anchor shifts the outcome.
There is no neutral frame. Every table, chart, and sentence in a risk report is a framing decision. The choice is not between “framing” and “not framing” — it is between framing deliberately and framing accidentally.
“He uses statistics as a drunken man uses lamp posts — for support rather than for illumination.” — attributed to Andrew Lang