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: Problem: Time values are in seconds, not timedeltasSolution: All time fields are already converted to float seconds. To convert to readable format:Problem: Acceleration values seem too largeSolution: Acceleration is in m/s², not G. Convert to G:
"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")
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}"
acc_g = acc_ms2 / 9.81
Next Steps
- Analyzing Telemetry - Detailed telemetry analysis patterns
- Lap Time Analysis - Lap time and sector analysis
- Data Reference - Complete field documentation
