Prathamesh Bhamare commited on
Commit
46c1c8b
·
1 Parent(s): 3209e63

Added crash detection feature and full 2014-2026 prerace dataset

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. agents/compare_agent.py +86 -0
  2. agents/critique_agent.py +12 -2
  3. agents/data_agent.py +4 -1
  4. agents/synthesis_agent.py +29 -4
  5. api/main.py +239 -12
  6. api/schemas.py +35 -1
  7. data/__init__.py +108 -0
  8. data/fastf1_pipeline.py +8 -2
  9. data_output/fastf1_races.parquet +2 -2
  10. data_output/prerace/prerace_70th_anniversary_gra_2020.parquet +3 -0
  11. data_output/prerace/prerace_abu_dhabi_grand_prix_2014.parquet +3 -0
  12. data_output/prerace/prerace_abu_dhabi_grand_prix_2015.parquet +3 -0
  13. data_output/prerace/prerace_abu_dhabi_grand_prix_2016.parquet +3 -0
  14. data_output/prerace/prerace_abu_dhabi_grand_prix_2017.parquet +3 -0
  15. data_output/prerace/prerace_abu_dhabi_grand_prix_2018.parquet +3 -0
  16. data_output/prerace/prerace_abu_dhabi_grand_prix_2019.parquet +3 -0
  17. data_output/prerace/prerace_abu_dhabi_grand_prix_2020.parquet +3 -0
  18. data_output/prerace/prerace_abu_dhabi_grand_prix_2021.parquet +3 -0
  19. data_output/prerace/prerace_abu_dhabi_grand_prix_2022.parquet +3 -0
  20. data_output/prerace/prerace_abu_dhabi_grand_prix_2023.parquet +3 -0
  21. data_output/prerace/prerace_abu_dhabi_grand_prix_2024.parquet +3 -0
  22. data_output/prerace/prerace_abu_dhabi_grand_prix_2025.parquet +3 -0
  23. data_output/prerace/prerace_australian_grand_pri_2014.parquet +3 -0
  24. data_output/prerace/prerace_australian_grand_pri_2015.parquet +3 -0
  25. data_output/prerace/prerace_australian_grand_pri_2016.parquet +3 -0
  26. data_output/prerace/prerace_australian_grand_pri_2017.parquet +3 -0
  27. data_output/prerace/prerace_australian_grand_pri_2018.parquet +3 -0
  28. data_output/prerace/prerace_australian_grand_pri_2019.parquet +3 -0
  29. data_output/prerace/prerace_australian_grand_pri_2022.parquet +3 -0
  30. data_output/prerace/prerace_australian_grand_pri_2023.parquet +3 -0
  31. data_output/prerace/prerace_australian_grand_pri_2024.parquet +3 -0
  32. data_output/prerace/prerace_australian_grand_pri_2025.parquet +3 -0
  33. data_output/prerace/prerace_australian_grand_pri_2026.parquet +3 -0
  34. data_output/prerace/prerace_austrian_grand_prix_2014.parquet +3 -0
  35. data_output/prerace/prerace_austrian_grand_prix_2015.parquet +3 -0
  36. data_output/prerace/prerace_austrian_grand_prix_2016.parquet +3 -0
  37. data_output/prerace/prerace_austrian_grand_prix_2017.parquet +3 -0
  38. data_output/prerace/prerace_austrian_grand_prix_2018.parquet +3 -0
  39. data_output/prerace/prerace_austrian_grand_prix_2019.parquet +3 -0
  40. data_output/prerace/prerace_austrian_grand_prix_2020.parquet +3 -0
  41. data_output/prerace/prerace_austrian_grand_prix_2021.parquet +3 -0
  42. data_output/prerace/prerace_austrian_grand_prix_2022.parquet +3 -0
  43. data_output/prerace/prerace_austrian_grand_prix_2023.parquet +3 -0
  44. data_output/prerace/prerace_austrian_grand_prix_2024.parquet +3 -0
  45. data_output/prerace/prerace_austrian_grand_prix_2025.parquet +3 -0
  46. data_output/prerace/prerace_azerbaijan_grand_pri_2017.parquet +3 -0
  47. data_output/prerace/prerace_azerbaijan_grand_pri_2018.parquet +3 -0
  48. data_output/prerace/prerace_azerbaijan_grand_pri_2019.parquet +3 -0
  49. data_output/prerace/prerace_azerbaijan_grand_pri_2021.parquet +3 -0
  50. data_output/prerace/prerace_azerbaijan_grand_pri_2022.parquet +3 -0
