140 lines
6.5 KiB
SQL
140 lines
6.5 KiB
SQL
-- =====================================================================
|
|
-- OpenAI match predictions + comparison views
|
|
-- Run against the "bookie" schema (Postgres).
|
|
-- Safe to run multiple times (IF NOT EXISTS / CREATE OR REPLACE).
|
|
-- =====================================================================
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- 1) Table: one row PER TEAM PER EXECUTION of the OpenAI predictor.
|
|
-- History is kept (no unique constraint) so you can inspect how
|
|
-- predictions change across runs; the views below always pick the
|
|
-- most recent row per (match, team).
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE IF NOT EXISTS bookie.match_prediction_openai (
|
|
prediction_id SERIAL PRIMARY KEY,
|
|
match_id INT NOT NULL REFERENCES bookie.match(match_id),
|
|
team_id INT NOT NULL REFERENCES bookie.team(team_id),
|
|
|
|
predicted_goals NUMERIC(6,2),
|
|
goals_low NUMERIC(6,2),
|
|
goals_high NUMERIC(6,2),
|
|
|
|
predicted_shots_on_target NUMERIC(6,2),
|
|
shots_on_target_low NUMERIC(6,2),
|
|
shots_on_target_high NUMERIC(6,2),
|
|
|
|
predicted_corners NUMERIC(6,2),
|
|
corners_low NUMERIC(6,2),
|
|
corners_high NUMERIC(6,2),
|
|
|
|
predicted_fouls NUMERIC(6,2),
|
|
fouls_low NUMERIC(6,2),
|
|
fouls_high NUMERIC(6,2),
|
|
|
|
predicted_yellow_cards NUMERIC(6,2),
|
|
yellow_cards_low NUMERIC(6,2),
|
|
yellow_cards_high NUMERIC(6,2),
|
|
|
|
predicted_red_cards NUMERIC(6,2),
|
|
red_cards_low NUMERIC(6,2),
|
|
red_cards_high NUMERIC(6,2),
|
|
|
|
confidence TEXT,
|
|
reasoning TEXT,
|
|
model TEXT,
|
|
predicted_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_match_prediction_openai_match_team
|
|
ON bookie.match_prediction_openai (match_id, team_id, predicted_at DESC);
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- 2) Latest OpenAI prediction per (match, team).
|
|
-- ---------------------------------------------------------------------
|
|
CREATE OR REPLACE VIEW bookie.v_match_prediction_openai_latest AS
|
|
SELECT DISTINCT ON (match_id, team_id) *
|
|
FROM bookie.match_prediction_openai
|
|
ORDER BY match_id, team_id, predicted_at DESC;
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- 3) Side-by-side comparison: LLM (bookie.match_prediction) vs OpenAI
|
|
-- vs ACTUAL, one row per (match, team). Actual goals come from the
|
|
-- final score; the other actuals come from bookie.match_team_stats.
|
|
-- ---------------------------------------------------------------------
|
|
CREATE OR REPLACE VIEW bookie.v_prediction_comparison AS
|
|
SELECT
|
|
m.match_id,
|
|
t.team_id,
|
|
t.name AS team_name,
|
|
m.kickoff_at,
|
|
m.status,
|
|
(m.home_team_id = t.team_id) AS is_home,
|
|
opp.name AS opponent_name,
|
|
|
|
-- goals
|
|
lp.predicted_goals AS llm_goals,
|
|
op.predicted_goals AS openai_goals,
|
|
CASE WHEN m.home_team_id = t.team_id THEN m.home_score_ft ELSE m.away_score_ft END AS actual_goals,
|
|
|
|
-- shots on target
|
|
lp.predicted_shots_on_target AS llm_shots_on_target,
|
|
op.predicted_shots_on_target AS openai_shots_on_target,
|
|
mts.shots_on_target AS actual_shots_on_target,
|
|
|
|
-- corners
|
|
lp.predicted_corners AS llm_corners,
|
|
op.predicted_corners AS openai_corners,
|
|
mts.corners AS actual_corners,
|
|
|
|
-- fouls
|
|
lp.predicted_fouls AS llm_fouls,
|
|
op.predicted_fouls AS openai_fouls,
|
|
mts.fouls AS actual_fouls,
|
|
|
|
-- yellow cards
|
|
lp.predicted_yellow_cards AS llm_yellow_cards,
|
|
op.predicted_yellow_cards AS openai_yellow_cards,
|
|
mts.yellow_cards AS actual_yellow_cards,
|
|
|
|
op.confidence AS openai_confidence,
|
|
op.model AS openai_model
|
|
FROM bookie.match m
|
|
JOIN bookie.team t
|
|
ON t.team_id IN (m.home_team_id, m.away_team_id)
|
|
JOIN bookie.team opp
|
|
ON opp.team_id = CASE WHEN m.home_team_id = t.team_id THEN m.away_team_id ELSE m.home_team_id END
|
|
LEFT JOIN bookie.match_prediction lp
|
|
ON lp.match_id = m.match_id AND lp.team_id = t.team_id
|
|
LEFT JOIN bookie.v_match_prediction_openai_latest op
|
|
ON op.match_id = m.match_id AND op.team_id = t.team_id
|
|
LEFT JOIN bookie.match_team_stats mts
|
|
ON mts.match_id = m.match_id AND mts.team_id = t.team_id
|
|
WHERE lp.prediction_id IS NOT NULL OR op.prediction_id IS NOT NULL;
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- 4) Accuracy summary: mean absolute error of each model vs actual,
|
|
-- per metric, over finished matches only. Lower = better.
|
|
-- ---------------------------------------------------------------------
|
|
CREATE OR REPLACE VIEW bookie.v_prediction_accuracy AS
|
|
WITH c AS (
|
|
SELECT * FROM bookie.v_prediction_comparison WHERE status = 'finished'
|
|
)
|
|
SELECT metric,
|
|
ROUND(AVG(ABS(llm_pred - actual)), 3) AS llm_mae,
|
|
ROUND(AVG(ABS(openai_pred - actual)), 3) AS openai_mae,
|
|
COUNT(*) FILTER (WHERE llm_pred IS NOT NULL AND actual IS NOT NULL) AS llm_samples,
|
|
COUNT(*) FILTER (WHERE openai_pred IS NOT NULL AND actual IS NOT NULL) AS openai_samples
|
|
FROM (
|
|
SELECT 'goals' AS metric, llm_goals AS llm_pred, openai_goals AS openai_pred, actual_goals AS actual FROM c
|
|
UNION ALL
|
|
SELECT 'shots_on_target', llm_shots_on_target, openai_shots_on_target, actual_shots_on_target FROM c
|
|
UNION ALL
|
|
SELECT 'corners', llm_corners, openai_corners, actual_corners FROM c
|
|
UNION ALL
|
|
SELECT 'fouls', llm_fouls, openai_fouls, actual_fouls FROM c
|
|
UNION ALL
|
|
SELECT 'yellow_cards', llm_yellow_cards, openai_yellow_cards, actual_yellow_cards FROM c
|
|
) x
|
|
GROUP BY metric
|
|
ORDER BY metric;
|