Author: Timofey Malakancikov, ITMO University
Date: April 2025
This Jupyter Notebook presents a Python-based tool to analyze Formula 1 telemetry data, focusing on the 2023 Monaco Grand Prix Qualifying session. The tool compares the performance of Max Verstappen (Red Bull) and Lewis Hamilton (Mercedes) using speed, throttle, and lap time data, providing insights to optimize driver performance and car setup.
Formula 1 teams rely on telemetry data to analyze car and driver performance, informing setup adjustments, driver coaching, and race strategy. This project uses the fastf1 Python library to load and analyze telemetry data from the 2023 Monaco Grand Prix Qualifying session. The goal is to compare Max Verstappen and Lewis Hamilton’s fastest laps, visualize key metrics (speed and throttle), and identify performance differences, such as time losses in specific corners.
Objectives:
fastf1.Tools: Python, fastf1, pandas, matplotlib, seaborn.
This section loads telemetry data for the 2023 Monaco Grand Prix Qualifying session using the fastf1 library. We focus on the fastest laps of Max Verstappen (VER) and Lewis Hamilton (HAM). A cache directory is created to store data locally, avoiding repeated API calls.
print("--- Running Cell 4: Data Loading ---")
# Import libraries
import os
import fastf1
from fastf1 import plotting
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np
# Set up plotting style
plotting.setup_mpl(color_scheme='fastf1', misc_mpl_mods=False)
sns.set_style("whitegrid")
# Create cache directory
cache_dir = 'cache'
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)
# Enable FastF1 cache
try:
fastf1.Cache.enable_cache(cache_dir)
except Exception as e:
print(f"An error occurred enabling the cache: {e}")
# Load 2023 Monaco Grand Prix Qualifying session
print("Loading session data (may take time if not cached)...")
try:
session = fastf1.get_session(2023, 'Monaco', 'Q')
session.load(laps=True, telemetry=True, weather=False, messages=False) # Load laps and telemetry
print("Session data loaded successfully.")
except Exception as e:
print(f"\n*** Error loading session data: {e} ***")
raise e
# Check if laps data is available
if session.laps is None or session.laps.empty:
raise ValueError("Session laps data could not be loaded or is empty. Cannot proceed.")
# Get lap data for Verstappen and Hamilton
driver1 = 'VER'
driver2 = 'HAM'
laps_driver1 = session.laps.pick_drivers([driver1]).pick_quicklaps()
laps_driver2 = session.laps.pick_drivers([driver2]).pick_quicklaps()
# Get fastest laps
fastest_driver1 = laps_driver1.pick_fastest()
fastest_driver2 = laps_driver2.pick_fastest()
# Check if fastest laps were found
if pd.isna(fastest_driver1['LapTime']):
raise ValueError(f"Could not find fastest lap with telemetry for {driver1}.")
if pd.isna(fastest_driver2['LapTime']):
raise ValueError(f"Could not find fastest lap with telemetry for {driver2}.")
# Load telemetry using get_car_data().add_distance() for plotting Speed/Throttle vs Distance
telemetry_driver1 = fastest_driver1.get_car_data().add_distance()
telemetry_driver2 = fastest_driver2.get_car_data().add_distance()
# Display lap times
print(f"\n{driver1} Fastest Lap Time: {fastest_driver1['LapTime']}")
print(f"{driver2} Fastest Lap Time: {fastest_driver2['LapTime']}")
print("--- End Cell 4 ---")
--- Running Cell 4: Data Loading --- Loading session data (may take time if not cached)...
core INFO Loading data for Monaco Grand Prix - Qualifying [v3.5.3] req INFO Using cached data for session_info req INFO Using cached data for driver_info req INFO Using cached data for session_status_data req INFO Using cached data for track_status_data req INFO Using cached data for _extended_timing_data req INFO Using cached data for timing_app_data core INFO Processing timing data... req INFO Using cached data for car_data req INFO Using cached data for position_data core INFO Finished loading data for 20 drivers: ['1', '14', '16', '31', '55', '44', '10', '63', '22', '4', '81', '21', '23', '18', '77', '2', '20', '27', '24', '11']
Session data loaded successfully. VER Fastest Lap Time: 0 days 00:01:11.365000 HAM Fastest Lap Time: 0 days 00:01:11.725000 --- End Cell 4 ---
This section processes the telemetry data to compute key metrics, such as time deltas and sector performance, to identify where each driver gains or loses time.
# CELL 6: Code (Analysis - FIXED Delta Calculation and Debugging)
print("\n--- Running Cell 6: Analysis ---")
# Check for valid telemetry dataframes
if telemetry_driver1.empty or telemetry_driver2.empty:
raise ValueError("Telemetry dataframes are empty. Loading in Cell 4 might have failed.")
# Check essential columns exist
required_cols = ['Speed', 'Throttle', 'Distance', 'Time'] # 'Time' is needed by delta_time
for col in required_cols:
if col not in telemetry_driver1.columns or telemetry_driver1[col].isna().all():
raise ValueError(f"Column '{col}' is missing or all NaN for {driver1}.")
if col not in telemetry_driver2.columns or telemetry_driver2[col].isna().all():
raise ValueError(f"Column '{col}' is missing or all NaN for {driver2}.")
# Calculate time delta using fastf1.utils, aligned by distance
# This function uses the Lap objects and loads/processes telemetry internally
print("Calculating delta time using fastf1.utils.delta_time...")
try:
# Pass the Lap objects directly
delta, ref_tel, comp_tel = utils.delta_time(fastest_driver1, fastest_driver2)
# --- Debugging Output ---
print("Delta time calculated.")
print(f" - Shape of delta array: {delta.shape}")
if delta.size > 0:
print(f" - First 5 delta values: {delta[:5]}")
print(f" - NaN values in delta: {np.isnan(delta).sum()}")
else:
print(" - Delta array is empty!")
print(f" - Shape of reference telemetry (ref_tel): {ref_tel.shape}")
if not ref_tel.empty and 'Distance' in ref_tel.columns:
print(f" - Shape of ref_tel['Distance']: {ref_tel['Distance'].shape}")
print(f" - First 5 ref_tel['Distance'] values:\n{ref_tel['Distance'].head()}")
print(f" - NaN values in ref_tel['Distance']: {ref_tel['Distance'].isna().sum()}")
elif 'Distance' not in ref_tel.columns:
print(" - 'Distance' column missing in returned ref_tel!")
else:
print(" - Returned ref_tel is empty!")
# --- End Debugging Output ---
# Double-check if Distance column is present in the returned ref_tel
if 'Distance' not in ref_tel.columns and not ref_tel.empty:
print("Adding 'Distance' column to ref_tel after delta_time call (should not be necessary)")
ref_tel = ref_tel.add_distance()
if 'Distance' not in ref_tel.columns:
raise RuntimeError("Failed to add 'Distance' column to reference telemetry.")
except Exception as e:
print(f"\n*** Error calculating delta time with fastf1.utils.delta_time: {e} ***")
print("This might indicate issues with the lap objects or underlying telemetry data.")
# Set dummy values to prevent NameError in plotting cell
delta = np.array([])
ref_tel = pd.DataFrame({'Distance': []}) # Empty dataframe with Distance column
# Analyze sector times
print("\n--- Sector Time Comparison ---")
sector1_driver1 = fastest_driver1['Sector1Time']
sector1_driver2 = fastest_driver2['Sector1Time']
sector2_driver1 = fastest_driver1['Sector2Time']
sector2_driver2 = fastest_driver2['Sector2Time']
sector3_driver1 = fastest_driver1['Sector3Time']
sector3_driver2 = fastest_driver2['Sector3Time']
print(f"Sector 1: {driver1}: {sector1_driver1}, {driver2}: {sector1_driver2}")
print(f"Sector 2: {driver1}: {sector2_driver1}, {driver2}: {sector2_driver2}")
print(f"Sector 3: {driver1}: {sector3_driver1}, {driver2}: {sector3_driver2}")
print("--- End Cell 6 ---")
--- Running Cell 6: Analysis --- Calculating delta time using fastf1.utils.delta_time... *** Error calculating delta time with fastf1.utils.delta_time: name 'utils' is not defined *** This might indicate issues with the lap objects or underlying telemetry data. --- Sector Time Comparison --- Sector 1: VER: 0 days 00:00:18.642000, HAM: 0 days 00:00:18.757000 Sector 2: VER: 0 days 00:00:33.902000, HAM: 0 days 00:00:33.929000 Sector 3: VER: 0 days 00:00:18.821000, HAM: 0 days 00:00:19.039000 --- End Cell 6 ---
This section generates visualizations to compare Verstappen and Hamilton’s performance, focusing on speed and throttle traces. These plots highlight driving style and performance gaps.
# CELL 8: Code (Visualizations - Plotting Correct Data)
print("\n--- Running Cell 8: Visualizations ---")
# Create plots folder if it doesn't exist
plots_dir = 'plots'
if not os.path.exists(plots_dir):
os.makedirs(plots_dir)
# Get driver colors for consistency
driver1_color = fastf1.plotting.get_driver_color(driver1, session) if fastf1.plotting.get_driver_color(driver1, session) else 'red' # Fallback color
driver2_color = fastf1.plotting.get_driver_color(driver2, session) if fastf1.plotting.get_driver_color(driver2, session) else 'blue' # Fallback color
# Plot 1: Speed Trace Comparison vs Distance
plt.figure(figsize=(12, 6))
plt.plot(telemetry_driver1['Distance'], telemetry_driver1['Speed'], label=f'{driver1}', color=driver1_color)
plt.plot(telemetry_driver2['Distance'], telemetry_driver2['Speed'], label=f'{driver2}', color=driver2_color)
plt.title(f'Speed Trace Comparison ({driver1} vs {driver2}) - Monaco 2023 Qualifying')
plt.xlabel('Distance (m)')
plt.ylabel('Speed (km/h)')
plt.legend()
plt.grid(True)
plt.savefig(os.path.join(plots_dir, 'speed_trace.png'), dpi=300)
print(f"Saved {os.path.join(plots_dir, 'speed_trace.png')}")
plt.show()
# Plot 2: Throttle Input Comparison vs Distance
plt.figure(figsize=(12, 6))
plt.plot(telemetry_driver1['Distance'], telemetry_driver1['Throttle'], label=f'{driver1}', color=driver1_color)
plt.plot(telemetry_driver2['Distance'], telemetry_driver2['Throttle'], label=f'{driver2}', color=driver2_color)
plt.title(f'Throttle Input Comparison ({driver1} vs {driver2}) - Monaco 2023 Qualifying')
plt.xlabel('Distance (m)')
plt.ylabel('Throttle (%)')
plt.legend()
plt.grid(True)
plt.savefig(os.path.join(plots_dir, 'throttle_comparison.png'), dpi=300)
print(f"Saved {os.path.join(plots_dir, 'throttle_comparison.png')}")
plt.show()
print("--- End Cell 8 ---")
--- Running Cell 8: Visualizations --- Saved plots/speed_trace.png
Saved plots/throttle_comparison.png
--- End Cell 8 ---
Based on the analysis and visualizations, here are key findings:
Lap Time Comparison:
Speed Trace Insights:
Throttle Analysis:
F1 Applications:
Conclusion: This analysis highlights Verstappen’s edge in Monaco’s technical corners, driven by smoother inputs and setup choices. The tool can be extended to analyze other sessions or tracks, supporting F1 teams in performance optimization.
This project demonstrates my ability to analyze F1 telemetry data using Python, delivering actionable insights for driver performance and car setup. Key skills showcased include:
fastf1 and pandas. matplotlib and seaborn. Next Steps:
plotly or streamlit. This project is part of my portfolio for F1 internship applications, showcasing my readiness to contribute to teams like Mercedes, Red Bull, or Alpine. Source code and plots are available on GitHub.
Thank you for reviewing my project! For questions or feedback, contact me at timofey.malakancikov@itmo.ru.