All projects

Early ML work · Cabify Spain · Nov 2025 to Mar 2026, extended in May

Demand & KPI Forecasting Engine

Before most of the automation work, I spent my first months building models in Colab for the Customer Operations team: forecasts of support volume and ride requests, quarterly KPI targets, a shift planner for a contact centre and a first computer-vision prototype. The team used them in its weekly reviews. This page is what they did and what I learned.

−58% forecast errorPython · Colab
PythonPythonGoogle ColabGoogle Colabpandaspandasscikit-learnscikit-learnGoogle OR-ToolsGoogle OR-ToolsGoogle SheetsGoogle SheetsTableauTableau
My roleBuilt alone, as a trainee and then analyst, with the team as users.
WhenNov 2025 to Mar 2026 · local rebuild May 2026
Built withPython, Prophet, NeuralProphet, LightGBM, statsmodels SARIMAX, OR-Tools CP-SAT, pandas, gspread, Google Colab
StatusForecasts fed the weekly control reviews; the method lives on in COPs Control.
−58%
forecast error against the official forecast on daily support volume
2,160
Prophet configurations searched per series, champion picked on a hidden 28-day window
~140
critical-process KPIs given quarterly targets by one model run
94.1%
weekly accuracy on the rider series, one minus weighted MAPE

1. Daily support volume

The team planned staff against an official daily forecast of contact volume per audience (riders, drivers, business clients) that was often far off. I went through about thirty notebook versions in three months. The main steps:

  • NeuralProphet with yearly and weekly seasonality, then with Spanish public holidays and a hand-kept events sheet, then an ensemble of 20 models with different seeds.
  • Back to Prophet, which was easier to explain to the team and much easier to tune.
  • Tuning. First an adaptive random search over the three prior scales, then a full grid of 2,160 configurations (changepoint, seasonality and holiday priors, additive or multiplicative, changepoint range), with the champion chosen by MAE on the last 28 days, which the model never saw.
  • Bias correction on recent errors, then the forecast is written to a frozen log before the actuals arrive.
History + eventsdaily series, publicholidays, an eventssheetGrid search2,160 ProphetconfigurationsHoldoutlast 28 days hidden,champion by MAEBias correctionon recent errors, beforefreezingFrozen logevery forecast storedbefore the actuals existWeekly checkaccuracy = 1 − WMAPE,compared with theofficial forecast
The version that stuck. Every forecast is logged before the actuals exist, so it cannot be edited after the fact.
Official forecastOfficial forecast: 34.9%34.9%My Prophet modelMy Prophet model: 14.4%14.4%
Mean absolute percentage error on daily support volume, 15 December to 14 February. Error only: no volumes are shown.

Over 15 December to 14 February the model's error was 14.4% against 34.9% for the official forecast. Weekly accuracy for the rider series reached 94.1% (one minus weighted MAPE, first week of February). Business-client volume was much harder: 74.5% accuracy in the same kind of check, because the daily counts are small and lumpy.

2. Ride requests by city

The same approach, run as a batch with one events tab per city and a 14-day bias correction applied only when the gap is between 1% and 25% (a larger gap usually means something changed that a factor would hide). In May I rebuilt it as a local pipeline with walk-forward validation: LightGBM with quantile loss on lags, rolling statistics, calendar features and event leads and lags; Prophet with Spanish holidays; and SARIMAX(1,1,1)(1,1,1,7). The ensemble weights each model by the inverse of its MAPE, gives a P10 to P90 band, and anomalies are flagged with a robust z-score on residuals (median absolute deviation above 3.5), marked as explained when an event matches.

In Madrid the single SARIMAX model had the lowest error (6.6% MAPE) and the ensemble came third at 7.5%. The ensemble stayed because it is more stable across cities, but it is not the best everywhere.

3. Quarterly KPI targets

Targets for about 140 critical-process KPIs across four audiences were set by hand every quarter. One notebook now fits a Prophet model per KPI, switches to logistic growth with a cap and floor when the KPI is a bounded percentage, and writes three scenarios (probable, optimistic, pessimistic, from the forecast and its interval) into a formatted sheet per audience. The same engine produced monthly targets for the mystery-shopper programme.

4. Contact-centre staffing

Demand per intervalforecast contacts perhour or 15 minErlang C90% answered in 20 s,195 s handling timeShrinkage15.6% for breaks,training, absenceCP-SAT rosterone boolean per agent,day and shift optionRoster + coveragemapone tab per agent,heatmap of gaps
From forecast to a legal weekly roster for a contact-centre team.

Erlang C, written with log-gamma terms so it stays numerically stable, gives the minimum agents per interval for the service level. OR-Tools CP-SAT then builds the roster: shifts of 4 to 10 hours starting at any hour, one shift per day, at most 44 hours and five days a week, at least 12 hours between shifts and a weekly rest. Uncovered demand is a slack variable with a very high cost, overtime a lower one, so the solver prefers any legal roster over an infeasible one. Days off and blocked slots (training, appointments) can be entered before solving.

5. First computer-vision prototype

For the vehicle audit I built a dataset of 2,156 photos in 10 vinyl classes and trained a CNN from scratch in Keras: four convolution blocks, 256 px input, augmentation without horizontal flips so logos are never mirrored. Plates were read with YOLOv8 to find the car and EasyOCR with several contrast and threshold variants, checked against the Spanish plate format. A third notebook aligned each photo to the reference design with SIFT features and a RANSAC homography to estimate how much of the design was covered.

It ran in batch over real submissions, but it confused similar designs, and I did not keep the evaluation results, so there is no number I can quote. That, plus the need to describe damage in words, is why the production system uses a vision LLM with a reference collage instead. A later ResNet18 cascade with a confidence gate, meant to handle the easy cases locally and send the rest to the LLM, got as far as a baseline. The full story is in the hybrid vision engine.

What I would change now

  • The notebooks were long single cells with interactive menus that only I could run. The May rebuild is a package with a config file, walk-forward folds and a report per city, which is how all of it should have started.
  • Every model should have shipped with its backtest next to it. The volume forecast had one because of the frozen log; the vision prototype did not, and it cost me the ability to say how good it was.

No volumes are shown: accuracy figures only. The contact-centre provider and internal datasets are not named.