A full end-to-end optimisation wizard using Streamlit, Python LP modelling and KPI forecasting to intelligently allocate marketing budget across platforms.
See the codeA decision-support framework for marketing budget allocation.
Published in SoftwareX (Elsevier): Rezvanjoo, H. (2026). CLARO: Constrained budget allocation with rule-based decision interpretation. SoftwareX, 35, 102935. https://doi.org/10.1016/j.softx.2026.102935
This project provides a complete decision support system that helps decision-makers allocate a limited marketing budget across platforms and objectives, accounting for real-world constraints.
Instead of looking at channels separately, the system treats marketing as a constrained optimisation problem. It integrates past performance data, business goals, and uncertainty to generate clear, structured recommendations.
The framework is designed to help managers make better decisions, not just to automate processes.
CLARO records anonymous usage events (session start, optimisation started, optimisation completed) in a Supabase table to measure adoption. No user inputs, budget figures, uploaded data, results, IP addresses or cookies are stored.
📖 Project wiki: https://github.com/Hoda834/digital-budget-optimisation-engine/wiki
In practice, marketing decisions rarely fail because of a lack of data. They fail because:
This project fills these gaps by dividing decision design, optimisation, and interpretation into clear, reviewable steps.
The wizard collects every input the optimiser needs, with nothing hidden:
All assumptions are visible on the results page and editable via the Refine and re-solve panel — no need to walk the wizard from step 1 to try a different floor or carve-out.
A PuLP/CBC LP allocates budget across platform-objective cells, maximising weighted productivity subject to all declared constraints.
The LP:
Twelve built-in platforms with curated KPI configs. Google appears as three distinct surfaces — Search, Display, and Performance Max — because their lead-gen productivities differ by an order of magnitude and lumping them obscures decisions a marketer actually needs to make.
Every canonical KPI is a count (the uniform-units refactor folded Engagement Rate / View Rate / CTR into summed count components like likes + comments + shares + saves). This means all four KPIs on a platform share the same unit, so the LP doesn't mix percentages and totals. Where the schema lists Purchases as a distinct conversion event, it has its own canonical alongside Leads / Conversions:
| Platform | KPIs (Awareness · Engagement · Traffic · Lead Gen) |
|---|---|
| Reach · Engagement (reactions+comments+shares+saves+follows) · Link Clicks · Leads + Purchases | |
| Reach · Engagement (likes+comments+shares+saves+follows) · Link Clicks · Leads + Purchases | |
| Impressions · Engagement (reactions+comments+shares+followers) · Clicks · Leads | |
| YouTube | Views · Engagement (likes+comments+shares+subscribers) · Clicks · Conversions + Purchases |
| Google Search | Impressions · (no engagement KPI) · Clicks · Conversions + Purchases |
| Google Display | Impressions · (no engagement KPI) · Clicks · Conversions + Purchases |
| Google Performance Max | Impressions · (no engagement KPI) · Clicks · Conversions + Purchases |
| TikTok | Video Views · Engagement (likes+comments+shares+saves+followers) · Clicks · Leads + Purchases |
| Impressions · Saves · Outbound Clicks · Leads + Checkouts | |
| X (Twitter) | Impressions · Engagement (likes+replies+reposts+bookmarks+followers) · Link Clicks · Leads |
| Snapchat | Reach · Engagement (story opens+shares+subscribers) · Swipe-ups · Leads + Purchases |
| Impressions · Engagement (upvotes+comments+shares+followers) · Clicks · Leads |
Google surfaces have no engagement KPI because CTR was a rate (violates uniform units within a platform) and "engaged clicks" would duplicate the Traffic KPI.
CSV import for every supported platform — drop the platform's standard export into the form and the parser:
, / ; / \t)Total --, Meta's summary rows)Engagement Rate column (no individual count breakouts) is still parsed — engagement count is derived as rate × awareness_countEach platform reports its own metrics — Facebook's "engagement" is a different beast from LinkedIn's. The LP needs one number per canonical category, so this layer makes the composition explicit:
FB_EN_ENGAGEMENT = reactions + comments + shares + saves. Link clicks are explicitly excluded — they're in the Traffic category, so including them here would double-count.The composition is auditable — every parsed CSV returns a breakdown showing per-component values, the operator, and which fallback (if any) was taken.
Module 6 produces per-KPI forecasts from the LP allocation. Uncertainty bands are data-driven (and are deliberately not called "confidence" — that concept is reserved for Module 7's diagnostic index, a separate concept):
√(30 / historical_days) — a 90-day history produces a ~17% band, a 7-day history ~62%.lp_used + reserve ≤ scenario_total always holds.Configurable via Module7Policy — every threshold (corner concentration, diagnostic-index penalties, Plan B cap) is a named, defaulted field on a frozen dataclass, not a magic literal.
Outputs per scenario:
The system is built to be modular and easy to expand, with each decision layer working as its own module.
It is a decision-support framework for media-mix planning: given declared constraints (budget, floors, goal values, seasonality), it produces an auditable allocation across platform-objective cells, with diagnostics for why the optimiser stopped there and how robust the recommendation is.
It isn't a marketing optimisation engine in the operator sense. Specifically:
For the audiences this is designed for (strategists, agency planning leads, in-house quarterly planning, decision-science teaching), the scope is right-sized. For continuous budget optimisation against live platform data, you'd want a different product (Northbeam, Triple Whale, Funnel.io class).
The engine (modules M1 to M7, without the Streamlit UI) is available on PyPI:
pip install claro-engine
from claro_engine.modules.module5 import run_module5
from claro_engine.core.wizard_state import WizardState
pip install -r requirements.txt
streamlit run src/app.py
The application opens in your browser at http://localhost:8501.
A hosted demo is also available at https://claro-decision-support.streamlit.app/
A quick way to verify the pipeline without the UI:
PYTHONPATH=src python tests/behavioural_check.py
That runs 10+ realistic scenarios (B2B SaaS, leads-only, engagement-only, with and without goal values, with and without test-and-learn reserve, with and without seasonality) and prints the full M1→M7 output for each.
The test suite uses pytest:
pip install pytest
pytest -q
The suite covers 243 cases across eight files — tests/smoke_test.py (happy-path and feature regressions), tests/test_edge_cases.py (encoding, malformed input, infeasibility, custom platforms, rate-only campaigns, multi-component composition, Monte Carlo, all-platforms stress), tests/test_accuracy.py (hand-computable numeric checks), tests/test_bug_fixes.py (named regression guards), tests/test_plan_b_feasibility.py (the risk-managed alternative stays within Module 2's floors), tests/test_diagnostic_index_label.py (narrative wording and score bounds), tests/test_examples_reproduce.py (runs every script in examples/ and pins its documented figures), and tests/test_behavioural_invariants.py (asserted behavioural expectations, Plan B floor/conservation/degradation guarantees, diagnostic-index invariants, a golden reference scenario, and randomised property checks).
The same command runs automatically on every push and pull request via GitHub Actions.
The examples/ folder contains runnable, self-verifying examples. Every
script executes the full pipeline (no mocked components) and prints the
allocation, classification, and diagnostic index it produces.
PYTHONPATH=src python examples/case_study/run_case_study.py
PYTHONPATH=src python examples/case_study/run_data_sensitivity.py
PYTHONPATH=src python examples/case_study/run_parameter_sensitivity.py
PYTHONPATH=src python examples/minimal_examples/run_balanced_example.py
PYTHONPATH=src python examples/minimal_examples/run_concentrated_example.py
PYTHONPATH=src python examples/benchmark/run_benchmark.py
examples/case_study/ is a five-platform, two-objective allocation
under three policy configurations. Its campaign_data.xlsx is a
filled copy of the app's unified import template, so the same run can
be reproduced by hand by uploading that exact workbook into Module 3
of the guided interface. SCENARIO.md documents the inputs,
provenance, and expected output. The two sensitivity scripts sweep
the input data and the engine's own heuristic constants respectively.examples/minimal_examples/ holds two small end-to-end cases: a
balanced two-platform optimum, and a concentrated three-platform
optimum where the risk-managed alternative visibly redistributes.examples/benchmark/ times the LP solver across problem sizes.
Absolute timings vary by hardware; the reproducible claim is the
sub-linear, sub-100ms scaling..
├── src/ # Source code
│ ├── app.py # Streamlit UI + wizard orchestration
│ └── claro_engine/ # Installable package (pip install claro-engine)
│ ├── core/
│ │ ├── wizard_state.py # State machine, custom platforms, goal values,
│ │ │ # carve-out, seasonality
│ │ ├── kpi_config.py # Built-in + custom platform KPI catalogue
│ │ └── csv_import.py # CSV parsing + composition layer
│ └── modules/
│ ├── module1.py # Objective, budget, currency, duration,
│ │ # goal values, carve-out, seasonality
│ ├── module2.py # Platform selection + priority ranks
│ ├── module3.py # Historical KPIs (manual or via CSV)
│ ├── module4.py # Cost-per-unit + outlier sweep
│ ├── module5.py # LP with shrinkage, Monte Carlo, diagnostics
│ ├── module6.py # Forecasts with data-driven uncertainty bands
│ └── module7.py # Insights + Module7Policy
├── conftest.py # Puts src/ on sys.path for tests
├── docs/ # Design + modelling documentation
├── examples/ # Runnable, self-verifying examples
│ ├── case_study/
│ │ ├── campaign_data.xlsx # Filled unified import workbook
│ │ ├── SCENARIO.md # Inputs, provenance, expected output
│ │ ├── run_case_study.py # Three policy configurations
│ │ ├── run_data_sensitivity.py # Input-data productivity sweep
│ │ └── run_parameter_sensitivity.py # Heuristic-parameter sweep (5 sub-analyses)
│ ├── minimal_examples/
│ │ ├── run_balanced_example.py # Balanced two-platform optimum
│ │ └── run_concentrated_example.py # Concentrated optimum + risk-managed plan
│ └── benchmark/
│ └── run_benchmark.py # LP solve-time scaling
├── tests/
│ ├── conftest.py # Headless session-state fixture
│ ├── smoke_test.py # Happy-path + feature regressions
│ ├── test_edge_cases.py # Adversarial: encoding, malformed, infeasible
│ ├── test_accuracy.py # Hand-computable numeric checks
│ ├── test_bug_fixes.py # Named regression guards
│ ├── test_plan_b_feasibility.py # Risk-managed plan stays within Module 2 floors
│ ├── test_diagnostic_index_label.py # Narrative wording + score bounds
│ ├── test_examples_reproduce.py # Runs every examples/ script, pins its figures
│ ├── test_behavioural_invariants.py # Asserted invariants, golden reference, property checks
│ └── behavioural_check.py # Realistic scenarios printed end-to-end (demo, not collected)
├── test_datasets/ # Scenario fixtures + internal verifiers
│ ├── 01_b2b_saas_leadgen/ … 06_accuracy_hand_computable/
│ ├── _generate.py # Regenerates the fixture CSVs
│ ├── _run_scenarios.py # Runs all scenarios end-to-end
│ └── _verify_lp.py # Hand-checked LP cases
├── .github/workflows/tests.yml # CI runs pytest on every push
├── LICENSE.txt
├── CITATION.cff
├── requirements.txt
└── README.md
Detailed explanations of system behaviour and modelling choices are provided in the docs/ directory:
These documents expand on the architectural and decision principles outlined above.
This framework is intended for:
The system supports decisions; it does not automate them.
This project demonstrates applied expertise in:
If you use this software in academic work, please cite it using the metadata in CITATION.cff, or as follows:
Rezvanjoo, H. (2026). CLARO: Constrained Linear Allocation and Resource Optimiser — a Decision-Support Framework for Marketing Budget Allocation (Version 0.2.1) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.21230206
Version DOI (v0.2.1, the archived snapshot): 10.5281/zenodo.21230206 Concept DOI (always resolves to the latest version): 10.5281/zenodo.20517492
The project is shared openly to invite technical review and constructive feedback, particularly on:
Issues and discussions are welcome.
This project is released under the MIT License. See LICENSE.txt for details.
Hoda Rezvanjoo Independent Researcher ORCID: 0009-0006-3882-2669 Website: hodarezvanjoo.com
A full end-to-end optimisation wizard using Streamlit, Python LP modelling and KPI forecasting to intelligently allocate marketing budget across platforms.
See the codeA decision-support framework for marketing budget allocation.
Published in SoftwareX (Elsevier): Rezvanjoo, H. (2026). CLARO: Constrained budget allocation with rule-based decision interpretation. SoftwareX, 35, 102935. https://doi.org/10.1016/j.softx.2026.102935
This project provides a complete decision support system that helps decision-makers allocate a limited marketing budget across platforms and objectives, accounting for real-world constraints.
Instead of looking at channels separately, the system treats marketing as a constrained optimisation problem. It integrates past performance data, business goals, and uncertainty to generate clear, structured recommendations.
The framework is designed to help managers make better decisions, not just to automate processes.
CLARO records anonymous usage events (session start, optimisation started, optimisation completed) in a Supabase table to measure adoption. No user inputs, budget figures, uploaded data, results, IP addresses or cookies are stored.
📖 Project wiki: https://github.com/Hoda834/digital-budget-optimisation-engine/wiki
In practice, marketing decisions rarely fail because of a lack of data. They fail because:
This project fills these gaps by dividing decision design, optimisation, and interpretation into clear, reviewable steps.
The wizard collects every input the optimiser needs, with nothing hidden:
All assumptions are visible on the results page and editable via the Refine and re-solve panel — no need to walk the wizard from step 1 to try a different floor or carve-out.
A PuLP/CBC LP allocates budget across platform-objective cells, maximising weighted productivity subject to all declared constraints.
The LP:
Twelve built-in platforms with curated KPI configs. Google appears as three distinct surfaces — Search, Display, and Performance Max — because their lead-gen productivities differ by an order of magnitude and lumping them obscures decisions a marketer actually needs to make.
Every canonical KPI is a count (the uniform-units refactor folded Engagement Rate / View Rate / CTR into summed count components like likes + comments + shares + saves). This means all four KPIs on a platform share the same unit, so the LP doesn't mix percentages and totals. Where the schema lists Purchases as a distinct conversion event, it has its own canonical alongside Leads / Conversions:
| Platform | KPIs (Awareness · Engagement · Traffic · Lead Gen) |
|---|---|
| Reach · Engagement (reactions+comments+shares+saves+follows) · Link Clicks · Leads + Purchases | |
| Reach · Engagement (likes+comments+shares+saves+follows) · Link Clicks · Leads + Purchases | |
| Impressions · Engagement (reactions+comments+shares+followers) · Clicks · Leads | |
| YouTube | Views · Engagement (likes+comments+shares+subscribers) · Clicks · Conversions + Purchases |
| Google Search | Impressions · (no engagement KPI) · Clicks · Conversions + Purchases |
| Google Display | Impressions · (no engagement KPI) · Clicks · Conversions + Purchases |
| Google Performance Max | Impressions · (no engagement KPI) · Clicks · Conversions + Purchases |
| TikTok | Video Views · Engagement (likes+comments+shares+saves+followers) · Clicks · Leads + Purchases |
| Impressions · Saves · Outbound Clicks · Leads + Checkouts | |
| X (Twitter) | Impressions · Engagement (likes+replies+reposts+bookmarks+followers) · Link Clicks · Leads |
| Snapchat | Reach · Engagement (story opens+shares+subscribers) · Swipe-ups · Leads + Purchases |
| Impressions · Engagement (upvotes+comments+shares+followers) · Clicks · Leads |
Google surfaces have no engagement KPI because CTR was a rate (violates uniform units within a platform) and "engaged clicks" would duplicate the Traffic KPI.
CSV import for every supported platform — drop the platform's standard export into the form and the parser:
, / ; / \t)Total --, Meta's summary rows)Engagement Rate column (no individual count breakouts) is still parsed — engagement count is derived as rate × awareness_countEach platform reports its own metrics — Facebook's "engagement" is a different beast from LinkedIn's. The LP needs one number per canonical category, so this layer makes the composition explicit:
FB_EN_ENGAGEMENT = reactions + comments + shares + saves. Link clicks are explicitly excluded — they're in the Traffic category, so including them here would double-count.The composition is auditable — every parsed CSV returns a breakdown showing per-component values, the operator, and which fallback (if any) was taken.
Module 6 produces per-KPI forecasts from the LP allocation. Uncertainty bands are data-driven (and are deliberately not called "confidence" — that concept is reserved for Module 7's diagnostic index, a separate concept):
√(30 / historical_days) — a 90-day history produces a ~17% band, a 7-day history ~62%.lp_used + reserve ≤ scenario_total always holds.Configurable via Module7Policy — every threshold (corner concentration, diagnostic-index penalties, Plan B cap) is a named, defaulted field on a frozen dataclass, not a magic literal.
Outputs per scenario:
The system is built to be modular and easy to expand, with each decision layer working as its own module.
It is a decision-support framework for media-mix planning: given declared constraints (budget, floors, goal values, seasonality), it produces an auditable allocation across platform-objective cells, with diagnostics for why the optimiser stopped there and how robust the recommendation is.
It isn't a marketing optimisation engine in the operator sense. Specifically:
For the audiences this is designed for (strategists, agency planning leads, in-house quarterly planning, decision-science teaching), the scope is right-sized. For continuous budget optimisation against live platform data, you'd want a different product (Northbeam, Triple Whale, Funnel.io class).
The engine (modules M1 to M7, without the Streamlit UI) is available on PyPI:
pip install claro-engine
from claro_engine.modules.module5 import run_module5
from claro_engine.core.wizard_state import WizardState
pip install -r requirements.txt
streamlit run src/app.py
The application opens in your browser at http://localhost:8501.
A hosted demo is also available at https://claro-decision-support.streamlit.app/
A quick way to verify the pipeline without the UI:
PYTHONPATH=src python tests/behavioural_check.py
That runs 10+ realistic scenarios (B2B SaaS, leads-only, engagement-only, with and without goal values, with and without test-and-learn reserve, with and without seasonality) and prints the full M1→M7 output for each.
The test suite uses pytest:
pip install pytest
pytest -q
The suite covers 243 cases across eight files — tests/smoke_test.py (happy-path and feature regressions), tests/test_edge_cases.py (encoding, malformed input, infeasibility, custom platforms, rate-only campaigns, multi-component composition, Monte Carlo, all-platforms stress), tests/test_accuracy.py (hand-computable numeric checks), tests/test_bug_fixes.py (named regression guards), tests/test_plan_b_feasibility.py (the risk-managed alternative stays within Module 2's floors), tests/test_diagnostic_index_label.py (narrative wording and score bounds), tests/test_examples_reproduce.py (runs every script in examples/ and pins its documented figures), and tests/test_behavioural_invariants.py (asserted behavioural expectations, Plan B floor/conservation/degradation guarantees, diagnostic-index invariants, a golden reference scenario, and randomised property checks).
The same command runs automatically on every push and pull request via GitHub Actions.
The examples/ folder contains runnable, self-verifying examples. Every
script executes the full pipeline (no mocked components) and prints the
allocation, classification, and diagnostic index it produces.
PYTHONPATH=src python examples/case_study/run_case_study.py
PYTHONPATH=src python examples/case_study/run_data_sensitivity.py
PYTHONPATH=src python examples/case_study/run_parameter_sensitivity.py
PYTHONPATH=src python examples/minimal_examples/run_balanced_example.py
PYTHONPATH=src python examples/minimal_examples/run_concentrated_example.py
PYTHONPATH=src python examples/benchmark/run_benchmark.py
examples/case_study/ is a five-platform, two-objective allocation
under three policy configurations. Its campaign_data.xlsx is a
filled copy of the app's unified import template, so the same run can
be reproduced by hand by uploading that exact workbook into Module 3
of the guided interface. SCENARIO.md documents the inputs,
provenance, and expected output. The two sensitivity scripts sweep
the input data and the engine's own heuristic constants respectively.examples/minimal_examples/ holds two small end-to-end cases: a
balanced two-platform optimum, and a concentrated three-platform
optimum where the risk-managed alternative visibly redistributes.examples/benchmark/ times the LP solver across problem sizes.
Absolute timings vary by hardware; the reproducible claim is the
sub-linear, sub-100ms scaling..
├── src/ # Source code
│ ├── app.py # Streamlit UI + wizard orchestration
│ └── claro_engine/ # Installable package (pip install claro-engine)
│ ├── core/
│ │ ├── wizard_state.py # State machine, custom platforms, goal values,
│ │ │ # carve-out, seasonality
│ │ ├── kpi_config.py # Built-in + custom platform KPI catalogue
│ │ └── csv_import.py # CSV parsing + composition layer
│ └── modules/
│ ├── module1.py # Objective, budget, currency, duration,
│ │ # goal values, carve-out, seasonality
│ ├── module2.py # Platform selection + priority ranks
│ ├── module3.py # Historical KPIs (manual or via CSV)
│ ├── module4.py # Cost-per-unit + outlier sweep
│ ├── module5.py # LP with shrinkage, Monte Carlo, diagnostics
│ ├── module6.py # Forecasts with data-driven uncertainty bands
│ └── module7.py # Insights + Module7Policy
├── conftest.py # Puts src/ on sys.path for tests
├── docs/ # Design + modelling documentation
├── examples/ # Runnable, self-verifying examples
│ ├── case_study/
│ │ ├── campaign_data.xlsx # Filled unified import workbook
│ │ ├── SCENARIO.md # Inputs, provenance, expected output
│ │ ├── run_case_study.py # Three policy configurations
│ │ ├── run_data_sensitivity.py # Input-data productivity sweep
│ │ └── run_parameter_sensitivity.py # Heuristic-parameter sweep (5 sub-analyses)
│ ├── minimal_examples/
│ │ ├── run_balanced_example.py # Balanced two-platform optimum
│ │ └── run_concentrated_example.py # Concentrated optimum + risk-managed plan
│ └── benchmark/
│ └── run_benchmark.py # LP solve-time scaling
├── tests/
│ ├── conftest.py # Headless session-state fixture
│ ├── smoke_test.py # Happy-path + feature regressions
│ ├── test_edge_cases.py # Adversarial: encoding, malformed, infeasible
│ ├── test_accuracy.py # Hand-computable numeric checks
│ ├── test_bug_fixes.py # Named regression guards
│ ├── test_plan_b_feasibility.py # Risk-managed plan stays within Module 2 floors
│ ├── test_diagnostic_index_label.py # Narrative wording + score bounds
│ ├── test_examples_reproduce.py # Runs every examples/ script, pins its figures
│ ├── test_behavioural_invariants.py # Asserted invariants, golden reference, property checks
│ └── behavioural_check.py # Realistic scenarios printed end-to-end (demo, not collected)
├── test_datasets/ # Scenario fixtures + internal verifiers
│ ├── 01_b2b_saas_leadgen/ … 06_accuracy_hand_computable/
│ ├── _generate.py # Regenerates the fixture CSVs
│ ├── _run_scenarios.py # Runs all scenarios end-to-end
│ └── _verify_lp.py # Hand-checked LP cases
├── .github/workflows/tests.yml # CI runs pytest on every push
├── LICENSE.txt
├── CITATION.cff
├── requirements.txt
└── README.md
Detailed explanations of system behaviour and modelling choices are provided in the docs/ directory:
These documents expand on the architectural and decision principles outlined above.
This framework is intended for:
The system supports decisions; it does not automate them.
This project demonstrates applied expertise in:
If you use this software in academic work, please cite it using the metadata in CITATION.cff, or as follows:
Rezvanjoo, H. (2026). CLARO: Constrained Linear Allocation and Resource Optimiser — a Decision-Support Framework for Marketing Budget Allocation (Version 0.2.1) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.21230206
Version DOI (v0.2.1, the archived snapshot): 10.5281/zenodo.21230206 Concept DOI (always resolves to the latest version): 10.5281/zenodo.20517492
The project is shared openly to invite technical review and constructive feedback, particularly on:
Issues and discussions are welcome.
This project is released under the MIT License. See LICENSE.txt for details.
Hoda Rezvanjoo Independent Researcher ORCID: 0009-0006-3882-2669 Website: hodarezvanjoo.com