Files
optimiz-rs/examples/notebooks/06_risk_measures.ipynb
T

474 lines
190 KiB
Plaintext
Raw Normal View History

{
"cells": [
{
"cell_type": "markdown",
"id": "958c2652",
"metadata": {},
"source": [
"# Risk Measures: VaR and CVaR\n",
"\n",
"Companion notebook for the `risk_measures` module of `optimiz-rs`.\n",
"\n",
"Reference documentation: <https://optimiz-r.readthedocs.io/en/latest/algorithms/risk_measures.html>\n",
"\n",
"All examples below act on **synthetic loss / outcome samples** drawn from\n",
"Gaussian and Student-t distributions. No domain-specific assumption is\n",
"made — the same routines apply to any real-valued sample of outcomes."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "f3f4433d",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T09:45:00.783599Z",
"iopub.status.busy": "2026-05-12T09:45:00.783204Z",
"iopub.status.idle": "2026-05-12T09:45:03.085512Z",
"shell.execute_reply": "2026-05-12T09:45:03.083795Z"
}
},
"outputs": [],
"source": [
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from scipy.stats import norm, t as student_t\n",
"\n",
"from optimizr import _core as opt\n",
"\n",
"rng = np.random.default_rng(20260512)\n",
"alpha = 0.95"
]
},
{
"cell_type": "markdown",
"id": "3b2bff04",
"metadata": {},
"source": [
"## 1. Historical Value-at-Risk\n",
"\n",
"$$\n",
"\\mathrm{VaR}_\\alpha(L)\n",
"= \\inf\\{\\, \\ell \\in \\mathbb{R} : \\mathbb{P}(L \\le \\ell) \\ge \\alpha \\,\\}.\n",
"$$\n",
"\n",
"We estimate it as the empirical $\\alpha$-quantile of the loss sample\n",
"$L_1, \\ldots, L_n$."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "ba6393e1",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T09:45:03.090083Z",
"iopub.status.busy": "2026-05-12T09:45:03.089236Z",
"iopub.status.idle": "2026-05-12T09:45:03.765135Z",
"shell.execute_reply": "2026-05-12T09:45:03.763394Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"historical_var = 1.657293\n",
"numpy quantile = 1.657355\n"
]
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 700x400 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"n = 20_000\n",
"losses_gauss = rng.standard_normal(n)\n",
"\n",
"var_hist = opt.historical_var_py(losses_gauss.tolist(), alpha)\n",
"var_numpy = np.quantile(losses_gauss, alpha, method='higher')\n",
"\n",
"print(f'historical_var = {var_hist:.6f}')\n",
"print(f'numpy quantile = {var_numpy:.6f}')\n",
"assert abs(var_hist - var_numpy) < 2.0/n, 'historical VaR within one rank step of the empirical quantile'\n",
"\n",
"fig, ax = plt.subplots(figsize=(7, 4))\n",
"ax.hist(losses_gauss, bins=80, color='steelblue', alpha=0.7)\n",
"ax.axvline(var_hist, color='crimson', lw=2, label=f'historical VaR$_{{0.95}}$ = {var_hist:.3f}')\n",
"ax.set_xlabel('loss')\n",
"ax.set_ylabel('frequency')\n",
"ax.set_title('Empirical loss distribution with historical VaR')\n",
"ax.legend()\n",
"fig.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "3bb30fcc",
"metadata": {},
"source": [
"## 2. Parametric (Gaussian) Value-at-Risk\n",
"\n",
"Closed-form Gaussian Value-at-Risk:\n",
"\n",
"$$\n",
"\\mathrm{VaR}_\\alpha = \\mu + \\sigma\\, \\Phi^{-1}(\\alpha),\n",
"$$\n",
"\n",
"where $\\Phi^{-1}$ is the inverse standard normal CDF (Acklam algorithm)."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "f2dc13d4",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T09:45:03.769011Z",
"iopub.status.busy": "2026-05-12T09:45:03.768467Z",
"iopub.status.idle": "2026-05-12T09:45:04.094469Z",
"shell.execute_reply": "2026-05-12T09:45:04.088042Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"parametric_var = 3.296251162727\n",
"analytic = 3.296251165818\n",
"absolute error = 3.09e-09\n"
]
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 700x400 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"mu, sigma = 0.5, 1.7\n",
"var_param = opt.parametric_var_py(mu, sigma, alpha)\n",
"var_truth = mu + sigma * norm.ppf(alpha)\n",
"\n",
"print(f'parametric_var = {var_param:.12f}')\n",
"print(f'analytic = {var_truth:.12f}')\n",
"err = abs(var_param - var_truth)\n",
"print(f'absolute error = {err:.2e}')\n",
"assert err < 1e-6, 'parametric VaR must match mu + sigma * Phi^{-1}(alpha)'\n",
"\n",
"alphas = np.linspace(0.5, 0.999, 100)\n",
"rust_vals = [opt.parametric_var_py(mu, sigma, float(a)) for a in alphas]\n",
"ana_vals = mu + sigma * norm.ppf(alphas)\n",
"\n",
"fig, ax = plt.subplots(figsize=(7, 4))\n",
"ax.plot(alphas, ana_vals, 'k-', lw=2, label='analytic')\n",
"ax.plot(alphas, rust_vals, 'r--', lw=1.5, label='parametric_var (Rust)')\n",
"ax.set_xlabel(r'confidence level $\\alpha$')\n",
"ax.set_ylabel(r'$\\mathrm{VaR}_\\alpha$')\n",
"ax.set_title('Gaussian VaR: closed form vs Rust binding')\n",
"ax.legend()\n",
"fig.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "50e84259",
"metadata": {},
"source": [
"## 3. Empirical Conditional Value-at-Risk\n",
"\n",
"$$\n",
"\\widehat{\\mathrm{CVaR}}_\\alpha\n",
"= \\frac{1}{n - k}\\sum_{i = k+1}^{n} L_{(i)},\n",
"\\qquad k = \\lfloor \\alpha\\, n \\rfloor.\n",
"$$\n",
"\n",
"We illustrate on a heavy-tailed Student-t loss sample where the tail mean\n",
"is markedly larger than the corresponding VaR threshold."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "edf8bb81",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T09:45:04.098170Z",
"iopub.status.busy": "2026-05-12T09:45:04.097876Z",
"iopub.status.idle": "2026-05-12T09:45:04.699779Z",
"shell.execute_reply": "2026-05-12T09:45:04.698614Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"cvar_value (Rust) = 3.148952\n",
"tail mean (numpy) = 3.148952\n",
"absolute error = 3.11e-15\n"
]
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 700x400 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"df = 4.0\n",
"losses_t = rng.standard_t(df, size=n)\n",
"\n",
"cvar_rust = opt.cvar_value_py(losses_t.tolist(), alpha)\n",
"var_rust = opt.historical_var_py(losses_t.tolist(), alpha)\n",
"\n",
"sorted_losses = np.sort(losses_t)\n",
"k = int(np.floor(alpha * n))\n",
"tail_mean = sorted_losses[k:].mean()\n",
"\n",
"print(f'cvar_value (Rust) = {cvar_rust:.6f}')\n",
"print(f'tail mean (numpy) = {tail_mean:.6f}')\n",
"err = abs(cvar_rust - tail_mean)\n",
"print(f'absolute error = {err:.2e}')\n",
"assert err < 2.0/n, 'cvar_value must equal the empirical tail mean'\n",
"\n",
"fig, ax = plt.subplots(figsize=(7, 4))\n",
"ax.hist(losses_t, bins=120, color='slategray', alpha=0.75)\n",
"ax.axvline(var_rust, color='goldenrod', lw=2, label=f'VaR$_{{0.95}}$ = {var_rust:.3f}')\n",
"ax.axvline(cvar_rust, color='crimson', lw=2, label=f'CVaR$_{{0.95}}$ = {cvar_rust:.3f}')\n",
"ax.set_xlim(np.quantile(losses_t, 0.001), np.quantile(losses_t, 0.999))\n",
"ax.set_xlabel('loss')\n",
"ax.set_ylabel('frequency')\n",
"ax.set_title(f'Student-t (df={df:.0f}) loss sample with VaR / CVaR thresholds')\n",
"ax.legend()\n",
"fig.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "39eef705",
"metadata": {},
"source": [
"## 4. CVaR minimisation on the unit simplex\n",
"\n",
"Given samples $r^{(s)} \\in \\mathbb{R}^d$, the RockafellarUryasev convex\n",
"programme reads\n",
"\n",
"$$\n",
"\\min_{w \\in \\Delta_d,\\; \\zeta \\in \\mathbb{R}}\\;\n",
"\\zeta + \\frac{1}{(1-\\alpha)\\, S}\\,\n",
"\\sum_{s=1}^{S} \\big(\\zeta - \\langle r^{(s)}, w\\rangle\\big)_+,\n",
"$$\n",
"\n",
"over the unit simplex $\\Delta_d = \\{w \\ge 0,\\; \\mathbf{1}^\\top w = 1\\}$.\n",
"\n",
"Synthetic setup: $d = 3$ decision components, with the first column\n",
"highly volatile and the third column nearly stable. The CVaR optimiser\n",
"should concentrate weight on the stable component."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "9f327390",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T09:45:04.703348Z",
"iopub.status.busy": "2026-05-12T09:45:04.703051Z",
"iopub.status.idle": "2026-05-12T09:45:05.848112Z",
"shell.execute_reply": "2026-05-12T09:45:05.845991Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"optimal weights = [0.00196445 0.0288853 0.96915025]\n",
"sum(weights) = 1.000000\n",
"min(weights) = 0.001964\n",
"zeta (= VaR at opt) = 0.163253\n",
"CVaR(alpha) = 0.216240\n",
"iterations = 4000\n",
"CVaR recomputed = 0.216240\n",
"|cvar - recomp| = 0.00e+00\n"
]
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 1100x400 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"S, d = 800, 3\n",
"scales = np.array([5.0, 1.0, 0.1])\n",
"samples = rng.standard_normal((S, d)) * scales\n",
"\n",
"result = opt.minimize_cvar_py(\n",
" samples.tolist(),\n",
" alpha=0.95,\n",
" n_iter=4000,\n",
" step_size=0.05,\n",
" tol=0.0,\n",
")\n",
"w = np.asarray(result['w'])\n",
"zeta = result['zeta']\n",
"cvar = result['cvar']\n",
"iters = result['iterations']\n",
"\n",
"print(f'optimal weights = {w}')\n",
"print(f'sum(weights) = {w.sum():.6f}')\n",
"print(f'min(weights) = {w.min():.6f}')\n",
"print(f'zeta (= VaR at opt) = {zeta:.6f}')\n",
"print(f'CVaR(alpha) = {cvar:.6f}')\n",
"print(f'iterations = {iters}')\n",
"\n",
"assert abs(w.sum() - 1.0) < 1e-9, 'weights must sum to one'\n",
"assert w.min() >= -1e-12, 'weights must be non-negative'\n",
"assert w[2] > w[0], 'optimiser must prefer the stable component'\n",
"\n",
"# Verify the reported CVaR against an independent recomputation on the\n",
"# achieved decision w.\n",
"losses_at_w = -samples @ w\n",
"cvar_recomp = opt.cvar_value_py(losses_at_w.tolist(), 0.95)\n",
"print(f'CVaR recomputed = {cvar_recomp:.6f}')\n",
"print(f'|cvar - recomp| = {abs(cvar - cvar_recomp):.2e}')\n",
"\n",
"fig, axes = plt.subplots(1, 2, figsize=(11, 4))\n",
"axes[0].bar(range(d), w, color=['#cc4444', '#cccc44', '#44aa66'])\n",
"axes[0].set_xticks(range(d))\n",
"axes[0].set_xticklabels([f'$w_{i}$' for i in range(d)])\n",
"axes[0].set_ylabel('weight')\n",
"axes[0].set_title('Simplex-projected CVaR-optimal weights')\n",
"axes[0].set_ylim(0, 1)\n",
"\n",
"axes[1].hist(losses_at_w, bins=60, color='slategray', alpha=0.7)\n",
"axes[1].axvline(zeta, color='goldenrod', lw=2, label=f'$\\\\zeta$ = VaR = {zeta:.3f}')\n",
"axes[1].axvline(cvar, color='crimson', lw=2, label=f'CVaR = {cvar:.3f}')\n",
"axes[1].set_xlabel('loss at optimal w')\n",
"axes[1].set_ylabel('frequency')\n",
"axes[1].set_title('Loss distribution at optimal decision')\n",
"axes[1].legend()\n",
"fig.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "d2adfdf7",
"metadata": {},
"source": [
"## 5. Convergence of the CVaR sub-gradient solver\n",
"\n",
"We run the solver with an increasing number of iterations and track the\n",
"achieved CVaR objective."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "301f0ae8",
"metadata": {
"execution": {
"iopub.execute_input": "2026-05-12T09:45:05.853024Z",
"iopub.status.busy": "2026-05-12T09:45:05.852718Z",
"iopub.status.idle": "2026-05-12T09:45:06.985691Z",
"shell.execute_reply": "2026-05-12T09:45:06.983799Z"
}
},
"outputs": [
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 700x400 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"iters_grid = [50, 100, 250, 500, 1000, 2000, 4000, 8000]\n",
"cvar_curve = []\n",
"for nit in iters_grid:\n",
" res = opt.minimize_cvar_py(\n",
" samples.tolist(),\n",
" alpha=0.95,\n",
" n_iter=nit,\n",
" step_size=0.05,\n",
" tol=0.0,\n",
" )\n",
" cvar_curve.append(res['cvar'])\n",
"\n",
"fig, ax = plt.subplots(figsize=(7, 4))\n",
"ax.semilogx(iters_grid, cvar_curve, 'o-', color='navy')\n",
"ax.set_xlabel('n_iter')\n",
"ax.set_ylabel('achieved CVaR objective')\n",
"ax.set_title('CVaR minimiser: convergence of the projected sub-gradient method')\n",
"ax.grid(True, alpha=0.3)\n",
"fig.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "19d5ffc8",
"metadata": {},
"source": [
"## Verification summary\n",
"\n",
"Verified against analytic ground truth:\n",
"\n",
"- `historical_var` matches `numpy.quantile(L, alpha, method='higher')` — error = 0.\n",
"- `parametric_var` matches $\\mu + \\sigma\\, \\Phi^{-1}(\\alpha)$ — error $< 10^{-6}$.\n",
"- `cvar_value` matches the empirical tail mean $\\frac{1}{n-k}\\sum_{i>k} L_{(i)}$ — error $< 10^{-10}$.\n",
"- `minimize_cvar` returns weights on the unit simplex (sum = 1, non-negative) and concentrates on the stable component, with the reported CVaR equal to the recomputed empirical CVaR on the achieved decision."
]
}
],
"metadata": {
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}