Skip to main content

Overview

Telemetry data is recorded at approximately 3.7 Hz (~270ms intervals) and includes car sensor data, driver inputs, position coordinates, and computed accelerations. This guide covers common analysis patterns using Python.

Data Structure

Telemetry data is stored in tel.json files with the following key fields:
Each telemetry file is identified by a unique dataKey in the format: {Year}-{EventName}-{Session}-{Driver}-{LapNumber}Example: "2026-Australian Grand Prix-Race-VER-9"

Available Fields

FieldTypeDescriptionUnit
timefloatTime from start of lapseconds
rpmfloatEngine RPMRPM
speedfloatCar speedkm/h
gearintCurrent gear (1-8)-
throttlefloatThrottle position0-100%
brakeintBrake application0 or 1
drsintDRS status0=off, 1=on
distancefloatDistance since lap startmeters
acc_xfloatLongitudinal accelerationm/s²
acc_yfloatLateral accelerationm/s²
acc_zfloatVertical accelerationm/s²
x, y, zfloat3D position coordinatesmeters
Acceleration values are computed, not raw IMU sensor data. They are derived using gradient analysis with smoothing and outlier filtering. See the DATA_REFERENCE.md for computation details.

Loading Telemetry Data

The extraction scripts use orjson for fast JSON handling:
import orjson
import numpy as np

# Load telemetry data
with open("Australian Grand Prix/Race/VER/15_tel.json", "rb") as f:
    data = orjson.loads(f.read())

tel = data["tel"]

# Access fields
speed = np.array(tel["speed"])
throttle = np.array(tel["throttle"])
distance = np.array(tel["distance"])

Using pandas DataFrame

Convert to DataFrame for easier analysis:
import pandas as pd
import orjson

with open("Australian Grand Prix/Race/VER/15_tel.json", "rb") as f:
    data = orjson.loads(f.read())

tel_df = pd.DataFrame(data["tel"])

# Handle "None" string values
for col in tel_df.columns:
    tel_df[col] = tel_df[col].replace("None", np.nan)

# Convert numeric columns
numeric_cols = ["time", "rpm", "speed", "throttle", "distance", 
                "acc_x", "acc_y", "acc_z", "x", "y", "z"]
for col in numeric_cols:
    if col in tel_df.columns:
        tel_df[col] = pd.to_numeric(tel_df[col], errors="coerce")

print(tel_df.head())

Speed Trace Analysis

Speed Through Corners

Analyze how speed changes through specific corners:
import matplotlib.pyplot as plt
import numpy as np

# Load telemetry for a specific lap
with open("Australian Grand Prix/Qualifying/LEC/5_tel.json", "rb") as f:
    data = orjson.loads(f.read())

tel = data["tel"]

# Convert to numpy arrays, filtering out "None"
speed = np.array([s for s in tel["speed"] if s != "None"], dtype=float)
distance = np.array([d for d in tel["distance"] if d != "None"], dtype=float)

# Plot speed vs distance
plt.figure(figsize=(14, 6))
plt.plot(distance, speed, linewidth=2, color="#E10600")
plt.xlabel("Distance (m)")
plt.ylabel("Speed (km/h)")
plt.title("Speed Trace - LEC Qualifying Lap 5")
plt.grid(True, alpha=0.3)
plt.show()

Minimum Corner Speed

Identify minimum speed in corner sections:
import orjson
import numpy as np

# Load corner data
with open("Australian Grand Prix/Qualifying/corners.json", "rb") as f:
    corners = orjson.loads(f.read())

# Load telemetry
with open("Australian Grand Prix/Qualifying/LEC/5_tel.json", "rb") as f:
    tel_data = orjson.loads(f.read())

tel = tel_data["tel"]
speed = np.array([s if s != "None" else np.nan for s in tel["speed"]], dtype=float)
distance = np.array([d if d != "None" else np.nan for d in tel["distance"]], dtype=float)

# Analyze each corner
corner_distances = corners["Distance"]
corner_numbers = corners["CornerNumber"]

