Draichi commited on
Commit
14d15a4
·
unverified ·
1 Parent(s): c502ece

wip(FastF1ToSQL): create the main class

Browse files
Files changed (1) hide show
  1. 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)