Predict-then-Optimize and Its Pitfalls

10 min read Module 3 of 9 Lesson 8 of 25

What you'll learn

  • Explain the optimizer's curse and why optimized plans systematically disappoint
  • Measure prediction models by the regret of the decisions they produce, not only by prediction error
  • Apply practical mitigations: shrinkage, uncertainty penalties, and holding out evaluation of the chosen plan
  • Describe decision-focused learning (such as SPO+) and when it is worth the added complexity
Building this at your company? For enterprise and company teams taking this to production: book a 30-minute session with our AI engineers for architecture guidance, code review, and a rollout plan for your use case.
Book a Team Session

The obvious architecture for a data-driven decision is two steps. Predict the uncertain inputs (demand, value, risk) with ML. Optimize the decision with those predictions plugged in as if they were true. It’s called predict-then-optimize, and it’s how most production systems work, including the one you built in the last lesson.

It works. But it has failure modes that surprise teams, because the two halves were built separately and each one assumes the other is perfect. This lesson is about where it breaks and what to do.

The optimizer’s curse

Suppose you estimate the value of 1,000 possible marketing campaigns. Each estimate is unbiased: on average it’s right. You fund the 20 with the highest estimated value. When results come in, those 20 disappoint, consistently, by a lot. Why?

Because you didn’t pick 20 at random. You picked the 20 with the highest estimates, and an estimate can be high for two reasons: the campaign is good, or the noise was large and positive. Selecting the top preferentially selects positive noise. The chosen campaigns’ estimates are biased upwards because you chose them by maximizing.

import numpy as np
rng = np.random.default_rng(0)

true_value = rng.normal(100, 10, size=1000)       # what each option is really worth
estimate = true_value + rng.normal(0, 20, 1000)    # unbiased, noisy estimates

top = np.argsort(estimate)[-20:]
print("estimated value of chosen:", estimate[top].mean())    # around 150
print("realized value of chosen: ", true_value[top].mean())  # around 110

This is the optimizer’s curse (Smith and Winkler, 2006). It isn’t a bug in your model. It’s what happens whenever you maximize over noisy estimates, and an optimizer is a very effective maximizer. The noisier the estimates and the more options you choose among, the worse it gets.

In the retention example, the customers with the highest estimated uplift are partly customers whose uplift is high and partly customers whose estimate is noisy (small segments, unusual features). The plan will overpromise, and finance will notice.

The solver exploits errors

A related problem: optimizers are drawn to the places where predictions are wrong in their favour.

If a pricing model slightly overestimates demand at high prices for one product, the optimizer will price that product high. If a routing model underestimates travel time on one road, every route will use it. The model’s average error might be tiny, but the optimizer doesn’t care about the average. It goes looking for the most favourable inputs, and those are disproportionately the erroneous ones.

That’s why “the model has a low MSE” is not reassuring on its own. MSE weights all errors equally. The decision cares enormously about errors on the items, regions and directions where it acts, and not at all about the rest.

Evaluate decisions, not just predictions

The first fix is measurement. Evaluate the whole pipeline by the quality of the decisions it produces, on held-out data with realized outcomes:

def regret(plan_from_predictions, outcomes, solve, evaluate):
    """solve(inputs) -> plan; evaluate(plan, outcomes) -> realized objective."""
    hindsight_plan = solve(outcomes)                 # best plan with perfect info
    best = evaluate(hindsight_plan, outcomes)
    actual = evaluate(plan_from_predictions, outcomes)
    return best - actual                             # >= 0; lower is better

# Compare two forecasting models by the regret of the plans they produce,
# averaged over many held-out weeks, not by MSE alone.
for name, model in [("gbm", gbm), ("gbm_quantile", gbm_q)]:
    r = [regret(solve(model.predict(X_w)), y_w, solve, evaluate)
         for X_w, y_w in held_out_weeks]
    print(name, np.mean(r))

Regret is the gap between the value of the decision you made and the value of the best decision in hindsight. It’s the decision-level counterpart of prediction error. Model selection, feature work and hyperparameter tuning should all look at it.

