The Optimizer's Curse

A budget optimizer built on a fitted MMM does something more specific than report a noisy ROI. It searches a whole range of possible allocations and hands back the one that scores best. In the jargon that winner is its argmax, the option that comes out on top. That search is not neutral arithmetic sitting on top of the posterior. Picking the best-looking option among many noisy candidates makes that option's own reported value look better than it really is, and the same statistical mechanism runs whether the candidates are three consulting studies, ten named channels, or the many spend levels a budget allocator walks through on its way to a recommendation. This framework already takes one flavor of that problem seriously. It re-optimizes under each posterior draw and reports how unstable the resulting plan is. What it has not yet attached an interval to are the two numbers a budget decision actually runs on: shadow_price and marginal_roas, the marginal return of the next dollar and the per-channel funding line. Both are read straight off the optimizer's own recommended point as plain two-decimal numbers. Simulating this framework's exact allocation mechanism at its own default sample size shows the plan's own reported value overstating its honest, out-of-sample value by 0.3–0.5%, before sampling error even enters the picture. A shadow price reported as "1.40" can carry a 90% interval that puts an 18% chance on the marginal dollar actually losing money.

None of that makes the model wrong, exactly. It makes it a model. Every fitted MMM is a simplification built to be useful for a decision, not a finished, perfectly correct description of the world, and there is always a sharper question left to ask of it. Finding one is what a measurement practice getting more mature looks like, not a bug report. This post is about one specific, buildable next question: how confident should the shadow price and marginal ROAS actually sound?

Picking a Winner Makes It Look Better Than It Is

Start with the everyday version. Three consultants each estimate how much value a project will add, and you go with whichever number is highest. Even if all three are equally skilled and equally unbiased, the number you picked is, on average, too optimistic. The consultant who produced it did not lie. The optimism came from the rule you used to choose. You filtered for the biggest number, and big numbers are exactly what noise produces when you go looking for them.

The same thing happens to a budget optimizer. Rather than comparing three named alternatives, it searches a whole continuum of possible spend levels, judging each one against a finite sample of posterior draws, and it hands back the plan whose estimated value happens to look best in that sample. That plan's own reported value is, for the same reason, an overstatement of what it will actually deliver.

In this framework, that search runs on a fixed sample of posterior draws (200 by default, fewer for some callers) averaged into a single response curve per channel, which a greedy step-by-step allocator then walks across to find the highest-scoring plan (planning/budget.py). Nothing about that is wrong. Greedy marginal allocation is the right algorithm for a concave response curve, and averaging over posterior draws is the obvious thing to do with a Bayesian fit. The module's own documentation is honest about the half of the mechanism it already handles well. Re-optimizing under each posterior draw turns parameter uncertainty into decision uncertainty, how stable the plan is rather than just how wide the individual ROIs are. optimize_budget does this for real, and reports expected_regret: the KPI a person would be leaving on the table, on average, by committing to this one plan instead of the truly best one.

Under the hood

compute_response_curves (planning/budget.py:92-131) samples each channel's channel_contributions at a handful of spend multipliers, keeping max_draws=200 by default (default_reallocation uses 150, and budget_frontier/goal_seek use 120, planning/frontier.py:189,302). objective_curves collapses those draws to a per-channel mean curve (budget.py:206-207), and _greedy_allocate (budget.py:134) walks a 400-step discretized simplex on top of it (line 576). The per-draw re-optimization behind expected_regret lives in per_draw_alloc (lines 594-610, field docstring at line 419).

The idea, more precisely

Selecting whichever noisy estimate is highest tends to overstate its own true value, on average, and the more alternatives you compare, the worse the overstatement gets. Smith and Winkler (2006) proved this formally and gave it a name: the optimizer's curse.

Formally: let \( V_1, \ldots, V_n \) be noisy but unbiased estimates of true values \( \mu_1, \ldots, \mu_n \), each \( V_i \) scattering around its own \( \mu_i \) with no systematic tilt. Let \( i^\star = \arg\max_i V_i \) be whichever one looks best. Then

$$ E\bigl[\mu_{i^\star} - V_{i^\star}\bigr] \;\le\; 0, $$

strictly negative whenever there is any real chance of picking a suboptimal alternative. This holds no matter how the \( n \) alternatives are packaged: three consulting reports, ten named channels, or the many spend levels a greedy allocator visits on its way toward its own best point.