for i, (corner_num, corner_dist) in enumerate(zip(corner_numbers, corner_distances)):
    if corner_dist == "None":
        continue
    
    corner_dist = float(corner_dist)
    # Define corner window (±50m)
    window = 50
    mask = (distance >= corner_dist - window) & (distance <= corner_dist + window)
    
    if np.any(mask):
        corner_speeds = speed[mask]
        min_speed = np.nanmin(corner_speeds)
        print(f"Corner {corner_num}: Min Speed = {min_speed:.1f} km/h")

Throttle and Brake Analysis

Throttle Application Patterns

import matplotlib.pyplot as plt
import numpy as np

with open("Australian Grand Prix/Race/HAM/20_tel.json", "rb") as f:
    data = orjson.loads(f.read())

tel = data["tel"]

# Filter out "None" values
valid_indices = [i for i, (s, t, d) in enumerate(zip(tel["speed"], tel["throttle"], tel["distance"])) 
                 if s != "None" and t != "None" and d != "None"]

speed = np.array([tel["speed"][i] for i in valid_indices], dtype=float)
throttle = np.array([tel["throttle"][i] for i in valid_indices], dtype=float)
distance = np.array([tel["distance"][i] for i in valid_indices], dtype=float)

# Create dual-axis plot
fig, ax1 = plt.subplots(figsize=(14, 6))

ax1.plot(distance, speed, color="#00D2BE", label="Speed", linewidth=2)
ax1.set_xlabel("Distance (m)")
ax1.set_ylabel("Speed (km/h)", color="#00D2BE")
ax1.tick_params(axis="y", labelcolor="#00D2BE")

ax2 = ax1.twinx()
ax2.fill_between(distance, throttle, color="#00D2BE", alpha=0.3, label="Throttle")
ax2.set_ylabel("Throttle (%)", color="#00D2BE")
ax2.set_ylim(0, 100)

plt.title("Speed and Throttle Application - HAM Lap 20")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Braking Points

Identify where the driver brakes:
import numpy as np
import orjson

with open("Australian Grand Prix/Race/VER/15_tel.json", "rb") as f:
    data = orjson.loads(f.read())

tel = data["tel"]

# Convert brake to numpy array (0 or 1)
brake = np.array([b if b != "None" else 0 for b in tel["brake"]], dtype=int)
distance = np.array([d if d != "None" else 0 for d in tel["distance"]], dtype=float)

# Find braking zones (where brake goes from 0 to 1)
brake_start_indices = np.where((brake[:-1] == 0) & (brake[1:] == 1))[0] + 1
brake_end_indices = np.where((brake[:-1] == 1) & (brake[1:] == 0))[0] + 1

print(f"Number of braking zones: {len(brake_start_indices)}")

for start_idx, end_idx in zip(brake_start_indices, brake_end_indices[:len(brake_start_indices)]):
    start_dist = distance[start_idx]
    end_dist = distance[end_idx] if end_idx < len(distance) else distance[-1]
    brake_distance = end_dist - start_dist
    print(f"Braking from {start_dist:.1f}m to {end_dist:.1f}m (Duration: {brake_distance:.1f}m)")

Acceleration Analysis

Acceleration data (acc_x, acc_y, acc_z) is computed from telemetry, not raw sensor data. The values are smoothed and filtered for outliers.

Longitudinal Acceleration (acc_x)

Forward/backward acceleration along the track:
import matplotlib.pyplot as plt
import numpy as np
import orjson

with open("Australian Grand Prix/Qualifying/NOR/3_tel.json", "rb") as f:
    data = orjson.loads(f.read())

tel = data["tel"]

# Filter valid data
valid = [(d, a) for d, a in zip(tel["distance"], tel["acc_x"]) 
         if d != "None" and a != "None"]

distance = np.array([v[0] for v in valid], dtype=float)
acc_x = np.array([v[1] for v in valid], dtype=float)

