Skip to main content

Overview

This guide provides complete, production-ready Python examples for working with the TracingInsights F1 data. All code is based on real patterns from the extraction scripts and tested with actual data structures.

Prerequisites

Required Dependencies

requirements.txt
numpy>=1.24.0
pandas>=2.0.0
orjson>=3.9.0
matplotlib>=3.7.0
scipy>=1.10.0

Optional Dependencies

optional.txt
plotly>=5.14.0  # Interactive visualizations
fastf1>=3.0.0   # For loading live data (not needed for JSON files)
Use orjson instead of the standard json library for 2-3x faster loading of large telemetry files.

Data Loading Utilities

JSON Loading with orjson

import orjson
import pandas as pd
import numpy as np
from pathlib import Path
from typing import Dict, List, Optional

def load_json_file(filepath: str) -> Dict:
    """
    Load JSON file using orjson for performance.
    
    Args:
        filepath: Path to JSON file
        
    Returns:
        Parsed JSON data as dictionary
    """
    with open(filepath, "rb") as f:
        return orjson.loads(f.read())

def load_telemetry(session_path: str, driver: str, lap: int) -> pd.DataFrame:
    """
    Load telemetry data for a specific lap.
    
    Args:
        session_path: Path to session directory (e.g., "Australian Grand Prix/Race")
        driver: Driver code (e.g., "VER", "HAM")
        lap: Lap number
        
    Returns:
        DataFrame with telemetry data
    """
    filepath = Path(session_path) / driver / f"{lap}_tel.json"
    
    if not filepath.exists():
        raise FileNotFoundError(f"Telemetry file not found: {filepath}")
    
    data = load_json_file(str(filepath))
    tel_df = pd.DataFrame(data["tel"])
    
    # Replace "None" strings with NaN
    for col in tel_df.columns:
        tel_df[col] = tel_df[col].replace("None", np.nan)
    
    # Convert numeric columns
    numeric_cols = ["time", "rpm", "speed", "gear", "throttle", "brake", "drs",
                    "distance", "rel_distance", "acc_x", "acc_y", "acc_z",
                    "x", "y", "z", "DistanceToDriverAhead"]
    
    for col in numeric_cols:
        if col in tel_df.columns:
            tel_df[col] = pd.to_numeric(tel_df[col], errors="coerce")
    
    return tel_df

def load_laptimes(session_path: str, driver: str) -> pd.DataFrame:
    """
    Load lap times data for a driver.
    
    Args:
        session_path: Path to session directory
        driver: Driver code
        
    Returns:
        DataFrame with lap times data
    """
    filepath = Path(session_path) / driver / "laptimes.json"
    
    if not filepath.exists():
        raise FileNotFoundError(f"Laptimes file not found: {filepath}")
    
    data = load_json_file(str(filepath))
    laps_df = pd.DataFrame(data)
    
    # Clean data
    for col in laps_df.columns:
        laps_df[col] = laps_df[col].replace("None", np.nan)
    
    # Convert numeric columns
    numeric_cols = ["time", "lap", "s1", "s2", "s3", "life", "stint", "pos",
                    "sesT", "lST", "pin", "pout", "s1T", "s2T", "s3T",
                    "vi1", "vi2", "vfl", "vst",
                    "wT", "wAT", "wH", "wP", "wTT", "wWD", "wWS"]
    
    for col in numeric_cols:
        if col in laps_df.columns:
            laps_df[col] = pd.to_numeric(laps_df[col], errors="coerce")
    
    # Convert boolean columns
    bool_cols = ["pb", "fresh", "del", "ff1G", "iacc", "wR"]
    for col in bool_cols:
        if col in laps_df.columns:
            laps_df[col] = laps_df[col].map({True: True, False: False, "None": np.nan})
    
    return laps_df

def load_session_data(session_path: str, data_type: str) -> Dict:
    """
    Load session-level data (weather, drivers, corners, RCM).
    
    Args:
        session_path: Path to session directory
        data_type: One of: "weather", "drivers", "corners", "rcm"
        
    Returns:
        Parsed JSON data
    """
    filepath = Path(session_path) / f"{data_type}.json"
    
    if not filepath.exists():
        raise FileNotFoundError(f"Session data file not found: {filepath}")
    
    return load_json_file(str(filepath))

Complete Analysis Examples

Example 1: Driver Comparison Dashboard

