All work

Machine Learning · Predictive Modeling

Hunter Metric

Applied ML platform for predictive sports analytics

.NET 8React 19 + TSPostgreSQLEF CoreML.NET / LightGBMHangfireClaude APIRailway / Docker

The problem

Hunter Metric predicts game outcomes across six professional sports. The real engineering challenge isn't building a model that looks good on paper — it's proving, continuously, that a model's stated probabilities are actually trustworthy before anyone relies on them. The system is built around calibration and governance: every model earns its way into production.

  • A model that says '70% chance' is worthless if that outcome only happens 55% of the time — raw scores must be calibrated into honest probabilities.
  • Six different sports each need their own model, but a shared, consistent evaluation and promotion standard.
  • Backtests are trivially easy to fake with future-leaking data, so evaluation has to be provably chronological.
  • A single model can be confidently wrong — you need a way to know when to trust a prediction and when not to.

Architecture

The platform is a staged pipeline: raw game and line data flows into six native per-sport models, whose outputs are combined by a multi-family ensemble and passed through a shared assembler that calibrates, governs, and gates every probability before it surfaces.

Data & ingestion

Sources
Sports API adaptersOdds / line snapshotsHangfire jobs

Per-sport win models

Six native models
MLBNBANFLWNBANCAAFNCAAB

Ensemble

Multi-family
Logistic RegressionLightGBMELOAgreement engine

Assembler & governance

Calibrate + gate
Platt calibrationReliability diagramsWalk-forward gatesShadow → Active

Surface

Delivery
REST APIReact SPA

Data flows downward — each stage feeds the next, from raw game data to a governed, calibrated probability.

Engineering in depth

The snippets below are illustrative of the techniques used — simplified to show the approach without exposing proprietary modeling logic.

PlattCalibrator.cs
// Fit p = sigmoid(A*z + B) so raw model scores become
// probabilities that match observed win frequencies.
public Calibration Fit(IReadOnlyList<(double Z, bool Won)> samples)
{
    double a = 1.0, b = 0.0;

    for (var iter = 0; iter < MaxIters; iter++)
    {
        double gradA = 0, gradB = 0;
        foreach (var (z, won) in samples)
        {
            var p = Sigmoid(a * z + b);
            var error = p - (won ? 1.0 : 0.0);   // gradient of log-loss
            gradA += error * z;
            gradB += error;
        }
        a -= LearningRate * gradA / samples.Count;
        b -= LearningRate * gradB / samples.Count;
    }

    return new Calibration(a, b);
}
Raw model scores aren't probabilities. Platt scaling fits a logistic curve so that when the model says 70%, it wins about 70% of the time — verified against reliability diagrams before a model is trusted.
WalkForwardBuilder.cs
// Build expanding-window folds and refuse any that could
// leak the future into evaluation.
foreach (var fold in BuildExpandingFolds(games, folds: 5))
{
    var maxTrain    = fold.Train.Max(g => g.Date);
    var minValidate = fold.Validate.Min(g => g.Date);

    if (maxTrain >= minValidate)
        throw new LeakageException(
            $"Training through {maxTrain:d} overlaps validation at {minValidate:d}");

    yield return Evaluate(fold);
}
A sports model is only as honest as its backtest. Every fold trains on the past and validates on strictly later games — and the invariant throws rather than silently let future information leak into evaluation.
WinProbAssembler.cs
// A model's predictions only surface once it's been promoted to
// Active. In Shadow it runs on real games but stays invisible.
public WinProbability? Assemble(GameContext game)
{
    var prediction = _calibrator.Apply(_model.Predict(game));

    // Always record — feeds later calibration + backtesting.
    _snapshots.Persist(game, prediction);

    return _mode switch
    {
        AssemblerMode.Active => prediction,   // authoritative
        AssemblerMode.Shadow => null,         // accrue data only
        _                    => null,         // Off
    };
}
New models run in Shadow first — producing real predictions on live games that are recorded but never shown. A model graduates to Active only after it clears calibration and walk-forward gates, so nothing unproven ever reaches a user.

Key decisions

Calibration before confidence

A probability is only useful if it's honest. Fitting Platt scaling and checking reliability diagrams — with a minimum sample threshold before a sport is considered calibrated — means the system never presents overconfident numbers it hasn't earned.

Shadow mode before production

Every new model runs invisibly on live games first, recording predictions without affecting anything. It only graduates to authoritative once it has proven calibrated skill against real outcomes — de-risking changes to a live system.

An ensemble of models that disagree

Logistic regression, LightGBM, and ELO capture different structure. Scoring their agreement — and treating high disagreement as lower confidence — turns three imperfect models into a signal that also knows when to stay quiet.