Optimizing Security Investment Allocation

Maximizing ROI with Python

Security budgets are never unlimited. Every CISO faces the same brutal question: where do I put the money? This post walks through a concrete, numerical example of security investment optimization β€” using linear programming, knapsack modeling, and Monte Carlo simulation β€” and visualizes the results in 2D and 3D.


πŸ” The Problem Setup

Imagine you’re a security manager with a budget of $500,000. You have 8 candidate security controls, each with:

  • An implementation cost
  • An expected annual loss reduction (risk reduction in dollars)
  • A probability of successful implementation

Your goal: maximize total expected ROI subject to the budget constraint.


πŸ“ Mathematical Formulation

Let $x_i \in {0, 1}$ be the binary decision variable for control $i$.

$$\text{Maximize} \quad \sum_{i=1}^{n} r_i \cdot p_i \cdot x_i$$

Subject to:

$$\sum_{i=1}^{n} c_i \cdot x_i \leq B$$

$$x_i \in {0, 1}, \quad \forall i$$

Where:

  • $r_i$ = expected annual loss reduction of control $i$
  • $p_i$ = success probability of control $i$
  • $c_i$ = cost of control $i$
  • $B$ = total budget

ROI per control is defined as:

$$\text{ROI}_i = \frac{r_i \cdot p_i - c_i}{c_i} \times 100 \quad (οΌ…)$$


🐍 Full Python Source Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# ============================================================
# Security Investment Allocation Optimization (ROI Maximization)
# ============================================================

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from mpl_toolkits.mplot3d import Axes3D
from scipy.optimize import linprog
from itertools import combinations
import warnings
warnings.filterwarnings('ignore')

# ── 1. Problem Definition ────────────────────────────────────

controls = [
"Endpoint EDR",
"Email Gateway",
"MFA Rollout",
"SIEM/SOC",
"WAF + CDN",
"Zero Trust NAC",
"Security Training",
"Vuln Scanner",
]

# Cost ($000s), Expected Loss Reduction ($000s), Success Probability
cost = np.array([120, 80, 50, 150, 90, 130, 30, 40], dtype=float)
benefit = np.array([300, 200, 180, 350, 220, 280, 90, 100], dtype=float)
prob = np.array([0.85, 0.90, 0.95, 0.75, 0.88, 0.80, 0.99, 0.97])

budget = 500.0 # $000s

n = len(controls)

# ── 2. Expected Value & ROI ──────────────────────────────────

expected_benefit = benefit * prob
roi_pct = (expected_benefit - cost) / cost * 100 # %

print("=" * 65)
print(f"{'Control':<20} {'Cost':>7} {'E[Benefit]':>11} {'ROI (%)':>9}")
print("-" * 65)
for i in range(n):
print(f"{controls[i]:<20} ${cost[i]:>5.0f}k ${expected_benefit[i]:>8.1f}k {roi_pct[i]:>8.1f}%")
print("=" * 65)

# ── 3. Exact Knapsack (Brute Force – feasible for n≀20) ──────

best_value = 0.0
best_combo = []

for r in range(1, n + 1):
for combo in combinations(range(n), r):
total_cost = sum(cost[i] for i in combo)
if total_cost <= budget:
total_val = sum(expected_benefit[i] for i in combo)
if total_val > best_value:
best_value = total_val
best_combo = list(combo)

selected = np.zeros(n, dtype=int)
selected[best_combo] = 1
total_cost_sel = cost[selected == 1].sum()
total_benefit_sel = expected_benefit[selected == 1].sum()
portfolio_roi = (total_benefit_sel - total_cost_sel) / total_cost_sel * 100

print(f"\nβœ… Optimal Portfolio (budget = ${budget:.0f}k)")
print("-" * 45)
for i in best_combo:
print(f" β€’ {controls[i]:<20} cost=${cost[i]:.0f}k ROI={roi_pct[i]:.1f}%")
print(f"\n Total Cost : ${total_cost_sel:.0f}k")
print(f" Total E[Benefit]: ${total_benefit_sel:.1f}k")
print(f" Portfolio ROI : {portfolio_roi:.1f}%")