Compare two drivers across multiple metrics:
driver_comparison.py
import matplotlib.pyplot as plt
import numpy as np
from pathlib import Path

def compare_drivers(session_path: str, driver1: str, driver2: str, lap: int):
    """
    Create comprehensive comparison of two drivers on the same lap.
    
    Args:
        session_path: Path to session directory
        driver1: First driver code
        driver2: Second driver code  
        lap: Lap number to compare
    """
    # Load telemetry
    tel1 = load_telemetry(session_path, driver1, lap)
    tel2 = load_telemetry(session_path, driver2, lap)
    
    # Create figure with subplots
    fig, axes = plt.subplots(3, 1, figsize=(14, 12))
    fig.suptitle(f"{driver1} vs {driver2} - Lap {lap}", fontsize=16, fontweight="bold")
    
    # 1. Speed comparison
    ax1 = axes[0]
    ax1.plot(tel1["distance"], tel1["speed"], label=driver1, linewidth=2, color="#3671C6")
    ax1.plot(tel2["distance"], tel2["speed"], label=driver2, linewidth=2, color="#DC0000")
    ax1.set_ylabel("Speed (km/h)", fontsize=12)
    ax1.set_title("Speed Trace", fontsize=14)
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    
    # 2. Throttle comparison
    ax2 = axes[1]
    ax2.fill_between(tel1["distance"], tel1["throttle"], alpha=0.5, color="#3671C6", label=driver1)
    ax2.fill_between(tel2["distance"], tel2["throttle"], alpha=0.5, color="#DC0000", label=driver2)
    ax2.set_ylabel("Throttle (%)", fontsize=12)
    ax2.set_title("Throttle Application", fontsize=14)
    ax2.set_ylim(0, 100)
    ax2.legend()
    ax2.grid(True, alpha=0.3)
    
    # 3. Lateral acceleration comparison
    ax3 = axes[2]
    acc_y1_g = tel1["acc_y"] / 9.81
    acc_y2_g = tel2["acc_y"] / 9.81
    ax3.plot(tel1["distance"], acc_y1_g, label=driver1, linewidth=1.5, color="#3671C6")
    ax3.plot(tel2["distance"], acc_y2_g, label=driver2, linewidth=1.5, color="#DC0000")
    ax3.axhline(y=0, color="black", linestyle="--", linewidth=0.8)
    ax3.set_xlabel("Distance (m)", fontsize=12)
    ax3.set_ylabel("Lateral Acceleration (G)", fontsize=12)
    ax3.set_title("Cornering Forces", fontsize=14)
    ax3.legend()
    ax3.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig(f"{driver1}_vs_{driver2}_lap{lap}.png", dpi=300)
    plt.show()
    
    # Print summary statistics
    print(f"\n{'=' * 60}")
    print(f"Comparison Summary - Lap {lap}")
    print(f"{'=' * 60}\n")
    
    print(f"{driver1}:")
    print(f"  Max Speed: {tel1['speed'].max():.1f} km/h")
    print(f"  Avg Speed: {tel1['speed'].mean():.1f} km/h")
    print(f"  Max Lateral G: {acc_y1_g.abs().max():.2f}g\n")
    
    print(f"{driver2}:")
    print(f"  Max Speed: {tel2['speed'].max():.1f} km/h")
    print(f"  Avg Speed: {tel2['speed'].mean():.1f} km/h")
    print(f"  Max Lateral G: {acc_y2_g.abs().max():.2f}g\n")

# Usage
compare_drivers("Australian Grand Prix/Qualifying", "VER", "HAM", 5)

Example 2: Tire Strategy Analyzer

tire_strategy.py
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from scipy import stats

