I had been away from Fantabuddy for twelve days. The player list, meanwhile, had not gone on holiday.

The file I had used on August 5 contained 494 rows, 491 of them active; the one I downloaded on August 18 contained 519 rows and 504 active players. Twenty-five new names, twelve players out of the active pool and 148 rows of the ranking changed. Four days from the first whistle of the Serie A season, my perfectly reproducible model was already only as useful as its freshest input.

This is the story of Fantabuddy. But it starts long before Python, DuckDB and Brier scores.

The first save is always Milan

I have played football management games since Championship Manager 03/04, known as Scudetto 03/04 in Italy. It was still Ancelotti’s great Milan, fresh from winning the 2002/03 Champions League against Juventus in Manchester. My first save in every Championship Manager or Football Manager has always been Milan. It is the club I have supported all my life; I doubt that tradition will change any time soon.

I did not know terms such as feature engineering or temporal validation yet, but the game was already a huge database for making decisions: minutes, form, roles, injuries, potential and price. Football was the story; data was how I entered it.

Moving to Fantacalcio — Italy’s version of fantasy football — felt natural. Over the last few years, as my data experience grew, recurring wins in friendly leagues followed. Not because a spreadsheet can predict a season, but because it helps me do a few unglamorous things more consistently: buy starters, spread the budget, recognise value before it becomes consensus, and avoid falling in love with too many gambles at once.

The almost perfect auction

The 2024/25 season is the one I remember as my almost perfect auction. Orsolini and Çalhanoğlu carried me through much of the league, but the squad worked because every department was well assembled.

PlayerSerie A 2024/25 result
Meret16 clean sheets, 25 goals conceded, 2 penalties saved
Dumfries7 goals, 2 assists
Çalhanoğlu5 goals, 6 assists
Orsolini15 goals, 4 assists
Krstović11 goals, 5 assists — bought for 1 credit

The four outfield players produced 38 goals and 17 assists between them. Krstović is the snapshot of that season: one credit, 37 rated appearances, 11 goals and 5 assists. The kind of bargain everyone talks about afterwards, but one that only works if the rest of the squad is not made up of seven more bargains waiting to happen.

That year I also used fantacalcio-py, an open-source project by Antonio Pio Volgarino. I later contributed four pull requests: a CLI, a fix for duplicate columns in Excel files, JSON export and a fix for output-directory handling. It was not Fantabuddy, but it was the first concrete bridge between this passion and open source: a real tool, used for a real auction, that I could improve for other people too.

The next season taught me the opposite lesson. In 2025/26 I entered two leagues, won both cup competitions and finished third in one, but the main title stayed out of reach. Vlahović suffered a high-grade adductor injury, Giménez needed ankle surgery, De Bruyne tore his biceps femoris and Lukaku also stopped with a serious thigh injury.

That is not an excuse. It is the difference between the quality of a decision and its outcome. You can build a good squad and still lose a volume of minutes no pre-auction model could have known about.

And then you still have to manage it. In the second league my co-manager Renato was fundamental: setting the lineup every week, reading the moment, not piling up auction gambles, and correcting mistakes as the season moved. He is not on GitHub or LinkedIn; he is simply a friend with whom I share the team. Data prepares the pitch. Someone still has to play the match.

What Fantabuddy can actually control

Fantabuddy exists to make the controllable part repeatable. It imports snapshots of the official player list, connects them to API-Football history, stores everything in DuckDB, and produces prices consistent with my league rules: ten teams, twenty-five slots per squad and 10,000 total credits, reconciled down to the last one. Every screenshot below is in Italian, like the auction it was built for.

The boundary between Fantabuddy’s repeatable work and human decisions

The August 18 snapshot contains 504 active players. An accepted API identity exists for 503; all 504 still have an explicit decision, because a match made on string similarity alone is never approved automatically — it waits in a pending queue until I look at it. For 471 players, the system can build a forecast from fixture-level history. Current squads, transfers, availability and fixtures all declare when they were observed.

Per-match granularity matters. Every row used by the model sees only what was known before that match: rolling averages stop at the previous appearance, while the current start, minutes, rating and bonuses live separately as labels. The future cannot sneak into training through a side door.

Then the model has to earn the right to appear in the report, and it has to earn it twice, because the two things I predict are checked in different ways.

Availability — will he start, and for how many minutes — trains on every season before the last one and is validated on the last one, which never enters training. That model ships only if it beats a simple baseline by at least 1%.

PredictionBaselineModelDecision
Starting probability, Brier score0.15810.1335model, -15.6%
Minutes per match, MAE20.1017.76model, -11.6%