agents/compare_agent.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ KRONECTOR - Compare Agent
3
+
4
+ Uses Groq's Llama3 to analyze the mathematical SHAP Delta between two drivers
5
+ and synthesize a "Tale of the Tape" style narrative.
6
+ """
7
+
8
+ import os
9
+ from groq import Groq
10
+
11
+ def compare_agent(
12
+ driver1_name: str,
13
+ driver1_prob: float,
14
+ driver1_status: str,
15
+ driver2_name: str,
16
+ driver2_prob: float,
17
+ driver2_status: str,
18
+ shap_deltas: dict[str, float],
19
+ race_context: str
20
+ ) -> str:
21
+ """Uses Llama3 to write a head-to-head comparison."""
22
+
23
+ api_key = os.getenv("GROQ_API_KEY")
24
+ if not api_key:
25
+ raise RuntimeError("GROQ_API_KEY environment variable is required.")
26
+
27
+ client = Groq(api_key=api_key)
28
+
29
+ # Sort deltas to find biggest advantages for driver 1 (positive) and driver 2 (negative)
30
+ sorted_deltas = sorted(shap_deltas.items(), key=lambda item: item[1], reverse=True)
31
+ d1_advantages = [f"{k}: +{v:.2f}" for k, v in sorted_deltas[:3] if v > 0]
32
+ d2_advantages = [f"{k}: +{abs(v):.2f}" for k, v in reversed(sorted_deltas) if v < 0][:3]
33
+
34
+ system_prompt = f"""
35
+ You are KRONECTOR, an elite Formula 1 Race Strategy AI.
36
+ You have been asked to compare two drivers head-to-head for an upcoming race.
37
+ You have a Machine Learning model that calculates 'SHAP Deltas' — exactly where one driver gains or loses mathematical advantage over the other.
38
+
39
+ YOUR JOB:
40
+ Write a "Tale of the Tape" style boxing-match breakdown (1-2 paragraphs max).
41
+ Explain who is the overall favorite based on Win Probability, but highlight the specific areas where the underdog might have an edge.
42
+ Make it sound like an expert F1 engineer breaking down a matchup.
43
+
44
+ IMPORTANT CONCEPTS:
45
+ - 'grid_position' or 'pole_conversion_rate' means track position/qualifying pace.
46
+ - 'career_race_starts' means veteran experience vs rookie volatility.
47
+ - 'driver_form_last3' means recent momentum.
48
+ - 'sector_1_time', etc. means raw track speed.
49
+ - CRASH DETECTION: You are provided with the 'Qualifying Status' for both drivers. If a driver's status indicates an Accident, Collision, or Crash, you MUST dramatically mention it as a major disadvantage or factor! (Note: a driver can still have a good grid position if they crashed in Q3. If so, highlight this loophole!)
50
+ """
51
+
52
+ user_prompt = f"""
53
+ RACE CONTEXT: {race_context}
54
+
55
+ MATCHUP:
56
+ {driver1_name} (Win Probability: {driver1_prob:.1f}%) | Quali Status: {driver1_status}
57
+ vs
58
+ {driver2_name} (Win Probability: {driver2_prob:.1f}%) | Quali Status: {driver2_status}
59
+
60
+ MATHEMATICAL ADVANTAGES (SHAP Deltas):
61
+ Biggest edges for {driver1_name}:
62
+ {', '.join(d1_advantages) if d1_advantages else "None"}
63
+
64
+ Biggest edges for {driver2_name}:
65
+ {', '.join(d2_advantages) if d2_advantages else "None"}
66
+
67
+ Write the Head-to-Head analysis:
68
+ """
69
+
70
+ chat_completion = client.chat.completions.create(
71
+ messages=[
72
+ {
73
+ "role": "system",
74
+ "content": system_prompt.strip(),
75
+ },
76
+ {
77
+ "role": "user",
78
+ "content": user_prompt.strip(),
79
+ }
80
+ ],
81
+ model="llama-3.3-70b-versatile",
82
+ temperature=0.7,
83
+ max_tokens=500,
84
+ )
85
+
86
+ return chat_completion.choices[0].message.content
agents/critique_agent.py CHANGED
@@ -17,16 +17,26 @@ def critique_agent(prediction: PredictionOutput) -> CritiqueOutput:
17
  """Critiques the prediction mathematically to safeguard the LLM."""
18
  prob = prediction["probability"]
19
  shap_values = prediction["shap_values"]
 
20
 
21
  approved = True
22
  confidence_rating = "Normal"
23
  critique_notes = ""
24
 
25
  # 1. Evaluate Probability Thresholds
26
- if prob < 0.20:
 
 
 
 
 
27
  approved = False
28
  confidence_rating = "Low"
29
- critique_notes += f"REJECTED: Win probability is {prob:.1%}, which is below the 20% threshold. The model is guessing. Do not make a definitive prediction.\n\n"
 
 
 
 
30
  elif prob > 0.95:
31
  confidence_rating = "Overconfident"
32
  critique_notes += f"FLAGGED: Win probability is unusually high ({prob:.1%}). In modern F1, >95% confidence may indicate data leakage or a hyper-dominant driver in the dataset. Proceed with caution.\n\n"
 
17
  """Critiques the prediction mathematically to safeguard the LLM."""
18
  prob = prediction["probability"]
19
  shap_values = prediction["shap_values"]
20
+ is_prerace = prediction.get("is_prerace", False)
21
 
22
  approved = True
23
  confidence_rating = "Normal"
24
  critique_notes = ""
25
 
26
  # 1. Evaluate Probability Thresholds
27
+ # Pre-race predictions use a lower threshold (5% = true random chance in a 22-car field)
28
+ # because features like tires/weather are missing and probabilities are naturally spread thin.
29
+ # Post-race/historical queries use the stricter 20% threshold.
30
+ reject_threshold = 0.05 if is_prerace else 0.20
31
+
32
+ if prob < reject_threshold:
33
  approved = False
34
  confidence_rating = "Low"
35
+ context = "pre-race estimate" if is_prerace else "historical query"
36
+ critique_notes += f"REJECTED: Win probability is {prob:.1%}, below the {reject_threshold:.0%} threshold for a {context}. The model is not confident.\n\n"
37
+ elif is_prerace and prob < 0.20:
38
+ confidence_rating = "PreRace"
39
+ critique_notes += f"PRE-RACE ESTIMATE: Win probability is {prob:.1%}. This is a pre-race prediction using qualifying data only — tires, weather, and race pace data are unavailable. Rankings are meaningful but absolute probabilities are lower than post-race queries.\n\n"
40
  elif prob > 0.95:
41
  confidence_rating = "Overconfident"
42
  critique_notes += f"FLAGGED: Win probability is unusually high ({prob:.1%}). In modern F1, >95% confidence may indicate data leakage or a hyper-dominant driver in the dataset. Proceed with caution.\n\n"
agents/data_agent.py CHANGED
@@ -159,7 +159,7 @@ def _load_feature_data(data_path: str | Path) -> pd.DataFrame:
159
  return pd.read_parquet(path)
160
 
161
 
162
- def _filter_by_intent(df: pd.DataFrame, intent: QueryIntent) -> pd.DataFrame:
163
  rows = df[df["season"] == intent["season"]].copy()
164
 
165
  round_num = intent.get("round")
@@ -188,6 +188,9 @@ def _filter_by_intent(df: pd.DataFrame, intent: QueryIntent) -> pd.DataFrame:
188
  f"No rows found for season={intent['season']} round={round_num} grand_prix={grand_prix}"
189
  )
190
 
 
 
 
191
  driver_id = _normalize_driver_id(intent.get("driver_id"))
192
  driver_name = intent.get("driver_name")
193
 
 
159
  return pd.read_parquet(path)
160
 
161
 
162
+ def _filter_by_intent(df: pd.DataFrame, intent: QueryIntent, filter_driver: bool = True) -> pd.DataFrame:
163
  rows = df[df["season"] == intent["season"]].copy()
164
 
165
  round_num = intent.get("round")
 
188
  f"No rows found for season={intent['season']} round={round_num} grand_prix={grand_prix}"
189
  )
190
 
191
+ if not filter_driver:
192
+ return rows.reset_index(drop=True)
193
+
194
  driver_id = _normalize_driver_id(intent.get("driver_id"))
195
  driver_name = intent.get("driver_name")
196
 
agents/synthesis_agent.py CHANGED
@@ -18,12 +18,16 @@ class SynthesisOutput(TypedDict):
18
  def synthesis_agent(
19
  query: str,
20
  prediction: PredictionOutput,
21
- critique: CritiqueOutput
 
22
  ) -> SynthesisOutput:
23
  """Uses Llama3 to write the final explanation for the user."""
24
 
25
- # If the mathematical critique rejected the prediction, bypass the LLM
26
- if not critique["approved"]:
 
 
 
27
  return {
28
  "final_response": (
29
  "I apologize, but my underlying machine learning model is not confident enough "
@@ -48,20 +52,41 @@ YOUR JOB:
48
  Synthesize this mathematical data into a clear, confident, and professional answer for the user.
49
  Speak like an F1 Race Engineer on the pit wall.
50
  Do NOT hallucinate reasons. Only use the features provided in the Critique Notes.
51
- Explain what the features mean in a natural way (e.g. if 'grid_position' has a positive impact, say starting on pole is a massive advantage here).
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  Keep it concise (1-2 paragraphs).
54
  """