Two cautions. First, evaluate the plan on outcomes it didn’t see, never on the same data used to estimate the values; otherwise the optimizer’s curse inflates your evaluation exactly as it inflates your plan. Second, regret evaluation assumes you can score a plan on realized outcomes. For decisions where the action changes the outcome (offers, prices), you need the causal and off-policy tools of Modules 4 and 5 to do that honestly.

Practical mitigations

Before reaching for anything sophisticated, these simple fixes go a long way:

Shrinkage. Pull noisy estimates towards a sensible prior before optimizing, more strongly for estimates with more uncertainty. Empirical Bayes shrinkage, hierarchical models, or simply blending a segment-level estimate with the global mean. Shrinkage directly counteracts the optimizer’s curse.

Penalize uncertainty. Optimize a pessimistic estimate: the mean minus k standard deviations, or a lower quantile. Options with uncertain value need to look better to be chosen. This is the same idea as the robust methods in the next lesson.

Constrain the plan to where the model is trustworthy. Limit how far prices can move from historical ranges, how much any single item’s quantity can change week to week, how concentrated the allocation can be. These “trust region” constraints keep the optimizer from wandering into regions with no data.

Hold out the chosen plan’s evaluation. Use one sample to estimate values and select, and an independent sample to estimate the value of what you selected. The second estimate is unbiased; report that number to finance.

Decision-focused learning

The more ambitious fix is to train the predictive model for the decision: instead of minimizing prediction error, minimize the regret of the decisions made from its predictions. This is called decision-focused learning or “smart predict-then-optimize”.

The challenge is that the optimizer’s output is a piecewise-constant function of its inputs, so gradients through it are zero almost everywhere. Approaches include:

  • SPO+ loss (Elmachtoub and Grigas): a convex surrogate for regret in linear optimization problems that gives useful gradients.
  • Differentiable optimization layers: solve a smoothed (regularized) version of the optimization so it can be differentiated.
  • Perturbation methods: add noise to the inputs and average solutions, which smooths the decision as a function of the predictions.
  • Weighted losses: a simple approximation that weights each training example’s prediction error by how much it matters to decisions, for example by the item’s margin or the sensitivity of the plan to it.

Decision-focused learning pays off when the model can’t be accurate everywhere (limited features, misspecified model) and should spend its accuracy where decisions are sensitive. If a flexible model already predicts well, two-step training is close to optimal and much simpler to operate. The weighted-loss approximation is a sensible first step: it fits into any existing training pipeline.

A checklist for predict-then-optimize systems

  • Report regret on held-out periods next to MSE, MAPE or AUC.
  • Shrink noisy estimates, especially for small segments and new items.
  • Add trust-region constraints around historical behaviour.
  • Estimate the value of the chosen plan on independent data.
  • Watch for the optimizer concentrating on a few items, segments or routes; it’s often chasing errors.

So far the predictions going into the optimizer have been point estimates. The next lesson brings in the uncertainty from Lesson 6 and optimizes against it directly.

Knowledge Check

4 questions to test your understanding

1 You estimate the value of 1,000 candidate projects with unbiased but noisy models and fund the 20 with the highest estimates. What should you expect when results come in?

2 Model A has lower mean squared error on demand than model B, but the inventory plan built from model B costs less. How is that possible?

3 What does 'regret' measure when evaluating a predict-then-optimize pipeline?

4 When is decision-focused learning (training the model on downstream decision loss) most worth it?

Go further with expert guidance

Ready to build production AI?
Talk to our R&D team.

These courses give you the foundation. Our embedded AI teams take you from prototype to production in 30–90 days, with your team, your codebase, your goals. Book a free strategy call to see how we can accelerate your AI initiative.

30 minutes · No obligation · Expert AI engineers, not sales reps

AI Architecture Review

Audit your current stack and identify high-impact improvements

Project Review

Get expert feedback on your AI implementation and codebase

Team Mentoring

Upskill your engineers with hands-on AI coaching sessions

AI Strategy

Define your AI roadmap, prioritization, and implementation plan