# ── 4. Monte Carlo Simulation ────────────────────────────────

N_SIM = 50_000
rng = np.random.default_rng(42)

# Simulate benefit as Normal( benefit, benefit*0.15 ) for uncertainty
sim_benefits = rng.normal(
loc = benefit,
scale = benefit * 0.15,
size = (N_SIM, n)
)
sim_benefits = np.clip(sim_benefits, 0, None)

# Each control succeeds with its success probability
successes = rng.random((N_SIM, n)) < prob

# Realized benefit per simulation
realized = sim_benefits * successes

# Portfolio realized benefit (selected controls only)
portfolio_realized = realized[:, selected == 1].sum(axis=1)
portfolio_net_gain = portfolio_realized - total_cost_sel

mc_mean = portfolio_net_gain.mean()
mc_std = portfolio_net_gain.std()
mc_var95 = np.percentile(portfolio_net_gain, 5) # 95% VaR
mc_cvar95 = portfolio_net_gain[portfolio_net_gain <= mc_var95].mean()

print(f"\nπŸ“Š Monte Carlo Results (N={N_SIM:,})")
print(f" Mean Net Gain : ${mc_mean:.1f}k")
print(f" Std Dev : ${mc_std:.1f}k")
print(f" VaR 95% : ${mc_var95:.1f}k")
print(f" CVaR 95% : ${mc_cvar95:.1f}k")

# ── 5. Budget Sensitivity ────────────────────────────────────

budgets = np.arange(50, 601, 10, dtype=float)
opt_vals = []
opt_rois = []

for B in budgets:
bv, bc = 0.0, []
for r in range(1, n + 1):
for combo in combinations(range(n), r):
tc = sum(cost[i] for i in combo)
if tc <= B:
tv = sum(expected_benefit[i] for i in combo)
if tv > bv:
bv, bc = tv, list(combo)
used = sum(cost[i] for i in bc) if bc else 0
opt_vals.append(bv)
roi_val = (bv - used) / used * 100 if used > 0 else 0
opt_rois.append(roi_val)

opt_vals = np.array(opt_vals)
opt_rois = np.array(opt_rois)

# ── 6. Efficient Frontier via Random Portfolios ───────────────

N_PORT = 8000
port_costs, port_benefits, port_rois = [], [], []

for _ in range(N_PORT):
mask = rng.integers(0, 2, size=n).astype(bool)
tc = cost[mask].sum()
if tc <= budget and tc > 0:
tb = expected_benefit[mask].sum()
port_costs.append(tc)
port_benefits.append(tb)
port_rois.append((tb - tc) / tc * 100)

port_costs = np.array(port_costs)
port_benefits = np.array(port_benefits)
port_rois = np.array(port_rois)

# ── 7. Visualization ─────────────────────────────────────────

plt.rcParams.update({
'figure.facecolor': '#0d1117',
'axes.facecolor' : '#161b22',
'axes.edgecolor' : '#30363d',
'axes.labelcolor' : '#e6edf3',
'xtick.color' : '#8b949e',
'ytick.color' : '#8b949e',
'text.color' : '#e6edf3',
'grid.color' : '#21262d',
'grid.linestyle' : '--',
'grid.alpha' : 0.6,
'font.family' : 'monospace',
})

fig = plt.figure(figsize=(20, 22))
fig.suptitle("Security Investment Allocation – ROI Optimization",
fontsize=18, fontweight='bold', color='#58a6ff', y=0.98)