55
 
 
 
 
 
 
56
  user_prompt = f"""
57
  USER QUERY:
58
  "{query}"
59
 
60
  PREDICTED DRIVER: {prediction.get('driver_name', 'Unknown Driver')}
 
61
 
62
  ML PREDICTION:
63
  Win Probability: {prediction['probability']:.1%}
64
  Confidence Rating: {critique['confidence_rating']}
 
 
 
 
65
 
66
  CRITIQUE AGENT NOTES (SHAP ANALYSIS):
67
  {critique['critique_notes']}
 
18
  def synthesis_agent(
19
  query: str,
20
  prediction: PredictionOutput,
21
+ critique: CritiqueOutput,
22
+ top_contenders: str = ""
23
  ) -> SynthesisOutput:
24
  """Uses Llama3 to write the final explanation for the user."""
25
 
26
+ is_prerace = prediction.get("is_prerace", False)
27
+
28
+ # If mathematically rejected AND it's not a pre-race query → hard reject
29
+ # For pre-race, always call the LLM because the ranking is still meaningful
30
+ if not critique["approved"] and not is_prerace:
31
  return {
32
  "final_response": (
33
  "I apologize, but my underlying machine learning model is not confident enough "
 
52
  Synthesize this mathematical data into a clear, confident, and professional answer for the user.
53
  Speak like an F1 Race Engineer on the pit wall.
54
  Do NOT hallucinate reasons. Only use the features provided in the Critique Notes.
55
+ Explain what the features mean in a natural way.
56
+ Specifically:
57
+ - If 'grid_position' or 'pole_conversion_rate' has a strong impact, explain that qualifying position is historically crucial at this specific circuit. Note that our model uses a smoothed historical average, so even newer tracks are grounded in global F1 reality.
58
+ - If 'career_race_starts' or 'driver_form_last3' heavily impact a prediction, explain the "experience factor". The model heavily trusts the recent form of proven veterans (100+ starts) but correctly discounts the form of rookies who have a very small sample size of races.
59
+ - IMPORTANT: If the user asks about new engine components, fresh power units, or new car parts/upgrades, you MUST acknowledge that our predictive model operates purely on historical telemetry and lap data, and DOES NOT have access to internal team data regarding unannounced parts upgrades or new engines.
60
+ - You have been provided the top contenders for this race. If the predicted driver is NOT one of the top favorites, you MUST mention the actual favorites to provide accurate context (e.g., "While Antonelli has a 5% chance, keep in mind Verstappen and Hamilton are the heavy favorites").
61
+ - CRASH DETECTION: You will be provided the driver's 'Qualifying Status'. If it says something like 'Accident', 'Collision', 'Spun off', or anything indicating a crash, you MUST dramatically mention that they crashed in qualifying. Note that a driver can still have a good grid position (e.g. they set a fast lap in Q3 and THEN crashed, which is a known loophole). If they crashed but have a good grid position, explicitly point out this loophole!
62
+
63
+ IMPORTANT — If Confidence Rating is "PreRace":
64
+ This is a PRE-RACE prediction using only qualifying data. Tire strategy, weather, and pit-stop data are not yet available.
65
+ The model's absolute probabilities are compressed across 22 drivers, so the RANKING matters more than the exact number.
66
+ Frame your response as a pre-race assessment, not a definitive prediction. Mention the key qualifying factors (sector times, grid position, driver form).
67
 
68
  Keep it concise (1-2 paragraphs).
69
  """
70
 
