Overview
Lap time data provides comprehensive timing information for every lap completed during a session. This guide covers analyzing lap times, sector performance, tire degradation, pit strategy, and track evolution.Data Structure
Lap times are stored inlaptimes.json files per driver with column-oriented arrays:
{
"time": [89.234, 88.912, 89.456, ...],
"lap": [1, 2, 3, ...],
"s1": [28.456, 28.234, ...],
"s2": [32.123, 31.987, ...],
"s3": [28.655, 28.691, ...],
"compound": ["SOFT", "SOFT", "SOFT", ...],
"life": [1, 2, 3, ...],
...
}
Key Fields
| Field | Type | Description | Unit |
|---|---|---|---|
time | float | Lap time | seconds |
lap | int | Lap number | - |
s1, s2, s3 | float | Sector times | seconds |
compound | string | Tire compound | SOFT/MEDIUM/HARD |
life | int | Tire age (laps on this tire) | laps |
stint | int | Stint number (1, 2, 3…) | - |
fresh | bool | Was tire new at stint start | true/false |
pos | int | Track position at lap end | 1-20 |
pb | bool | Is this the personal best lap | true/false |
status | string | Track status codes | ”1”, “24”, etc. |
pin | float | Pit entry time | seconds |
pout | float | Pit exit time | seconds |
sesT | float | Session time at lap end | seconds |
lST | float | Lap start time | seconds |
All time values are in seconds (converted from timedeltas). Missing values are represented as the string
"None".Loading Lap Time Data
Basic Loading with orjson
import orjson
import pandas as pd
import numpy as np
# Load lap times for a driver
with open("Australian Grand Prix/Race/VER/laptimes.json", "rb") as f:
lap_data = orjson.loads(f.read())
# Convert to DataFrame
laps_df = pd.DataFrame(lap_data)
# Replace "None" strings with NaN
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"]
for col in numeric_cols:
if col in laps_df.columns:
laps_df[col] = pd.to_numeric(laps_df[col], errors="coerce")
print(f"Total laps: {len(laps_df)}")
print(laps_df.head())
Loading Multiple Drivers
import os
import orjson
import pandas as pd
def load_driver_laps(session_path, driver_code):
"""Load lap times for a specific driver."""
file_path = f"{session_path}/{driver_code}/laptimes.json"
if not os.path.exists(file_path):
return pd.DataFrame()
with open(file_path, "rb") as f:
data = orjson.loads(f.read())
df = pd.DataFrame(data)
# Clean and convert data
for col in df.columns:
df[col] = df[col].replace("None", np.nan)
numeric_cols = ["time", "lap", "s1", "s2", "s3", "life", "stint",
"pos", "sesT", "lST", "pin", "pout"]
for col in numeric_cols:
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors="coerce")
return df
# Load multiple drivers
session_path = "Australian Grand Prix/Race"
drivers = ["VER", "HAM", "LEC", "NOR"]
all_laps = {}
for driver in drivers:
all_laps[driver] = load_driver_laps(session_path, driver)
print(f"{driver}: {len(all_laps[driver])} laps")
Fastest Lap Analysis
Finding the Fastest Lap
import pandas as pd
import numpy as np
# Load lap data
laps_df = load_driver_laps("Australian Grand Prix/Qualifying", "VER")
# Filter valid laps (remove outlaps, inlaps, deleted laps)
valid_laps = laps_df[
(laps_df["time"].notna()) &
(laps_df["pin"].isna()) & # Not an in-lap
(laps_df["pout"].isna()) & # Not an out-lap
(~laps_df["del"].fillna(False)) # Not deleted
].copy()
# Find fastest lap
if len(valid_laps) > 0:
fastest_lap = valid_laps.loc[valid_laps["time"].idxmin()]
print(f"Fastest Lap: {fastest_lap['lap']:.0f}")
print(f"Lap Time: {fastest_lap['time']:.3f}s")
print(f"S1: {fastest_lap['s1']:.3f}s")
print(f"S2: {fastest_lap['s2']:.3f}s")
print(f"S3: {fastest_lap['s3']:.3f}s")
print(f"Tire: {fastest_lap['compound']}")
print(f"Tire Life: {fastest_lap['life']:.0f} laps")
Personal Best Lap
The data includes apb field marking the official personal best:
# Find official personal best
pb_lap = laps_df[laps_df["pb"] == True]
if len(pb_lap) > 0:
print("Official Personal Best:")
print(pb_lap[["lap", "time", "s1", "s2", "s3", "compound"]].iloc[0])
else:
print("No personal best lap marked")
If a lap is quicker than the marked personal best, it means the quicker lap was invalidated (e.g., track limits violation).
Sector Time Analysis
Best Sector Times
import pandas as pd
import numpy as np
laps_df = load_driver_laps("Australian Grand Prix/Qualifying", "LEC")
# Filter valid laps
valid_laps = laps_df[
(laps_df["s1"].notna()) &
(laps_df["s2"].notna()) &
(laps_df["s3"].notna()) &
(laps_df["pin"].isna()) &
(laps_df["pout"].isna())
]
# Best sectors
best_s1 = valid_laps["s1"].min()
best_s2 = valid_laps["s2"].min()
best_s3 = valid_laps["s3"].min()
# Theoretical best lap (sum of best sectors)
theoretical_best = best_s1 + best_s2 + best_s3
actual_best = valid_laps["time"].min()
print(f"Best S1: {best_s1:.3f}s (Lap {valid_laps.loc[valid_laps['s1'].idxmin(), 'lap']:.0f})")
print(f"Best S2: {best_s2:.3f}s (Lap {valid_laps.loc[valid_laps['s2'].idxmin(), 'lap']:.0f})")
print(f"Best S3: {best_s3:.3f}s (Lap {valid_laps.loc[valid_laps['s3'].idxmin(), 'lap']:.0f})")
print(f"\nTheoretical Best: {theoretical_best:.3f}s")
print(f"Actual Best: {actual_best:.3f}s")
print(f"Gap: {actual_best - theoretical_best:.3f}s")
Sector Comparison Between Drivers
import matplotlib.pyplot as plt
import numpy as np
# Load data for multiple drivers
ver_laps = load_driver_laps("Australian Grand Prix/Qualifying", "VER")
ham_laps = load_driver_laps("Australian Grand Prix/Qualifying", "HAM")
# Get best sectors
def best_sectors(df):
valid = df[(df["pin"].isna()) & (df["pout"].isna())]
return {
"S1": valid["s1"].min(),
"S2": valid["s2"].min(),
"S3": valid["s3"].min()
}
ver_sectors = best_sectors(ver_laps)
ham_sectors = best_sectors(ham_laps)
# Plot comparison
sectors = ["S1", "S2", "S3"]
ver_times = [ver_sectors[s] for s in sectors]
ham_times = [ham_sectors[s] for s in sectors]
x = np.arange(len(sectors))
width = 0.35
fig, ax = plt.subplots(figsize=(10, 6))
ax.bar(x - width/2, ver_times, width, label="VER", color="#3671C6")
ax.bar(x + width/2, ham_times, width, label="HAM", color="#00D2BE")
ax.set_ylabel("Time (s)")
ax.set_title("Best Sector Times - VER vs HAM")
ax.set_xticks(x)
ax.set_xticklabels(sectors)
ax.legend()
ax.grid(True, axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
print(f"VER Total: {sum(ver_times):.3f}s")
print(f"HAM Total: {sum(ham_times):.3f}s")
print(f"Delta: {sum(ver_times) - sum(ham_times):.3f}s")
Tire Degradation Analysis
Lap Time vs Tire Age
import matplotlib.pyplot as plt
import numpy as np
from scipy import stats
laps_df = load_driver_laps("Australian Grand Prix/Race", "VER")
# Filter racing laps (exclude in/out laps)
race_laps = laps_df[
(laps_df["time"].notna()) &
(laps_df["life"].notna()) &
(laps_df["pin"].isna()) &
(laps_df["pout"].isna()) &
(laps_df["status"] == "1") # Clean track conditions
].copy()
# Group by compound
for compound in race_laps["compound"].unique():
if pd.isna(compound):
continue
compound_laps = race_laps[race_laps["compound"] == compound]
if len(compound_laps) < 3:
continue
tire_life = compound_laps["life"].values
lap_times = compound_laps["time"].values
# Calculate degradation rate (linear fit)
slope, intercept, r_value, _, _ = stats.linregress(tire_life, lap_times)
# Plot
plt.figure(figsize=(10, 6))
plt.scatter(tire_life, lap_times, s=50, alpha=0.6, label="Actual lap times")
# Trend line
x_trend = np.linspace(tire_life.min(), tire_life.max(), 100)
y_trend = slope * x_trend + intercept
plt.plot(x_trend, y_trend, 'r--', linewidth=2,
label=f"Degradation: {slope*10:.3f}s per 10 laps")
plt.xlabel("Tire Life (laps)")
plt.ylabel("Lap Time (s)")
plt.title(f"Tire Degradation - {compound} Compound (VER)")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print(f"{compound} Compound:")
print(f" Degradation rate: {slope:.4f}s per lap")
print(f" Over 10 laps: {slope*10:.3f}s")
print(f" R² value: {r_value**2:.3f}")
print(f" Stint length: {len(compound_laps)} laps\n")
Comparing Tire Compounds
import matplotlib.pyplot as plt
import pandas as pd
laps_df = load_driver_laps("Australian Grand Prix/Race", "LEC")
# Filter clean racing laps
race_laps = laps_df[
(laps_df["time"].notna()) &
(laps_df["compound"].notna()) &
(laps_df["pin"].isna()) &
(laps_df["pout"].isna()) &
(laps_df["status"] == "1")
].copy()
# Plot lap times colored by compound
compound_colors = {
"SOFT": "#DA291C",
"MEDIUM": "#FFF200",
"HARD": "#EBEBEB"
}
fig, ax = plt.subplots(figsize=(14, 6))
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")
ax.scatter(compound_laps["lap"], compound_laps["time"],
label=compound, color=color, s=60, alpha=0.7,
edgecolors="black", linewidth=0.5)
ax.set_xlabel("Lap Number")
ax.set_ylabel("Lap Time (s)")
ax.set_title("Lap Times by Tire Compound - LEC")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Pit Stop Analysis
Identifying Pit Stops
import pandas as pd
laps_df = load_driver_laps("Australian Grand Prix/Race", "VER")
# Find pit in-laps and out-laps
in_laps = laps_df[laps_df["pin"].notna()].copy()
out_laps = laps_df[laps_df["pout"].notna()].copy()
print(f"Number of pit stops: {len(in_laps)}\n")
for idx, in_lap in in_laps.iterrows():
lap_num = int(in_lap["lap"])
# Find corresponding out lap
out_lap = out_laps[out_laps["lap"] == lap_num + 1]
if len(out_lap) > 0:
out_lap = out_lap.iloc[0]
pit_duration = out_lap["pout"] - in_lap["pin"]
print(f"Pit Stop at Lap {lap_num}:")
print(f" In: {in_lap['pin']:.1f}s (session time)")
print(f" Out: {out_lap['pout']:.1f}s")
print(f" Duration: {pit_duration:.1f}s")
print(f" Tire Change: {in_lap['compound']} → {out_lap['compound']}")
print(f" Stint: {int(in_lap['stint'])} → {int(out_lap['stint'])}")
print()
Pit Stop Strategy Comparison
import matplotlib.pyplot as plt
import numpy as np
# Load multiple drivers
drivers = ["VER", "HAM", "LEC", "NOR"]
driver_colors = {
"VER": "#3671C6",
"HAM": "#00D2BE",
"LEC": "#DC0000",
"NOR": "#FF8700"
}
fig, ax = plt.subplots(figsize=(14, 8))
for i, driver in enumerate(drivers):
laps_df = load_driver_laps("Australian Grand Prix/Race", driver)
# Plot stint markers
stints = laps_df.groupby("stint")
for stint_num, stint_data in stints:
if pd.isna(stint_num):
continue
stint_num = int(stint_num)
lap_start = stint_data["lap"].min()
lap_end = stint_data["lap"].max()
compound = stint_data["compound"].iloc[0]
# Draw stint as horizontal bar
color = compound_colors.get(compound, "gray")
ax.barh(i, lap_end - lap_start + 1, left=lap_start,
height=0.8, color=color, edgecolor="black", linewidth=1)
# Add compound label
ax.text((lap_start + lap_end) / 2, i, compound[0] if not pd.isna(compound) else "",
ha="center", va="center", fontsize=10, fontweight="bold")
ax.set_yticks(range(len(drivers)))
ax.set_yticklabels(drivers)
ax.set_xlabel("Lap Number")
ax.set_title("Pit Stop Strategy Comparison")
ax.grid(True, axis="x", alpha=0.3)
plt.tight_layout()
plt.show()
Track Evolution
Session Progression
Analyze how lap times improve as the track rubbers in:import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
# Load all drivers in a practice session
session_path = "Australian Grand Prix/Practice 3"
drivers = ["VER", "HAM", "LEC", "NOR", "SAI"]
all_laps = []
for driver in drivers:
laps_df = load_driver_laps(session_path, driver)
laps_df["driver"] = driver
all_laps.append(laps_df)
combined = pd.concat(all_laps, ignore_index=True)
# Filter valid laps
valid_laps = combined[
(combined["time"].notna()) &
(combined["sesT"].notna()) &
(combined["pin"].isna()) &
(combined["pout"].isna())
].copy()
# Convert session time to minutes
valid_laps["session_minutes"] = valid_laps["sesT"] / 60
# Group by 5-minute windows and find best lap time
valid_laps["time_window"] = (valid_laps["session_minutes"] // 5) * 5
best_per_window = valid_laps.groupby("time_window")["time"].min()
# Plot
plt.figure(figsize=(12, 6))
plt.plot(best_per_window.index, best_per_window.values,
marker="o", linewidth=2, markersize=8, color="#E10600")
plt.xlabel("Session Time (minutes)")
plt.ylabel("Best Lap Time (s)")
plt.title("Track Evolution - FP3")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
track_improvement = best_per_window.iloc[0] - best_per_window.iloc[-1]
print(f"Track improvement: {track_improvement:.3f}s from start to end of session")
Weather Impact on Lap Times
import matplotlib.pyplot as plt
import pandas as pd
laps_df = load_driver_laps("Australian Grand Prix/Qualifying", "VER")
# Filter valid laps with weather data
valid_laps = laps_df[
(laps_df["time"].notna()) &
(laps_df["wTT"].notna()) & # Track temperature
(laps_df["pin"].isna()) &
(laps_df["pout"].isna())
].copy()
# Convert track temp to numeric
valid_laps["wTT"] = pd.to_numeric(valid_laps["wTT"], errors="coerce")
# Plot lap time vs track temperature
fig, ax1 = plt.subplots(figsize=(12, 6))
ax1.scatter(valid_laps["lap"], valid_laps["time"],
color="#3671C6", s=80, alpha=0.6, label="Lap Time")
ax1.set_xlabel("Lap Number")
ax1.set_ylabel("Lap Time (s)", color="#3671C6")
ax1.tick_params(axis="y", labelcolor="#3671C6")
ax2 = ax1.twinx()
ax2.plot(valid_laps["lap"], valid_laps["wTT"],
color="#FF8700", linewidth=2, marker="o", label="Track Temp")
ax2.set_ylabel("Track Temperature (°C)", color="#FF8700")
ax2.tick_params(axis="y", labelcolor="#FF8700")
plt.title("Lap Times vs Track Temperature")
fig.tight_layout()
plt.grid(True, alpha=0.3)
plt.show()
Race Pace Analysis
Stint-by-Stint Pace
import matplotlib.pyplot as plt
import pandas as pd
laps_df = load_driver_laps("Australian Grand Prix/Race", "VER")
# Filter clean racing laps
race_laps = laps_df[
(laps_df["time"].notna()) &
(laps_df["stint"].notna()) &
(laps_df["pin"].isna()) &
(laps_df["pout"].isna()) &
(laps_df["status"] == "1")
].copy()
# Calculate average pace per stint
stint_pace = race_laps.groupby("stint").agg({
"time": ["mean", "std", "count"],
"compound": "first",
"fresh": "first"
})
print("Stint Analysis:")
print("=" * 70)
for stint in stint_pace.index:
if pd.isna(stint):
continue
stint_num = int(stint)
avg_time = stint_pace.loc[stint, ("time", "mean")]
std_time = stint_pace.loc[stint, ("time", "std")]
lap_count = int(stint_pace.loc[stint, ("time", "count")])
compound = stint_pace.loc[stint, ("compound", "first")]
fresh = stint_pace.loc[stint, ("fresh", "first")]
print(f"Stint {stint_num}: {compound} ({'Fresh' if fresh else 'Used'})")
print(f" Average: {avg_time:.3f}s (±{std_time:.3f}s)")
print(f" Laps: {lap_count}")
print()
Complete Analysis Example
Here’s a comprehensive lap time analysis function:import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
def analyze_driver_session(session_path, driver):
"""Complete lap time analysis for a driver."""
laps_df = load_driver_laps(session_path, driver)
if len(laps_df) == 0:
print(f"No data found for {driver}")
return
print(f"\n{'=' * 70}")
print(f"Analysis for {driver}")
print(f"{'=' * 70}\n")
# Overall statistics
valid_laps = laps_df[
(laps_df["time"].notna()) &
(laps_df["pin"].isna()) &
(laps_df["pout"].isna())
]
print(f"Total laps: {len(laps_df)}")
print(f"Valid laps: {len(valid_laps)}")
print(f"Pit stops: {laps_df['pin'].notna().sum()}\n")
# Fastest lap
if len(valid_laps) > 0:
fastest = valid_laps.loc[valid_laps["time"].idxmin()]
print(f"Fastest Lap: {fastest['lap']:.0f}")
print(f" Time: {fastest['time']:.3f}s")
print(f" S1: {fastest['s1']:.3f}s | S2: {fastest['s2']:.3f}s | S3: {fastest['s3']:.3f}s")
print(f" Tire: {fastest['compound']} (Life: {fastest['life']:.0f})\n")
# Tire compounds used
compounds_used = laps_df["compound"].dropna().unique()
print(f"Tire compounds used: {', '.join(compounds_used)}\n")
# Stint analysis
stints = laps_df.groupby("stint")
print("Stint Summary:")
for stint_num, stint_data in stints:
if pd.isna(stint_num):
continue
valid_stint = stint_data[
(stint_data["time"].notna()) &
(stint_data["pin"].isna()) &
(stint_data["pout"].isna())
]
if len(valid_stint) == 0:
continue
print(f" Stint {int(stint_num)}: {stint_data['compound'].iloc[0]} "
f"({len(stint_data)} laps, avg: {valid_stint['time'].mean():.3f}s)")
# Run analysis
analyze_driver_session("Australian Grand Prix/Race", "VER")
Next Steps
- Analyzing Telemetry - Deep dive into speed, throttle, and acceleration
- Python Examples - Complete working code examples
- Data Reference - Field documentation