def analyze_tire_strategy(session_path: str, driver: str):
    """
    Analyze tire strategy and degradation for a race.
    
    Args:
        session_path: Path to race session directory
        driver: Driver code
    """
    laps_df = load_laptimes(session_path, driver)
    
    # Filter clean racing laps
    race_laps = laps_df[
        (laps_df["time"].notna()) &
        (laps_df["compound"].notna()) &
        (laps_df["life"].notna()) &
        (laps_df["pin"].isna()) &
        (laps_df["pout"].isna()) &
        (laps_df["status"] == "1")
    ].copy()
    
    if len(race_laps) == 0:
        print("No valid race laps found")
        return
    
    # Create visualization
    fig = plt.figure(figsize=(16, 10))
    gs = fig.add_gridspec(3, 2, hspace=0.3, wspace=0.3)
    
    # 1. Lap times throughout the race
    ax1 = fig.add_subplot(gs[0, :])
    
    compound_colors = {
        "SOFT": "#DA291C",
        "MEDIUM": "#FFF200",
        "HARD": "#EBEBEB"
    }
    
    for compound in race_laps["compound"].unique():
        if pd.isna(compound):
            continue
        
        compound_laps = race_laps[race_laps["compound"] == compound]
        color = compound_colors.get(compound, "gray")
        
        ax1.scatter(compound_laps["lap"], compound_laps["time"],
                   label=compound, color=color, s=60, alpha=0.7,
                   edgecolors="black", linewidth=0.5)
    
    # Mark pit stops
    pit_laps = laps_df[laps_df["pin"].notna()]["lap"].values
    for pit_lap in pit_laps:
        ax1.axvline(x=pit_lap, color="red", linestyle="--", linewidth=2, alpha=0.5)
        ax1.text(pit_lap, ax1.get_ylim()[1], "PIT", ha="center", va="bottom",
                fontsize=10, fontweight="bold", color="red")
    
    ax1.set_xlabel("Lap Number", fontsize=12)
    ax1.set_ylabel("Lap Time (s)", fontsize=12)
    ax1.set_title(f"Race Lap Times - {driver}", fontsize=14, fontweight="bold")
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    
    # 2-4. Degradation analysis per compound
    ax_idx = 1
    for compound in ["SOFT", "MEDIUM", "HARD"]:
        compound_laps = race_laps[race_laps["compound"] == compound]
        
        if len(compound_laps) < 3:
            continue
        
        ax = fig.add_subplot(gs[ax_idx // 2 + 1, ax_idx % 2])
        ax_idx += 1
        
        tire_life = compound_laps["life"].values
        lap_times = compound_laps["time"].values
        
        # Linear regression
        slope, intercept, r_value, _, _ = stats.linregress(tire_life, lap_times)
        
        # Plot
        color = compound_colors.get(compound, "gray")
        ax.scatter(tire_life, lap_times, s=50, alpha=0.6, color=color,
                  edgecolors="black", linewidth=0.5)
        
        # Trend line
        x_trend = np.linspace(tire_life.min(), tire_life.max(), 100)
        y_trend = slope * x_trend + intercept
        ax.plot(x_trend, y_trend, 'r--', linewidth=2,
               label=f"Deg: {slope:.3f}s/lap")
        
        ax.set_xlabel("Tire Life (laps)", fontsize=11)
        ax.set_ylabel("Lap Time (s)", fontsize=11)
        ax.set_title(f"{compound} Compound Degradation", fontsize=12, fontweight="bold")
        ax.legend()
        ax.grid(True, alpha=0.3)
        
        # Add statistics text
        stats_text = f"R² = {r_value**2:.3f}\nLaps: {len(compound_laps)}"
        ax.text(0.05, 0.95, stats_text, transform=ax.transAxes,
               fontsize=10, verticalalignment="top",
               bbox=dict(boxstyle="round", facecolor="white", alpha=0.8))
    
    plt.suptitle(f"Tire Strategy Analysis - {driver}", fontsize=16, fontweight="bold", y=0.995)
    plt.savefig(f"{driver}_tire_strategy.png", dpi=300, bbox_inches="tight")
    plt.show()
    
    # Print detailed statistics
    print(f"\n{'=' * 70}")
    print(f"Tire Strategy Report - {driver}")
    print(f"{'=' * 70}\n")
    
    stints = race_laps.groupby("stint")
    for stint_num, stint_data in stints:
        if pd.isna(stint_num):
            continue
        
        stint_num = int(stint_num)
        compound = stint_data["compound"].iloc[0]
        fresh = stint_data["fresh"].iloc[0]
        stint_length = len(stint_data)
        avg_time = stint_data["time"].mean()
        first_lap = stint_data["time"].iloc[0]
        last_lap = stint_data["time"].iloc[-1]
        degradation = last_lap - first_lap
        
        print(f"Stint {stint_num}: {compound} ({'Fresh' if fresh else 'Used'})")
        print(f"  Length: {stint_length} laps")
        print(f"  Average: {avg_time:.3f}s")
        print(f"  First lap: {first_lap:.3f}s")
        print(f"  Last lap: {last_lap:.3f}s")
        print(f"  Degradation: {degradation:.3f}s ({degradation/stint_length:.3f}s per lap)")
        print()

# Usage
analyze_tire_strategy("Australian Grand Prix/Race", "VER")

Example 3: Corner Analysis

Analyze speed and acceleration through specific corners:
corner_analysis.py
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy.interpolate import interp1d

def analyze_corner(session_path: str, driver: str, lap: int, corner_number: int, window: float = 150):
    """
    Detailed analysis of a specific corner.
    
    Args:
        session_path: Path to session directory
        driver: Driver code
        lap: Lap number
        corner_number: Corner number to analyze
        window: Distance window around corner (meters)
    """
    # Load telemetry
    tel = load_telemetry(session_path, driver, lap)
    
    # Load corner data
    corners = load_session_data(session_path, "corners")
    corners_df = pd.DataFrame(corners)
    
    # Find corner position
    corner_data = corners_df[corners_df["CornerNumber"] == corner_number]
    
    if len(corner_data) == 0 or corner_data["Distance"].iloc[0] == "None":
        print(f"Corner {corner_number} data not available")
        return
    
    corner_distance = float(corner_data["Distance"].iloc[0])
    
    # Filter telemetry around corner
    corner_tel = tel[
        (tel["distance"] >= corner_distance - window) &
        (tel["distance"] <= corner_distance + window)
    ].copy()
    
    if len(corner_tel) == 0:
        print(f"No telemetry data found around corner {corner_number}")
        return
    
    # Relative distance (corner at 0)
    corner_tel["rel_dist"] = corner_tel["distance"] - corner_distance
    corner_tel["acc_y_g"] = corner_tel["acc_y"] / 9.81
    
    # Create visualization
    fig, axes = plt.subplots(4, 1, figsize=(14, 12), sharex=True)
    fig.suptitle(f"Corner {corner_number} Analysis - {driver} Lap {lap}",
                fontsize=16, fontweight="bold")
    
    # 1. Speed
    ax1 = axes[0]
    ax1.plot(corner_tel["rel_dist"], corner_tel["speed"], linewidth=2.5, color="#3671C6")
    ax1.axvline(x=0, color="red", linestyle="--", linewidth=2, alpha=0.5, label="Corner apex")
    ax1.set_ylabel("Speed (km/h)", fontsize=12)
    ax1.set_title("Speed Trace", fontsize=13)
    ax1.grid(True, alpha=0.3)
    ax1.legend()
    
    # Mark min speed
    min_speed_idx = corner_tel["speed"].idxmin()
    min_speed = corner_tel.loc[min_speed_idx, "speed"]
    min_speed_dist = corner_tel.loc[min_speed_idx, "rel_dist"]
    ax1.plot(min_speed_dist, min_speed, 'ro', markersize=10, label=f"Min: {min_speed:.1f} km/h")
    ax1.legend()
    
    # 2. Throttle and Brake
    ax2 = axes[1]
    ax2.fill_between(corner_tel["rel_dist"], corner_tel["throttle"],
                    alpha=0.6, color="#00D2BE", label="Throttle")
    ax2_brake = ax2.twinx()
    ax2_brake.fill_between(corner_tel["rel_dist"], corner_tel["brake"] * 100,
                          alpha=0.6, color="#DC0000", label="Brake")
    ax2.axvline(x=0, color="red", linestyle="--", linewidth=2, alpha=0.5)
    ax2.set_ylabel("Throttle (%)", fontsize=12, color="#00D2BE")
    ax2_brake.set_ylabel("Brake", fontsize=12, color="#DC0000")
    ax2.set_title("Driver Inputs", fontsize=13)
    ax2.set_ylim(0, 100)
    ax2_brake.set_ylim(0, 100)
    ax2.grid(True, alpha=0.3)
    
    # 3. Lateral acceleration
    ax3 = axes[2]
    ax3.plot(corner_tel["rel_dist"], corner_tel["acc_y_g"], linewidth=2, color="#FF8700")
    ax3.axhline(y=0, color="black", linestyle="--", linewidth=0.8)
    ax3.axvline(x=0, color="red", linestyle="--", linewidth=2, alpha=0.5)
    ax3.set_ylabel("Lateral G", fontsize=12)
    ax3.set_title("Lateral Forces", fontsize=13)
    ax3.grid(True, alpha=0.3)
    
    # Mark max lateral G
    max_lat_g = corner_tel["acc_y_g"].abs().max()
    ax3.text(0.02, 0.98, f"Max: {max_lat_g:.2f}g", transform=ax3.transAxes,
            fontsize=11, verticalalignment="top",
            bbox=dict(boxstyle="round", facecolor="white", alpha=0.8))
    
    # 4. Gear
    ax4 = axes[3]
    ax4.plot(corner_tel["rel_dist"], corner_tel["gear"], linewidth=2.5,
            color="#E10600", drawstyle="steps-post")
    ax4.axvline(x=0, color="red", linestyle="--", linewidth=2, alpha=0.5)
    ax4.set_xlabel("Distance from Corner Apex (m)", fontsize=12)
    ax4.set_ylabel("Gear", fontsize=12)
    ax4.set_title("Gear Selection", fontsize=13)
    ax4.set_yticks(range(1, 9))
    ax4.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig(f"{driver}_corner{corner_number}_lap{lap}.png", dpi=300)
    plt.show()
    
    # Print statistics
    print(f"\n{'=' * 60}")
    print(f"Corner {corner_number} Statistics")
    print(f"{'=' * 60}\n")
    print(f"Minimum Speed: {corner_tel['speed'].min():.1f} km/h")
    print(f"Maximum Lateral G: {corner_tel['acc_y_g'].abs().max():.2f}g")
    print(f"Entry Speed: {corner_tel.iloc[0]['speed']:.1f} km/h")
    print(f"Exit Speed: {corner_tel.iloc[-1]['speed']:.1f} km/h")
    print(f"Speed Gain: {corner_tel.iloc[-1]['speed'] - corner_tel.iloc[0]['speed']:.1f} km/h")
    
    # Find braking point
    brake_points = corner_tel[corner_tel["brake"] == 1]
    if len(brake_points) > 0:
        brake_start = brake_points.iloc[0]["rel_dist"]
        print(f"Braking Point: {brake_start:.1f}m before apex")
    
    # Find throttle application
    throttle_on = corner_tel[corner_tel["throttle"] > 95]
    if len(throttle_on) > 0:
        throttle_point = throttle_on.iloc[0]["rel_dist"]
        if throttle_point < 0:
            print(f"Full Throttle: {abs(throttle_point):.1f}m before apex")
        else:
            print(f"Full Throttle: {throttle_point:.1f}m after apex")

# Usage
analyze_corner("Australian Grand Prix/Qualifying", "VER", 5, corner_number=3)

Data Filtering Patterns

Common Filter Functions

filters.py
import pandas as pd
import numpy as np

def filter_valid_laps(laps_df: pd.DataFrame) -> pd.DataFrame:
    """
    Filter for valid racing laps (exclude in/out laps, deleted laps).
    
    Args:
        laps_df: DataFrame with lap data
        
    Returns:
        Filtered DataFrame
    """
    return laps_df[
        (laps_df["time"].notna()) &
        (laps_df["pin"].isna()) &
        (laps_df["pout"].isna()) &
        (~laps_df["del"].fillna(False))
    ].copy()

def filter_clean_conditions(laps_df: pd.DataFrame) -> pd.DataFrame:
    """
    Filter for clean track conditions (green flag, no yellow/safety car).
    
    Args:
        laps_df: DataFrame with lap data
        
    Returns:
        Filtered DataFrame
    """
    return laps_df[
        (laps_df["status"] == "1") &  # Track clear
        filter_valid_laps(laps_df).index.isin(laps_df.index)
    ].copy()

def filter_by_compound(laps_df: pd.DataFrame, compound: str) -> pd.DataFrame:
    """
    Filter laps by tire compound.
    
    Args:
        laps_df: DataFrame with lap data
        compound: Tire compound ("SOFT", "MEDIUM", "HARD")
        
    Returns:
        Filtered DataFrame
    """
    return laps_df[laps_df["compound"] == compound].copy()

def filter_by_stint(laps_df: pd.DataFrame, stint_number: int) -> pd.DataFrame:
    """
    Filter laps by stint number.
    
    Args:
        laps_df: DataFrame with lap data
        stint_number: Stint number (1, 2, 3...)
        
    Returns:
        Filtered DataFrame
    """
    return laps_df[laps_df["stint"] == stint_number].copy()

def filter_by_session_time(laps_df: pd.DataFrame, 
                          start_time: float, 
                          end_time: float) -> pd.DataFrame:
    """
    Filter laps by session time window.
    
    Args:
        laps_df: DataFrame with lap data
        start_time: Start time in seconds
        end_time: End time in seconds
        
    Returns:
        Filtered DataFrame
    """
    return laps_df[
        (laps_df["sesT"] >= start_time) &
        (laps_df["sesT"] <= end_time)
    ].copy()

def get_fastest_lap(laps_df: pd.DataFrame) -> pd.Series:
    """
    Get the fastest valid lap.
    
    Args:
        laps_df: DataFrame with lap data
        
    Returns:
        Series with fastest lap data
    """
    valid_laps = filter_valid_laps(laps_df)
    
    if len(valid_laps) == 0:
        return pd.Series()
    
    return valid_laps.loc[valid_laps["time"].idxmin()]

Visualization Templates

Track Map with Speed Heatmap

track_visualization.py
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.collections import LineCollection
from matplotlib.colors import LinearSegmentedColormap

def plot_track_speed_heatmap(session_path: str, driver: str, lap: int):
    """
    Create track map colored by speed.
    
    Args:
        session_path: Path to session directory
        driver: Driver code
        lap: Lap number
    """
    tel = load_telemetry(session_path, driver, lap)
    
    # Filter valid position data
    valid = tel[(tel["x"].notna()) & (tel["y"].notna()) & (tel["speed"].notna())]
    
    x = valid["x"].values
    y = valid["y"].values
    speed = valid["speed"].values
    
    # Create line segments
    points = np.array([x, y]).T.reshape(-1, 1, 2)
    segments = np.concatenate([points[:-1], points[1:]], axis=1)
    
    # Custom colormap (blue -> green -> yellow -> red)
    colors = ["#0000FF", "#00FF00", "#FFFF00", "#FF0000"]
    n_bins = 100
    cmap = LinearSegmentedColormap.from_list("speed", colors, N=n_bins)
    
    # Create plot
    fig, ax = plt.subplots(figsize=(14, 14))
    
    lc = LineCollection(segments, cmap=cmap, linewidth=4)
    lc.set_array(speed)
    lc.set_clim(speed.min(), speed.max())
    
    line = ax.add_collection(lc)
    
    # Add corner markers
    try:
        corners = load_session_data(session_path, "corners")
        corners_df = pd.DataFrame(corners)
        
        for _, corner in corners_df.iterrows():
            if corner["X"] == "None" or corner["Y"] == "None":
                continue
            
            corner_x = float(corner["X"])
            corner_y = float(corner["Y"])
            corner_num = corner["CornerNumber"]
            
            ax.plot(corner_x, corner_y, 'wo', markersize=12, 
                   markeredgecolor='black', markeredgewidth=2, zorder=10)
            ax.text(corner_x, corner_y, str(corner_num), 
                   ha='center', va='center', fontsize=10, 
                   fontweight='bold', zorder=11)
    except:
        pass
    
    ax.autoscale()
    ax.set_aspect('equal')
    ax.set_xlabel('X Position (m)', fontsize=12)
    ax.set_ylabel('Y Position (m)', fontsize=12)
    ax.set_title(f'Track Map - {driver} Lap {lap}', fontsize=16, fontweight='bold')
    
    # Colorbar
    cbar = plt.colorbar(line, ax=ax, pad=0.02)
    cbar.set_label('Speed (km/h)', fontsize=12)
    
    plt.tight_layout()
    plt.savefig(f'{driver}_trackmap_lap{lap}.png', dpi=300, bbox_inches='tight')
    plt.show()

# Usage
plot_track_speed_heatmap("Australian Grand Prix/Qualifying", "VER", 5)

Common Pitfalls and Solutions

Problem: "None" strings in data cause type errorsSolution: Always replace "None" with np.nan before numeric operations:
df = df.replace("None", np.nan)
df["time"] = pd.to_numeric(df["time"], errors="coerce")
Problem: Time values are in seconds, not timedeltasSolution: All time fields are already converted to float seconds. To convert to readable format:
def format_lap_time(seconds):
    """Convert seconds to MM:SS.mmm format."""
    if pd.isna(seconds):
        return "--:---.---"
    minutes = int(seconds // 60)
    secs = seconds % 60
    return f"{minutes}:{secs:06.3f}"
Problem: Acceleration values seem too largeSolution: Acceleration is in m/s², not G. Convert to G:
acc_g = acc_ms2 / 9.81

Next Steps

Build docs developers (and LLMs) love