Smith and Winkler didn't stop at theory. They reanalyzed Clemen and Kwit's 38 published Eastman Kodak decision-analysis studies, a portfolio that claimed 487 million dollars in value added, and added plausible estimation noise of 5%, 10% and 25% to the studies' own numbers. At those three noise levels the claimed value overstated the true value by 14%, 39% and 128% respectively. That is the size of the effect in a setting with far fewer options to choose from than a continuous ad-spend allocation offers.

Fitting Alongside “Advertising Is a Stock”

Advertising Is a Stock already covers the decision-instability side of this story well, in a section titled "Not the Optimizer's Curse." That post says, correctly, that this framework re-optimizes under each posterior draw to turn parameter uncertainty into decision uncertainty. Alongside that, it reports the odds the plan beats the current allocation and prices the expected KPI left on the table by committing to one plan. Those are the right instruments, and they all accept the objective and question the estimate.

That's still true, and this post doesn't walk it back. This is the piece that naturally comes next. expected_regret, expected_uplift, and the per-channel spread between a plan's high and low posterior spend levels (allocation_instability) are honest, correctly-averaged statements about how much the recommended plan could move. What they don't cover yet is the two specific numbers a person actually reads off that plan to decide whether one channel's next dollar is worth spending: the shadow price (the marginal value of the next dollar, the level greedy allocation equalizes across channels) and marginal ROAS, the per-channel funding line. Both are computed once, at the recommended plan, and neither carries a distribution yet. The fields sitting right next to them already report a spend-level range across draws, and no equivalent range exists for the shadow price. The instability of where the money goes is already priced. What deserves the same treatment next is the pair of numbers that justify why.

Where this lives in the code

BudgetOptimizationResult.shadow_price and .marginal_roas (planning/budget.py:419-431) sit beside optimal_spend_p5/_p95 and allocation_instability (lines 646-652), which already summarize a spend-level range across the same 200 draws. The shadow price has no analogous _p5/_p95 yet.

So the two posts stack rather than compete. Every point the sibling post makes survives being handed the model's exact response curves with zero uncertainty, because it concerns the objective rather than the estimate. This post is the mirror image, entirely about the estimate, and specifically about the two estimates the existing decision-uncertainty machinery doesn't reach yet. A plan can carry a well-characterized expected_regret and a shadow price with no error bar at all, at the same time. They answer different questions about the same optimizer.

Two Ways of Describing the Same Effect

Smith and Winkler's result is about choosing among a handful of named options: three consulting studies, ten channels. A budget optimizer does something related but not identical. It searches a continuous range of spend levels, using a finite sample of posterior draws to judge each point along the way. The intuition carries over directly. Taking the best of many noisy guesses and taking the average of many noisy guesses are different operations, and they don't commute. The best-of tends to run high. Statisticians who study this kind of search (a Sample Average Approximation, or SAA, meaning you optimize an average built from a limited sample rather than the true underlying quantity) have shown the same optimistic bias shows up here too, and that it shrinks, but never fully disappears, as the sample of draws grows (Mak, Morton & Wood, 1999).

The formal version, for readers who want it

A budget optimizer solving \( \max_a \hat{f}_N(a) \) over a continuous allocation \( a \), built from \( N \) posterior draws, is an SAA of the true program \( \max_a E[f(a)] \). Jensen's inequality gives \( E[\max_a \hat{f}_N(a)] \ge \max_a E[f(a)] \) for the same reason a max and an expectation don't commute. Mak, Morton and Wood (1999) bound how fast that gap shrinks as \( N \) grows. Shapiro's (2003) chapter states the same result for this continuous, convex case directly, with the bias shrinking at roughly \( O(1/N) \) but never reaching zero at any finite \( N \). Kleywegt, Shapiro and Homem-de-Mello's 2002 paper is the standard reference for the SAA method itself, though its own setup is a finite feasible set. It is cited here for the general method, not the continuous-simplex bias claim.

These are two ways of describing the same regression-to-the-mean-at-selection effect: one for choosing among named alternatives, one for choosing the best point in a continuous space from a finite sample. A budget optimizer walking a spend simplex on 200 posterior draws is doing the second thing. The first is the easiest way to build intuition for it, so the figure below runs the discrete version directly, MMM-flavored: several channels share an identical true marginal ROI, and nothing separates the winner from the pack except which one happened to draw favorable noise.

The winner regresses

\( K \) channels share one true marginal ROI (dashed line). Round 1 draws a noisy estimate per channel and highlights the argmax. Round 2 draws a fresh, independent estimate for the same channels. Nothing about the world changed, only the noise. Slider the channel count and the noise level. The readout is the Monte-Carlo expected disappointment, \( E[\mu_{i^\star}-V_{i^\star}] \), averaged over 6,000 repeated rounds at the current settings.

