File size: 10,788 Bytes
2532605 | 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 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 | """
KRONECTOR β Driver ID Mapping Builder
Generates drivers_map.json mapping FastF1 abbreviations β Jolpica slugs.
Run this ONCE at project init before any pipeline runs:
python -m data.build_driver_map
Rule: FastF1 3-letter abbreviation is the MASTER driver_id throughout
the entire system. Jolpica slugs are only used for Jolpica API calls.
Output: drivers_map.json at project root
Format: {"VER": "max_verstappen", "HAM": "lewis_hamilton", ...}
"""
import json
import logging
import os
import time
from pathlib import Path
from typing import Any, Optional
import requests
from dotenv import load_dotenv
load_dotenv()
logger = logging.getLogger(__name__)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
)
# Path to output mapping file
DRIVER_MAP_PATH = Path(__file__).parent / "drivers_map.json"
JOLPICA_BASE_URL = os.getenv(
"JOLPICA_BASE_URL", "https://api.jolpi.ca/ergast/f1"
)
# ---------------------------------------------------------------------------
# Jolpica request helper (reuse from jolpica_pipeline)
# ---------------------------------------------------------------------------
def _jolpica_get(
url: str, retries: int = 3, base_delay: float = 0.2
) -> Optional[dict[str, Any]]:
"""Rate-limited Jolpica GET with exponential backoff."""
for attempt in range(retries):
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
time.sleep(base_delay)
return response.json()
except requests.exceptions.RequestException as e:
wait = base_delay * (2 ** attempt)
logger.warning(
f"Request failed (attempt {attempt + 1}): {e}. "
f"Retrying in {wait:.1f}s"
)
time.sleep(wait)
logger.error(f"Failed after {retries} attempts: {url}")
return None
# ---------------------------------------------------------------------------
# Fetch all Jolpica drivers
# ---------------------------------------------------------------------------
def _fetch_jolpica_drivers() -> dict[str, str]:
"""
Fetch all F1 drivers from Jolpica API.
Returns dict: {full_name_lower: jolpica_slug}
Example: {"max verstappen": "max_verstappen"}
"""
# Fetch with high limit to get all drivers
url = f"{JOLPICA_BASE_URL}/drivers.json?limit=1000"
data = _jolpica_get(url)
if data is None:
return {}
try:
drivers = data["MRData"]["DriverTable"]["Drivers"]
result = {}
for driver in drivers:
full_name = (
f"{driver['givenName']} {driver['familyName']}"
).lower()
slug = driver["driverId"]
result[full_name] = slug
logger.info(f"Fetched {len(result)} drivers from Jolpica")
return result
except (KeyError, TypeError) as e:
logger.error(f"Failed to parse Jolpica drivers: {e}")
return {}
# ---------------------------------------------------------------------------
# Fetch FastF1 driver abbreviations
# ---------------------------------------------------------------------------
def _fetch_fastf1_drivers(
start_year: int = 2014, end_year: int = 2026
) -> dict[str, str]:
"""
Fetch driver abbreviations from FastF1 across multiple seasons.
Returns dict: {full_name_lower: abbreviation}
Example: {"max verstappen": "VER"}
"""
try:
import fastf1
except ImportError:
logger.error("fastf1 not installed β cannot fetch driver abbreviations")
return {}
# Enable cache if possible
cache_dir = os.getenv("FASTF1_CACHE_DIR", "./cache/fastf1")
Path(cache_dir).mkdir(parents=True, exist_ok=True)
try:
fastf1.Cache.enable_cache(cache_dir)
except Exception:
pass
result = {}
for year in range(start_year, end_year + 1):
try:
schedule = fastf1.get_event_schedule(year, include_testing=False)
# Pick first race of the season to get driver list
first_round = schedule[schedule["RoundNumber"] > 0].iloc[0]
round_num = first_round["RoundNumber"]
session = fastf1.get_session(year, round_num, "R")
session.load()
if session.results is not None and not session.results.empty:
for _, driver in session.results.iterrows():
abbrev = driver.get("Abbreviation", "")
full_name = str(driver.get("FullName", "")).lower()
if abbrev and full_name:
result[full_name] = abbrev
logger.info(
f"Fetched FastF1 drivers for {year}: "
f"{len(session.results)} drivers"
)
except Exception as e:
logger.warning(f"Could not fetch FastF1 drivers for {year}: {e}")
continue
logger.info(f"Total unique FastF1 drivers: {len(result)}")
return result
# ---------------------------------------------------------------------------
# Build the mapping
# ---------------------------------------------------------------------------
def build_driver_map(
start_year: int = 2014, end_year: int = 2026
) -> dict[str, str]:
"""
Build the master driver mapping: FastF1 abbreviation β Jolpica slug.
Strategy:
1. Fetch all Jolpica drivers β {full_name: slug}
2. Fetch all FastF1 drivers β {full_name: abbreviation}
3. Match on full_name β {abbreviation: slug}
Returns:
Dict mapping FastF1 abbreviation to Jolpica slug
Example: {"VER": "max_verstappen", "HAM": "lewis_hamilton"}
"""
logger.info("Building driver map...")
# Fetch from both sources
jolpica_drivers = _fetch_jolpica_drivers()
fastf1_drivers = _fetch_fastf1_drivers(start_year, end_year)
# Match on full name
driver_map = {}
unmatched_fastf1 = []
for full_name, abbreviation in fastf1_drivers.items():
if full_name in jolpica_drivers:
driver_map[abbreviation] = jolpica_drivers[full_name]
else:
# Try fuzzy match: remove accents, extra spaces
matched = False
for jolpica_name, slug in jolpica_drivers.items():
# Simple contains check for partial name matches
name_parts = full_name.split()
if len(name_parts) >= 2:
last_name = name_parts[-1]
if last_name in jolpica_name:
driver_map[abbreviation] = slug
matched = True
break
if not matched:
unmatched_fastf1.append((abbreviation, full_name))
if unmatched_fastf1:
logger.warning(
f"Unmatched FastF1 drivers ({len(unmatched_fastf1)}): "
f"{unmatched_fastf1}"
)
logger.info(
f"Driver map built: {len(driver_map)} mapped, "
f"{len(unmatched_fastf1)} unmatched"
)
return driver_map
# ---------------------------------------------------------------------------
# Known fallback map (covers common edge cases)
# ---------------------------------------------------------------------------
KNOWN_DRIVER_MAP = {
# 2014β2026 comprehensive fallback for edge cases
"VER": "max_verstappen",
"HAM": "lewis_hamilton",
"LEC": "charles_leclerc",
"NOR": "lando_norris",
"SAI": "carlos_sainz",
"PER": "perez",
"RUS": "george_russell",
"PIA": "oscar_piastri",
"ALO": "alonso",
"STR": "stroll",
"GAS": "gasly",
"OCO": "ocon",
"ALB": "albon",
"TSU": "tsunoda",
"BOT": "bottas",
"ZHO": "zhou",
"MAG": "kevin_magnussen",
"HUL": "hulkenberg",
"RIC": "ricciardo",
"LAW": "lawson",
"SAR": "sargeant",
"DEV": "de_vries",
"VET": "vettel",
"RAI": "raikkonen",
"GRO": "grosjean",
"KVY": "kvyat",
"MAL": "maldonado",
"MAS": "massa",
"BUT": "button",
"ROS": "rosberg",
"VAN": "vandoorne",
"WEH": "wehrlein",
"ERI": "ericsson",
"NAK": "nakajima",
"PAL": "palmer",
"HAR": "hartley",
"SIR": "sirotkin",
"GIO": "giovinazzi",
"KUB": "kubica",
"LAT": "latifi",
"AIT": "aitken",
"FIT": "pietro_fittipaldi",
"MSC": "mick_schumacher",
"MAZ": "mazepin",
"BEA": "bearman",
"COL": "colapinto",
"DOO": "doohan",
"ANT": "antonelli",
"HAD": "hadjar",
"BOR": "bortoleto",
}
def save_driver_map(driver_map: dict[str, str]) -> None:
"""Save driver map to JSON file at project root."""
# Merge with known fallback (API results take priority)
merged = {**KNOWN_DRIVER_MAP, **driver_map}
with open(DRIVER_MAP_PATH, "w") as f:
json.dump(merged, f, indent=2, sort_keys=True)
logger.info(
f"Saved driver map to {DRIVER_MAP_PATH} ({len(merged)} entries)"
)
def load_driver_map() -> dict[str, str]:
"""Load driver map from JSON file. Falls back to known map if missing."""
if DRIVER_MAP_PATH.exists():
with open(DRIVER_MAP_PATH, "r") as f:
return json.load(f)
else:
logger.warning(
f"Driver map not found at {DRIVER_MAP_PATH} β "
f"using KNOWN_DRIVER_MAP fallback"
)
return KNOWN_DRIVER_MAP
# Module-level constant: always available for import
DRIVER_MAP = load_driver_map()
# ---------------------------------------------------------------------------
# CLI entrypoint
# ---------------------------------------------------------------------------
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(
description="KRONECTOR β Build Driver ID Mapping"
)
parser.add_argument(
"--start",
type=int,
default=2014,
help="Start year (default: 2014)",
)
parser.add_argument(
"--end",
type=int,
default=2026,
help="End year (default: 2026)",
)
parser.add_argument(
"--fallback-only",
action="store_true",
help="Skip API calls, save KNOWN_DRIVER_MAP only",
)
args = parser.parse_args()
if args.fallback_only:
save_driver_map({})
print(f"Saved fallback map: {len(KNOWN_DRIVER_MAP)} entries")
else:
driver_map = build_driver_map(args.start, args.end)
save_driver_map(driver_map)
print(f"\nDriver map saved to: {DRIVER_MAP_PATH}")
print(f"Total entries: {len({**KNOWN_DRIVER_MAP, **driver_map})}")
# Print sample
final = {**KNOWN_DRIVER_MAP, **driver_map}
for abbrev in ["VER", "HAM", "LEC", "NOR", "SAI"]:
print(f" {abbrev} β {final.get(abbrev, '???')}")
|