71
+ # For pre-race rejected predictions (below 5% threshold), explain it's a long-shot
72
+ prerace_note = ""
73
+ if not critique["approved"] and is_prerace:
74
+ prerace_note = f"\nNOTE: This driver's win probability ({prediction['probability']:.1%}) is very low — they are considered a long-shot for this race based on qualifying data."
75
+
76
  user_prompt = f"""
77
  USER QUERY:
78
  "{query}"
79
 
80
  PREDICTED DRIVER: {prediction.get('driver_name', 'Unknown Driver')}
81
+ Qualifying Status: {prediction.get('quali_status', 'Finished')}
82
 
83
  ML PREDICTION:
84
  Win Probability: {prediction['probability']:.1%}
85
  Confidence Rating: {critique['confidence_rating']}
86
+ {prerace_note}
87
+
88
+ OVERALL RACE FAVORITES:
89
+ {top_contenders if top_contenders else "Unknown"}
90
 
91
  CRITIQUE AGENT NOTES (SHAP ANALYSIS):
92
  {critique['critique_notes']}
api/main.py CHANGED
@@ -26,6 +26,9 @@ from fastapi import FastAPI, HTTPException, Query
26
  from fastapi.middleware.cors import CORSMiddleware
27
 
28
  from agents.data_agent import data_agent, QueryIntent
 
 
 
29
  from api.schemas import (
30
  DriverInfo,
31
  ErrorResponse,
@@ -34,6 +37,8 @@ from api.schemas import (
34
  PredictionRequest,
35
  PredictionResponse,
36
  RaceInfo,
 
 
37
  )
38
  from ml.predict import load_model_and_encoders, predict_dataframe
39
 
@@ -49,6 +54,7 @@ _model = None
49
  _encoders = None
50
  _races_data: pd.DataFrame | None = None
51
  _drivers_set: set[str] = set()
 
52
 
53
 
54
  def patch_mlruns_paths():
@@ -139,6 +145,22 @@ async def lifespan(app: FastAPI):
139
  else:
140
  logger.warning(f"Data not found: {data_path}")
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  except Exception as e:
143
  logger.error(f"Startup error: {e}")
144
 
@@ -215,28 +237,78 @@ async def predict_f1(request: PredictionRequest) -> PredictionResponse:
215
  detail="Model not loaded. Set KRONECTOR_MODEL_RUN_ID.",
216
  )
217
 
218
- if _races_data is None:
219
  raise HTTPException(
220
  status_code=503,
221
  detail="Race data not loaded. Check data_output/fastf1_races.parquet",
222
  )
223
 
224
  try:
225
- # Parse query → intent
226
- result = data_agent(request.query)
227
- intent = result["intent"]
228
- df = result["dataframe"]
 
 
 
229
 
230
- if len(df) == 0:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  raise HTTPException(
232
  status_code=400,
233
- detail=f"No matching data for query. "
234
- f"Season {intent['season']}, round {intent['round']}",
 
 
 
 
235
  )
236
 
237
  # Generate predictions
238
  predictions = predict_dataframe(df, _model, _encoders, explain=True)
239
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  # If the user asked "Who will win?" and matched multiple drivers,
241
  # sort by probability to find the most likely winner!
242
  if len(predictions) > 1:
@@ -244,6 +316,14 @@ async def predict_f1(request: PredictionRequest) -> PredictionResponse:
244
 
245
  pred_row = predictions.iloc[0]
246
 
 
 
 
 
 
 
 
 
247
  # 1. Format raw prediction output
248
  raw_prob = float(pred_row["win_probability"])
249
  shap_values = pred_row.get("shap_values", {})
@@ -254,24 +334,26 @@ async def predict_f1(request: PredictionRequest) -> PredictionResponse:
254
  "feature_names": list(shap_values.keys()) if shap_values else [],
255
  "model_version": "latest",
256
  "run_id": "api",
257
- "driver_name": str(pred_row["driver_name"])
 
 
258
  }
259
 
260
  # 2. Mathematical Critique
261
  critique = critique_agent(prediction_output)
262
 
263
  # 3. LLM Synthesis
264
- synthesis = synthesis_agent(request.query, prediction_output, critique)
265
 
266
  return PredictionResponse(
267
- win_probability=raw_prob,
268
  metadata=PredictionMetadata(
269
  season=int(pred_row["season"]),
270
  round=int(pred_row["round"]),
271
  driver_id=str(pred_row["driver_id"]),
272
  driver_name=str(pred_row["driver_name"]),
273
  team=str(pred_row["team"]),
274
- grid_position=float(pred_row.get("grid_position", 0.0)), # Default if missing
275
  ),
276
  shap_values=shap_values,
277
  llm_explanation=synthesis["final_response"],
@@ -289,6 +371,151 @@ async def predict_f1(request: PredictionRequest) -> PredictionResponse:
289
  ) from e
290
 
291
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  @app.get("/drivers", response_model=list[DriverInfo])
293
  async def list_drivers(
294
  season: Optional[int] = Query(None, description="Filter by season"),
 
26
  from fastapi.middleware.cors import CORSMiddleware
27
 
28
  from agents.data_agent import data_agent, QueryIntent
29
+ from agents.critique_agent import critique_agent
30
+ from agents.synthesis_agent import synthesis_agent
31
+ from agents.compare_agent import compare_agent
32
  from api.schemas import (
33
  DriverInfo,
34
  ErrorResponse,
 
37
  PredictionRequest,
38
  PredictionResponse,
39
  RaceInfo,
40
+ CompareRequest,
41
+ CompareResponse
42
  )
43
  from ml.predict import load_model_and_encoders, predict_dataframe
44
 
 
54
  _encoders = None
55
  _races_data: pd.DataFrame | None = None
56
  _drivers_set: set[str] = set()
57
+ _prerace_data: dict[str, pd.DataFrame] = {} # keyed by "season_round" e.g. "2026_7"
58
 
59
 
60
  def patch_mlruns_paths():
 
145
  else:
146
  logger.warning(f"Data not found: {data_path}")
147
 
148
+ # Load any pre-race parquet files from data_output/prerace/
149
+ prerace_dir = Path("data_output/prerace")
150
+ if prerace_dir.exists():
151
+ for pf in prerace_dir.glob("*.parquet"):
152
+ try:
153
+ prerace_df = pd.read_parquet(pf)
154
+ if not prerace_df.empty:
155
+ season = int(prerace_df["season"].iloc[0])
156
+ round_num = int(prerace_df["round"].iloc[0])
157
+ key = f"{season}_{round_num}"
158
+ _prerace_data[key] = prerace_df
159
+ circuit = prerace_df["circuit_id"].iloc[0]
160
+ logger.info(f"✓ Pre-race data loaded: {key} ({circuit}) — {len(prerace_df)} drivers")
161
+ except Exception as e:
162
+ logger.warning(f"Could not load pre-race file {pf}: {e}")
163
+
164
  except Exception as e:
165
  logger.error(f"Startup error: {e}")
166
 
 
237
  detail="Model not loaded. Set KRONECTOR_MODEL_RUN_ID.",
238
  )
239
 
240
+ if _races_data is None and not _prerace_data:
241
  raise HTTPException(
242
  status_code=503,
243
  detail="Race data not loaded. Check data_output/fastf1_races.parquet",
244
  )
245
 
246
  try:
247
+ # Parse query → intent using the main historical parquet
248
+ # If the round isn't in the main parquet, fall back to pre-race data
249
+ from agents.data_agent import parse_query_with_groq, _filter_by_intent
250
+ intent = parse_query_with_groq(request.query)
251
+
252
+ df = pd.DataFrame()
253
+ is_prerace = False
254
 
255
+ # 1. Try the main historical parquet first
256
+ if _races_data is not None:
257
+ try:
258
+ df = _filter_by_intent(_races_data, intent, filter_driver=False)
259
+ except (ValueError, KeyError):
260
+ df = pd.DataFrame()
261
+
262
+ # 2. If not found in historical data, try pre-race data
263
+ if df.empty and _prerace_data:
264
+ logger.info(f"Round not in historical data — searching pre-race store for intent: {intent}")
265
+ for key, prerace_df in _prerace_data.items():
266
+ try:
267
+ df = _filter_by_intent(prerace_df, intent, filter_driver=False)
268
+ if not df.empty:
269
+ is_prerace = True
270
+ logger.info(f"✓ Found match in pre-race data: {key}")
271
+ break
272
+ except (ValueError, KeyError):
273
+ continue
274
+
275
+ if df.empty:
276
  raise HTTPException(
277
  status_code=400,
278
+ detail=(
279
+ f"No matching data for query. Season={intent.get('season')}, "
280
+ f"GP='{intent.get('grand_prix')}'. "
281
+ "If this is an upcoming race, run: "
282
+ "python -m scripts.build_prerace_rows --season <year> --round <num>"
283
+ ),
284
  )
285
 
286
  # Generate predictions
287
  predictions = predict_dataframe(df, _model, _encoders, explain=True)
288
 
289
+ # Get top 3 contenders for context before filtering
290
+ top_df = predictions.sort_values(by="win_probability", ascending=False).head(3)
291
+ top_contenders = ", ".join(
292
+ [f"{r['driver_name']} ({r['win_probability']*100:.1f}%)" for _, r in top_df.iterrows()]
293
+ )
294
+
295
+ # Now filter down to the requested driver if specified
296
+ driver_id = intent.get("driver_id")
297
+ driver_name = intent.get("driver_name")
298
+
299
+ import re
300
+ if driver_id:
301
+ d_id = str(driver_id).strip().upper()
302
+ predictions = predictions[predictions["driver_id"].astype(str).str.upper() == d_id]
303
+ elif driver_name:
304
+ d_name = str(driver_name).strip().lower()
305
+ predictions = predictions[
306
+ predictions["driver_name"].astype(str).str.lower().str.contains(re.escape(d_name), na=False)
307
+ ]
308
+
309
+ if predictions.empty:
310
+ raise HTTPException(status_code=400, detail=f"No predictions matched driver intent: {intent}")
311
+
312
  # If the user asked "Who will win?" and matched multiple drivers,
313
  # sort by probability to find the most likely winner!
314
  if len(predictions) > 1:
 
316
 
317
  pred_row = predictions.iloc[0]
318
 
319
+ # grid_position is stripped from metadata by predict_dataframe — look it up
320
+ # directly from the source df using the matched driver_id.
321
+ matched_driver_id = str(pred_row["driver_id"])
322
+ grid_pos_series = df.loc[
323
+ df["driver_id"].astype(str) == matched_driver_id, "grid_position"
324
+ ]
325
+ grid_pos = float(grid_pos_series.iloc[0]) if not grid_pos_series.empty else 0.0
326
+
327
  # 1. Format raw prediction output
328
  raw_prob = float(pred_row["win_probability"])
329
  shap_values = pred_row.get("shap_values", {})
 
334
  "feature_names": list(shap_values.keys()) if shap_values else [],
335
  "model_version": "latest",
336
  "run_id": "api",
337
+ "driver_name": str(pred_row["driver_name"]),
338
+ "is_prerace": is_prerace,
339
+ "quali_status": "Crash" if getattr(request, "crashed_in_quali", False) else str(pred_row.get("quali_status", "Finished")),
340
  }
341
 
342
  # 2. Mathematical Critique
343
  critique = critique_agent(prediction_output)
344
 
345
  # 3. LLM Synthesis
346
+ synthesis = synthesis_agent(request.query, prediction_output, critique, top_contenders=top_contenders)
347
 
348
  return PredictionResponse(
349
+ win_probability=round(raw_prob * 100, 2),
350
  metadata=PredictionMetadata(
351
  season=int(pred_row["season"]),
352
  round=int(pred_row["round"]),
353
  driver_id=str(pred_row["driver_id"]),
354
  driver_name=str(pred_row["driver_name"]),
355
  team=str(pred_row["team"]),
356
+ grid_position=grid_pos,
357
  ),
358
  shap_values=shap_values,
359
  llm_explanation=synthesis["final_response"],
 
371
  ) from e
372
 
373
 
374
+ @app.post(
375
+ "/predict/compare",
376
+ response_model=CompareResponse,
377
+ description="Compare two drivers head-to-head using mathematical SHAP Deltas",
378
+ tags=["Prediction"]
379
+ )
380
+ def compare_drivers(request: CompareRequest):
381
+ """Head-to-head driver matchup endpoint."""
382
+ if _model is None or _encoders is None:
383
+ raise HTTPException(
384
+ status_code=503, detail="Model not loaded. Set KRONECTOR_MODEL_RUN_ID."
385
+ )
386
+
387
+ try:
388
+ import re
389
+ import json
390
+ is_prerace = False
391
+ df = pd.DataFrame()
392
+ race_name = "Unknown Race"
393
+
394
+ # 1. Season and round specified?
395
+ if request.season and request.round:
396
+ prerace_file = Path(f"data_output/prerace/prerace_*_{request.season}.parquet")
397
+ import glob
398
+ files = glob.glob(str(prerace_file))
399
+
400
+ found = False
401
+ for f in files:
402
+ pdf = pd.read_parquet(f)
403
+ if not pdf.empty and int(pdf["season"].iloc[0]) == request.season and int(pdf["round"].iloc[0]) == request.round:
404
+ df = pdf
405
+ is_prerace = True
406
+ race_name = f"{request.season} Round {request.round}"
407
+ found = True
408
+ break
409
+
410
+ if not found:
411
+ global _races_data
412
+ if _races_data is not None and not _races_data.empty:
413
+ df = _races_data[
414
+ (_races_data["season"] == request.season) &
415
+ (_races_data["round"] == request.round)
416
+ ]
417
+ race_name = f"{request.season} Round {request.round}"
418
+
419
+ # 2. No season/round specified, use latest
420
+ if df.empty:
421
+ prerace_dir = Path("data_output/prerace")
422
+ if prerace_dir.exists():
423
+ files = list(prerace_dir.glob("*.parquet"))
424
+ if files:
425
+ df = pd.read_parquet(files[0])
426
+ is_prerace = True
427
+ s = int(df["season"].iloc[0])
428
+ r = int(df["round"].iloc[0])
429
+ race_name = f"{s} Round {r}"
430
+
431
+ if df.empty and _races_data is not None and not _races_data.empty:
432
+ latest_season = _races_data["season"].max()
433
+ latest_round = _races_data[_races_data["season"] == latest_season]["round"].max()
434
+ df = _races_data[
435
+ (_races_data["season"] == latest_season) &
436
+ (_races_data["round"] == latest_round)
437
+ ]
438
+ race_name = f"{latest_season} Round {latest_round}"
439
+
440
+ if df.empty:
441
+ raise HTTPException(status_code=400, detail="Could not find race data for comparison.")
442
+
443
+ predictions = predict_dataframe(df, _model, _encoders, explain=True)
444
+
445
+ d1_str = request.driver1.strip().lower()
446
+ d1_df = predictions[
447
+ (predictions["driver_id"].str.lower() == d1_str) |
448
+ (predictions["driver_name"].str.lower().str.contains(re.escape(d1_str), na=False))
449
+ ]
450
+ if d1_df.empty:
451
+ raise HTTPException(status_code=400, detail=f"Driver '{request.driver1}' not found in race {race_name}.")
452
+ d1_row = d1_df.iloc[0]
453
+
454
+ d2_str = request.driver2.strip().lower()
455
+ d2_df = predictions[
456
+ (predictions["driver_id"].str.lower() == d2_str) |
457
+ (predictions["driver_name"].str.lower().str.contains(re.escape(d2_str), na=False))
458
+ ]
459
+ if d2_df.empty:
460
+ raise HTTPException(status_code=400, detail=f"Driver '{request.driver2}' not found in race {race_name}.")
461
+ d2_row = d2_df.iloc[0]
462
+
463
+ d1_prob = float(d1_row["win_probability"]) * 100
464
+ d2_prob = float(d2_row["win_probability"]) * 100
465
+
466
+ d1_shap = d1_row["shap_values"]
467
+ d2_shap = d2_row["shap_values"]
468
+ if isinstance(d1_shap, str):
469
+ d1_shap = json.loads(d1_shap)
470
+ if isinstance(d2_shap, str):
471
+ d2_shap = json.loads(d2_shap)
472
+
473
+ deltas = {}
474
+ for feature in d1_shap.keys():
475
+ if feature in d2_shap:
476
+ deltas[feature] = d1_shap[feature] - d2_shap[feature]
477
+
478
+ llm_analysis = compare_agent(
479
+ driver1_name=str(d1_row["driver_name"]),
480
+ driver1_prob=d1_prob,
481
+ driver1_status=str(d1_row.get("quali_status", "Finished")),
482
+ driver2_name=str(d2_row["driver_name"]),
483
+ driver2_prob=d2_prob,
484
+ driver2_status=str(d2_row.get("quali_status", "Finished")),
485
+ shap_deltas=deltas,
486
+ race_context=f"{race_name} {'(Pre-Race)' if is_prerace else '(Historical)'}"
487
+ )
488
+
489
+ return CompareResponse(
490
+ driver1=PredictionMetadata(
491
+ season=int(d1_row["season"]),
492
+ round=int(d1_row["round"]),
493
+ driver_id=str(d1_row["driver_id"]),
494
+ driver_name=str(d1_row["driver_name"]),
495
+ team=str(d1_row["team"]),
496
+ grid_position=float(d1_row.get("grid_position", 0.0)),
497
+ ),
498
+ driver2=PredictionMetadata(
499
+ season=int(d2_row["season"]),
500
+ round=int(d2_row["round"]),
501
+ driver_id=str(d2_row["driver_id"]),
502
+ driver_name=str(d2_row["driver_name"]),
503
+ team=str(d2_row["team"]),
504
+ grid_position=float(d2_row.get("grid_position", 0.0)),
505
+ ),
506
+ driver1_win_probability=round(d1_prob, 2),
507
+ driver2_win_probability=round(d2_prob, 2),
508
+ shap_deltas=deltas,
509
+ llm_analysis=llm_analysis
510
+ )
511
+
512
+ except HTTPException:
513
+ raise
514
+ except Exception as e:
515
+ logger.error(f"Compare prediction error: {e}", exc_info=True)
516
+ raise HTTPException(status_code=500, detail=f"Comparison failed: {e}")
517
+
518
+
519
  @app.get("/drivers", response_model=list[DriverInfo])
520
  async def list_drivers(
521
  season: Optional[int] = Query(None, description="Filter by season"),
api/schemas.py CHANGED
@@ -16,6 +16,9 @@ class PredictionRequest(BaseModel):
16
  max_length=500,
17
  examples=["What's Max's win probability at Monaco 2023?"],
18
  )
 
 
 
19
 
20
 
21
  class PredictionMetadata(BaseModel):
@@ -33,7 +36,7 @@ class PredictionResponse(BaseModel):
33
  """Win probability prediction with SHAP explanations."""
34
 
35
  win_probability: float = Field(
36
- ..., ge=0, le=1, description="Probability of winning (0.0-1.0)"
37
  )
38
  metadata: PredictionMetadata
39
  shap_values: Optional[dict[str, float]] = Field(
@@ -77,3 +80,34 @@ class ErrorResponse(BaseModel):
77
 
78
  detail: str
79
  error_code: str = Field(..., examples=["INVALID_QUERY", "MODEL_NOT_LOADED"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  max_length=500,
17
  examples=["What's Max's win probability at Monaco 2023?"],
18
  )
19
+ crashed_in_quali: bool = Field(
20
+ False, description="Manually flag if the driver crashed in qualifying"
21
+ )
22
 
23
 
24
  class PredictionMetadata(BaseModel):
 
36
  """Win probability prediction with SHAP explanations."""
37
 
38
  win_probability: float = Field(
39
+ ..., ge=0, le=100, description="Win probability as a percentage (0-100)"
40
  )
41
  metadata: PredictionMetadata
42
  shap_values: Optional[dict[str, float]] = Field(
 
80
 
81
  detail: str
82
  error_code: str = Field(..., examples=["INVALID_QUERY", "MODEL_NOT_LOADED"])
83
+
84
+
85
+ class CompareRequest(BaseModel):
86
+ """Request for Head-to-Head Driver Comparison."""
87
+
88
+ driver1: str = Field(
89
+ ...,
90
+ examples=["Max Verstappen", "VER"],
91
+ description="First driver to compare (name or 3-letter code)"
92
+ )
93
+ driver2: str = Field(
94
+ ...,
95
+ examples=["Kimi Antonelli", "ANT"],
96
+ description="Second driver to compare (name or 3-letter code)"
97
+ )
98
+ season: Optional[int] = Field(None, description="Optional year (defaults to latest available)")
99
+ round: Optional[int] = Field(None, description="Optional round number (defaults to latest available)")
100
+
101
+
102
+ class CompareResponse(BaseModel):
103
+ """Head-to-Head Comparison Response."""
104
+
105
+ driver1: PredictionMetadata
106
+ driver2: PredictionMetadata
107
+ driver1_win_probability: float
108
+ driver2_win_probability: float
109
+ shap_deltas: dict[str, float] = Field(
110
+ ...,
111
+ description="Mathematical difference between driver 1 and driver 2 SHAP values (positive means driver 1 has the edge, negative means driver 2 has the edge)"
112
+ )
113
+ llm_analysis: str = Field(..., description="Groq's Head-to-Head tale of the tape")
data/__init__.py CHANGED
@@ -61,6 +61,85 @@ def _get_track_type(circuit_id: str) -> str:
61
  return "permanent"
62
 
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  # ---------------------------------------------------------------------------
65
  # Safety car probability computation (Correction 2)
66
  # ---------------------------------------------------------------------------
@@ -298,6 +377,35 @@ def merge_datasets(
298
  "No lap data — safety_car_probability set to 0 for all rows"
299
  )
300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  # -------------------------------------------------------------------
302
  # Step 9: Add target variable
303
  # -------------------------------------------------------------------
 
61
  return "permanent"
62
 
63
 
64
+ def compute_pole_conversion_rate(df: pd.DataFrame) -> pd.DataFrame:
65
+ """
66
+ Compute historical pole-to-win conversion rate per circuit.
67
+
68
+ For each circuit, calculates what % of races the pole-sitter went on to win.
69
+ This directly encodes "qualifying matters at this track" — e.g.:
70
+ - Monaco ~75-80% (almost impossible to overtake)
71
+ - Monza ~40-45% (long straights, slipstream, DRS)
72
+
73
+ Uses only COMPLETED races (requires finish_position data).
74
+
75
+ Args:
76
+ df: Historical race DataFrame with circuit_id, grid_position, finish_position
77
+
78
+ Returns:
79
+ DataFrame with columns (circuit_id, pole_conversion_rate)
80
+ """
81
+ if df.empty or "finish_position" not in df.columns:
82
+ return pd.DataFrame(columns=["circuit_id", "pole_conversion_rate"])
83
+
84
+ # Only use rows where we have actual race results
85
+ completed = df.dropna(subset=["finish_position"]).copy()
86
+
87
+ # Get pole-sitters (grid_position == 1) for each race
88
+ poles = completed[completed["grid_position"] == 1].copy()
89
+
90
+ if poles.empty:
91
+ return pd.DataFrame(columns=["circuit_id", "pole_conversion_rate"])
92
+
93
+ poles["pole_won"] = (poles["finish_position"] == 1).astype(int)
94
+
95
+ global_mean = poles["pole_won"].mean()
96
+ C = 3.0 # Confidence weight (pseudo-observations)
97
+
98
+ # Calculate wins and total poles per circuit
99
+ stats = poles.groupby("circuit_id").agg(
100
+ wins=("pole_won", "sum"),
101
+ total=("pole_won", "count")
102
+ ).reset_index()
103
+
104
+ # Apply Bayesian smoothing: pulls low-N circuits towards the global average
105
+ stats["pole_conversion_rate"] = (stats["wins"] + C * global_mean) / (stats["total"] + C)
106
+
107
+ conversion = stats[["circuit_id", "pole_conversion_rate"]]
108
+
109
+ logger.info(
110
+ f"Computed pole_conversion_rate for {len(conversion)} circuits. "
111
+ f"Range: {conversion['pole_conversion_rate'].min():.1%} – "
112
+ f"{conversion['pole_conversion_rate'].max():.1%}"
113
+ )
114
+
115
+ return conversion
116
+
117
+ return conversion
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # Career Race Starts computation
122
+ # ---------------------------------------------------------------------------
123
+
124
+ def compute_career_race_starts(df: pd.DataFrame) -> pd.DataFrame:
125
+ """
126
+ Compute cumulative career race starts for each driver before each race.
127
+ This provides an 'experience' feature so the model knows if driver form
128
+ is based on a veteran's long track record or a rookie's small sample size.
129
+ """
130
+ if df.empty or "driver_id" not in df.columns:
131
+ return pd.DataFrame(columns=["season", "round", "driver_id", "career_race_starts"])
132
+
133
+ # Sort chronologically
134
+ df_sorted = df.sort_values(by=["driver_id", "season", "round"]).copy()
135
+
136
+ # cumcount() gives 0 for the 1st race, 1 for the 2nd, etc.
137
+ # This exactly represents 'starts prior to this race'
138
+ df_sorted["career_race_starts"] = df_sorted.groupby("driver_id").cumcount()
139
+
140
+ return df_sorted[["season", "round", "driver_id", "career_race_starts"]]
141
+
142
+
143
  # ---------------------------------------------------------------------------
144
  # Safety car probability computation (Correction 2)
145
  # ---------------------------------------------------------------------------
 
377
  "No lap data — safety_car_probability set to 0 for all rows"
378
  )
379
 
380
+ # -------------------------------------------------------------------
381
+ # Step 8.3: Compute career_race_starts per driver
382
+ # -------------------------------------------------------------------
383
+ starts_df = compute_career_race_starts(merged)
384
+ if not starts_df.empty:
385
+ if "career_race_starts" in merged.columns:
386
+ merged = merged.drop(columns=["career_race_starts"])
387
+ merged = merged.merge(starts_df, on=["season", "round", "driver_id"], how="left")
388
+ merged["career_race_starts"] = merged["career_race_starts"].fillna(0)
389
+ logger.info("Joined career_race_starts from cumulative history")
390
+ else:
391
+ merged["career_race_starts"] = 0
392
+ logger.warning("Could not compute career_race_starts — defaulting to 0")
393
+
394
+ # -------------------------------------------------------------------
395
+ # Step 8.5: Compute pole_conversion_rate per circuit
396
+ # -------------------------------------------------------------------
397
+ pcr = compute_pole_conversion_rate(merged)
398
+ if not pcr.empty:
399
+ if "pole_conversion_rate" in merged.columns:
400
+ merged = merged.drop(columns=["pole_conversion_rate"])
401
+ merged = merged.merge(pcr, on="circuit_id", how="left")
402
+ # Circuits with no data default to 50% (neutral)
403
+ merged["pole_conversion_rate"] = merged["pole_conversion_rate"].fillna(0.5)
404
+ logger.info("Joined pole_conversion_rate from historical data")
405
+ else:
406
+ merged["pole_conversion_rate"] = 0.5
407
+ logger.warning("Could not compute pole_conversion_rate — defaulting to 0.5")
408
+
409
  # -------------------------------------------------------------------
410
  # Step 9: Add target variable
411
  # -------------------------------------------------------------------
data/fastf1_pipeline.py CHANGED
@@ -54,7 +54,9 @@ def enable_cache(cache_dir: Optional[str] = None) -> None:
54
  # ---------------------------------------------------------------------------
55
 
56
  def _load_session(
57
- season: int, round_num: int, session_type: str
 
 
58
  ) -> Optional[fastf1.core.Session]:
59
  """
