A personal project with a serious engineering problem
An old college friend wanted to build a prediction model for MMA, his favorite sport, and give his friends a place to follow its progress. I helped with the website UI and the machine-learning algorithm used in the prediction model.
The interesting challenge was not simply displaying a winner. It was making model inputs understandable and preserving what the model predicted before the outcome was known. The source repository behind Drake’s Pick documents a broader data and prediction platform, whose architecture informs this case study.
The technology framework
The public application runs on Cloudflare Workers with D1 SQLite storage. React supplies the interactive UI, TypeScript defines application contracts, and Drizzle ORM is part of the application data stack. Python modules perform scoring, feature construction, training, chronological evaluation, and artifact export. Cheerio supports HTML parsing in ingestion workflows.
Public page requests read stored data and predictions. They do not scrape providers or train models. That separation keeps expensive imports and historical rebuilds out of the request path and prevents a normal visit from changing the published forecast.
Canonical fighter identities and provenance
Different data providers assign different IDs and spell athlete names differently. A canonical fighter record and provider-to-canonical ID mappings prevent duplicate identities from contaminating the training set. Ambiguous identity matches require review rather than a guessed merge.
The documented ingestion pipeline retains source provenance, ingestion runs, source hashes, dated records, bout outcomes, and round statistics. Current profiles and historical feature snapshots are separate: using a fighter’s current statistics to forecast a past fight would leak future information into evaluation.
Feature engineering and model selection
The model includes Elo differences, time-normalized striking differential, striking accuracy and defense, takedown rates, control share, reach, layoff, experience, and reliability. Sparse samples are shrunk toward priors so a tiny history does not produce implausibly strong estimates.
Illustrative, simplified code explaining the implementation pattern. This is not a copy of private production source.
# Simplified feature patterns found in the modeling pipeline.
def shrunk_ratio(numerator, denominator, prior, strength):
return (numerator + prior * strength) / (denominator + strength)
def reliability(effective_fights):
return effective_fights / (effective_fights + 5.0)
def strike_feature(landed, absorbed, seconds, effective_fights):
minutes = max(seconds / 60.0, 1e-9)
return ((landed - absorbed) / minutes) * reliability(effective_fights)The training pipeline evaluates logistic-regression candidates chronologically using log loss, Brier score, winner accuracy, and calibration. Training cutoffs precede validation events. Randomly mixing past and future bouts would obscure temporal leakage and make measured performance look better than the deployed forecasting task.
Validated artifacts are promoted through explicit gates. When supported feature snapshots or a promoted model are unavailable, Elo provides a transparent fallback. An output probability needs its model version, data cutoff, and applicability context, rather than being presented as an unexplained confidence number.
Forecast policy and immutable publication
The documented forecast policy can blend a model probability with a de-vigged two-sided market probability. The policy gives the model 70% weight and the market 30%, while bounding the market-driven adjustment to eight percentage points. Missing or invalid market observations leave a model-only forecast.
Model version and forecast-policy version are recorded separately. Official article forecasts use an event ID and pre-event snapshot cutoff; after event start, the published forecast is frozen. Results and recaps compare against that same snapshot instead of substituting the latest model output.
Illustrative, simplified code explaining the implementation pattern. This is not a copy of private production source.
// Illustrative stored forecast contract.
type PublishedForecast = {
eventId: string;
boutId: string;
snapshotAt: string;
trainingCutoff: string;
modelVersion: string;
policyVersion: string;
redProbability: number;
blueProbability: number;
};
assert(forecast.snapshotAt <= event.startsAt);
assert(forecast.trainingCutoff <= forecast.snapshotAt);
assert(Math.abs(forecast.redProbability + forecast.blueProbability - 1) < 1e-9);UI for model progress rather than opaque predictions
The website lets visitors compare fighters, inspect attributes and style differences, see stored upcoming forecasts, and review historical outcomes against saved picks. The UI must distinguish official-card forecasts from hypothetical cross-division simulator outputs because their validation contexts differ.
Past-event scoring excludes draws, no contests, cancellations, unresolved results, and bouts without a saved pick. Displayed accuracy therefore needs an explicit denominator and exclusion policy. This makes progress interpretable for the friend group rather than producing a flattering but unstable percentage.
Testing and the project’s outcome
The repository includes tests for snapshot integrity, historical rebuilds, market blending, physical adjustments, prediction publication, past events, and result synchronization. The important invariants are stable identities, pre-fight-only features, immutable forecasts, and consistent scoring.
The project gave my friend and his friends an interface for following the model’s progress. My work combined UI support with modeling collaboration. No revenue or predictive accuracy claim has been supplied, so the outcome is described through functionality and traceability.