Probabilistic revenue forecasting across Google, Meta, and Bing ad channels for one e-commerce brand. Generates 30/60/90-day P10/P50/P90 revenue estimates from planned ad spend. Built for the Netelixir hackathon. Data note: this repo does NOT include the source dataset (output/master_df.parquet), which is confidential client data. Place the parquet at output/master_df.parquet locally to run the code.
Revenue = ROAS x Spend. Spend is a user input (never predicted, so no leakage). ROAS is forecast as a seasonal P10/P50/P90 distribution per channel.
- Google (src/google_forecaster.py): Search + PMax seasonal band engine with center-shrinkage; Shopping log-saturation curve + borrowed PMax seasonality. Backtest: 94% coverage, 10.5% avg MAPE.
- Meta (src/meta_forecaster.py): Prospecting + Remarketing pooled; Generic breakeven. 100% coverage, 35.5% MAPE overall / 7.4% on two-year months.
- Bing (src/bing_forecaster.py): Adaptive Shrinkage ROAS, scored by dollar error (~3.4K mean), 83% coverage.
Correlation-aware band combination (not naive sum). Relative sigma floored at the pooled estimate; Google-Meta residual correlation set to the upper 95% CI bound. ~56% narrower bands than naive while holding coverage. Mode A (total budget, seasonal split) and Mode B (explicit per-channel).
82 rolling-origin windows (mature-model period, Jul 2025+): mean calibration error 3.8pp; P10/P50/P90 observed 4.9/53.7/92.7; avg MAPE 11.7%, coverage 87.8%.
What-if forecasting with guardrails. Three seasonal regimes, day-weighted across boundary-crossing windows. Four zones vs seasonal max, asymmetric band widening, continuous disclosed ROAS haircut prior beyond +30% overshoot.
Bounded reallocation: starts from seasonal split, searches +/-10% (Google/Meta), +/-5% (Bing), maximizing P50 while staying GREEN. Gains reported against the forecast's own uncertainty band; gains inside the noise are labeled hypotheses.
- Never trust a fix without backtest evidence on real held-out data.
- Real ground truth only; never hand-built targets.
- Leak guards: training strictly before the test/forecast start.
- Quantiles from real history beat parametric assumptions.
- Shrinkage everywhere; noisy months pull toward stable baselines.
- Honest disclosure of limits; documented anomalies, not overfitting.
- Don't invent structure the data doesn't support.
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
python src/blended_forecaster.py
python rolling_calibration_v2.py
python src/budget_simulator.py
python src/budget_optimizer.py
Python version: 3.11 (tested on 3.11.9)
./run.sh ./data ./pickle/model.pkl ./output/predictions.csv All three arguments are optional and default to the paths shown. The pipeline: (1) ingests every CSV found in the data folder, (2) validates and normalizes them into a canonical daily table, (3) loads the pickled model configuration, (4) fits seasonal ROAS quantile tables from the ingested history, and (5) writes probabilistic revenue and ROAS forecasts (P10/P50/P90) at blended, channel, campaign-type, and campaign level for 30/60/90-day horizons to the output CSV.
The pipeline reads whatever CSVs are present in the data folder. Exact adapters are included for the three source schemas provided at launch (Google Ads, Meta Ads, Bing Ads exports), detected by column signature. Files with other schemas are handled by a generic column-synonym mapper requiring at minimum: a date column, a spend column, and a revenue column. The committed data folder contains a trimmed 180-day sample; the model performs best with one year or more of history (a warning is printed otherwise).
Core: Revenue = ROAS x Spend. ROAS per channel is modeled as a seasonal monthly P10/P50/P90 distribution estimated from the ingested history, shrunk toward the channel global median (validated shrinkage strengths), with bands widened when a calendar month has under two years of history. Channel bands are combined with a validated Google-Meta residual correlation (rho = 0.455). Future spend is assumed at the trailing-30-day average daily rate per channel, projected over each horizon. The pickled artifact holds the locked, validated model configuration; seasonal tables are fit at predict time from whatever data is provided, so the pipeline generalizes to unseen rows of the same schema. Deterministic: no randomness, no network calls at run time.
The output columns are a documented default (see src/predict.py::write_output, isolated in one function) and will be aligned to the officially announced format when available.