60
  Load a FastF1 session with error handling.
@@ -63,13 +65,17 @@ def _load_session(
63
  season: F1 season year (2018–2026)
64
  round_num: Race round number within season
65
  session_type: One of 'R' (Race), 'Q' (Qualifying), 'FP2', 'FP3'
 
 
 
 
66
 
67
  Returns:
68
  Loaded FastF1 Session object, or None if unavailable.
69
  """
70
  try:
71
  session = fastf1.get_session(season, round_num, session_type)
72
- session.load()
73
  return session
74
  except Exception as e:
75
  logger.warning(
 
54
  # ---------------------------------------------------------------------------
55
 
56
  def _load_session(
57
+ season: int, round_num: int, session_type: str,
58
+ laps: bool = True, telemetry: bool = False,
59
+ weather: bool = True, messages: bool = False,
60
  ) -> Optional[fastf1.core.Session]:
61
  """
62
  Load a FastF1 session with error handling.
 
65
  season: F1 season year (2018–2026)
66
  round_num: Race round number within season
67
  session_type: One of 'R' (Race), 'Q' (Qualifying), 'FP2', 'FP3'
68
+ laps: Load lap timing data (default True)
69
+ telemetry: Load car telemetry (default False — not needed for features)
70
+ weather: Load weather data (default True)
71
+ messages: Load race control messages (default False)
72
 
73
  Returns:
74
  Loaded FastF1 Session object, or None if unavailable.
75
  """
76
  try:
77
  session = fastf1.get_session(season, round_num, session_type)
78
+ session.load(laps=laps, telemetry=telemetry, weather=weather, messages=messages)
79
  return session
80
  except Exception as e:
81
  logger.warning(
data_output/fastf1_races.parquet CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:8d2fba650f3d5031cd6ac5bf9c3507f471ba4a4ddc59b297e978c0c4fdb045b6
3
- size 132567
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2849f1eec94e44f124f04f2df23970214fb004aa5ce64080ed5520b3ec666b88
3
+ size 111244
data_output/prerace/prerace_70th_anniversary_gra_2020.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5cd269f7255addee293be9f2edb819a9aee26b922dc13445a7858386fc4797fc
3
+ size 17243
data_output/prerace/prerace_abu_dhabi_grand_prix_2014.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b4edb066e72f51c97af2b827a0372b9537bab50931f1d1a41350203110f0427a
3
+ size 16024
data_output/prerace/prerace_abu_dhabi_grand_prix_2015.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1f968dac8035d2a6b98fdd2cb948a2919f67afb82c803e1e8fc00a652c1fdc7e
3
+ size 16086
data_output/prerace/prerace_abu_dhabi_grand_prix_2016.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1a5601f3056795c2ece6ef7df44574b6af73ff62585112a432387f4fc1de2695
3
+ size 16184
data_output/prerace/prerace_abu_dhabi_grand_prix_2017.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:502d00fd14943e14cbf616fadb5bc1a425255eee7b4aa5e3bca31f2366ab4491
3
+ size 16121
data_output/prerace/prerace_abu_dhabi_grand_prix_2018.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fee82cb6eb4323b71fec2694d9af9dbd6c83278230d8b46a17fc62e374424fc0
3
+ size 17219
data_output/prerace/prerace_abu_dhabi_grand_prix_2019.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4a4297d21fe4c3c53af0efa99d4629cb1712cb617f4d7f3fb9fe4b6a697a3b53
3
+ size 17171
data_output/prerace/prerace_abu_dhabi_grand_prix_2020.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c8f2097399aeca4cc13c1196013b819ca82644a3de9060dd091558e04ad9b0fe
3
+ size 17197
data_output/prerace/prerace_abu_dhabi_grand_prix_2021.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fd73c940a2039393c25e4375c3a95d55a81a7337bb2dccd0c96112be8c54804f
3
+ size 17224
data_output/prerace/prerace_abu_dhabi_grand_prix_2022.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1b81b44cce7074d8280e7d3cdddf67178e86429d679b8481bb9052e625b95d8a
3
+ size 17173
data_output/prerace/prerace_abu_dhabi_grand_prix_2023.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:808cbab2d0ebf3fd4cdbfc3dff58ed68f98f5ee804041664ca3f530ee40dc283
3
+ size 17197
data_output/prerace/prerace_abu_dhabi_grand_prix_2024.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3385b06f5db6ac54b06d75f87f275865612a678c2a0e72c716031707ed5e6b81
3
+ size 17170
data_output/prerace/prerace_abu_dhabi_grand_prix_2025.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1c6b778ca45dfdd67dfb532cd258910c1b97ee0f894de5484ffbd0854c6df252
3
+ size 17116
data_output/prerace/prerace_australian_grand_pri_2014.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:375f0e17bc001fb25ca6d713b4d60dcb97e4ed836465a53f79fd000f70da8199
3
+ size 15799
data_output/prerace/prerace_australian_grand_pri_2015.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5b5892ff5cdf24d49855631396fdc65c5ab89ff8ec0bc8e028dda87ad4041145
3
+ size 16020
data_output/prerace/prerace_australian_grand_pri_2016.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:163b6ac086ec568316728d6bfa44616e66f8fdccc89022cda5d2790492f7d313
3
+ size 16179
data_output/prerace/prerace_australian_grand_pri_2017.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a3bff47910334b98b4785fa55cdcef4ea30936fa63f9c38e9e6ed14fcc05fe97
3
+ size 16120
data_output/prerace/prerace_australian_grand_pri_2018.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b763f2c4b3e5db0d1e732df1598d0452f0ef723352a503fd2538ad35d94e8482
3
+ size 17141
data_output/prerace/prerace_australian_grand_pri_2019.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:933f9ccd3555a89525d451e6988dd5f138fb5b5d908f4ed203a0bd04250e7a41
3
+ size 17176
data_output/prerace/prerace_australian_grand_pri_2022.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:eb2e8fdeb0e7c937b02ba882d0e289f086010c5f40ada27b69df26b021160ab4
3
+ size 17157
data_output/prerace/prerace_australian_grand_pri_2023.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7d7e04018dfe44b4c19881f2699a5e5aa0534de11b3a4e1a5ff0647447c23841
3
+ size 17158
data_output/prerace/prerace_australian_grand_pri_2024.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:93a9ab6e87502fb12b4e0d74c8d8642f8d9364afb18c1cb1e1afd3153da552d2
3
+ size 17098
data_output/prerace/prerace_australian_grand_pri_2025.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fe70b5ae3c1e4e3a4ae1a7183d3f05b01dcc9202595a1f998be88883ec7fe98e
3
+ size 17084
data_output/prerace/prerace_australian_grand_pri_2026.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:89ab900ac7feea4a1eef01372034f778bb3093f3ac425e0c2897eff4cd07dfd4
3
+ size 17193
data_output/prerace/prerace_austrian_grand_prix_2014.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2b89fd196fde105cdc5c7434932b847c4e72ff30ef1e816a1b87410b8a339b42
3
+ size 16208
data_output/prerace/prerace_austrian_grand_prix_2015.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7c780f2f2d9dc96e289b12b829044bb1f4a0a1c9684aac51711ff9e38f8f60c8
3
+ size 16090
data_output/prerace/prerace_austrian_grand_prix_2016.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d17a96cc7d923b4072d9aaadc452087acc00f040fe61cb4428b55c8fd75e7b36
3
+ size 16198
data_output/prerace/prerace_austrian_grand_prix_2017.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e3671471e0cecbdb8d7d2a8d76f7367ad1fd766e44f91119ba1913f04d98d8dd
3
+ size 16113
data_output/prerace/prerace_austrian_grand_prix_2018.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3ec2fba62b991af17cbc38847515e3196dc62f9126c168bbae2a2c7a3eae1713
3
+ size 17193
data_output/prerace/prerace_austrian_grand_prix_2019.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5275dcf00c1580e9c965a018db62d37c2d302505317756c56d6de7d34ad113c2
3
+ size 17180
data_output/prerace/prerace_austrian_grand_prix_2020.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aed36837f04ba4f69a63f0fcdd1e7faa34e1810cc5202048f6dd3381cea1cfd1
3
+ size 17186
data_output/prerace/prerace_austrian_grand_prix_2021.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d890d04b763101f954125f24f77b4650224d5a618b4244ae3689c47834ed1ed0
3
+ size 17194
data_output/prerace/prerace_austrian_grand_prix_2022.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a276394021e03e59b10fcd03c0280df22b3b947d03ccebfd809f95e60f68968d
3
+ size 17180
data_output/prerace/prerace_austrian_grand_prix_2023.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0235cc3b5754c9ad95c8bbc0b14005b463670f416ca85fa52905319c2498e9fb
3
+ size 16910
data_output/prerace/prerace_austrian_grand_prix_2024.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:21ddd483cdd6160159986b984a80336aae7162cb4bed35812fe9dedc827ccbd1
3
+ size 16906
data_output/prerace/prerace_austrian_grand_prix_2025.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b880dbc20428d8c62bf4f2742e8834e7196e73b3f0c677c478c0e8f84d58eca6
3
+ size 17108
data_output/prerace/prerace_azerbaijan_grand_pri_2017.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3991df986866d3b237f2510da2537346ca7b75b86636d0886191aee461a5549b
3
+ size 16079
data_output/prerace/prerace_azerbaijan_grand_pri_2018.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aea98ac4c994c98e4818e105bffdd81a798451ffd2db8bfc53f6dad5a61e215f
3
+ size 17205
data_output/prerace/prerace_azerbaijan_grand_pri_2019.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fdb2c0d9ae8af4fe0afbe35e0fa00b4495d301cc8744cceb8c2b71298cfc2c83
3
+ size 17210
data_output/prerace/prerace_azerbaijan_grand_pri_2021.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:63e61f2415e99646c7d9360479ff0456dcbff83d0efcd530ff74261dd466fd7a
3
+ size 17246
data_output/prerace/prerace_azerbaijan_grand_pri_2022.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5529cc4110424f30674d2c4323a63c273e48b0140d128d1100493d135b4bb919
3
+ size 17180