10
0.15
Winner, round 1
Same channel, round 2
Expected disappointment
…in noise-σ units

At the defaults (K = 10 channels, σ = 0.15 ROI units, true marginal ROI = 1.00 for every channel), the argmax's round-1 estimate overstates its own true value by an expected 0.23 ROI units, or 1.54σ. That reproduces, live, Smith and Winkler's own order-statistics result for ten alternatives. Drop to K = 3 and the expected overstatement falls to roughly 0.84σ. Push to K = 20 and it climbs past 1.85σ. The channel that looks best in round 1 is, on average, no better than any other channel. It just drew good noise, and round 2 regresses it back toward the pack.

One more related post is worth placing carefully, because it looks like the same idea but works differently. Hundreds of Models, One Winner is also about a winner looking better than it is, and its fix is hierarchical shrinkage across a short list of named alternatives (geo ROIs, model specs), borrowing strength across a finite set of parallel estimates before anyone ranks them. A continuous budget simplex has no finite set of channels to pool across at the resolution the optimizer actually searches. It walks hundreds of increments of a simplex rather than comparing a handful of named options. So that post's shrinkage machinery doesn't carry over mechanically here. The diagnosis is the same family, but the remedy that actually fits a continuous search is the Mak-Morton-Wood one above, and the fix section below builds on it directly.

Why This Shows Up in Every Fit, Not Just Bad Ones

Nothing pathological is required for this mechanism to bite. Every response curve the optimizer builds is already a finite posterior sample (120 to 200 draws depending on the caller) evaluated on a coarse grid and averaged. The greedy allocator walks toward whichever point on that averaged curve currently looks best. That's the SAA setup exactly: optimize an average built from a sample, and the sample's own quirks get baked into the winning allocation before anyone checks whether the win would survive a fresh sample.

The figure below runs that mechanism for real, on a synthetic six-channel world with known concave response curves, using this framework's own greedy-allocation logic. For each posterior sample size it draws that many noisy curve realizations, with multiplicative noise calibrated to this framework's own default media-coefficient prior, Gamma(mu=1.5, sigma=1), whose coefficient of variation of 0.82 sets the noise slider's upper range. It averages those realizations the same way objective_curves does and runs the identical greedy walk to pick an allocation. The figure then compares two numbers for that plan: the in-sample value (what the mean curve says the plan is worth, which is what the framework would report) against the true value (the same plan scored against the exact population curve the draws came from, which is what a fresh, independent look would find).

The optimizer's own curse, run on its own mechanism

Six channels, known concave truth, this framework's own greedy marginal-allocation algorithm. Each point on the curve is 250 independently repeated draw-then-optimize trials at that sample size \( D \). The y-axis is how much the in-sample reported value overstates the true value of the same recommended plan, as a percent of the true optimum. Wherever you set the draw count, the marker follows. Drag it to the framework's own real defaults (120, 150, 200) to see the actual exposure.

0.72
200
In-sample reported value
True value, same plan
Overstatement
…as % of true optimum

At this framework's real default sample sizes, the recommended plan's own reported value overstates its true value by 0.52% at 120 draws, 0.41% at 150, and 0.33% at 200, before a single sampling-error interval is even drawn. The gap shrinks as the draw count grows, exactly as Mak and Morton and Wood's bound predicts, but at every draw count tested here, up to 1,000, it never reaches zero. Doubling max_draws from 200 to 400 roughly halves the exposure. It does not retire the mechanism.

Nothing about this world is stacked against the framework. The response curves are smooth, concave, and correctly specified, and the noise is centered exactly on the truth, so no channel's posterior is biased. What drives the gap is that a mean curve built from a finite sample differs from the population curve, and the greedy allocator finds the sample's own idiosyncratic high points before it finds the population's. That is the ordinary case, not an edge case. It happens every time this optimizer runs on a real fit, because every fit's response curves are a finite posterior sample by construction.

The Number With No Error Bar

The part of this that actually reaches a person is narrower than "the reported uplift is a bit optimistic." The two numbers used to justify a specific keep-or-cut call come out bare, with nothing attached to them. When the agent reports a budget plan, the shadow price shows up as a plain two-decimal figure (the marginal return of the next dollar), and the per-channel marginal ROAS is a plain dict of floats with the same shape (agents/model_ops.py).

What actually prints

