-
Notifications
You must be signed in to change notification settings - Fork 1
Module 4 — Cost per Unit (CPU)
File: modules/module4.py · Result: Module4Result
Module 4 turns "budget × KPI value" into "cost per unit KPI" for
every canonical count KPI on every active (platform, goal):
cpu_per_goal[platform][goal][var] = platform_budget / kpi_value
This is the productivity backbone the LP optimises against.
- No CPU for rate KPIs. "Cost per percentage point" isn't meaningful in this model; rate KPIs (where present) are forecast in Module 6 but not LP'd.
-
No imputation. A missing or zero KPI value drops the row with
a warning recorded in
skipped_rows. Module 5 sees only cells that survived this step.
CPU_OUTLIER_MULTIPLE = 100.0. Any CPU greater than 100× the
per-goal median is flagged and skipped. This catches:
- Typos (5 leads instead of 500)
- Unit errors (£1 ad spend for "100 impressions" when the export was in thousands)
- Near-zero KPI values that would otherwise produce an effectively infinite "productivity" and dominate the LP
Skipped rows surface in the diagnostics panel rather than being silently dropped, so the user can see what was filtered and why.
Module 4 also stamps the current values of min_spend_per_platform,
min_budget_per_goal, scenario_multipliers, and
scenario_goal_multipliers onto its result. Module 5 then reads a
single immutable snapshot rather than reaching back into
WizardState, eliminating any risk of mid-solve mutation.
This pattern — modules consume snapshots, not live state — is what keeps each module unit-testable in isolation.