Labs

PostgreSQL & Claude Power an Automated AI Pipeline for Horse Race Predictions

PostgreSQL & Claude Power an Automated AI Pipeline for Horse Race Predictions

Over the past six months, a developer has been building an AI system for horse racing predictions. This is not a simple "past results → prediction" model, but rather a multi-stage pipeline encompassing data quality management, feature engineering, Claude inference, and ranked recommendations.

Here's what was learned during its development.

Architecture Overview

The core architecture of the AI prediction system is structured as follows:

  • Data Scrape: netkeiba scrape
  • Data Storage: PostgreSQL (horse_races / horse_entries)
  • Daily Batch Processing: fetch_horse_racing.py
  • AI Inference Endpoint: Supabase Edge Function (ai-hub: horse.predict)
  • LLM Inference: Claude haiku
  • Results Ensemble: horse_race_predictions_ensemble
  • Accuracy Evaluation: evaluate_accuracy.ts (weekly evaluation)

Data Quality Score (DQS)

Prediction accuracy is primarily determined by data quality. The developer scores 15 fields to produce a DQS (0-100). A partial example of the DQS calculation logic is shown below:

(  CASE WHEN weight IS NOT NULL THEN 10 ELSE 0 END +  CASE WHEN weight_diff IS NOT NULL THEN 10 ELSE 0 END +  CASE WHEN last_3f IS NOT NULL THEN 15 ELSE 0 END +  CASE WHEN prev_last_3f IS NOT NULL THEN 10 ELSE 0 END +  CASE WHEN jockey_id IS NOT NULL THEN 10 ELSE 0 END +  CASE WHEN trainer_id IS NOT NULL THEN 10 ELSE 0 END +  CASE WHEN odds IS NOT NULL THEN 15 ELSE 0 END  -- + 8 more fields...) AS data_quality_score

Entries with a DQS below 60 are skipped. This single filter improved prediction accuracy more significantly than any changes made to the prediction model itself.

Feature Engineering: Ranking Score

Eight factors are weighted based on their empirical contribution to generate a ranking score. Key factors and their weights include:

  • Historical place rate: 25% (Most stable signal)
  • Final 3F time (last_3f): 20% (Late speed is a predictive indicator)
  • Inverse of odds: 15% (Market wisdom)
  • Jockey win rate: 15% (Jockey effect is substantial)
  • Weight change: 10% (Condition signal)
  • Last 3F vs previous race: 10% (Momentum trend)
  • Best time record: 5% (Ceiling indicator of performance)

Claude Inference Prompt

Claude is utilized to generate explanations, not just scores. The structure of the inference prompt is as follows:

const prompt = `You are a horse racing prediction specialist.[RACE INFORMATION]<<>>${raceInfo}<<>>[HORSE DATA]<<>>${horseData}<<>>Recommend top 3 horses considering:1. Prioritize horses with DQS >= 702. Emphasize best time record and final 3F3. Flag weight changes of ±10kg as risk factors4. Explain each recommendation in under 100 charactersOutput format: JSON`;

The <<>> blocks within the prompt are crucial for protecting against prompt injection from scraped race data.

Solving the N+1 Query Problem

The initial implementation suffered from an N+1 query problem, resulting in 2 queries per race for 50 races, totaling 100 queries per evaluation run.

// Before: N+1 queriesfor (const race of races) {  // query for race details  // query for horse entries}

To optimize this, the developer leveraged PostgreSQL's WITH RECURSIVE CTE (Common Table Expression) and the JSONB_AGG function to consolidate multiple queries into a single, efficient operation:

-- After: Single optimized queryWITH race_data AS (  SELECT    r.id AS race_id,    JSONB_BUILD_OBJECT(      'race_details', r,      'horse_entries', (        SELECT JSONB_AGG(he)        FROM horse_entries he        WHERE he.race_id = r.id      )    ) AS data  FROM    races r  WHERE    r.date = '2023-10-26')SELECT JSONB_AGG(data) FROM race_data;

This approach retrieves all race details and their respective horse entries in a single database roundtrip, significantly enhancing data fetching efficiency.

↗ Read original source