Skip to main content

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 in laptimes.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

FieldTypeDescriptionUnit
timefloatLap timeseconds
lapintLap number-
s1, s2, s3floatSector timesseconds
compoundstringTire compoundSOFT/MEDIUM/HARD
lifeintTire age (laps on this tire)laps
stintintStint number (1, 2, 3…)-
freshboolWas tire new at stint starttrue/false
posintTrack position at lap end1-20
pbboolIs this the personal best laptrue/false
statusstringTrack status codes”1”, “24”, etc.
pinfloatPit entry timeseconds
poutfloatPit exit timeseconds
sesTfloatSession time at lap endseconds
lSTfloatLap start timeseconds
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 a pb 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

Build docs developers (and LLMs) love