Brettapps's picture
Upload folder using huggingface_hub (part 21)
e23172f verified
Raw History Blame Contribute Delete
6.19 kB
"""Comprehensive feature engineering from existing FormFav data."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Optional
from trifecta_bro.data.models import RunnerModel, RaceModel
def extract_features(runner: RunnerModel, race: RaceModel) -> dict:
"""Extract all possible features from runner data for ML model."""
features = {}
# Basic features
features['number'] = runner.number
features['barrier'] = runner.barrier or 0
features['weight'] = runner.weight or 57.0
features['age'] = runner.age or 4
# Sex encoding
sex = (runner.sex or '').lower()
features['is_gelding'] = 1 if 'gelding' in sex else 0
features['is_mare'] = 1 if 'mare' in sex else 0
features['is_filly'] = 1 if 'filly' in sex else 0
features['is_colt'] = 1 if 'colt' in sex else 0
# Career stats
overall = runner.stats.get('overall', {})
features['career_starts'] = overall.get('starts', 0) or 0
features['career_wins'] = overall.get('wins', 0) or 0
features['career_places'] = overall.get('places', 0) or 0
features['career_win_pct'] = overall.get('winPercent', 0) or 0
features['career_place_pct'] = overall.get('placePercent', 0) or 0
# Track stats
track = runner.stats.get('track', {})
features['track_starts'] = track.get('starts', 0) or 0
features['track_wins'] = track.get('wins', 0) or 0
features['track_win_pct'] = track.get('winPercent', 0) or 0
features['track_place_pct'] = track.get('placePercent', 0) or 0
# Distance stats
distance = runner.stats.get('distance', {})
features['distance_starts'] = distance.get('starts', 0) or 0
features['distance_wins'] = distance.get('wins', 0) or 0
features['distance_win_pct'] = distance.get('winPercent', 0) or 0
features['distance_place_pct'] = distance.get('placePercent', 0) or 0
# Track + Distance combo
track_dist = runner.stats.get('trackDistance', {})
features['track_distance_starts'] = track_dist.get('starts', 0) or 0
features['track_distance_wins'] = track_dist.get('wins', 0) or 0
features['track_distance_win_pct'] = track_dist.get('winPercent', 0) or 0
features['track_distance_place_pct'] = track_dist.get('placePercent', 0) or 0
# Condition stats (match by race condition)
cond = (race.condition or '').lower()
cond_stats = runner.stats.get('conditions', {})
cond_key = None
for key in cond_stats:
if cond and cond[:4] in key.lower():
cond_key = key
break
if cond_key:
cs = cond_stats[cond_key]
features['cond_starts'] = cs.get('starts', 0) or 0
features['cond_wins'] = cs.get('wins', 0) or 0
features['cond_win_pct'] = cs.get('winPercent', 0) or 0
features['cond_place_pct'] = cs.get('placePercent', 0) or 0
else:
features['cond_starts'] = 0
features['cond_wins'] = 0
features['cond_win_pct'] = 0
features['cond_place_pct'] = 0
# First-up / Second-up stats
first_up = runner.stats.get('firstUp', {})
second_up = runner.stats.get('secondUp', {})
features['first_up_starts'] = first_up.get('starts', 0) or 0
features['first_up_wins'] = first_up.get('wins', 0) or 0
features['first_up_win_pct'] = first_up.get('winPercent', 0) or 0
features['second_up_starts'] = second_up.get('starts', 0) or 0
features['second_up_wins'] = second_up.get('wins', 0) or 0
features['second_up_win_pct'] = second_up.get('winPercent', 0) or 0
# Form analysis
form = runner.form or ''
features['form_length'] = len(form)
# Recent form (last 5)
recent = form[-5:] if len(form) >= 5 else form
features['recent_wins'] = sum(1 for c in recent if c == '1')
features['recent_places'] = sum(1 for c in recent if c in ['1', '2', '3'])
features['recent_win_pct'] = features['recent_wins'] / len(recent) if recent else 0
features['recent_place_pct'] = features['recent_places'] / len(recent) if recent else 0
# Very recent (last 3)
very_recent = form[-3:] if len(form) >= 3 else form
features['very_recent_wins'] = sum(1 for c in very_recent if c == '1')
features['very_recent_win_pct'] = features['very_recent_wins'] / len(very_recent) if very_recent else 0
# Last start result
features['last_start_pos'] = _parse_last_position(form[-1]) if form else 0
# Career prize money
prize = runner.career_prize_money
if prize:
try:
features['career_prize'] = float(str(prize).replace('$', '').replace(',', ''))
except:
features['career_prize'] = 0
else:
features['career_prize'] = 0
# Prize money per start
features['prize_per_start'] = features['career_prize'] / features['career_starts'] if features['career_starts'] > 0 else 0
# Race features
features['num_runners'] = race.number_of_runners or 10
# Distance in metres
distance_str = race.distance or '1200m'
try:
features['distance_metres'] = int(''.join(filter(str.isdigit, str(distance_str))))
except:
features['distance_metres'] = 1200
# Barrier advantage (relative to field)
features['barrier_central'] = abs(features['barrier'] - features['num_runners'] / 2)
# Weight relative to field
features['weight_relative'] = features['weight'] - 57.0 # 57kg is average
return features
def _parse_last_position(char: str) -> int:
"""Parse a form character to a finishing position."""
if char == '1':
return 1
elif char == '2':
return 2
elif char == '3':
return 3
elif char == '4':
return 4
elif char == '5':
return 5
elif char == '6':
return 6
elif char == '7':
return 7
elif char == '8':
return 8
elif char == '9':
return 9
elif char == '0':
return 10 # spell
elif char in ['x', 'X']:
return 0 # spell
elif char in ['f', 'F']:
return 0 # fell
elif char in ['p', 'P']:
return 0 # pulled up
else:
return 0