# Plot
plt.figure(figsize=(14, 6))
plt.plot(distance, acc_x, linewidth=1.5, color="#FF8700")
plt.axhline(y=0, color="black", linestyle="--", linewidth=0.8)
plt.xlabel("Distance (m)")
plt.ylabel("Longitudinal Acceleration (m/s²)")
plt.title("Longitudinal Acceleration - NOR Qualifying Lap 3")
plt.grid(True, alpha=0.3)
plt.show()

# Summary statistics
print(f"Max acceleration: {np.max(acc_x):.2f} m/s²")
print(f"Max braking (deceleration): {np.min(acc_x):.2f} m/s²")
print(f"Mean acceleration: {np.mean(acc_x):.2f} m/s²")

Lateral Acceleration (acc_y)

Side-to-side G-forces through corners:
import numpy as np
import matplotlib.pyplot as plt

with open("Australian Grand Prix/Race/SAI/10_tel.json", "rb") as f:
    data = orjson.loads(f.read())

tel = data["tel"]

# Extract lateral acceleration
valid = [(d, a) for d, a in zip(tel["distance"], tel["acc_y"]) 
         if d != "None" and a != "None"]

distance = np.array([v[0] for v in valid], dtype=float)
acc_y = np.array([v[1] for v in valid], dtype=float)

# Convert to G-forces (1g ≈ 9.81 m/s²)
acc_y_g = acc_y / 9.81

plt.figure(figsize=(14, 6))
plt.plot(distance, acc_y_g, linewidth=1.5, color="#DC0000")
plt.axhline(y=0, color="black", linestyle="--", linewidth=0.8)
plt.xlabel("Distance (m)")
plt.ylabel("Lateral Acceleration (G)")
plt.title("Lateral G-Forces - SAI Race Lap 10")
plt.grid(True, alpha=0.3)
plt.show()

print(f"Max lateral G: {np.max(np.abs(acc_y_g)):.2f}g")

Comparing Driver Telemetry

Two-Driver Speed Comparison

import matplotlib.pyplot as plt
import numpy as np
import orjson

# Load telemetry for two drivers
with open("Australian Grand Prix/Qualifying/VER/5_tel.json", "rb") as f:
    ver_data = orjson.loads(f.read())

with open("Australian Grand Prix/Qualifying/HAM/5_tel.json", "rb") as f:
    ham_data = orjson.loads(f.read())

ver_tel = ver_data["tel"]
ham_tel = ham_data["tel"]

# Extract speed and distance
def extract_valid(tel, field1, field2):
    valid = [(f1, f2) for f1, f2 in zip(tel[field1], tel[field2]) 
             if f1 != "None" and f2 != "None"]
    return (np.array([v[0] for v in valid], dtype=float), 
            np.array([v[1] for v in valid], dtype=float))

ver_dist, ver_speed = extract_valid(ver_tel, "distance", "speed")
ham_dist, ham_speed = extract_valid(ham_tel, "distance", "speed")

# Plot comparison
plt.figure(figsize=(14, 7))
plt.plot(ver_dist, ver_speed, label="VER", linewidth=2, color="#3671C6")
plt.plot(ham_dist, ham_speed, label="HAM", linewidth=2, color="#00D2BE", alpha=0.8)
plt.xlabel("Distance (m)")
plt.ylabel("Speed (km/h)")
plt.title("Speed Comparison - VER vs HAM (Qualifying Lap 5)")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

Speed Delta Analysis

import numpy as np
from scipy.interpolate import interp1d

# Interpolate to common distance points
common_distance = np.linspace(
    max(ver_dist[0], ham_dist[0]), 
    min(ver_dist[-1], ham_dist[-1]), 
    num=500
)

ver_speed_interp = interp1d(ver_dist, ver_speed, kind="linear")(common_distance)
ham_speed_interp = interp1d(ham_dist, ham_speed, kind="linear")(common_distance)

# Calculate delta
speed_delta = ver_speed_interp - ham_speed_interp