# ── Plot 1: Individual Control ROI ───────────────────────────
ax1 = fig.add_subplot(3, 3, 1)
colors_bar = ['#3fb950' if s else '#f85149' for s in selected]
bars = ax1.barh(controls, roi_pct, color=colors_bar, edgecolor='#30363d', height=0.6)
ax1.axvline(0, color='#8b949e', linewidth=1)
ax1.set_xlabel("ROI (%)")
ax1.set_title("ROI per Control\n(green=selected)", color='#58a6ff')
ax1.grid(axis='x')
for bar, val in zip(bars, roi_pct):
ax1.text(val + 2, bar.get_y() + bar.get_height()/2,
f"{val:.0f}%", va='center', fontsize=8, color='#e6edf3')

# ── Plot 2: Cost vs Expected Benefit bubble ───────────────────
ax2 = fig.add_subplot(3, 3, 2)
sel_mask = selected == 1
sc = ax2.scatter(cost[~sel_mask], expected_benefit[~sel_mask],
s=roi_pct[~sel_mask]*3, c='#8b949e', alpha=0.7,
edgecolors='white', linewidths=0.5, label='Not selected')
ax2.scatter(cost[sel_mask], expected_benefit[sel_mask],
s=roi_pct[sel_mask]*3, c='#3fb950', alpha=0.9,
edgecolors='white', linewidths=0.8, label='Selected')
for i in range(n):
ax2.annotate(controls[i], (cost[i], expected_benefit[i]),
fontsize=6.5, color='#e6edf3',
xytext=(4, 4), textcoords='offset points')
ax2.set_xlabel("Cost ($000s)")
ax2.set_ylabel("E[Benefit] ($000s)")
ax2.set_title("Cost vs Expected Benefit\n(bubble size ∝ ROI)", color='#58a6ff')
ax2.legend(fontsize=7)
ax2.grid(True)

# ── Plot 3: Monte Carlo Distribution ─────────────────────────
ax3 = fig.add_subplot(3, 3, 3)
ax3.hist(portfolio_net_gain, bins=80, color='#58a6ff', alpha=0.75,
edgecolor='none', density=True)
ax3.axvline(mc_mean, color='#3fb950', linewidth=2, label=f'Mean=${mc_mean:.0f}k')
ax3.axvline(mc_var95, color='#f85149', linewidth=2, label=f'VaR95=${mc_var95:.0f}k')
ax3.axvline(mc_cvar95, color='#d29922', linewidth=2, label=f'CVaR95=${mc_cvar95:.0f}k')
ax3.set_xlabel("Net Gain ($000s)")
ax3.set_ylabel("Density")
ax3.set_title("Monte Carlo Net Gain\nDistribution", color='#58a6ff')
ax3.legend(fontsize=7)
ax3.grid(True)

# ── Plot 4: Budget Sensitivity ────────────────────────────────
ax4 = fig.add_subplot(3, 3, 4)
ax4_r = ax4.twinx()
ax4.plot(budgets, opt_vals, color='#58a6ff', linewidth=2, label='E[Benefit]')
ax4_r.plot(budgets, opt_rois, color='#d29922', linewidth=2,
linestyle='--', label='Portfolio ROI%')
ax4.axvline(budget, color='#f85149', linewidth=1.5,
linestyle=':', label=f'Current=${budget:.0f}k')
ax4.set_xlabel("Budget ($000s)")
ax4.set_ylabel("Optimal E[Benefit] ($000s)", color='#58a6ff')
ax4_r.set_ylabel("Portfolio ROI (%)", color='#d29922')
ax4.set_title("Budget Sensitivity", color='#58a6ff')
ax4.legend(loc='upper left', fontsize=7)
ax4_r.legend(loc='lower right', fontsize=7)
ax4.grid(True)

