-
Notifications
You must be signed in to change notification settings - Fork 1
Module 5 — LP Optimiser
File: modules/module5.py (≈1,500 lines — the engine's heart)
Result types: Module5LPResult, Module5ScenarioBundle,
Module5MonteCarloResult
Builds a linear program over (platform, goal) cells, solves it
under three scenarios, returns allocations + diagnostics +
(optionally) Monte Carlo robustness analysis.
One variable per active (p, g) cell:
x_{p,g} = budget allocated to platform p for goal g (£)
…plus bracket variables for the piecewise-linear diminishing-returns structure (see below).
-
w_g— system goal weight: priority-frequency, or value-derived (value × productivity) when goal values are set. -
\tilde r_{p,g}— per-goal-normalised productivity after shrinkage.
| Constraint | Form | Why |
|---|---|---|
| Total budget cap | Σ x_{p,g} ≤ B × (1 − tl_pct) |
Test-and-learn carve-out |
| Per-platform floor | Σ_g x_{p,g} ≥ floor_p |
User-declared minimum spend |
| Per-goal floor | Σ_p x_{p,g} ≥ floor_g |
User-declared per-objective minimum |
| Goal validity |
x_{p,g} = 0 when g ∉ goals_by_platform[p]
|
A platform can't be funded for a goal it doesn't serve |
| Diminishing-returns brackets | 3 slices per cell at 25 / 35 / 40 % of budget at yields 1.0 / 0.65 / 0.35 | No single cell can absorb the whole plan |
Every constraint is created as a named LpConstraint, so
Module5LPResult.binding_constraints returns labels a human can
read with their shadow prices attached. "Why is Facebook capped?"
has a literal answer.
r_{p,g} = (1 / cpu_{p,g,var}) · (1 / Σ_p 1/cpu_{p,g,var})
After normalisation, productivities within a goal sum to 1. Cross- goal comparison becomes scale-free — the goal weights are the control knob, not whether a goal happens to be measured in clicks (large numbers) or leads (small numbers).
A 30-day history gets α = 0.5 (half its own data, half the
cross-platform prior). A 7-day history α ≈ 0.19. A 365-day
history α ≈ 0.92. This is the standard remedy for the
small-sample-winner failure mode in allocation problems —
without it, a fluky strong week on one platform out-competes a
year of mediocre data on another.
Same LP, three solves:
- Conservative — multiplier 1/1.2 ≈ 0.83. Per-goal multipliers raise upper-funnel productivity.
- Base — multiplier 1.0. No goal adjustments.
- Optimistic — multiplier 1.15. Per-goal multipliers raise conversion productivity.
This labelled-triple format matches how strategists communicate; the opt-in Monte Carlo (below) is for users who want a full distribution.
run_module5_montecarlo re-solves the base LP N times (default
200, max 1000) with productivities perturbed by their observed
per-cell coefficient of variation (mean-preserving lognormal
shocks). Returns:
- Per-platform allocation mean / std / p5 / p50 / p95
-
unstable_platforms— those with allocation CV above the threshold (DEFAULT_INSTABILITY_CV = 0.20) -
stability_score—1 − mean CVacross materially-funded platforms (mean > £1), clamped to[0, 1]. A plan-level scalar a marketer can quote. -
cell_sigma— the per-cell noise scale used for sampling, so the user can see which assumptions had the most noise
Solves are deterministic given a seed — identical inputs and seed produce identical distributions. Reproducible.
-
Module5ValidationError— infeasible LP (impossible floor combination), all-zero productivities, scenario multiplier ≤ 0. -
FlowStateError— Module 4 not finalised.