agents/model_ops.py:1276-1280 emits, verbatim: "- Budget shadow price (marginal return of the next dollar): {res.shadow_price:.2f}". The same pattern repeats at model_ops.py:1613-1690 for the standing budget-plan artifact. The efficient-frontier sibling has the same shape: FrontierPoint.marginal_roi (planning/frontier.py:44) is a bare finite difference of two mean returns (frontier.py:248-252), even though the return at each frontier point already gets a return_p5/return_p95 band from the same per-draw array the slope is built on top of.

That asymmetry is the specific opening worth building next. The level already gets an interval. Nothing similar exists for the slope, the number that actually answers "should I add or cut the next dollar here," even though the per-draw values needed to build it already sit right above the line where they get collapsed to a single mean.

Shadow price has no error bar

Under the hood, a shadow price is a median (or mean) collapsed from a per-draw distribution, the same way _segment_marginal (planning/budget.py:235-244) reads a local slope off each posterior draw's own curve. Set the reported point value and the posterior noise (CV) to see what the underlying spread actually looks like, and how much of it sits on the wrong side of break-even.

1.40
0.35
What the chat message shows
90% interval, same draws
P(marginal return < break-even)

The defaults here are a reported shadow price of 1.40 and a posterior CV of 0.35 (a plausible, not exaggerated, per-channel marginal-ROI spread for a fitted MMM) over 200 draws. At those settings the 90% interval implied by those same draws spans 0.80 to 2.43, and 18.5% of the draws sit below the 1.0 break-even the number is supposed to justify a decision against. Drag the noise up toward this framework's own prior coefficient-of-variation of 0.82 and the interval widens to roughly 0.43–4.46, with a third of the mass below break-even. A figure that reads as "confidently profitable" to two decimal places is, on its own posterior, close to a coin flip.

💡 This isn't true of every number in the report

classify_tier (reporting/helpers/reallocation.py:82-99), which drives the Augur report's Scale/Test/Hold/Reduce language, is already built entirely from a channel's full ROI interval (mean, lower, upper) sourced from the estimands subsystem, not from the budget optimizer's point-estimate shadow_price/marginal_roas at all. A channel whose interval straddles break-even is correctly flagged "test," not "scale." The opportunity this post describes lives specifically in the budget optimizer's own numeric outputs, the ones a user reads directly off optimize_budget, default_reallocation, or budget_frontier. The evidence-tiered report tables already carry this uncertainty.

What This Doesn't Claim

This isn't a claim that the framework's decision-uncertainty reporting is hollow. expected_uplift, expected_regret and allocation_instability are legitimately honest, correctly-averaged statements, and the credit the sibling post gives them stands. This post's claim adds to that. Those statements describe the plan, and they do not yet describe the shadow price and marginal ROAS numbers a reader actually cites to justify moving one channel's spend.

Nor is it a claim that more posterior draws would fix it. Figure 2's gap shrinks with sample size, matching the Mak-Morton-Wood bound, but it doesn't vanish at any draw count tested, and doubling max_draws doesn't change what kind of number shadow_price is: a point estimate at the argmax, carrying a shrinking but real residual optimism.

The Kleywegt-Shapiro-Homem-de-Mello (2002) reference is cited narrowly. Their paper's formal object is a finite feasible set, so it's cited here for the general SAA method rather than for the continuous-simplex bias claim itself. That claim belongs to Shapiro (2003) and Mak-Morton-Wood (1999), both cited above.

The simulations are demonstrations, not a measurement of any one real fit's exposure. Both figures use stylized worlds (six channels, known response curves, noise calibrated to this framework's own default prior scale) built to make the mechanism visible and roughly size it. A real fit has more channels, more collinearity, and priors shaped by real data rather than free-floating noise. The 0.3–0.5% and 18% figures above describe these particular simulated worlds, not universal constants.

One more current-research note. Berman, Zhang and Zhao's 2026 arXiv preprint on correcting for winner-selection bias in randomized experiments isn't peer-reviewed yet. It's cited here only to note that this whole family of questions, whether and how to correct a number for having been selected as the best-looking, is an active area of current statistical research, not a settled question that's been sitting unaddressed.

