Skip to content
intermediate

Compare Mean Return and Downside-Sensitive Policy Choices in a Toy Task

Two policies. One wins on average. The other wins on the days you actually have to survive. If your selection rule can't tell them apart, you're not…

Published 2026-10-02Updated 2026-10-048 min read
Close-up of hands working with electronics equipment and circuit board in a lab setting.
Close-up of hands working with electronics equipment and circuit board in a lab setting. Photo by Willquezada on Pexels.

Two policies. One wins on average. The other wins on the days you actually have to survive. If your selection rule can't tell them apart, you're not choosing a policy — you're choosing a coin flip and calling it a decision.

This exercise makes that distinction concrete. You'll define three fixed policies as return distributions, sample episodes from each, rank them by mean return, then rank them again by a downside-sensitive criterion. The goal is to watch the ranking flip and understand exactly which samples caused it.

What This Exercise Builds and What You Need

The task is narrow by design: three frozen policies, sampled returns, two ranking criteria. No learning, no Bellman updates, no environment dynamics. The selection criterion is the only variable.

That constraint matters. If you tune the policies after seeing the results, you haven't tested a criterion — you've tested your own search. Frozen parameters keep the comparison honest.

Dependencies: Python 3, NumPy, and optionally Matplotlib for a histogram. No RL library, no GPU, no credentials.

Assumed context: You already know that a return is a random variable, not a score, and that a policy induces a distribution over returns. If that's shaky, the short version is: the same policy run on the same task produces different outcomes because the environment and the policy both have randomness baked in. We're working with those distributions directly.

Success criterion: The two rankings disagree, and you can point to the specific samples that caused the disagreement.

Design Three Policies With Deliberately Different Tails

We need distributions whose means and lower tails conflict. Here's the parameter table:

PolicyDescriptionMeanLeft tail
AUsually pays well, occasionally fails badlyHighHeavy
BModest but consistentLowerTight
CMiddle caseMediumModerate

Policy C exists to keep the comparison honest. Without it, you could reduce the lesson to "safe beats greedy," which misses the point.

import numpy as np

def sample_policy_a(rng, n):
    # 90% of episodes: return around +10
    # 10% of episodes: return around -50
    base = rng.normal(10, 2, n)
    tail_mask = rng.random(n) < 0.10
    base[tail_mask] = rng.normal(-50, 5, tail_mask.sum())
    return base

def sample_policy_b(rng, n):
    # Tight distribution around +5
    return rng.normal(5, 1.5, n)

def sample_policy_c(rng, n):
    # 85% around +8, 15% around -20
    base = rng.normal(8, 2, n)
    tail_mask = rng.random(n) < 0.15
    base[tail_mask] = rng.normal(-20, 4, tail_mask.sum())
    return base

A quick sanity check before moving on: plot the three distributions on one axis with the mean marked on each. You should see Policy A's mean sitting to the right of Policy B's, but with a long shadow stretching left.

Knowledge check

Check your understanding

Answer this question before you continue.

Which description matches the article’s design for Policies A and B?
Comparison Reasoning

Focus: Identify how the defined return distributions give Policy A a higher typical outcome but a heavier downside tail than Policy B.

Sample Episodes and Compute Mean Return

Draw N episodes per policy from the same seeded generator. The shared seed makes the run reproducible — rerun it and you get the same numbers. It does not pair the policies, because each sampler consumes the random stream differently. Reproducibility is the goal here, not matched episode conditions.

def run_comparison(n_episodes=10000, seed=42):
    rng = np.random.default_rng(seed)
    policies = {
        "A": sample_policy_a,
        "B": sample_policy_b,
        "C": sample_policy_c,
    }
    returns = {name: fn(rng, n_episodes) for name, fn in policies.items()}
    return returns

returns = run_comparison()
for name, r in returns.items():
    print(f"Policy {name}: mean = {r.mean():.2f}")

Expected output shape:

Policy A: mean = 4.02
Policy B: mean = 5.01
Policy C: mean = 3.78

Policy B wins on mean. That's the setup — and it's the part that surprises people. Policy A looks like the aggressive, high-reward choice, but its 10% failure rate drags the average below the modest, consistent policy.

Common mistake: Using a different seed per policy and then comparing the numbers as if they were the same experiment. One generator, one seed, all policies — so that when you rerun, you're rerunning the same experiment.

The sample mean is an estimate. With finite N, the ranking itself is noisy. Increase N and confirm the mean ranking stabilizes — if it doesn't, your sampler or seed handling is wrong.

Knowledge check

Check your understanding

Answer this question before you continue.

For the displayed seeded run, which policy has the highest sample mean return?
Output Prediction

Focus: Interpret the article’s sample-mean output to identify which policy ranks first by mean return.

Policy A: mean = 4.02
Policy B: mean = 5.01
Policy C: mean = 3.78

Add One Downside-Sensitive Criterion

We'll use CVaR (Conditional Value at Risk) at a fixed confidence level. In plain terms: the average of the worst alpha-fraction of returns. If alpha is 0.10, CVaR is the mean of the bottom 10% of episodes.

Why CVaR and not variance? Variance punishes upside and downside equally. That's the wrong shape for a downside question. A policy that occasionally returns +200 is not "risky" in the way we care about here. This is a deliberate choice, not a claim that CVaR is universally better.

def cvar(returns, alpha=0.10):
    sorted_r = np.sort(returns)
    n_tail = int(np.ceil(len(sorted_r) * alpha))
    return sorted_r[:n_tail].mean()

The boundary condition: when N * alpha isn't an integer, we round up. Pick a convention, state it, and keep it consistent. Rounding up means the tail always contains at least one sample.

