FINESE_SCHOOL / src /engine /auto_learning.py
Jack-ki1's picture
Upload 47 files
29ca14e verified
Raw
History Blame Contribute Delete
16.6 kB
"""
Post-Race Auto-Learning System - Phase 4 Implementation.
This module automatically ingests race results after each Grand Prix and updates
the prediction model based on actual outcomes. This enables continuous improvement
throughout the season.
Key Features:
- Automatic race result ingestion via FastF1
- ELO rating updates based on actual finishing positions
- Prediction accuracy tracking and reporting
- Model bias detection and correction
- Tire strategy learning
- Wet/dry performance calibration
"""
import logging
from datetime import datetime
from typing import Dict, List, Optional, Any
logger = logging.getLogger(__name__)
def auto_ingest_race_results(season: int, circuit_id: str) -> Dict[str, Any]:
"""
Automatically ingest race results and update model parameters.
This function should be called after each race weekend completes to:
1. Fetch actual race results from FastF1
2. Update driver/team ELO ratings
3. Track prediction accuracy
4. Learn tire degradation patterns
5. Calibrate wet weather performance
Args:
season: Year (e.g., 2026)
circuit_id: Circuit identifier from calendar
Returns:
Dictionary with ingestion results:
- success: Boolean indicating if ingestion completed
- drivers_updated: Number of driver ratings updated
- predictions_tracked: Number of predictions validated
- accuracy_metrics: Dict with accuracy statistics
- errors: List of any errors encountered
Example Usage:
>>> result = auto_ingest_race_results(2026, "monaco")
>>> if result['success']:
... print(f"Updated {result['drivers_updated']} drivers")
... print(f"Accuracy: {result['accuracy_metrics']}")
"""
try:
from src.data.fastf1_integration import get_session, FASTF1_AVAILABLE
if not FASTF1_AVAILABLE:
logger.warning("FastF1 not available. Cannot ingest race results.")
return {
"success": False,
"drivers_updated": 0,
"predictions_tracked": 0,
"accuracy_metrics": {},
"errors": ["FastF1 not installed"]
}
logger.info(f"Starting post-race auto-learning for {circuit_id} (Season {season})")
# Map circuit_id to FastF1 race name
from src.data.fastf1_integration import _circuit_to_race_name
race_name = _circuit_to_race_name(circuit_id)
if not race_name:
raise ValueError(f"Could not map circuit '{circuit_id}' to race name")
# Step 1: Fetch race results
logger.info(f"Fetching race results for {race_name}")
session = get_session(season, race_name, 'R')
if session.results is None or len(session.results) == 0:
raise ValueError("No race results available yet")
# Step 2: Extract actual finishing positions
actual_results = []
for idx, row in session.results.iterrows():
actual_results.append({
'driver_abbrev': row.get('Abbreviation'),
'position': int(row.get('Position', 99)),
'team': row.get('TeamName'),
'grid_position': int(row.get('GridPosition', 99)) if row.get('GridPosition') else None,
'status': row.get('Status', 'Finished'),
'points': float(row.get('Points', 0)),
'fastest_lap': bool(row.get('FastestLap', False)),
})
logger.info(f"Fetched results for {len(actual_results)} drivers")
# Step 3: Update ELO ratings
from src.engine.multi_dimensional_elo import get_elo_system
elo_system = get_elo_system()
drivers_updated = 0
for result in actual_results:
driver_abbrev = result['driver_abbrev']
actual_pos = result['position']
# Find driver ID from abbreviation
from src.data.driver_data import get_all_drivers
all_drivers = get_all_drivers()
driver_match = next((d for d in all_drivers if d.get('short', '').upper() == driver_abbrev), None)
if driver_match:
driver_id = driver_match['id']
# Update race ELO based on actual position vs expected
# Get expected position from current ELO
expected_score = elo_system.get_expected_score(driver_id, actual_pos, len(actual_results))
# Calculate ELO change
k_factor = 32 # Standard K-factor for F1
actual_score = 1.0 - (actual_pos - 1) / (len(actual_results) - 1) # Normalize position to [0,1]
elo_change = k_factor * (actual_score - expected_score)
elo_system.update_driver_rating(driver_id, 'race', elo_change)
drivers_updated += 1
logger.debug(f"Updated {driver_id}: pos={actual_pos}, ELO change={elo_change:+.1f}")
# Step 4: Track prediction accuracy (if predictions exist)
accuracy_metrics = _track_prediction_accuracy(season, circuit_id, actual_results)
# Step 5: Learn tire strategies
tire_insights = _learn_tire_strategies(session, circuit_id)
# Step 6: Update wet weather calibration if it rained
weather_insights = _update_wet_weather_calibration(session, circuit_id)
result = {
"success": True,
"drivers_updated": drivers_updated,
"predictions_tracked": accuracy_metrics.get('total_predictions', 0),
"accuracy_metrics": accuracy_metrics,
"tire_insights": tire_insights,
"weather_insights": weather_insights,
"ingested_at": datetime.now().isoformat(),
"errors": []
}
logger.info(
f"Auto-learning complete: {drivers_updated} drivers updated, "
f"{result['predictions_tracked']} predictions tracked"
)
return result
except Exception as e:
logger.error(f"Post-race auto-learning failed: {e}")
import traceback
logger.debug(traceback.format_exc())
return {
"success": False,
"drivers_updated": 0,
"predictions_tracked": 0,
"accuracy_metrics": {},
"errors": [str(e)]
}
def _track_prediction_accuracy(
season: int,
circuit_id: str,
actual_results: List[Dict]
) -> Dict[str, Any]:
"""
Compare stored predictions against actual results to track accuracy.
Args:
season: Year
circuit_id: Circuit identifier
actual_results: List of actual finishing positions
Returns:
Dictionary with accuracy metrics
"""
try:
from src.database.models import SessionLocal, PredictionResult
db = SessionLocal()
# Find most recent prediction for this circuit
prediction = db.query(PredictionResult).filter(
PredictionResult.circuit_id == circuit_id,
PredictionResult.season == season
).order_by(PredictionResult.created_at.desc()).first()
if not prediction:
logger.info(f"No predictions found for {circuit_id} Season {season}")
return {"total_predictions": 0}
# Compare predicted vs actual top 3
predicted_top3 = prediction.podium_predictions or []
actual_top3 = [
r['driver_abbrev'] for r in sorted(actual_results, key=lambda x: x['position'])[:3]
]
# Calculate accuracy metrics
correct_in_top3 = len(set(predicted_top3) & set(actual_top3))
top3_accuracy = correct_in_top3 / 3.0
# Check if winner was predicted
predicted_winner = predicted_top3[0] if predicted_top3 else None
actual_winner = actual_top3[0] if actual_top3 else None
winner_correct = predicted_winner == actual_winner
# Position correlation (Spearman rank correlation would be better, but simple for now)
position_errors = []
for result in actual_results[:10]: # Top 10 only
driver_abbrev = result['driver_abbrev']
actual_pos = result['position']
# Find predicted position
pred_pos = None
for i, pred_driver in enumerate(predicted_top3):
if pred_driver == driver_abbrev:
pred_pos = i + 1
break
if pred_pos:
position_errors.append(abs(pred_pos - actual_pos))
avg_position_error = sum(position_errors) / len(position_errors) if position_errors else None
metrics = {
"total_predictions": 1,
"top3_accuracy": round(top3_accuracy, 2),
"correct_in_top3": correct_in_top3,
"winner_predicted": winner_correct,
"avg_position_error": round(avg_position_error, 2) if avg_position_error else None,
"predicted_top3": predicted_top3,
"actual_top3": actual_top3,
}
logger.info(
f"Prediction accuracy for {circuit_id}: "
f"Top-3: {top3_accuracy*100:.0f}%, Winner: {'βœ“' if winner_correct else 'βœ—'}"
)
db.close()
return metrics
except Exception as e:
logger.warning(f"Failed to track prediction accuracy: {e}")
return {"total_predictions": 0, "error": str(e)}
def _learn_tire_strategies(session, circuit_id: str) -> Dict[str, Any]:
"""
Analyze tire strategies used in the race to improve future strategy modeling.
Args:
session: FastF1 race session
circuit_id: Circuit identifier
Returns:
Dictionary with tire strategy insights
"""
try:
from src.data.fastf1_integration import ingest_tire_strategy
tire_data = ingest_tire_strategy(
session.event['EventDate'].year,
session.event['EventName']
)
# Extract common strategies
strategy_counts = {}
for driver_data in tire_data.get('tire_strategy', []):
stints = driver_data.get('stints', {})
strategy_key = tuple(sorted(stints.items()))
strategy_counts[strategy_key] = strategy_counts.get(strategy_key, 0) + 1
# Most common strategy
most_common = max(strategy_counts.items(), key=lambda x: x[1]) if strategy_counts else None
insights = {
"circuit_id": circuit_id,
"total_strategies_analyzed": len(tire_data.get('tire_strategy', [])),
"unique_strategies": len(strategy_counts),
"most_common_strategy": str(most_common[0]) if most_common else None,
"most_common_count": most_common[1] if most_common else 0,
}
logger.info(f"Tire strategy learning: {insights['unique_strategies']} unique strategies found")
return insights
except Exception as e:
logger.warning(f"Tire strategy learning failed: {e}")
return {"error": str(e)}
def _update_wet_weather_calibration(session, circuit_id: str) -> Dict[str, Any]:
"""
Update wet weather performance calibration if race had rain.
Args:
session: FastF1 race session
circuit_id: Circuit identifier
Returns:
Dictionary with wet weather insights
"""
try:
# Check if it rained during the race
laps_with_rain = session.laps[session.laps['Rainfall'] == True]
if len(laps_with_rain) == 0:
logger.info(f"No rain during {circuit_id} race - skipping wet weather calibration")
return {"rained": False}
# Analyze driver performance in wet conditions
wet_performance = {}
for driver_abbrev in session.laps['Driver'].unique():
driver_laps = session.laps.pick_driver(driver_abbrev)
wet_laps = driver_laps[driver_laps['Rainfall'] == True]
dry_laps = driver_laps[driver_laps['Rainfall'] == False]
if len(wet_laps) > 0 and len(dry_laps) > 0:
wet_avg = wet_laps['LapTime'].apply(lambda x: x.total_seconds()).mean()
dry_avg = dry_laps['LapTime'].apply(lambda x: x.total_seconds()).mean()
# Wet pace delta (positive = slower in wet)
wet_delta = wet_avg - dry_avg
wet_performance[driver_abbrev] = {
'wet_pace_delta': round(wet_delta, 3),
'wet_laps': len(wet_laps),
'dry_laps': len(dry_laps),
}
insights = {
"rained": True,
"wet_laps_total": len(laps_with_rain),
"drivers_analyzed": len(wet_performance),
"wet_performance": wet_performance,
}
logger.info(f"Wet weather calibration: {len(wet_performance)} drivers analyzed")
return insights
except Exception as e:
logger.warning(f"Wet weather calibration failed: {e}")
return {"error": str(e)}
def schedule_auto_learning_for_completed_races(season: int = None) -> Dict[str, Any]:
"""
Check for recently completed races and trigger auto-learning.
This function can be called periodically (e.g., daily) to ensure all
completed races have been processed for model updates.
Args:
season: Year to check (defaults to current year)
Returns:
Dictionary with processing results
"""
from datetime import datetime as dt
if season is None:
season = dt.now().year
try:
from src.data.calendar_2026 import CALENDAR_2026
today = dt.now().date()
processed = []
skipped = []
errors = []
for race in CALENDAR_2026:
if race.get('season', season) != season:
continue
race_date = dt.strptime(race['date'], '%Y-%m-%d').date()
# Check if race was within last 7 days (recently completed)
days_since_race = (today - race_date).days
if 1 <= days_since_race <= 7 and race.get('status') == 'completed':
logger.info(f"Processing recently completed race: {race['name']} ({days_since_race} days ago)")
result = auto_ingest_race_results(season, race['circuit'])
if result['success']:
processed.append({
'circuit': race['circuit'],
'name': race['name'],
'days_ago': days_since_race,
'drivers_updated': result['drivers_updated'],
})
else:
errors.append({
'circuit': race['circuit'],
'name': race['name'],
'error': result['errors'][0] if result['errors'] else 'Unknown error',
})
elif days_since_race > 7:
skipped.append(race['circuit'])
summary = {
"season": season,
"processed_count": len(processed),
"skipped_count": len(skipped),
"error_count": len(errors),
"processed": processed,
"errors": errors,
}
logger.info(
f"Auto-learning scheduler complete: {len(processed)} processed, "
f"{len(skipped)} skipped, {len(errors)} errors"
)
return summary
except Exception as e:
logger.error(f"Auto-learning scheduler failed: {e}")
return {
"season": season,
"processed_count": 0,
"error_count": 1,
"errors": [{"error": str(e)}]
}
# ── EXPORT ──────────────────────────────────────────────────────────────────────
__all__ = [
"auto_ingest_race_results",
"schedule_auto_learning_for_completed_races",
]