This isn't a gap unique to this framework, either. Google's own Meridian documentation and API reference say much the same thing about the industry more broadly. Its scenario-planning and budget-optimization guide doesn't discuss uncertainty propagation, a credible interval, or a probability of outperforming for the optimizer's recommended allocation, and its OptimizationResults API class carries spend, ROI, mROI, incremental outcome, and CPIK as plain point fields, with no confidence or probability field on any of them, despite the rest of Meridian being fully Bayesian end to end. A secondhand summary claiming Meridian's optimizer reports a probability of outperforming didn't hold up against the primary documentation, so it isn't repeated here. None of this is a knock on Meridian. That documentation is evidence that a bare point estimate at the argmax is the industry's current default for this exact output, not something one framework overlooked. If anything, this framework's own expected_regret and allocation_instability already go a step further than the point of comparison does.

A Good Next Upgrade

None of this argues for less optimization. Equimarginal allocation under a concave response is still the right target, and greedy marginal allocation is still the right algorithm for finding it. The upgrade worth building is giving the number the algorithm hands back the same epistemic honesty already built for the allocation's location. Three changes would get there, each cheap relative to a full re-architecture.

Attach a distribution to the two numbers that don't have one yet. per_draw_alloc and the curve's per-draw contributions already carry every draw optimize_budget needs (lines 594-610). The per-draw marginal value at the recommended spend is the same quantity already computed once on the mean curve, and it could be computed once per draw instead, at essentially no additional posterior sampling. shadow_price_p5/_p95 and per-channel marginal-ROAS intervals would just be a reuse of arrays the optimizer already holds in memory, not a new fit.

Evaluate the committed plan against an independent sample, not the sample that picked it. This mirrors Mak-Morton-Wood's own recommended practice for SAA: draw one sample to solve for a candidate plan, and a separate, independent sample to estimate how far it sits from the true optimum. In practice, split the available posterior draws (or resample in two disjoint halves), pick the allocation on one half, and report expected_uplift and the shadow price evaluated on the other half. The moment the evaluation sample isn't the selection sample, the reported numbers stop being circular.

Shrink the mean curve itself before optimizing. This is the direct translation of Smith and Winkler's own prescribed fix. Their Bayesian-shrinkage correction blends the raw winning estimate toward a population or prior curve, and is provably curse-free. A smoothness prior or light regularization on the response curve, applied before the greedy walk runs, would do the same job here, damping the finite sample's idiosyncratic high points before the walk can lock onto them. That is cheaper and better targeted than simply adding more posterior draws.

None of this changes what the optimizer recommends most of the time. The mean curve and the true curve usually point toward a similar allocation, and the figures above show overstatement measured in fractions of a percent to a handful of percent, not a reversal of the ranking. What changes is the reading. Someone deciding whether a channel's marginal dollar is worth spending would get those two numbers with the same honest uncertainty this framework already built for the plan they sit inside.

Take those three changes as one buildable increment in the same direction the framework is already headed, rather than a verdict on the current optimizer. No model, this one included, is ever going to be a finished, perfectly correct description of the world. A model gets more useful for the decisions people actually need to make one honest increment at a time, and a shadow price with a real interval attached is a good next one.

Takeaways

  • Picking the best of several noisy estimates makes that estimate look better than it really is. That is Smith and Winkler's result for named alternatives, and the Sample Average Approximation optimism gap (Mak, Morton & Wood, 1999) for a continuous decision space searched via a finite posterior sample. A budget optimizer walking a spend simplex on 200 draws is doing the second thing.
  • This framework already prices decision instability (expected_regret, expected_uplift, per-channel allocation_instability), and that machinery is correctly credited elsewhere on this blog. The next number worth adding uncertainty to is the pair a budget call actually runs on: shadow_price and marginal_roas, both currently bare floats read off the argmax.
  • A simulation of this framework's own greedy-allocation mechanism, at its real default sample sizes, shows the recommended plan's in-sample reported value overstating its own true value by 0.52% (120 draws), 0.41% (150) and 0.33% (200) of the true optimum. The gap shrinks with more draws and never quite reaches zero.
  • A shadow price reported as "1.40" to two decimal places can carry a 90% interval of roughly 0.80–2.43 at a realistic posterior spread, with an 18% chance the marginal dollar is actually below break-even, and up to a one-in-three chance at this framework's own prior-scale noise level.
  • classify_tier, which drives the Augur report's Scale/Test/Hold/Reduce language, is already built from full ROI intervals and doesn't need this upgrade. The opportunity sits specifically in the budget optimizer's own numeric outputs, not the evidence-tiered report tables.
  • The upgrade mirrors Smith-Winkler's own prescription and Mak-Morton-Wood's own practice: attach a per-draw distribution to shadow price and marginal ROAS (the draws already exist), evaluate the committed plan against an independent sample rather than the one that picked it, and shrink the curve before optimizing rather than after.

References