# ── Plot 5: Efficient Frontier ────────────────────────────────
ax5 = fig.add_subplot(3, 3, 5)
sc5 = ax5.scatter(port_costs, port_benefits, c=port_rois,
cmap='RdYlGn', s=8, alpha=0.5)
ax5.scatter(total_cost_sel, total_benefit_sel, s=200, c='#58a6ff',
marker='*', zorder=5, label=f'Optimal (ROI={portfolio_roi:.0f}%)')
plt.colorbar(sc5, ax=ax5, label='ROI (%)')
ax5.set_xlabel("Portfolio Cost ($000s)")
ax5.set_ylabel("Portfolio E[Benefit] ($000s)")
ax5.set_title("Efficient Frontier\n(random portfolios)", color='#58a6ff')
ax5.legend(fontsize=7)
ax5.grid(True)

# ── Plot 6: Waterfall – Benefit Contribution ──────────────────
ax6 = fig.add_subplot(3, 3, 6)
sel_names = [controls[i] for i in best_combo]
sel_eb = [expected_benefit[i] for i in best_combo]
sel_eb_sorted = sorted(zip(sel_eb, sel_names), reverse=True)
eb_sorted, nm_sorted = zip(*sel_eb_sorted)
cumulative = np.cumsum(eb_sorted)
ax6.bar(range(len(nm_sorted)), eb_sorted, color='#3fb950',
edgecolor='#30363d', alpha=0.85)
ax6.plot(range(len(nm_sorted)), cumulative, 'o--',
color='#58a6ff', linewidth=1.5, label='Cumulative')
ax6.set_xticks(range(len(nm_sorted)))
ax6.set_xticklabels(nm_sorted, rotation=35, ha='right', fontsize=7)
ax6.set_ylabel("E[Benefit] ($000s)")
ax6.set_title("Benefit Contribution\n(selected controls)", color='#58a6ff')
ax6.legend(fontsize=7)
ax6.grid(axis='y')

# ── Plot 7: 3D – Cost / Benefit / ROI surface ────────────────
ax7 = fig.add_subplot(3, 3, 7, projection='3d')
ax7.set_facecolor('#161b22')
ax7.scatter(port_costs, port_benefits, port_rois,
c=port_rois, cmap='plasma', s=5, alpha=0.4)
ax7.scatter([total_cost_sel], [total_benefit_sel], [portfolio_roi],
c='cyan', s=150, marker='*', zorder=10, label='Optimal')
ax7.set_xlabel("Cost ($k)", fontsize=7, labelpad=4)
ax7.set_ylabel("E[Benefit] ($k)", fontsize=7, labelpad=4)
ax7.set_zlabel("ROI (%)", fontsize=7, labelpad=4)
ax7.set_title("3D Portfolio Space\nCost–Benefit–ROI", color='#58a6ff')
ax7.legend(fontsize=7)
ax7.tick_params(labelsize=6)

# ── Plot 8: 3D – Budget Γ— Controls Heat ──────────────────────
ax8 = fig.add_subplot(3, 3, 8, projection='3d')
ax8.set_facecolor('#161b22')

# For each budget level record which controls are in optimal set
budgets_sparse = np.arange(100, 601, 50, dtype=float)
control_sel_matrix = np.zeros((len(budgets_sparse), n))

for bi, B in enumerate(budgets_sparse):
bv, bc = 0.0, []
for r in range(1, n + 1):
for combo in combinations(range(n), r):
tc = sum(cost[i] for i in combo)
if tc <= B:
tv = sum(expected_benefit[i] for i in combo)
if tv > bv:
bv, bc = tv, list(combo)
for i in bc:
control_sel_matrix[bi, i] = roi_pct[i]

X3, Y3 = np.meshgrid(np.arange(n), budgets_sparse)
ax8.plot_surface(X3, Y3, control_sel_matrix, cmap='viridis', alpha=0.85)
ax8.set_xticks(np.arange(n))
ax8.set_xticklabels([c[:6] for c in controls], rotation=45, fontsize=5)
ax8.set_ylabel("Budget ($k)", fontsize=7, labelpad=4)
ax8.set_zlabel("ROI if selected (%)", fontsize=7, labelpad=4)
ax8.set_title("3D: Budget Γ— Control\nSelection Landscape", color='#58a6ff')
ax8.tick_params(labelsize=6)

