Mathematical programming / Stochastic programming

Two-Stage Stochastic Programming

Make a decision now, observe uncertainty, then make a follow-up decision — optimizing the first-stage choice against the expected cost of every possible second-stage outcome.

Note Advanced

Short note. A compact reference, correct as far as it goes. It will be expanded into a full guide with a worked example and an interactive demonstration.

Problem

Some decisions have to be made before uncertainty resolves (how much capacity to build, how much inventory to pre-position), while other, corrective decisions can be made after (how much to actually produce, once demand is known). A deterministic model that just plugs in expected demand systematically gets this trade-off wrong — it’s why “plan to the average” so often underperforms in practice.

When to use it

Capacity planning, pre-positioning inventory ahead of a demand season, network design under uncertain future demand — anywhere a “commit now, adjust later” structure exists and you have (or can construct) a set of plausible future scenarios.

Formulation

First-stage decision x (made before uncertainty), second-stage recourse decision y_s for each scenario s with probability p_s:

minimize   c^T x + sum_s(p_s * q^T y_s)
subject to:
  A x = b                          # first-stage constraints
  W y_s = h_s - T x   for all s    # second-stage constraints, per scenario
  x >= 0, y_s >= 0

The second-stage constraints depend on both the scenario’s data (h_s) and the first-stage decision (T x) — that coupling is what makes this genuinely a two-stage problem rather than just solving each scenario independently and averaging.

Code sketch

import pulp

scenarios = {"low": (0.3, 300), "mid": (0.5, 500), "high": (0.2, 800)}

prob = pulp.LpProblem("two_stage", pulp.LpMinimize)
capacity = pulp.LpVariable("capacity", lowBound=0)   # first-stage
produced = {s: pulp.LpVariable(f"produced_{s}", lowBound=0) for s in scenarios}
shortage = {s: pulp.LpVariable(f"shortage_{s}", lowBound=0) for s in scenarios}

prob += (build_cost * capacity +
         pulp.lpSum(p * (prod_cost * produced[s] + shortage_penalty * shortage[s])
                    for s, (p, _) in scenarios.items()))

for s, (_, demand) in scenarios.items():
    prob += produced[s] <= capacity
    prob += produced[s] + shortage[s] >= demand

Pitfalls

  • The number of second-stage variables grows with the number of scenarios — with enough scenarios this becomes a genuinely large LP/MILP. For big scenario trees, Benders decomposition (solve the first stage, then solve scenario subproblems and feed cuts back) is the standard scaling trick, not brute-force enumeration.
  • Scenario quality matters more than model sophistication. Three badly-chosen scenarios will mislead you more than “plan to the average” would — invest in getting a representative scenario set (or sampling approach) before adding modeling complexity.
  • This is fundamentally different from robust optimization, which optimizes against the worst case rather than the expected case — pick based on whether your business cares about average performance or worst-case protection.