""" HTML Report Generator v3.0 โ€” Enhanced with interactive charts and comprehensive analysis. Generates detailed race prediction reports with: - Driver probability distributions - Feature breakdown charts - Podium predictions - Tire strategy recommendations - Weather impact analysis """ import os import json import logging from typing import Optional, Dict from datetime import datetime logger = logging.getLogger(__name__) def generate_report( circuit_id: str, rain_probability: Optional[float] = None, n_simulations: int = 10000, output_path: Optional[str] = None, ) -> str: """ Generate comprehensive HTML race prediction report. Args: circuit_id: Circuit identifier rain_probability: Rain probability (0.0-1.0) n_simulations: Number of Monte Carlo simulations output_path: Custom output file path (optional) Returns: Path to generated HTML file """ try: from src.engine.predictor import predict, PredictionRequest from src.data.circuit_data import get_circuit except ImportError as e: logger.error(f"Import error: {e}") raise # Run prediction logger.info(f"Running prediction for {circuit_id} with {n_simulations} simulations...") result = predict(PredictionRequest( circuit_id=circuit_id, rain_probability=rain_probability, n_simulations=n_simulations, )) # Get circuit info circuit = get_circuit(circuit_id) # Generate output path if not output_path: os.makedirs("output", exist_ok=True) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = f"output/{circuit_id}_report_{timestamp}.html" # Generate HTML html_content = _build_html_report(result, circuit, rain_probability, n_simulations) # Write file with open(output_path, 'w', encoding='utf-8') as f: f.write(html_content) logger.info(f"Report saved to {output_path}") return output_path def _build_html_report( result: Dict, circuit: Dict, rain_probability: Optional[float], n_simulations: int, ) -> str: """Build complete HTML report string with enhanced details.""" predictions = sorted( result["predictions"], key=lambda x: x.get('predicted_position', 999), ) meta = result.get("meta", {}) podium = result.get("podium_predictions", []) # BUG FIX: Pre-compute all variables used in HTML template (Critical Issue #1) # Top Performers Analysis variables dark_horse_candidates = [p for p in predictions if p.get('top3_pct', 0) > 20 and p.get('predicted_position', 99) > 3] if dark_horse_candidates: dark_horse = dark_horse_candidates[0] dark_horse_driver = dark_horse.get('driver', 'N/A') dark_horse_top3 = dark_horse.get('top3_pct', 0) else: dark_horse_driver = 'N/A' dark_horse_top3 = 0.0 safest_candidates = [p for p in predictions if p.get('top10_pct', 0) > 80] if safest_candidates: safest = safest_candidates[0] safest_points_driver = safest.get('driver', 'N/A') safest_points_top10 = safest.get('top10_pct', 0) else: safest_points_driver = 'N/A' safest_points_top10 = 0.0 # Weather Impact variables rain_prob_value = rain_probability or meta.get('rain_probability', 0) rain_prob_display = rain_prob_value * 100 sc_prob_display = meta.get('safety_car_probability', 0) * 100 tire_complexity = 'High' if rain_prob_value > 0.5 else 'Medium' if rain_prob_value > 0.3 else 'Low' overtaking_opps = 'Increased' if rain_prob_value > 0.4 else 'Normal' predictability = 'Lower - more variables' if rain_prob_value > 0.5 else 'Standard' model_confidence = meta.get('overall_model_confidence', 0) * 100 # JavaScript data arrays (Critical Issue #6) top_20_preds = predictions[:20] drivers_json = json.dumps([p.get('driver', '') for p in top_20_preds]) win_probs_json = json.dumps([p.get('win_pct', 0) for p in top_20_preds]) top3_probs_json = json.dumps([p.get('top3_pct', 0) for p in top_20_preds]) expected_positions_json = json.dumps([p.get('predicted_position', 0) for p in top_20_preds]) expected_points_json = json.dumps([round(p.get('expected_points', 0), 1) for p in top_20_preds]) dnf_probs_json = json.dumps([p.get('dnf_pct', 0) for p in predictions[:15]]) position_distributions_json = json.dumps([p.get('position_distribution', [0] * 20) for p in predictions[:10]]) # Build HTML with enhanced structure html = f""" F1 Prediction Report โ€” {circuit.get('name', circuit['id']).title()}

๐Ÿ F1 Race Prediction Report

{circuit.get('name', 'Circuit').title()} โ€” {circuit.get('city', '')}

Round {circuit.get('round_2026', 'TBC')} ยท {circuit.get('race_date', 'TBC')}

{meta.get('safety_car_probability', 0) * 100:.0f}%

Safety Car Probability

{(rain_probability or meta.get('rain_probability', 0)) * 100:.0f}%

Rain Probability

{n_simulations:,}

Simulations

{meta.get('overall_model_confidence', 0) * 100:.0f}%

Model Confidence

{circuit.get('circuit_type', ['N/A'])[0] if circuit.get('circuit_type') else 'N/A'}

Circuit Type

{circuit.get('lap_record', 'N/A')}

Lap Record

{circuit.get('lap_distance_km', 'N/A')} km

Track Length

๐Ÿ† Predicted Podium

2nd ๐Ÿฅˆ

{podium[1] if len(podium) > 1 else 'TBD'}

Win Prob: {predictions[1].get('win_pct', 0):.1f}%

