Spaces:
Sleeping
Sleeping
wip(FastF1ToSQL): create the main class
Browse files- notebooks/formula1_databases.py +375 -0
notebooks/formula1_databases.py
ADDED
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Any, cast
|
2 |
+
import sqlite3
|
3 |
+
import pandas as pd
|
4 |
+
from datetime import datetime
|
5 |
+
from fastf1.core import Session
|
6 |
+
import fastf1
|
7 |
+
|
8 |
+
|
9 |
+
class FastF1ToSQL:
|
10 |
+
def __init__(self, db_path: str) -> None:
|
11 |
+
"""
|
12 |
+
Initialize the FastF1ToSQL class.
|
13 |
+
|
14 |
+
Args:
|
15 |
+
db_path (str): Path to the SQLite database file.
|
16 |
+
"""
|
17 |
+
self.db_path = db_path
|
18 |
+
self.conn = sqlite3.connect(db_path)
|
19 |
+
self.cursor = self.conn.cursor()
|
20 |
+
self.create_tables()
|
21 |
+
|
22 |
+
def create_tables(self) -> None:
|
23 |
+
"""Create all necessary tables and indexes if they don't exist."""
|
24 |
+
self.cursor.executescript('''
|
25 |
+
CREATE TABLE IF NOT EXISTS Drivers (
|
26 |
+
driver_id INTEGER PRIMARY KEY,
|
27 |
+
driver_name TEXT NOT NULL,
|
28 |
+
team TEXT NOT NULL
|
29 |
+
);
|
30 |
+
|
31 |
+
CREATE TABLE IF NOT EXISTS Tracks (
|
32 |
+
track_id INTEGER PRIMARY KEY,
|
33 |
+
track_name TEXT NOT NULL,
|
34 |
+
country TEXT NOT NULL
|
35 |
+
);
|
36 |
+
|
37 |
+
CREATE TABLE IF NOT EXISTS Event (
|
38 |
+
event_id INTEGER PRIMARY KEY,
|
39 |
+
round_number INTEGER,
|
40 |
+
country TEXT,
|
41 |
+
location TEXT,
|
42 |
+
event_date DATE,
|
43 |
+
event_name TEXT,
|
44 |
+
session_1_date_utc DATETIME,
|
45 |
+
session_1_name TEXT CHECK(session_1_name IN ('practice', 'qualify', 'race')),
|
46 |
+
session_2_date_utc DATETIME,
|
47 |
+
session_2_name TEXT CHECK(session_2_name IN ('practice', 'qualify', 'race')),
|
48 |
+
session_3_date_utc DATETIME,
|
49 |
+
session_3_name TEXT CHECK(session_3_name IN ('practice', 'qualify', 'race')),
|
50 |
+
session_4_date_utc DATETIME,
|
51 |
+
session_4_name TEXT CHECK(session_4_name IN ('practice', 'qualify', 'race')),
|
52 |
+
session_5_date_utc DATETIME,
|
53 |
+
session_5_name TEXT CHECK(session_5_name IN ('practice', 'qualify', 'race'))
|
54 |
+
);
|
55 |
+
|
56 |
+
CREATE TABLE IF NOT EXISTS Sessions (
|
57 |
+
session_id INTEGER PRIMARY KEY,
|
58 |
+
event_id INTEGER,
|
59 |
+
track_id INTEGER,
|
60 |
+
session_type TEXT NOT NULL,
|
61 |
+
date TEXT NOT NULL,
|
62 |
+
FOREIGN KEY (event_id) REFERENCES Event(event_id),
|
63 |
+
FOREIGN KEY (track_id) REFERENCES Tracks(track_id)
|
64 |
+
);
|
65 |
+
|
66 |
+
CREATE TABLE IF NOT EXISTS Weather (
|
67 |
+
weather_id INTEGER PRIMARY KEY,
|
68 |
+
session_id INTEGER,
|
69 |
+
datetime DATETIME,
|
70 |
+
air_temperature_in_celsius REAL,
|
71 |
+
relative_air_humidity_in_percentage REAL,
|
72 |
+
air_pressure_in_mbar REAL,
|
73 |
+
is_raining BOOLEAN,
|
74 |
+
track_temperature_in_celsius REAL,
|
75 |
+
wind_direction_in_grads REAL,
|
76 |
+
wind_speed_in_meters_per_seconds REAL,
|
77 |
+
FOREIGN KEY (session_id) REFERENCES Sessions(session_id)
|
78 |
+
);
|
79 |
+
|
80 |
+
CREATE TABLE IF NOT EXISTS Laps (
|
81 |
+
lap_id INTEGER PRIMARY KEY,
|
82 |
+
session_id INTEGER,
|
83 |
+
driver_name TEXT NOT NULL,
|
84 |
+
lap_number INTEGER NOT NULL,
|
85 |
+
stint INTEGER,
|
86 |
+
sector_1_speed_trap_in_km REAL,
|
87 |
+
sector_2_speed_trap_in_km REAL,
|
88 |
+
finish_line_speed_trap_in_km REAL,
|
89 |
+
longest_strait_speed_trap_in_km REAL,
|
90 |
+
is_personal_best BOOLEAN,
|
91 |
+
tyre_compound TEXT,
|
92 |
+
tyre_life_in_laps INTEGER,
|
93 |
+
is_fresh_tyre BOOLEAN,
|
94 |
+
position INTEGER,
|
95 |
+
lap_time_in_seconds REAL,
|
96 |
+
sector_1_time_in_seconds REAL,
|
97 |
+
sector_2_time_in_seconds REAL,
|
98 |
+
sector_3_time_in_seconds REAL,
|
99 |
+
lap_start_time_in_datetime DATETIME,
|
100 |
+
pin_in_time_in_datetime DATETIME,
|
101 |
+
pin_out_time_in_datetime DATETIME,
|
102 |
+
FOREIGN KEY (session_id) REFERENCES Sessions(session_id),
|
103 |
+
UNIQUE (session_id, driver_name, lap_number)
|
104 |
+
);
|
105 |
+
|
106 |
+
CREATE TABLE IF NOT EXISTS Telemetry (
|
107 |
+
telemetry_id INTEGER PRIMARY KEY,
|
108 |
+
lap_id INTEGER,
|
109 |
+
speed_in_km REAL,
|
110 |
+
RPM INTEGER,
|
111 |
+
gear_number INTEGER,
|
112 |
+
throttle_input REAL CHECK (throttle_input BETWEEN 0 AND 100),
|
113 |
+
is_brake_pressed BOOLEAN,
|
114 |
+
is_DRS_open BOOLEAN,
|
115 |
+
x_position REAL,
|
116 |
+
y_position REAL,
|
117 |
+
z_position REAL,
|
118 |
+
is_off_track BOOLEAN,
|
119 |
+
datetime DATETIME,
|
120 |
+
FOREIGN KEY (lap_id) REFERENCES Laps(lap_id)
|
121 |
+
);
|
122 |
+
|
123 |
+
CREATE INDEX IF NOT EXISTS idx_laps_driver_name ON Laps(driver_name);
|
124 |
+
CREATE INDEX IF NOT EXISTS idx_laps_session_id ON Laps(session_id);
|
125 |
+
CREATE INDEX IF NOT EXISTS idx_telemetry_lap_id ON Telemetry(lap_id);
|
126 |
+
CREATE INDEX IF NOT EXISTS idx_telemetry_datetime ON Telemetry(datetime);
|
127 |
+
CREATE INDEX IF NOT EXISTS idx_weather_session_id ON Weather(session_id);
|
128 |
+
CREATE INDEX IF NOT EXISTS idx_weather_datetime ON Weather(datetime);
|
129 |
+
CREATE INDEX IF NOT EXISTS idx_event_date ON Event(event_date);
|
130 |
+
''')
|
131 |
+
self.conn.commit()
|
132 |
+
|
133 |
+
def process_session(self, session: Session) -> None:
|
134 |
+
"""
|
135 |
+
Process a session and insert the data into the database.
|
136 |
+
|
137 |
+
Args:
|
138 |
+
session (Session): The session to process.
|
139 |
+
"""
|
140 |
+
# Load session data
|
141 |
+
session.load()
|
142 |
+
|
143 |
+
# Insert data into tables
|
144 |
+
self.insert_event(session)
|
145 |
+
self.insert_session(session)
|
146 |
+
self.insert_drivers(session)
|
147 |
+
self.insert_laps(session)
|
148 |
+
self.insert_telemetry(session)
|
149 |
+
self.insert_weather(session)
|
150 |
+
|
151 |
+
# Commit changes and close connection
|
152 |
+
self.conn.commit()
|
153 |
+
self.conn.close()
|
154 |
+
|
155 |
+
def insert_event(self, session: Session) -> None:
|
156 |
+
"""
|
157 |
+
Insert the event data into the database.
|
158 |
+
|
159 |
+
Args:
|
160 |
+
session (Session): The FastF1 session object.
|
161 |
+
"""
|
162 |
+
event_data: dict[str, Any] = {
|
163 |
+
'round_number': session.event.RoundNumber,
|
164 |
+
'country': session.event.Country,
|
165 |
+
'location': session.event.Location,
|
166 |
+
'event_date': session.event.EventDate.date(),
|
167 |
+
'event_name': session.event.EventName,
|
168 |
+
'session_1_date_utc': session.event.Session1DateUtc,
|
169 |
+
'session_1_name': session.event.Session1.lower(),
|
170 |
+
'session_2_date_utc': session.event.Session2DateUtc,
|
171 |
+
'session_2_name': session.event.Session2.lower(),
|
172 |
+
'session_3_date_utc': session.event.Session3DateUtc,
|
173 |
+
'session_3_name': session.event.Session3.lower(),
|
174 |
+
'session_4_date_utc': session.event.Session4DateUtc,
|
175 |
+
'session_4_name': session.event.Session4.lower(),
|
176 |
+
'session_5_date_utc': session.event.Session5DateUtc,
|
177 |
+
'session_5_name': session.event.Session5.lower(),
|
178 |
+
}
|
179 |
+
|
180 |
+
columns = ', '.join(event_data.keys())
|
181 |
+
placeholders = ', '.join(['?' for _ in event_data])
|
182 |
+
query = f"INSERT OR REPLACE INTO Event ({columns}) VALUES ({placeholders})"
|
183 |
+
self.cursor.execute(query, list(event_data.values()))
|
184 |
+
|
185 |
+
def insert_session(self, session: Session) -> None:
|
186 |
+
"""
|
187 |
+
Insert the session data into the database.
|
188 |
+
|
189 |
+
Args:
|
190 |
+
session (Session): The FastF1 session object.
|
191 |
+
"""
|
192 |
+
session_data: dict[str, Any] = {
|
193 |
+
# Assuming this is called right after insert_event
|
194 |
+
'event_id': self.cursor.lastrowid,
|
195 |
+
'track_id': self.get_or_create_track(session.event.Location, session.event.Country),
|
196 |
+
'session_type': session.name,
|
197 |
+
'date': session.date,
|
198 |
+
}
|
199 |
+
columns = ', '.join(session_data.keys())
|
200 |
+
placeholders = ':' + ', :'.join(session_data.keys())
|
201 |
+
query = f"INSERT INTO Sessions ({columns}) VALUES ({placeholders})"
|
202 |
+
self.cursor.execute(query, session_data)
|
203 |
+
|
204 |
+
def insert_drivers(self, session: Session) -> None:
|
205 |
+
"""
|
206 |
+
Insert the drivers data into the database.
|
207 |
+
|
208 |
+
Args:
|
209 |
+
session (Session): The FastF1 session object.
|
210 |
+
"""
|
211 |
+
for driver in session.drivers:
|
212 |
+
driver_info = session.get_driver(driver)
|
213 |
+
driver_data = {
|
214 |
+
'driver_name': driver_info['FullName'],
|
215 |
+
'team': driver_info['TeamName']
|
216 |
+
}
|
217 |
+
columns = ', '.join(driver_data.keys())
|
218 |
+
placeholders = ':' + ', :'.join(driver_data.keys())
|
219 |
+
query = f"INSERT OR IGNORE INTO Drivers ({columns}) VALUES ({placeholders})"
|
220 |
+
self.cursor.execute(query, driver_data)
|
221 |
+
|
222 |
+
def insert_laps(self, session: Session) -> None:
|
223 |
+
"""
|
224 |
+
Insert the laps data into the database.
|
225 |
+
|
226 |
+
Args:
|
227 |
+
session (Session): The FastF1 session object.
|
228 |
+
"""
|
229 |
+
laps_df = session.laps.copy()
|
230 |
+
# Assuming this is called right after insert_session
|
231 |
+
laps_df['session_id'] = self.cursor.lastrowid
|
232 |
+
laps_df['lap_start_time_in_datetime'] = pd.to_datetime(
|
233 |
+
laps_df['LapStartDate'])
|
234 |
+
laps_df['pin_in_time_in_datetime'] = pd.to_datetime(
|
235 |
+
laps_df['PitInTime'], unit='ns')
|
236 |
+
laps_df['pin_out_time_in_datetime'] = pd.to_datetime(
|
237 |
+
laps_df['PitOutTime'], unit='ns')
|
238 |
+
|
239 |
+
for _, lap in laps_df.iterrows():
|
240 |
+
lap_data: dict[str, Any] = {
|
241 |
+
'session_id': lap['session_id'],
|
242 |
+
'driver_name': lap['Driver'],
|
243 |
+
'lap_number': lap['LapNumber'],
|
244 |
+
'sector_1_time_in_seconds': lap['Sector1Time'].total_seconds() if pd.notnull(lap['Sector1Time']) else None,
|
245 |
+
'sector_2_time_in_seconds': lap['Sector2Time'].total_seconds() if pd.notnull(lap['Sector2Time']) else None,
|
246 |
+
'sector_3_time_in_seconds': lap['Sector3Time'].total_seconds() if pd.notnull(lap['Sector3Time']) else None,
|
247 |
+
'lap_time_in_seconds': lap['LapTime'].total_seconds() if pd.notnull(lap['LapTime']) else None,
|
248 |
+
'finish_line_speed_trap_in_km': lap['SpeedFL'],
|
249 |
+
'longest_strait_speed_trap_in_km': lap['SpeedST'],
|
250 |
+
'is_personal_best': lap['IsPersonalBest'],
|
251 |
+
'tyre_compound': lap['Compound'],
|
252 |
+
'tyre_life_in_laps': lap['TyreLife'],
|
253 |
+
'is_fresh_tyre': lap['FreshTyre'],
|
254 |
+
'position': lap['Position'],
|
255 |
+
'lap_start_time_in_datetime': lap['lap_start_time_in_datetime'],
|
256 |
+
'pin_in_time_in_datetime': lap['pin_in_time_in_datetime'],
|
257 |
+
'pin_out_time_in_datetime': lap['pin_out_time_in_datetime'],
|
258 |
+
}
|
259 |
+
columns = ', '.join(lap_data.keys())
|
260 |
+
placeholders = ':' + ', :'.join(lap_data.keys())
|
261 |
+
query = f"INSERT INTO Laps ({columns}) VALUES ({placeholders})"
|
262 |
+
self.cursor.execute(query, lap_data)
|
263 |
+
|
264 |
+
def insert_telemetry(self, session: Session) -> None:
|
265 |
+
"""
|
266 |
+
Insert the telemetry data into the database.
|
267 |
+
|
268 |
+
Args:
|
269 |
+
session (Session): The FastF1 session object.
|
270 |
+
"""
|
271 |
+
for driver in session.drivers:
|
272 |
+
car_data = session.car_data[driver].copy()
|
273 |
+
pos_data = session.pos_data[driver].copy()
|
274 |
+
|
275 |
+
telemetry = car_data.merge(
|
276 |
+
pos_data, left_index=True, right_index=True, suffixes=('', '_pos'))
|
277 |
+
telemetry['lap_id'] = self.get_lap_id(
|
278 |
+
session, driver, telemetry.index)
|
279 |
+
|
280 |
+
for _, sample in telemetry.iterrows():
|
281 |
+
telemetry_data: dict[str, Any] = {
|
282 |
+
'lap_id': sample['lap_id'],
|
283 |
+
'speed_in_km': sample['Speed'],
|
284 |
+
'RPM': sample['RPM'],
|
285 |
+
'gear_number': sample['nGear'],
|
286 |
+
'throttle_input': sample['Throttle'],
|
287 |
+
'is_brake_pressed': sample['Brake'],
|
288 |
+
'is_DRS_open': sample['DRS'],
|
289 |
+
'x_position': sample['X'],
|
290 |
+
'y_position': sample['Y'],
|
291 |
+
'z_position': sample['Z'],
|
292 |
+
'is_off_track': sample['Status'] == 'OffTrack',
|
293 |
+
'datetime': sample.name,
|
294 |
+
}
|
295 |
+
columns = ', '.join(telemetry_data.keys())
|
296 |
+
placeholders = ':' + ', :'.join(telemetry_data.keys())
|
297 |
+
query = f"INSERT INTO Telemetry ({columns}) VALUES ({placeholders})"
|
298 |
+
self.cursor.execute(query, telemetry_data)
|
299 |
+
|
300 |
+
def insert_weather(self, session: Session) -> None:
|
301 |
+
"""
|
302 |
+
Insert weather data into the Weather table.
|
303 |
+
|
304 |
+
Args:
|
305 |
+
session (Session): The FastF1 session containing weather data.
|
306 |
+
"""
|
307 |
+
weather_data = cast(pd.DataFrame, session.weather_data)
|
308 |
+
# Assuming this is called right after insert_session
|
309 |
+
weather_data['session_id'] = self.cursor.lastrowid
|
310 |
+
|
311 |
+
for _, sample in weather_data.iterrows():
|
312 |
+
weather_sample: dict[str, Any] = {
|
313 |
+
'session_id': sample['session_id'],
|
314 |
+
'air_temperature_in_celsius': sample['AirTemp'],
|
315 |
+
'track_temperature_in_celsius': sample['TrackTemp'],
|
316 |
+
'wind_speed_in_meters_per_seconds': sample['WindSpeed'],
|
317 |
+
'wind_direction_in_grads': sample['WindDirection'],
|
318 |
+
'relative_air_humidity_in_percentage': sample['Humidity'],
|
319 |
+
'air_pressure_in_mbar': sample['Pressure'],
|
320 |
+
'is_raining': sample['Rainfall'],
|
321 |
+
'datetime': sample.name,
|
322 |
+
}
|
323 |
+
columns = ', '.join(weather_sample.keys())
|
324 |
+
placeholders = ':' + ', :'.join(weather_sample.keys())
|
325 |
+
query = f"INSERT INTO Weather ({columns}) VALUES ({placeholders})"
|
326 |
+
self.cursor.execute(query, weather_sample)
|
327 |
+
|
328 |
+
def get_or_create_track(self, track_name: str, country: str) -> int:
|
329 |
+
"""
|
330 |
+
Get the track_id for a given track, or create a new track if it doesn't exist.
|
331 |
+
|
332 |
+
Args:
|
333 |
+
track_name (str): The name of the track.
|
334 |
+
country (str): The country where the track is located.
|
335 |
+
|
336 |
+
Returns:
|
337 |
+
int: The track_id of the existing or newly created track.
|
338 |
+
"""
|
339 |
+
self.cursor.execute(
|
340 |
+
"SELECT track_id FROM Tracks WHERE track_name = ? AND country = ?", (track_name, country))
|
341 |
+
result = self.cursor.fetchone()
|
342 |
+
if result:
|
343 |
+
return result[0]
|
344 |
+
else:
|
345 |
+
self.cursor.execute(
|
346 |
+
"INSERT INTO Tracks (track_name, country) VALUES (?, ?)", (track_name, country))
|
347 |
+
return self.cursor.lastrowid or 0
|
348 |
+
|
349 |
+
def get_lap_id(self, session: Session, driver: str, time: datetime) -> int:
|
350 |
+
"""
|
351 |
+
Get the lap_id for a given driver and time.
|
352 |
+
|
353 |
+
Args:
|
354 |
+
session (fastf1.core.Session): The FastF1 session.
|
355 |
+
driver (str): The driver's name or abbreviation.
|
356 |
+
time (pd.Timestamp): The timestamp to find the corresponding lap.
|
357 |
+
|
358 |
+
Returns:
|
359 |
+
int: The lap_id of the found lap.
|
360 |
+
"""
|
361 |
+
laps = session.laps.pick_driver(driver)
|
362 |
+
lap = laps.loc[laps['LapStartTime'] <= time].iloc[-1]
|
363 |
+
|
364 |
+
if self.cursor.lastrowid is None:
|
365 |
+
raise ValueError("No ID was generated")
|
366 |
+
|
367 |
+
self.cursor.execute("SELECT lap_id FROM Laps WHERE session_id = ? AND driver_name = ? AND lap_number = ?",
|
368 |
+
(self.cursor.lastrowid, driver, lap['LapNumber']))
|
369 |
+
return self.cursor.fetchone()[0]
|
370 |
+
|
371 |
+
|
372 |
+
# Usage example:
|
373 |
+
session = fastf1.get_session(2023, 'Bahrain', 'Q')
|
374 |
+
converter = FastF1ToSQL('f1_data.db')
|
375 |
+
converter.process_session(session)
|