# Plot delta
plt.figure(figsize=(14, 6))
plt.plot(common_distance, speed_delta, linewidth=2, color="#E10600")
plt.axhline(y=0, color="black", linestyle="--", linewidth=1)
plt.fill_between(common_distance, 0, speed_delta, 
                 where=(speed_delta > 0), color="#3671C6", alpha=0.3, label="VER faster")
plt.fill_between(common_distance, 0, speed_delta, 
                 where=(speed_delta < 0), color="#00D2BE", alpha=0.3, label="HAM faster")
plt.xlabel("Distance (m)")
plt.ylabel("Speed Delta (km/h)")
plt.title("Speed Delta: VER - HAM")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

print(f"Max VER advantage: +{np.max(speed_delta):.1f} km/h")
print(f"Max HAM advantage: {np.min(speed_delta):.1f} km/h")

Visualization Tips

Using Plotly for Interactive Charts

import plotly.graph_objects as go
import orjson
import numpy as np

with open("Australian Grand Prix/Race/LEC/25_tel.json", "rb") as f:
    data = orjson.loads(f.read())

tel = data["tel"]

# Extract valid data
valid = [(d, s, t, b) for d, s, t, b in zip(
    tel["distance"], tel["speed"], tel["throttle"], tel["brake"]
) if all(x != "None" for x in [d, s, t, b])]

distance = np.array([v[0] for v in valid], dtype=float)
speed = np.array([v[1] for v in valid], dtype=float)
throttle = np.array([v[2] for v in valid], dtype=float)
brake = np.array([v[3] for v in valid], dtype=int)

# Create interactive plot
fig = go.Figure()

fig.add_trace(go.Scatter(
    x=distance, y=speed, 
    mode="lines", name="Speed",
    line=dict(color="#E10600", width=2)
))

fig.add_trace(go.Scatter(
    x=distance, y=throttle, 
    mode="lines", name="Throttle",
    line=dict(color="#00D2BE", width=1.5),
    yaxis="y2"
))

fig.update_layout(
    title="Interactive Telemetry Analysis - LEC Lap 25",
    xaxis=dict(title="Distance (m)"),
    yaxis=dict(title="Speed (km/h)", side="left"),
    yaxis2=dict(title="Throttle (%)", side="right", overlaying="y", range=[0, 100]),
    hovermode="x unified",
    height=600
)

fig.show()

Color-Coded Speed Map

Visualize speed on track coordinates:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.collections import LineCollection

with open("Australian Grand Prix/Qualifying/VER/3_tel.json", "rb") as f:
    data = orjson.loads(f.read())

tel = data["tel"]

# Extract x, y, speed
valid = [(x, y, s) for x, y, s in zip(tel["x"], tel["y"], tel["speed"]) 
         if x != "None" and y != "None" and s != "None"]

x = np.array([v[0] for v in valid], dtype=float)
y = np.array([v[1] for v in valid], dtype=float)
speed = np.array([v[2] for v in valid], dtype=float)

# Create line segments
points = np.array([x, y]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)

# Create LineCollection with speed colormap
lc = LineCollection(segments, cmap="plasma", linewidth=3)
lc.set_array(speed)

fig, ax = plt.subplots(figsize=(12, 12))
ax.add_collection(lc)
ax.autoscale()
ax.set_aspect("equal")
ax.set_xlabel("X Position (m)")
ax.set_ylabel("Y Position (m)")
ax.set_title("Speed Heatmap on Track")

cbar = plt.colorbar(lc, ax=ax)
cbar.set_label("Speed (km/h)")
plt.tight_layout()
plt.show()

Common Pitfalls

  1. “None” values: The JSON contains string "None" for missing data, not Python None. Always filter or convert these.
  2. Acceleration units: Values are in m/s², not G-forces. Convert by dividing by 9.81.
  3. Computed accelerations: These are mathematically derived with smoothing, not raw IMU data.
  4. Sample rate: Data is ~3.7 Hz, not uniform. Use time or distance for accurate analysis.

Next Steps

Build docs developers (and LLMs) love