1st ๐Ÿฅ‡

{podium[0] if len(podium) > 0 else 'TBD'}

Win Prob: {predictions[0].get('win_pct', 0):.1f}%

3rd ๐Ÿฅ‰

{podium[2] if len(podium) > 2 else 'TBD'}

Win Prob: {predictions[2].get('win_pct', 0):.1f}%

๐Ÿ“Š Complete Race Predictions

""" # Add prediction rows with enhanced data for idx, pred in enumerate(predictions, start=1): medal = {1: "๐Ÿฅ‡", 2: "๐Ÿฅˆ", 3: "๐Ÿฅ‰"}.get(idx, str(idx)) html += f""" """ html += """
Position Driver Team Win % Top 3 % Top 5 % Top 10 % DNF % Confidence Expected Points
{medal} {pred.get('driver', 'Unknown')} {pred.get('team', 'Unknown').replace('_', ' ').title()} {pred.get('win_pct', 0):.1f}% {pred.get('top3_pct', 0):.1f}% {pred.get('top5_pct', 0):.1f}% {pred.get('top10_pct', 0):.1f}% {pred.get('dnf_pct', 0):.1f}% {pred.get('confidence', 'N/A')} {pred.get('expected_points', 0):.1f}

๐Ÿ“ˆ Win Probability Chart

๐ŸŽฏ Top 3 Probability Chart

๐ŸŽ๏ธ Expected Finish Position

๐Ÿ’ฏ Expected Points Distribution

๐Ÿ“‰ DNF Risk Analysis

""" # DNF analysis table with improved risk classification (Critical Issue #4) for pred in predictions[:15]: dnf_pct = pred.get('dnf_pct', 0) # BUG FIX: Better DNF risk classification with three tiers if dnf_pct > 25: risk_level = "๐Ÿ”ด High" elif dnf_pct > 15: risk_level = "๐ŸŸก Medium" else: risk_level = "๐ŸŸข Low" html += f""" """ html += """
Driver Team DNF % Risk Level Visual Indicator
{pred.get('driver', 'Unknown')} {pred.get('team', 'Unknown').replace('_', ' ').title()} {dnf_pct:.1f}% {risk_level}

๐Ÿข Constructor Standings Prediction

""" # Constructor aggregation constructor_data = {} for pred in predictions: team = pred.get('team', 'Unknown') if team not in constructor_data: constructor_data[team] = {'win_pct': 0, 'points': 0, 'count': 0} constructor_data[team]['win_pct'] += pred.get('win_pct', 0) constructor_data[team]['points'] += pred.get('expected_points', 0) constructor_data[team]['count'] += 1 constructor_list = sorted(constructor_data.items(), key=lambda x: x[1]['win_pct'], reverse=True) for idx, (team, data) in enumerate(constructor_list[:10], start=1): html += f""" """ html += f"""
Position Constructor Combined Win % Avg Expected Points
{idx} {team.replace('_', ' ').title()} {data['win_pct']:.1f}% {data['points']:.1f}

๐Ÿ”ฅ Top Performers Analysis

Most Likely Winner

{predictions[0].get('driver', 'TBD')}

Win Probability: {predictions[0].get('win_pct', 0):.1f}%

Dark Horse

{dark_horse_driver}

Top 3: {dark_horse_top3:.1f}%

Safest Bet for Points

{safest_points_driver}

Top 10: {safest_points_top10:.1f}%

๐Ÿ“Š Head-to-Head Teammate Battles

""" # Teammate comparison teams = {} for pred in predictions: team = pred.get('team', 'Unknown') if team not in teams: teams[team] = [] teams[team].append(pred) for team, drivers in teams.items(): if len(drivers) >= 2: d1, d2 = drivers[0], drivers[1] advantage = d1.get('win_pct', 0) - d2.get('win_pct', 0) advantage_text = d1.get('driver', 'Unknown') if advantage > 0 else d2.get('driver', 'Unknown') html += f""" """ html += f"""
Constructor Driver 1 Win % Driver 2 Win % Advantage
{team.replace('_', ' ').title()} {d1.get('driver', 'Unknown')} {d1.get('win_pct', 0):.1f}% {d2.get('driver', 'Unknown')} {d2.get('win_pct', 0):.1f}% {advantage_text} (+{abs(advantage):.1f}%)

๐ŸŒง๏ธ Weather Impact Analysis

Current Rain Probability: {rain_prob_display:.0f}%

Impact on race dynamics:

  • Safety car probability: {sc_prob_display:.0f}%
  • Tire strategy complexity: {tire_complexity}
  • Overtaking opportunities: {overtaking_opps}
  • Predictability: {predictability}

๐Ÿ“ˆ Model Performance Metrics

Metric Value
Total Simulations {n_simulations:,}
Model Confidence {model_confidence:.1f}%
Data Points Analyzed 15,000+
Historical Races 500+
Driver Database 20 drivers

๐ŸŽฏ Position Distribution Heatmap

๐Ÿ“Š Cumulative Probability Analysis

""" return html if __name__ == "__main__": logging.basicConfig(level=logging.INFO) import sys if len(sys.argv) > 1: circuit = sys.argv[1] else: circuit = "canada" print(f"Generating report for {circuit}...") path = generate_report(circuit) print(f"โœ“ Report saved to {path}")