Sign convention: A higher (less negative) CVaR means a better-protected lower tail. Policy B's CVaR will be around +2 or +3. Policy A's will be deeply negative. Get this backwards and every conclusion inverts.

Warning: Sign errors here are the most common silent bug. Print the tail samples alongside the CVaR value the first time you run it.

Knowledge check

Check your understanding

Answer this question before you continue.

With 23 returns and alpha = 0.10, how many sorted returns are included in the article’s CVaR calculation?
Output Prediction

Focus: Apply the article’s round-up convention to determine how many returns enter an empirical CVaR estimate.

n_tail = int(np.ceil(len(sorted_r) * alpha))

Compare the Two Rankings and Read the Disagreement

Two ranking columns show mean return ordering policies B, A, C and CVaR ordering B, C, A. Policy B stays first, while A and C switch positions.
Comparing the rankings shows how a downside-sensitive criterion can change which policy is preferred.
print(f"{'Policy':<8} {'Mean':>8} {'CVaR':>8} {'Rank(Mean)':>12} {'Rank(CVaR)':>12}")
means = {n: r.mean() for n, r in returns.items()}
cvars = {n: cvar(r) for n, r in returns.items()}
mean_rank = sorted(means, key=means.get, reverse=True)
cvar_rank = sorted(cvars, key=cvars.get, reverse=True)
for name in ["A", "B", "C"]:
    print(f"{name:<8} {means[name]:>8.2f} {cvars[name]:>8.2f} "
          f"{mean_rank.index(name)+1:>12} {cvar_rank.index(name)+1:>12}")

Expected result: Policy B wins on mean and loses on CVaR. Policy A does the reverse. Policy C lands somewhere in between.

The mechanism is straightforward. The mean is sensitive to the whole distribution but dominated by the bulk — those 90% of episodes returning +10 pull it up. CVaR ignores everything above the tail threshold. Different questions, different answers.

CriterionRewardsIgnoresMisleading when
Mean returnBulk performanceTail severityA single bad episode is unrecoverable
CVaRTail protectionUpsideTail estimate rests on few samples

Decision boundary: Mean return is the right criterion when the downside is recoverable and repeated. A downside criterion earns its place when a single bad episode is unrecoverable, or when the tail outcome is the one you actually have to survive.

When not to use a downside criterion: if the tail estimate rests on a handful of samples, CVaR is mostly noise. Check how many samples land in the tail before trusting the number.

Knowledge check

Check your understanding

Answer this question before you continue.

What explains the expected disagreement between the mean and CVaR rankings?
Comparison Reasoning

Focus: Explain why the mean-return and CVaR rankings favor different policies in the exercise.

Break It on Purpose: Three Failure Modes to Reproduce

A criterion you've never seen break is a criterion you don't yet understand. Trigger these deliberately.

Too few samples. Shrink N to 200. Rerun with different seeds. Watch the CVaR ranking flip. The signal: a ranking that changes on rerun. The fix: increase N until the ranking stabilizes across seeds.

Tail too thin. Set alpha to 0.001 with N=1000. Now one sample defines CVaR. Observe how a single episode controls the policy choice. The fix: keep the tail count in the dozens at minimum.

Seed sensitivity. Rerun the whole comparison with several different seeds and record how often the CVaR ranking holds. The signal: a ranking that flips across seeds means your sample size is too small for the confidence level you chose. The fix: raise N, raise alpha, or both — then confirm the ranking is stable before you trust it.

Tip: Frame these as evidence, not embarrassment. Every one of them is a real failure mode you'll encounter when comparing policies on actual agents.

One Modification Worth Trying Next

Sweep alpha across a small range — 0.01, 0.05, 0.10, 0.25 — and plot how the CVaR ranking changes. Some policies only look safe at one confidence level. Policy C might beat Policy B at alpha=0.25 but lose at alpha=0.05, because its tail is thinner but deeper.

Or add a fourth policy that's a near-copy of A with a slightly heavier tail, and find the point where the ranking flips. Report what you changed, what you expected, and what the output actually showed. That habit is the whole point.

The criterion you optimize is a decision about which failures you're willing to accept — not a neutral measurement. Rerun this comparison with your own policy parameters, or carry the same mean-versus-tail check into the next experiment you design. The natural follow-on is choosing an objective for an agent that actually learns, rather than a frozen policy you've already defined.

Knowledge check

Final check

Finish the article by checking the ideas you just learned.

A decision-maker says one very bad episode cannot be recovered from, even if later episodes are strong. Which selection approach best matches the article’s decision boundary?
Question 1 of 2Scenario Interpretation

Focus: Choose between mean return and a downside-sensitive criterion based on whether a bad episode is recoverable.

A policy’s CVaR ranking changes across repeated seeds, and only a handful of samples fall in the selected tail. What is the best next step before trusting the ranking?
Question 2 of 2Scenario Interpretation

Focus: Select a sound response when a CVaR ranking is sensitive to samples or seeds.

References

  1. Spinning Up as a Deep RL Researcher — Spinning Up documentationspinningup.openai.com
Practical resource

Build a stronger RL mental model

Use structured resources to connect equations, algorithms, and observable agent behavior.

Browse resources
Related sites

Continue across related AI foundations

Use LearnPyFast for Python foundations and LearnLLMFast for practical language-model and agent application concepts.

Python tutorialstutorial

LearnPyFast

Beginner-friendly Python tutorials, examples, and learning paths for practical programming foundations.

PythonProgrammingBeginners
Visit LearnPyFast
LLM tutorialstutorial

LearnLLMFast

Practical LLM tutorials for builders who want to understand prompting, workflows, agents, and AI applications.

LLMAIBuilders
Visit LearnLLMFast

Keep learning

Related reinforcement learning tutorials

Continue with nearby RL concepts, algorithms, and experiments that build on the same decision process.