# ── Plot 9: Risk-Adjusted Return Radar ───────────────────────
ax9 = fig.add_subplot(3, 3, 9, polar=True)
metrics = ['E[Benefit]', 'ROI', 'Budget\nEfficiency', 'Risk\nReduction', 'Success\nRate']
n_met = len(metrics)
angles = np.linspace(0, 2 * np.pi, n_met, endpoint=False).tolist()
angles += angles[:1]

# Normalize to [0,1] for radar
vals = [
total_benefit_sel / 700,
portfolio_roi / 200,
(budget - total_cost_sel) / budget,
total_benefit_sel / (total_benefit_sel + total_cost_sel),
prob[selected == 1].mean(),
]
vals += vals[:1]

ax9.plot(angles, vals, 'o-', linewidth=2, color='#58a6ff')
ax9.fill(angles, vals, alpha=0.25, color='#58a6ff')
ax9.set_xticks(angles[:-1])
ax9.set_xticklabels(metrics, fontsize=8)
ax9.set_ylim(0, 1)
ax9.set_title("Portfolio Quality Radar\n(normalized)", color='#58a6ff', pad=15)
ax9.grid(color='#30363d')

plt.tight_layout(rect=[0, 0, 1, 0.97])
plt.savefig("security_roi_optimization.png", dpi=150,
bbox_inches='tight', facecolor='#0d1117')
plt.show()
print("\nβœ… All plots rendered successfully.")

πŸ” Code Walkthrough

Section 1 β€” Data Definition

We define 8 real-world security controls with three attributes each: cost, expected benefit, and success probability. All monetary values are in thousands of dollars ($k). This keeps the matrix math clean and the numbers interpretable.

Section 2 β€” Expected Benefit & ROI Calculation

$$\text{E[Benefit]}_i = r_i \times p_i$$

We weight raw benefit by success probability. The ROI percentage is then:

$$\text{ROI}_i = \frac{E[\text{Benefit}]_i - c_i}{c_i} \times 100$$

This gives a risk-adjusted ROI β€” not just the best-case number. A control that costs $80k but has a 90% chance of saving $200k is better than one that costs $80k with a 60% chance of saving $220k.

Section 3 β€” Exact Knapsack Solver (Brute Force)

For $n \leq 20$, iterating over all $2^n$ subsets is fast enough (256 combinations here). We check every non-empty subset, filter those within budget, and track the maximum expected benefit. This is exact β€” no LP relaxation errors.

For larger $n$ (say, 30+), you’d switch to dynamic programming (pseudo-polynomial $O(nB)$) or branch-and-bound.

Section 4 β€” Monte Carlo Simulation (50,000 runs)

Real-world benefits are never deterministic. We model uncertainty with:

$$\tilde{r}_i \sim \mathcal{N}(r_i,\ (0.15 \cdot r_i)^2)$$

And control success as a Bernoulli draw with probability $p_i$. We compute:

  • VaR 95% β€” the worst-case net gain at the 5th percentile
  • CVaR 95% β€” the expected loss given we’re in the worst 5%

This quantifies downside risk, not just expected upside.

Section 5 β€” Budget Sensitivity Analysis

We sweep the budget from $50k to $600k in $10k steps and solve the knapsack at each level. This reveals inflection points where adding budget unlocks disproportionate ROI jumps.

Section 6 β€” Efficient Frontier (Random Portfolios)

We sample 8,000 random valid portfolios and plot cost vs. benefit, colored by ROI. The efficient frontier emerges as the upper-left envelope β€” the set of portfolios you can’t improve without spending more.


πŸ“Š Graph Explanations

Plot 1 – ROI per Control: Green bars are selected by the optimizer; red are not. Notice that the highest individual ROI doesn’t always make the cut β€” budget interactions matter.

