File size: 6,185 Bytes
e23172f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
"""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