Seasonal scoring is a harder room. Each role is backtested walk-forward across folds, and what reaches the report is never the model on its own: it is a blend of the official market value and the model, weighted at most 60% towards the model. To be accepted, that blend has to cut the baseline’s error by at least 3% without losing rank correlation. Goalkeepers, defenders and midfielders cleared it. Forwards did not, so their column is the baseline, untouched.

That is my favourite result in the entire project. A useful model is not one that always has a prediction. It is one allowed to lose when a simpler answer is better.

Model validation, coverage and freshness in the Fantabuddy report

A price is an opinion, and mine lives in a YAML file

Something has to turn a score into a number I can shout across a table, and that step is not science. It is my league’s constitution, sitting in config/league.default.yaml where anyone can disagree with it.

Ten squads of twenty-five slots means exactly 250 players can be bought. The other 254 in the list are worth one credit each and are marked tier E: the model still ranks them, the budget never sees them. Every one of the 250 slots gets one credit as a floor, and the remaining 9,750 are divided by role — 48% to forwards, 28% to midfielders, 16% to defenders, 8% to goalkeepers.

Inside a role, the money does not follow the score. It follows the gap between a player and the last man who still makes a squad — replacement level — raised to the power 1.15, so the curve leans towards the top without collapsing onto it. Then a hard cap per role: 90, 130, 280, 500. On August 18 that produced 810 credits for goalkeepers, 1,640 for defenders, 2,810 for midfielders and 4,740 for forwards. Exactly 10,000; the build raises an assertion and dies if it is not.

The part I find honest is what the caps do. In all four roles, exactly one name is sitting on the ceiling — Svilar, Dimarco, Nico Paz, Malen. The curve would happily spend more on each of them and I do not let it. And that 48% to forwards is not a discovery either: it is a bet on how my league behaves at an auction, one number in a file, and one I can be wrong about for an entire season.

Even APIs do not know everything

Having thousands of calls available is not a reason to spend them. Before the season started, API-Football still marked player statistics and injuries for Serie A 2026/27 as unavailable. I refreshed what could genuinely have changed — squads, transfers, availability history and fixtures — and left the rest alone. The provider itself recommends checking coverage before every acquisition.

Coverage does not mean completeness either. Reconstructing 2025/26, the history I ingested correctly contained Lukaku’s thigh injury, but no episode at all for De Bruyne, nothing for Vlahović — who by then had left the pool my squad queries cover — and, for Giménez, a summer ankle sprain instead of the December operation I actually lived through. Three players, three different ways of being wrong. That is why injuries are alerts to verify in the report, not medical records. The API reduces uncertainty; it does not erase it.

The ratings underneath the whole model are the provider’s, too, not the official Fantacalcio votes. Everything Fantabuddy predicts is a proxy for the thing my league actually scores. I would rather write that here than let a reader discover it in the fourth column of the ranking.

The product is the HTML

A Parquet file does not follow me to an auction. A half-open notebook cannot quickly show me which likely starters cost forty credits or fewer. That is why Fantabuddy’s main output is a self-contained, filterable HTML report that works offline.

Fantabuddy report overview on August 18, 2026

It shows top picks by position, possible low-cost starters, availability alerts, recent transfers and changes since the previous snapshot. The full ranking exposes price, FVM, starting probability, expected minutes, bonuses, reliability and the explanation behind the estimate. At the bottom, instead of hiding them, it shows model gates, coverage and the freshness of every source.

Operational report signals: low-cost starters, alerts and transfers

It will not tell me whom to buy without thinking. It will not stop four forwards from getting injured, and it will not set the lineup with Renato. It does something less spectacular and more useful: it preserves what we knew, when we knew it, and why a decision looked good.

In Championship Manager 03/04, the database was the world in which I imagined my next season with Milan. More than twenty years later, I built a much smaller one to prepare for an auction among friends. The passion is the same. Now it has immutable snapshots and an HTML report.

Where to find the work

Fantabuddy is available at AndreaBozzo/fantabuddy. The project I used during the 2024/25 season is piopy/fantacalcio-py; my four contributions are collected in this pull-request search.

The 2024/25 statistics above can be checked in the Fantacalcio archives for Orsolini, Çalhanoğlu, Dumfries, Krstović and Meret. Official club statements document the injuries suffered by Lukaku, De Bruyne, Vlahović and Giménez.

Thanks to Antonio for making fantacalcio-py open source, and to Renato for everything that happens after the report is closed and the matchday lineup still has to be set.