Skip to content

07: VaR and CVaR for the newsvendor, and the profit variables that lie - #6

Merged
yamierick merged 1 commit into
mainfrom
newsvendor/risk
Oct 27, 2026
Merged

yamierick merged 1 commit into
mainfrom
newsvendor/risk

Conversation

@yamierick

Copy link
Copy Markdown
Contributor

Stacked on #5. Its base is games/saddle-points. Merge #4, then #5, then this.

Adds notebooks/07_stochastic_and_newsvendor/risk_measures.ipynb and package functions solve_cvar, solve_var and tail_stats. These are the three newsvendor models IE 5301 teaches that the library didn't have:

  • CVaR as a linear program;
  • a dial that weights average profit against CVaR;
  • VaR as an integer program.

The notebook uses 400 scenarios, so every model fits the free Gurobi licence: it runs on Colab without a key, and its tests run in CI.

The lesson it's built around: if you read the VaR model's own profit variables, its worst-quarter average comes out at −456.49. Scoring the same order against every scenario gives 3,220.62. The 75 scenarios where the two differ are all ones the model excused, so their profit variables can sit anywhere the solver leaves them. My own first draft of a test made exactly that misreading; it's fixed, and the package now always re-scores profits from the order.

🤖 Generated with Claude Code

IE 5301 teaches seven newsvendor models in 2026
(02_Stochastic_Optimization_Newsvendor.ipynb); the library had four and
stopped at the worst case. risk_measures.ipynb adds the other three: CVaR at
75% as an LP (the Rockafellar-Uryasev form), a dial weighting the mean
against CVaR, and VaR at 75% as a MIP with one binary per scenario.

400 scenarios, the first 400 of newsvendor_demand.csv: every model is at
most 4n + 2 = 1602 variables, inside the size-limited licence, so it runs
on Colab with no key and its tests run in CI. With ALPHA 0.75 the tail is
exactly 100 scenarios, so VaR (the 101st-worst profit) and CVaR (the mean
of the worst 100) need no fractional-tail convention. tail_stats() refuses
a sample that does not divide.

The finding the notebook is built around, found while writing it: the VaR
model's own profit variables put the order's tail CVaR at -456.49; scoring
the same order against every scenario gives 3220.62. Excused scenarios'
sales are free, so the solver leaves them anywhere. All 75 scenarios where
the two differ are among the 100 excused. The first draft of a test here
claimed "VaR abandons the tail" from exactly that misreading; the honest
numbers say the VaR order beats the risk-neutral one on both tail
measures, and the test now says so. solve_cvar and solve_var therefore
return profits scored from the order, never the model's variables.

Results (all four orders unique, checked by min/max of the order over each
optimal set): risk-neutral 459.7374, which matches solve_stochastic; mix
391.1796; CVaR 306.6096; VaR 334.6840. The dial falls monotonically. Each
order wins its own column (mean, VaR, CVaR); tests pin that as invariants,
plus the orders.

Gate: 201 passed with a full licence; 194 passed and 7 skipped (the
existing newsvendor set) on the size-limited one; 19 notebooks and 13
seeds clean; ruff clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yamierick
yamierick changed the base branch from games/saddle-points to main October 27, 2026 14:31
@yamierick
yamierick merged commit f684d31 into main Oct 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant