Conditional Value at Risk (CVaR) Optimization
CVaR optimization trains a policy for its bad days, not its average day. It maximizes the average return of the worst alpha fraction of episodes, so rare crashes count more than lucky wins.
Why Does This Exist?
Standard RL chases the mean. It picks the policy with the highest expected return and calls it done. That works until the mean hides a cliff edge.
Picture two delivery robot policies. Both average +4.3 per shift. One never drops below -2. The other scores +22 on lucky shifts and -20 when it tips down stairs. A mean-only optimizer calls them tied. Your hardware budget does not. You want the policy whose worst fifth of shifts still pays, even if its highlight reel looks plainer.
That is what this page covers. You will see how VaR marks the edge of the bad tail and how CVaR averages everything past that edge, with a 10-episode worked example you can check by hand. It then sketches how a policy gradient lifts that tail.
Two neighbours do the adjacent jobs. Quantile Regression DQN teaches how to estimate the return distribution that VaR and CVaR read from. Safe RL teaches how to add hard constraints during training and deployment. This page teaches the tail objective itself, the workhorse of risk-sensitive RL. For the fixed-grid distribution that started this family, see Categorical DQN.
You only need one prerequisite idea. Each episode produces a total discounted return . Run the same policy ten times and you get ten different values because starts, noise, and slips differ. VaR and CVaR are just two ways of summarizing the low end of those numbers.
Think of It Like This
The ferry captain judged on stormy crossings
You run a harbor ferry. Each crossing earns a score: punctual arrivals score high, late or rough crossings score low.
The owner offers two bonus schemes. Scheme A pays on your average over all 100 crossings. Scheme B pays on your average over your worst 20 crossings. Under Scheme A you chase calm-day speed records. Under Scheme B you practice docking in wind and rain, add fenders, and slow down when chop builds, because one -20 day drags your bonus more than three +22 days lift it.
Map it point for point: crossings are episodes, scores are returns , the cutoff for your worst 20 is VaR, and the average of those 20 is CVaR. Optimizing CVaR is choosing Scheme B.
Where the analogy stops: ferry crossings are close to independent, while RL episodes come from one policy that you keep changing. Push the tail up and the whole distribution shifts, which is why CVaR training needs fresh rollouts instead of one fixed logbook.
How It Actually Works
Let be the random episodic return under policy , where holds the policy weights. Let be its cumulative distribution, so is the chance a return lands at or below . Let in be the tail fraction you care about, here for the worst fifth.
VaR marks the edge, CVaR averages past it
VaR (Value at Risk) is the quantile at :
In plain words, is the cutoff where the worst fraction begins. With , about 20 percent of episodes score at or below VaR.
CVaR (Conditional Value at Risk) is the mean inside that tail:
So VaR is one number on the edge, while CVaR averages the whole bad slice. CVaR is always at or below VaR for returns, which is why traders say VaR tells you where the cliff starts and CVaR tells you how deep the drop is.
One convention trap to fix now: finance often defines CVaR on losses with and averages the worst 5 percent above a high quantile. Here we define it on returns and average the worst below a low quantile. Same idea, mirrored axis. Always check which side the author averages.
Optimization uses a handy variational form from Rockafellar and Uryasev. For returns:
Here is a free threshold guess and is the shortfall below it. The best lands exactly on . That form matters because it turns a conditional average into an expectation you can sample.
Worked example with 10 sampled returns
Say one policy produces these 10 episode returns, in the order they happened:
Sort them lowest first:
The plain mean is:
since the values sum to . A mean-only reader sees +4.3 and moves on.
Now take . The tail holds episodes. Using the order-statistic rule, VaR is the second smallest value:
CVaR averages the two tail values:
So the policy averages +4.3 overall but -14 across its worst fifth. That gap is the whole argument for CVaR: the mean says fine, the tail says fix the stairs fall first. The diagram at the top draws these same numbers, with VaR as the dashed edge and CVaR as the solid tail mean left of it.
How a policy gradient lifts the tail
A CVaR policy gradient keeps the tail and mostly ignores the rest. In each update:
- Roll out episodes and collect returns to .
- Estimate as the empirical -quantile, exactly as above.
- Keep only episodes with and push in the direction that makes those episodes both likelier and higher scoring, weighted by their shortfall times the score gradient .
You will often see the update written with an indicator that zeroes out non-tail episodes, which is why only the bad slice teaches. The threshold from the variational form acts as a built-in baseline, so episodes exactly at VaR contribute nothing.
This costs data. With you throw away four of every five episodes per update, so gradients wobble and small test batches mislead. In practice teams pair the objective with a distributional critic such as Quantile Regression DQN, which reads quantiles straight off learned locations instead of re-sorting thousands of rollouts, and they often warm-start on the mean before tightening . Pick CVaR when rare failures dominate your true cost, such as hardware damage or large trading drawdowns. Stick with the mean when failures are cheap and you need every sample to count.
Code
The snippet below computes the same VaR and CVaR from the worked example. It is short on purpose: sort, cut the tail, average it.
from typing import List, Tuple
def var_cvar(returns: List[float], alpha: float = 0.2) -> Tuple[float, float, float]: """Return (mean, VaR_alpha, CVaR_alpha) for lower-tail returns.""" ordered = sorted(returns) n = len(ordered) mean = sum(ordered) / n # Order-statistic quantile: first index whose CDF reaches alpha. k = max(1, int(-(-alpha * n // 1))) # ceil(alpha * n) var = ordered[k - 1] tail = ordered[:k] cvar = sum(tail) / len(tail) return mean, var, cvar
episode_returns = [12, 5, -8, 22, 3, -20, 9, 15, -2, 7]mean, var, cvar = var_cvar(episode_returns, alpha=0.2)print(f"sorted: {sorted(episode_returns)}")print(f"mean: {mean:.1f}")print(f"VaR_0.2: {var}")print(f"CVaR_0.2: {cvar:.1f}")# -> sorted: [-20, -8, -2, 3, 5, 7, 9, 12, 15, 22]# -> mean: 4.3# -> VaR_0.2: -8# -> CVaR_0.2: -14.0Watch Out For
Alpha points opposite ways in finance and RL
Finance papers average losses above a high quantile, so conservative means or . Return-based RL averages gains below a low quantile, so conservative means or . Copy the 0.95 into RL code and your tail holds nearly everything, so CVaR collapses back to the mean and the extra machinery does nothing.
Symptom: CVaR training behaves exactly like mean training and the tail never improves. Fix: name the side in a comment, such as lower-tail alpha equals 0.2, and assert that the kept fraction matches, for example 2 of 10 episodes for alpha 0.2.
CVaR starves on small batches
CVaR learns only from its tail slice. With and 50 episodes per update, just 10 episodes carry gradient and the rest are discarded. Small batches then swing wildly: one lucky batch says the tail is fixed, the next says it collapsed.
Symptom: CVaR loss zigzags while the mean looks steady, and evaluation on 20 episodes disagrees with training. Fix: raise episodes per update, warm-start on the mean, then tighten alpha in steps, and read quantiles off a distributional critic instead of raw sorts when rollouts cost a lot.
The Quick Version
- CVaR optimization maximizes the average return of the worst alpha fraction of episodes instead of the overall mean.
- VaR at level alpha is the cutoff where that worst slice starts, while CVaR is the average inside the slice.
- With returns
[12, 5, -8, 22, 3, -20, 9, 15, -2, 7], the mean is 4.3, VaR at 0.2 is minus 8, and CVaR at 0.2 is minus 14. - A CVaR policy gradient keeps only tail episodes and weights each by its shortfall below VaR.
- Small alpha values give stronger protection but need larger batches because most episodes are discarded each update.