CLARO: Constrained Linear Allocation and Resource Optimiser. A decision-support engine for constrained marketing budget allocation using Linear Programming.
Project description
CLARO โ Constrained Linear Allocation and Resource Optimiser
A decision-support framework for marketing budget allocation.
Overview
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.
๐ Project wiki: https://github.com/Hoda834/digital-budget-optimisation-engine/wiki
What Problem Does This Solve?
In practice, marketing decisions rarely fail because of a lack of data. They fail because:
- Budgets are limited and must satisfy competing objectives
- Platform performance is interdependent, not independent
- Business constraints are applied informally or too late
- Scenario uncertainty is ignored or oversimplified
- Outputs lack interpretation, making decisions hard to justify
This project fills these gaps by dividing decision design, optimisation, and interpretation into clear, reviewable steps.
Core Capabilities
1. Structured Decision Design (Wizard-Based)
The wizard collects every input the optimiser needs, with nothing hidden:
- Marketing objectives (Awareness, Engagement, Website Traffic, Lead Generation)
- Total budget, currency, and campaign duration
- Platform selection with priority ranking per objective
- Per-platform minimum spend and per-objective minimum budget
- Three scenarios (conservative / base / optimistic) with editable multipliers
- Goal value weights โ what ยฃ value the business places on one unit of each KPI (e.g. ยฃ200 per lead, ยฃ0.001 per impression). When present, drives utility-weighted optimisation instead of rank-based heuristics.
- Test-and-learn reserve โ % of every scenario's budget held back for new audiences / creative tests (10โ15% is standard strategist practice).
- Seasonality multipliers โ per-goal expected productivity vs. historical (e.g. 0.4ร for December reach if Q4 CPMs typically inflate).
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.
2. Linear Programming Optimisation Engine
A PuLP/CBC LP allocates budget across platform-objective cells, maximising weighted productivity subject to all declared constraints.
The LP:
- Diminishing-returns brackets per cell (25% / 35% / 40% of budget at yields 1.0 / 0.65 / 0.35) so a single cell can't absorb the whole plan even when it has the best productivity.
- Per-goal productivity normalisation so cross-objective comparisons are scale-free โ the goal weights are the actual control knob.
- Data-quality shrinkage (James-Stein-style) pulls short-history platforms toward the cross-platform mean so a 7-day estimate doesn't get the same authority as a 365-day one.
- Named, auditable constraints โ every floor and cap is named, and the LP returns binding constraints + shadow prices so you can see exactly what's shaping the allocation.
3. Platform Catalogue + CSV Import
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:
- Sniffs encoding (UTF-8, UTF-8-BOM, Latin-1) and delimiter (
,/;/\t) - Filters totals rows (Google's
Total --, Meta's summary rows) - Composes canonical KPIs from raw columns with documented rationale (see below)
- Legacy back-compat: an older export with only an
Engagement Ratecolumn (no individual count breakouts) is still parsed โ engagement count is derived asrate ร awareness_count
4. KPI Composition (the layer between platform metrics and the LP)
Each 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:
- Every canonical KPI declares its components (raw columns the platform exports), an operator (sum / first / max / mean), and a rationale explaining what's included and what's deliberately excluded to avoid double-counting.
- Example:
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. - When the broken-out columns aren't in the export, the parser falls back to the platform's bundled metric (Meta's "Post engagement") with a prominent warning that surfaces the double-count risk.
- User override: the "Customise composition" panel lets a marketer re-weight components per platform (count saves 3ร because they're high-intent, reactions 0.5ร, etc.) and recompose.
The composition is auditable โ every parsed CSV returns a breakdown showing per-component values, the operator, and which fallback (if any) was taken.
5. Forecasting Layer with Honest Uncertainty
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):
- If Module 3 has โฅ3 historical observations per KPI, the band is the sample coefficient of variation โ true noise from data.
- Otherwise, the band scales by
โ(30 / historical_days)โ a 90-day history produces a ~17% band, a 7-day history ~62%. - Seasonality multipliers apply to count-KPI forecasts so the predicted volume matches what the LP optimised against.
6. Scenario Comparison and Monte Carlo Robustness
- Three scenarios (conservative / base / optimistic) run side-by-side with their own goal multipliers โ optimistic raises conversion productivity, conservative raises upper-funnel.
- Test-and-learn reserve scales per scenario so
lp_used + reserve โค scenario_totalalways holds. - Opt-in Monte Carlo re-solves the base LP hundreds of times with productivities perturbed by their observed noise. Flags platforms whose share is sensitive to plausible data perturbation โ the platforms whose rank should be treated with caution.
7. Decision Interpretation Layer (Module 7)
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:
- Executive summary explaining the allocation logic
- Classification (Corner-dominant / Concentrated / Balanced / Scenario-sensitive)
- Diagnostic-index (40โ100) penalising concentration, instability, missing forecasts, data-quality flags
- Binding vs non-binding constraints (with shadow prices)
- Plan A (performance-first) and Plan B (risk-managed) with the efficiency trade-off explicit
- Risks and recommendations
- Forecast caveat โ every output carries a paragraph about attribution bias, last-click over-crediting, and the absence of incrementality modelling. The tool is honest about its own epistemic limits.
8. Exportable Decision Artefacts
- PDF decision reports with structured summaries and tables
- Excel files containing all allocation and forecast data
- Clear separation between inputs, assumptions, and results
Technical Architecture
- Language: Python 3.11
- UI: Streamlit
- Optimisation: Linear Programming (PuLP with CBC solver)
- State Management: Wizard-based state controller
- Reporting: PDF (ReportLab), Excel (OpenPyXL)
The system is built to be modular and easy to expand, with each decision layer working as its own module.
What This Tool Is (and Isn't)
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:
- The CSV import is a last-mile parser, not an ETL pipeline. There are no platform-API connectors, no persistent staging, no scheduled refresh.
- Allocations are produced at the platform-objective level, not at the campaign / ad-set / creative level. The hard part of paid media starts after the channel split.
- The LP inherits whatever attribution is in your KPIs. If your platform-reported numbers over-credit Meta or under-credit Search (last-click bias), the LP will inherit that. Incrementality (would these conversions have happened anyway?) is not modelled.
- "Productivity" is a sample mean โ there is no Bayesian recalibration between runs, no learning from outcomes, no causal estimation.
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).
Installation
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
Running the App
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.
Tests
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.
Examples and reproducibility
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. Itscampaign_data.xlsxis 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.mddocuments 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.
Project Structure
.
โโโ 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
Documentation
Detailed explanations of system behaviour and modelling choices are provided in the docs/ directory:
- MODULES.md โ role and responsibility of each module
- SCENARIOS.md โ scenario design and interpretation logic
- DECISION_LOGIC.md โ optimisation assumptions and modelling rationale
- CALIBRATION.md โ every hand-set numeric constant in the engine, its source / intuition, what changes if the value moves, and whether a user-facing override exists
These documents expand on the architectural and decision principles outlined above.
Intended Use
This framework is intended for:
- Marketing and growth decision-makers
- Analytics and strategy professionals
- SMEs operating under constrained budgets
- Researchers working on decision support systems and explainable optimisation
The system supports decisions; it does not automate them.
Why This Project Matters
This project demonstrates applied expertise in:
- Decision Support Systems
- Constrained optimisation
- Data-driven strategy under uncertainty
- Translating analytics into managerial insight
- Responsible and explainable decision design
Citation
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
Feedback
The project is shared openly to invite technical review and constructive feedback, particularly on:
- Optimisation logic and constraints
- Scenario design and robustness
- Decision interpretability and usefulness
Issues and discussions are welcome.
License
This project is released under the MIT License. See LICENSE.txt for details.
Author
Hoda Rezvanjoo Independent Researcher ORCID: 0009-0006-3882-2669 Website: hodarezvanjoo.com
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file claro_engine-0.2.1.post1.tar.gz.
File metadata
- Download URL: claro_engine-0.2.1.post1.tar.gz
- Upload date:
- Size: 114.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b9458da5cfea2f67fe34f201413f2cb1d890ebdc8f9c2b53a7ad44391c779734
|
|
| MD5 |
1fdaf4001281826c3de911e26775f096
|
|
| BLAKE2b-256 |
76ab43d2ccb42bd1331e6021860d539eaa55765a6837eb155f9f8392641104f1
|
File details
Details for the file claro_engine-0.2.1.post1-py3-none-any.whl.
File metadata
- Download URL: claro_engine-0.2.1.post1-py3-none-any.whl
- Upload date:
- Size: 76.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7afa7abf56e978000a165a5d6c9cbc71cd3ecac92e1d52c50bf36f67074809d
|
|
| MD5 |
5424713094e11a707a34a1aff558a921
|
|
| BLAKE2b-256 |
36f57f55b86fd22e3823b0261aaf9915c61bab4c0f01b1d92e7aab5ec8ef2735
|