Skip to main content

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

DOI

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)
Facebook Reach ยท Engagement (reactions+comments+shares+saves+follows) ยท Link Clicks ยท Leads + Purchases
Instagram Reach ยท Engagement (likes+comments+shares+saves+follows) ยท Link Clicks ยท Leads + Purchases
LinkedIn 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
Pinterest 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
Reddit 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 Rate column (no individual count breakouts) is still parsed โ€” engagement count is derived as rate ร— 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_total always 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. 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.

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

claro_engine-0.2.1.post1.tar.gz (114.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

claro_engine-0.2.1.post1-py3-none-any.whl (76.1 kB view details)

Uploaded Python 3

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

Hashes for claro_engine-0.2.1.post1.tar.gz
Algorithm Hash digest
SHA256 b9458da5cfea2f67fe34f201413f2cb1d890ebdc8f9c2b53a7ad44391c779734
MD5 1fdaf4001281826c3de911e26775f096
BLAKE2b-256 76ab43d2ccb42bd1331e6021860d539eaa55765a6837eb155f9f8392641104f1

See more details on using hashes here.

File details

Details for the file claro_engine-0.2.1.post1-py3-none-any.whl.

File metadata

File hashes

Hashes for claro_engine-0.2.1.post1-py3-none-any.whl
Algorithm Hash digest
SHA256 f7afa7abf56e978000a165a5d6c9cbc71cd3ecac92e1d52c50bf36f67074809d
MD5 5424713094e11a707a34a1aff558a921
BLAKE2b-256 36f57f55b86fd22e3823b0261aaf9915c61bab4c0f01b1d92e7aab5ec8ef2735

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page