Alpha Zest.win is officially in Alpha — and the first month is on us. Read more →
← Back to blog
· 4 min read · By ZestWin Data Desk · technical update , prediction model , performance , caching , infrastructure

Zest.win version v1.1.0: Behind the Scenes: Ensemble Predictions and a Caching System That Never Goes Stale

zest.win upgraded its prediction engine with ensemble models, Platt calibration, and an event-driven caching system that keeps every page fresh.

Zest.win version v1.1.0: Behind the Scenes: Ensemble Predictions and a Caching System That Never Goes Stale

We recently shipped two under-the-hood upgrades that touch every page on zest.win — a retooled prediction pipeline and a fundamentally new approach to caching. Here's what changed and why it matters.

From one model to a voting ensemble

Until recently, our prediction pipeline trained a single XGBoost model per target — series winner, map handicap, first tower, and so on — using the same hyperparameters across all three games. If the XGBoost model overfit a particular tournament meta, there was no second opinion to temper it. We've moved to an ensemble. Every match target is now trained independently by both XGBoost and LightGBM, two gradient-boosted tree learners with complementary bias profiles. At inference time, the pipeline runs both models, applies calibration independently to each, and produces a soft-vote average. If one model type is unavailable — say LightGBM hasn't trained yet for a new target — the ensemble silently falls back to what's available. No breaking, no gaps.

Training also got smarter. The pipeline now uses a three-way temporal split: one slice for training, one for grid search and early stopping, and a dedicated hold-out slice for Platt calibration fitting. This means the calibration data never leaks into hyperparameter selection. We also added a do-no-harm guard: if the Platt calibrator's slope comes back non-positive — a sign the calibration fit is pathological — the system rejects it and serves the raw model probabilities instead of miscalibrated ones.

Every prediction target in every game ecosystem — 15 targets for League of Legends, 10 each for CS2 and Dota 2 — now runs through the same shared core. The game-specific code is a thin spec file: which targets exist, which side is "home," what backfill windows to use. The training, inference, settlement, health checking, and event handling all live in one place, so a fix in calibration logic benefits all three games simultaneously.

Why the model's 65% should actually mean 65%

Raw model scores can be overconfident — a model might output 72% when its historical accuracy on similar predictions is closer to 64%. Platt calibration corrects this by fitting a sigmoid curve that maps raw scores to their empirical accuracy. We compute Expected Calibration Error (ECE) across 10 probability bins and log it to a model_history.csv with every training run. If accuracy drifts or calibration degrades across versions, the health monitor catches it before users see degrading predictions.

You can verify the calibration yourself on any track-record pages — the calibration chart shows exactly how well predicted probabilities line up with real outcomes.

Active caching: why pages never wait for stale data

The old caching approach was simple — set a TTL and wait. Long TTLs meant stale match lists after a schedule update. Short TTLs meant every visitor triggered a database query even when nothing had changed.

We replaced this with two-layer active caching. Layer one is server OutputCache for anonymous HTTP responses, served from Cloudflare's edge network with a 5-minute browser TTL and 1-hour edge TTL. Layer two is an in-process TaggedMemoryCache that holds expensive computations — like full match lists with model probabilities — for logged-in subscribers who bypass the OutputCache entirely.

The key difference: the cache doesn't just expire. A CacheInvalidationConsumer listens to our event broker and evicts only the entries that actually need refreshing. When a scrape completes and new match data arrives, game-wide caches for that game evict. When new odds land, match and game caches evict. When a prediction is computed, match and prediction caches evict — but tournament pages stay warm. The system maps 6 event types to specific cache tags, and an event that changed nothing (zero new matches, zero new odds) evicts nothing.

And we don't stop at eviction. After clearing stale entries, the system immediately pre-fetches the canonical subscriber match list in the background. The next logged-in user who opens zest.win gets a warm cache — no cold-start penalty.

What this means day to day:

Share this analysis
Post on X

Related Analysis & Predictions

View all posts →