Plot 2 – Cost vs Benefit Bubble: Bubble area is proportional to ROI. The ideal control is in the top-left (cheap, high benefit). Green dots are the optimal selection.

Plot 3 – Monte Carlo Distribution: The distribution of net gain across 50,000 simulated futures. The vertical lines mark the mean (green), VaR (red), and CVaR (gold). A tighter, right-skewed distribution is desirable.

Plot 4 – Budget Sensitivity: As budget increases, expected benefit rises in stair-step fashion β€” each stair is a new control being unlocked. ROI often decreases at higher budgets as cheaper, higher-ROI controls are already included.

Plot 5 – Efficient Frontier: Each dot is a randomly generated portfolio within budget. The star marks our optimal portfolio. Anything below the upper envelope is dominated β€” you can do better at the same cost.

Plot 6 – Benefit Contribution Waterfall: Ranks the selected controls by individual expected benefit. The blue line shows the cumulative benefit build-up. The first 2–3 controls typically drive ~70% of total value (Pareto principle in action).

Plot 7 – 3D Portfolio Space (Cost–Benefit–ROI): The three axes create a 3D cloud of valid portfolios. The optimal (cyan star) sits at the high-ROI, high-benefit, moderate-cost region. This 3D view reveals that the efficient frontier is actually a curved surface, not a line.

Plot 8 – 3D Budget Γ— Control Landscape: Each column is a control; each row is a budget level. Height shows ROI of that control when it’s selected. The landscape reveals which controls β€œenter” the portfolio first as budget grows, and how they interact.

Plot 9 – Portfolio Quality Radar: Five normalized dimensions of portfolio quality. A perfect portfolio would fill the pentagon entirely. The gap in β€œBudget Efficiency” tells us we have unspent budget β€” a potential opportunity or a cushion.


πŸ“‹ Execution Results

=================================================================
Control                 Cost  E[Benefit]   ROI (%)
-----------------------------------------------------------------
Endpoint EDR         $  120k  $   255.0k     112.5%
Email Gateway        $   80k  $   180.0k     125.0%
MFA Rollout          $   50k  $   171.0k     242.0%
SIEM/SOC             $  150k  $   262.5k      75.0%
WAF + CDN            $   90k  $   193.6k     115.1%
Zero Trust NAC       $  130k  $   224.0k      72.3%
Security Training    $   30k  $    89.1k     197.0%
Vuln Scanner         $   40k  $    97.0k     142.5%
=================================================================

βœ… Optimal Portfolio  (budget = $500k)
---------------------------------------------
  β€’ Endpoint EDR          cost=$120k  ROI=112.5%
  β€’ Email Gateway         cost=$80k  ROI=125.0%
  β€’ MFA Rollout           cost=$50k  ROI=242.0%
  β€’ WAF + CDN             cost=$90k  ROI=115.1%
  β€’ Zero Trust NAC        cost=$130k  ROI=72.3%
  β€’ Security Training     cost=$30k  ROI=197.0%

  Total Cost    : $500k
  Total E[Benefit]: $1112.7k
  Portfolio ROI : 122.5%

πŸ“Š Monte Carlo Results (N=50,000)
  Mean Net Gain  : $613.2k
  Std Dev        : $200.5k
  VaR 95%        : $234.9k
  CVaR 95%       : $134.4k

βœ… All plots rendered successfully.

🧠 Key Takeaways

The optimization makes three non-obvious decisions clear:

  1. Security Training ($30k) has the highest individual ROI (~200%) and is always selected first. It’s the β€œfree lunch” of security investments.
  2. SIEM/SOC ($150k) is expensive but has such high expected loss reduction that it enters the optimal set despite lower per-dollar ROI.
  3. The Monte Carlo VaR shows that even the optimal portfolio has a 5% chance of failing to break even β€” this is the residual risk your cyber insurance should cover.

The efficient frontier visualization is the most actionable output: it tells you exactly how much more you should ask for in your next budget cycle and what return to promise.