├── img ├── f1.png └── placeholder.png ├── mkdwn ├── demo.gif ├── sectors3.png ├── fastestlap.png └── fulltelem.png ├── requirements.txt ├── data ├── laps.csv ├── drivers.csv └── events.csv ├── LICENSE ├── README.md ├── script.py └── gui.py /img/f1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynesconnor/formula1-telemetry-tool/HEAD/img/f1.png -------------------------------------------------------------------------------- /mkdwn/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynesconnor/formula1-telemetry-tool/HEAD/mkdwn/demo.gif -------------------------------------------------------------------------------- /mkdwn/sectors3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynesconnor/formula1-telemetry-tool/HEAD/mkdwn/sectors3.png -------------------------------------------------------------------------------- /img/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynesconnor/formula1-telemetry-tool/HEAD/img/placeholder.png -------------------------------------------------------------------------------- /mkdwn/fastestlap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynesconnor/formula1-telemetry-tool/HEAD/mkdwn/fastestlap.png -------------------------------------------------------------------------------- /mkdwn/fulltelem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynesconnor/formula1-telemetry-tool/HEAD/mkdwn/fulltelem.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.4 2 | attrs==21.4.0 3 | cattrs==22.1.0 4 | certifi==2022.6.15 5 | charset-normalizer==2.1.0 6 | cycler==0.11.0 7 | exceptiongroup==1.0.0rc8 8 | fastf1==2.2.9 9 | fonttools==4.34.4 10 | idna==3.3 11 | kiwisolver==1.4.3 12 | matplotlib==3.5.2 13 | numpy==1.23.1 14 | packaging==21.3 15 | pandas==1.4.3 16 | Pillow==9.2.0 17 | pyparsing==3.0.9 18 | PyQt5==5.15.7 19 | PyQt5-Qt5==5.15.2 20 | PyQt5-sip==12.11.0 21 | python-dateutil==2.8.2 22 | pytz==2022.1 23 | requests==2.28.1 24 | requests-cache==0.9.5 25 | scipy==1.8.1 26 | signalr-client-aio==0.0.1.6.2 27 | six==1.16.0 28 | thefuzz==0.19.0 29 | timple==0.1.5 30 | url-normalize==1.4.3 31 | urllib3==1.26.10 32 | websockets==10.3 33 | -------------------------------------------------------------------------------- /data/laps.csv: -------------------------------------------------------------------------------- 1 | event,laps 2 | 70th Anniversary Grand Prix,52 3 | Abu Dhabi Grand Prix,58 4 | Australian Grand Prix,58 5 | Austrian Grand Prix,71 6 | Azerbaijan Grand Prix,51 7 | Bahrain Grand Prix,57 8 | Belgian Grand Prix,44 9 | Brazilian Grand Prix,71 10 | British Grand Prix,52 11 | Canadian Grand Prix,70 12 | Chinese Grand Prix,56 13 | Dutch Grand Prix,72 14 | Eifel Grand Prix,60 15 | Emilia Romagna Grand Prix,63 16 | French Grand Prix,53 17 | German Grand Prix,67 18 | Hungarian Grand Prix,70 19 | Italian Grand Prix,53 20 | Japanese Grand Prix,53 21 | Mexico City Grand Prix,71 22 | Miami Grand Prix,57 23 | Monaco Grand Prix,78 24 | Portuguese Grand Prix,66 25 | Qatar Grand Prix,57 26 | Russian Grand Prix,53 27 | São Paulo Grand Prix,71 28 | Sakhir Grand Prix,87 29 | Saudi Arabian Grand Prix,50 30 | Singapore Grand Prix,61 31 | Spanish Grand Prix,66 32 | Styrian Grand Prix,71 33 | Turkish Grand Prix,58 34 | Tuscan Grand Prix,59 35 | United States Grand Prix,56 36 | Las Vegas Grand Prix,50 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Connor Hynes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /data/drivers.csv: -------------------------------------------------------------------------------- 1 | ,2024,2023,2022,2021,2020,2019,2018 2 | 0,1 Max Verstappen,1 Max Verstappen,16 Charles Leclerc,44 Lewis Hamilton,44 Lewis Hamilton,44 Lewis Hamilton,3 Daniel Ricciardo 3 | 1,11 Sergio Perez,11 Sergio Perez,11 Sergio Perez,33 Max Verstappen,33 Max Verstappen,77 Valtteri Bottas,77 Valtteri Bottas 4 | 2,16 Charles Leclerc,16 Charles Leclerc,63 George Russell,77 Valtteri Bottas,77 Valtteri Bottas,5 Sebastian Vettel,7 Kimi Räikkönen 5 | 3,55 Carlos Sainz,55 Carlos Sainz,44 Lewis Hamilton,11 Sergio Perez,18 Lance Stroll,33 Max Verstappen,44 Lewis Hamilton 6 | 4,44 Lewis Hamilton,44 Lewis Hamilton,4 Lando Norris,4 Lando Norris,23 Alexander Albon,16 Charles Leclerc,33 Max Verstappen 7 | 5,63 George Russell,63 George Russell,3 Daniel Ricciardo,16 Charles Leclerc,5 Sebastian Vettel,10 Pierre Gasly,27 Nico Hulkenberg 8 | 6,4 Lando Norris,10 Pierre Gasly,31 Esteban Ocon,31 Esteban Ocon,11 Sergio Perez,3 Daniel Ricciardo,14 Fernando Alonso 9 | 7,81 Oscar Piastri,31 Esteban Ocon,77 Valtteri Bottas,14 Fernando Alonso,3 Daniel Ricciardo,11 Sergio Perez,5 Sebastian Vettel 10 | 8,14 Fernando Alonso,4 Lando Norris,10 Pierre Gasly,3 Daniel Ricciardo,55 Carlos Sainz,7 Kimi Räikkönen,55 Carlos Sainz 11 | 9,18 Lance Stroll,81 Oscar Piastri,23 Alexander Albon,10 Pierre Gasly,20 Kevin Magnussen,23 Alexander Albon,20 Kevin Magnussen 12 | 10,10 Pierre Gasly,24 Zhou Guanyu,24 Zhou Guanyu,55 Carlos Sainz,16 Charles Leclerc,8 Romain Grosjean,31 Esteban Ocon 13 | 11,31 Esteban Ocon,77 Valtteri Bottas,18 Lance Stroll,99 Antonio Giovinazzi,26 Daniil Kvyat,18 Lance Stroll,11 Sergio Perez 14 | 12,2 Logan Sargeant,14 Fernando Alonso,47 Mick Schumacher,5 Sebastian Vettel,4 Lando Norris,20 Kevin Magnussen,2 Stoffel Vandoorne 15 | 13,23 Alexander Albon,18 Lance Stroll,20 Kevin Magnussen,18 Lance Stroll,7 Kimi Räikkönen,55 Carlos Sainz,18 Lance Stroll 16 | 14,3 Daniel Ricciardo,20 Kevin Magnussen,22 Yuki Tsunoda,22 Yuki Tsunoda,8 Romain Grosjean,99 Antonio Giovinazzi,35 Sergey Sirotkin 17 | 15,22 Yuki Tsunoda,27 Nico Hulkenberg,6 Nicholas Latifi,63 George Russell,99 Antonio Giovinazzi,63 George Russell,9 Marcus Ericsson 18 | 16,24 Zhou Guanyu,22 Yuki Tsunoda,14 Fernando Alonso,47 Mick Schumacher,63 George Russell,88 Robert Kubica,8 Romain Grosjean 19 | 17,77 Valtteri Bottas,21 Nyck de Vries,1 Max Verstappen,6 Nicholas Latifi,6 Nicholas Latifi,4 Lando Norris,10 Pierre Gasly 20 | 18,20 Kevin Magnussen,2 Logan Sargeant,5 Sebastian Vettel,9 Nikita Mazepin,10 Pierre Gasly,26 Daniil Kvyat,16 Charles Leclerc 21 | 19,27 Nico Hulkenberg,23 Alexander Albon,55 Carlos Sainz,7 Kimi Räikkönen,,27 Nico Hulkenberg,28 Brendon Hartley 22 | 20,,3 Daniel Ricciardo,,,,, 23 | 21,,40 Liam Lawson,,,,, 24 | -------------------------------------------------------------------------------- /data/events.csv: -------------------------------------------------------------------------------- 1 | ,2024,2023,2022,2021,2020,2019,2018 2 | 0,Bahrain Grand Prix,Bahrain Grand Prix,Bahrain Grand Prix,Bahrain Grand Prix,Austrian Grand Prix,Australian Grand Prix,Australian Grand Prix 3 | 1,Saudi Arabian Grand Prix,Saudi Arabian Grand Prix,Saudi Arabian Grand Prix,Emilia Romagna Grand Prix,Styrian Grand Prix,Bahrain Grand Prix,Bahrain Grand Prix 4 | 2,Australian Grand Prix,Australian Grand Prix,Australian Grand Prix,Portuguese Grand Prix,Hungarian Grand Prix,Chinese Grand Prix,Chinese Grand Prix 5 | 3,Japanese Grand Prix,Chinese Grand Prix,Emilia Romagna Grand Prix,Spanish Grand Prix,British Grand Prix,Azerbaijan Grand Prix,Azerbaijan Grand Prix 6 | 4,Chinese Grand Prix,Azerbaijan Grand Prix,Miami Grand Prix,Monaco Grand Prix,70th Anniversary Grand Prix,Spanish Grand Prix,Spanish Grand Prix 7 | 5,Miami Grand Prix,Miami Grand Prix,Spanish Grand Prix,Azerbaijan Grand Prix,Spanish Grand Prix,Monaco Grand Prix,Monaco Grand Prix 8 | 6,Italian Grand Prix,Emilia Romagna Grand Prix,Monaco Grand Prix,French Grand Prix,Belgian Grand Prix,Canadian Grand Prix,Canadian Grand Prix 9 | 7,Monaco Grand Prix,Monaco Grand Prix,Azerbaijan Grand Prix,Styrian Grand Prix,Italian Grand Prix,French Grand Prix,French Grand Prix 10 | 8,Canadian Grand Prix,Spanish Grand Prix,Canadian Grand Prix,Austrian Grand Prix,Tuscan Grand Prix,Austrian Grand Prix,Austrian Grand Prix 11 | 9,Spanish Grand Prix,Canadian Grand Prix,British Grand Prix,British Grand Prix,Russian Grand Prix,British Grand Prix,British Grand Prix 12 | 10,Austrian Grand Prix,Austrian Grand Prix,Austrian Grand Prix,Hungarian Grand Prix,Eifel Grand Prix,German Grand Prix,German Grand Prix 13 | 11,British Grand Prix,British Grand Prix,French Grand Prix,Belgian Grand Prix,Portuguese Grand Prix,Hungarian Grand Prix,Hungarian Grand Prix 14 | 12,Hungarian Grand Prix,Hungarian Grand Prix,Hungarian Grand Prix,Dutch Grand Prix,Emilia Romagna Grand Prix,Belgian Grand Prix,Belgian Grand Prix 15 | 13,Belgian Grand Prix,Belgian Grand Prix,Belgian Grand Prix,Italian Grand Prix,Turkish Grand Prix,Italian Grand Prix,Italian Grand Prix 16 | 14,Dutch Grand Prix,Dutch Grand Prix,Dutch Grand Prix,Russian Grand Prix,Bahrain Grand Prix,Singapore Grand Prix,Singapore Grand Prix 17 | 15,Italian Grand Prix,Italian Grand Prix,Italian Grand Prix,Turkish Grand Prix,Sakhir Grand Prix,Russian Grand Prix,Russian Grand Prix 18 | 16,Qatar Grand Prix,Singapore Grand Prix,Singapore Grand Prix,United States Grand Prix,Abu Dhabi Grand Prix,Japanese Grand Prix,Japanese Grand Prix 19 | 17,Singapore Grand Prix,Japanese Grand Prix,Japanese Grand Prix,Mexico City Grand Prix,,Mexican Grand Prix,United States Grand Prix 20 | 18,United States Grand Prix,Qatar Grand Prix,United States Grand Prix,São Paulo Grand Prix,,United States Grand Prix,Mexican Grand Prix 21 | 19,Mexico City Grand Prix,United States Grand Prix,Mexico City Grand Prix,Qatar Grand Prix,,Brazilian Grand Prix,Brazilian Grand Prix 22 | 20,São Paulo Grand Prix,Mexico City Grand Prix,São Paulo Grand Prix,Saudi Arabian Grand Prix,,Abu Dhabi Grand Prix,Abu Dhabi Grand Prix 23 | 21,Las Vegas Grand Prix,São Paulo Grand Prix,Abu Dhabi Grand Prix,Abu Dhabi Grand Prix,,, 24 | 22,Qatar Grand Prix,Las Vegas Grand Prix,,,,, 25 | 23,Abu Dhabi Grand Prix,Abu Dhabi Grand Prix,,,,, 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Formula 1 Telemetry Analysis Tool 2 | A fast, GUI based application, to gain insights into Formula 1 telemetry data. 3 | 4 | **Hamilton to Ferrari!!** 5 | 6 | Query every race and each driver for the 2018-2023 racing seasons. Built leveraging theOehrly's [FastF1](https://github.com/theOehrly/Fast-F1) python package for race data and [PyQt5](https://pypi.org/project/PyQt5/) for the GUI. Currently tested on Windows based operating systems. 7 | 8 | ![](/mkdwn/demo.gif) 9 | 10 | The goal of this project is to provide a simple way to gather driver data for each race. Formula 1 only provides some data for each race such as basic performance metrics, tire history, and lap charts. This is not detailed enough and requires the use of an account. 11 | 12 | At the end of each race, Formula 1 releases race data which FastF1 has been built to access. Having direct access to these statistics allows fully customizable methods of analyzing and comparing driver performance. Building this application with a GUI also makes it much faster to generate statistics, especially when you want to quickly switch drivers, races, and analysis types, avoiding the need to constantly edit scripts. 13 | 14 | ## Installation and Configuration 15 | - Download the latest release [here](https://github.com/hynesconnor/formula1_telemetry_tool/releases). 16 | - Using cmd and pip, navigate to the repository directory and run the following command to install necessary packages: `pip install -r requirements.txt` 17 | - Run `gui.py` to start the program. 18 | - The proper file directory will be created, and the GUI pictured above will open. You can begin running analyses. 19 | - Each time you want to launch the program, run `gui.py`. 20 | - The program makes use of a cache system, allowing it to access previously queried race data. This data can be found in the `/cache` folder and cleared if the file size becomes too large. 21 | - Compatible with python v3.7 - 3.10. 22 | 23 | ## Usage 24 | - To run an analysis, select your desired **year, grand prix location, session, driver 1, driver 2,** and finally the **type of analysis**. 25 | - Click the **'Run Analysis'** button to begin and wait for the generated plot to appear on the right of the panel. 26 | - This tool currently supports four analysis functions: **Lap Time, Fastest Lap, Fastest Sectors, and Full Telemetry**. 27 | - Once the plot is displayed, you can click the **'Save Plot to Desktop'** button to save a .png version to your desktop. 28 | - Important for getting a better view of the plot. 29 | - There is no need to reset the program after analysis. Continue to adjust parameters and generate plots as desired. 30 | 31 | ## Examples 32 | 1) A fastest lap comparison for the 2022 Austrian Grand Prix Race between Esteban Ocon and Carlos Sainz: 33 | 34 | ![](/mkdwn/fastestlap.png) 35 | 36 | 2) A lap 21 fastest sectors comparison for the 2022 Bahrain Gran Prix Race between Lewis Hamilton and Lando Norris: 37 | 38 | ![](/mkdwn/sectors3.png) 39 | 40 | 3) A full telemetry comparison of the 2022 Austrian Grand Prix Race between Charles Leclerc and Lewis Hamilton: 41 | 42 | ![](/mkdwn/fulltelem.png) 43 | 44 | ## Limitations and Issues 45 | 46 | - F1 car data is not always complete. This means that depending on the session/track/driver, data will sometimes be missing or incorrect. This could be caused by a number of things like sensor failure, crashes, irregular driving, etc. It tends to be clear where data is incorrect as graphs will generate with sharp lines or missing sections. I've found these issues to be more plentiful with Free Practice sessions. Race sessions tend to have more complete data. 47 | 48 | - When generating average fastest sectors do keep in mind that incorrect plot generation, especially on lap 1 where driving tends to be more erratic, can occur. The plot is drawn using X and Y values coming directly from sensors on the car. This means that if a driver was to go off track, an area will be drawn that is not part of the race track. The same can be seen on lap 1 in the starting area, which is not drawn. 49 | 50 | - There is currently an error with the DRS plot on the full telemetry analysis. Working on a fix for this. DRS data is supposed to be a binary but sometimes values are reported as fractional. 51 | 52 | - To make plots more readable, consider downloading to the desktop to view them at a higher DPI. Currently, the plots displayed in app suffer visually do to downscaling. Working on a fix. 53 | -------------------------------------------------------------------------------- /script.py: -------------------------------------------------------------------------------- 1 | # imports 2 | import os 3 | import matplotlib 4 | import numpy as np 5 | import pandas as pd 6 | import fastf1 as ff1 7 | from fastf1 import api 8 | from fastf1 import utils 9 | from fastf1 import plotting 10 | from matplotlib.lines import Line2D 11 | from matplotlib import pyplot as plt 12 | from matplotlib.collections import LineCollection 13 | 14 | # enables cache, allows storage of race data locally 15 | ff1.Cache.enable_cache('formula/cache') 16 | 17 | # patches matplotlib for time delta support 18 | ff1.plotting.setup_mpl(mpl_timedelta_support = True, color_scheme = 'fastf1') 19 | 20 | # gets race data from fastf1 based on input data parameter 21 | # runs appropriate plot function based on user input 22 | def get_race_data(input_data): 23 | #['2022', 'Austria', 'FP1', 'VER', 'VER', 'Lap Time'] 24 | race = ff1.get_session(int(input_data[0]), input_data[1], input_data[2]) 25 | race.load() 26 | 27 | if input_data[5] == 'Lap Time': 28 | plot_laptime(race, input_data) 29 | elif input_data[5] == 'Fastest Lap': 30 | plot_fastest_lap(race, input_data) 31 | elif input_data[5] == 'Fastest Sectors': 32 | plot_fastest_sectors(race, input_data) 33 | elif input_data[5] == 'Full Telemetry': 34 | plot_full_telemetry(race, input_data) 35 | 36 | # takes in speed/distance data for both drivers and determines which is faster 37 | # returns dataframe of which driver was the fastest in each sector 38 | def get_sectors(average_speed, input_data): 39 | sectors_combined = average_speed.groupby(['Driver', 'Minisector'])['Speed'].mean().reset_index() 40 | final = pd.DataFrame({ 41 | 'Driver': [], 42 | 'Minisector': [], 43 | 'Speed': [] 44 | }) 45 | 46 | d1 = sectors_combined.loc[sectors_combined['Driver'] == input_data[3].split()[0]] 47 | d2 = sectors_combined.loc[sectors_combined['Driver'] == input_data[4].split()[0]] 48 | 49 | for i in range(0, len(d1)): #issue, sometimes length of d1 is not 25 50 | d1_sector = d1.iloc[[i]].values.tolist() 51 | d1_speed = d1_sector[0][2] 52 | d2_sector = d2.iloc[[i]].values.tolist() 53 | d2_speed = d2_sector[0][2] 54 | if d1_speed > d2_speed: 55 | final.loc[len(final)] = d1_sector[0] 56 | else: 57 | final.loc[len(final)] = d2_sector[0] 58 | 59 | return final 60 | 61 | # plots a laptime/distance comparison for both specified drivers 62 | # returns a saved version of the generated plot 63 | def plot_laptime(race, input_data): 64 | plt.clf() 65 | d1 = input_data[3].split()[0] 66 | d2 = input_data[4].split()[0] 67 | 68 | laps_d1 = race.laps.pick_driver(d1) 69 | laps_d2 = race.laps.pick_driver(d2) 70 | 71 | color1 = ff1.plotting.driver_color(input_data[3]) 72 | color2 = ff1.plotting.driver_color(input_data[4]) 73 | 74 | fig, ax = plt.subplots() 75 | ax.plot(laps_d1['LapNumber'], laps_d1['LapTime'], color = color1, label = input_data[3]) 76 | ax.plot(laps_d2['LapNumber'], laps_d2['LapTime'], color = color2, label = input_data[4]) 77 | ax.set_xlabel('Lap Number') 78 | ax.set_ylabel('Lap Time') 79 | ax.legend() 80 | plt.suptitle(f"Lap Time Comparison \n" f"{race.event.year} {race.event['EventName']} {input_data[2]}") 81 | 82 | img_path = os.getcwd() + (f'/formula/plot/{input_data[5]}.png') 83 | plt.savefig(img_path, dpi = 200) 84 | 85 | # speed comaprison by distance for the fastest lap of both drivers 86 | # returns a saved version of the generated plot 87 | def plot_fastest_lap(race, input_data): 88 | plt.clf() 89 | d1 = input_data[3].split()[0] 90 | d2 = input_data[4].split()[0] 91 | 92 | fastest_d1 = race.laps.pick_driver(d1).pick_fastest() 93 | fastest_d2 = race.laps.pick_driver(d2).pick_fastest() 94 | 95 | tel_d1 = fastest_d1.get_car_data().add_distance() 96 | tel_d2 = fastest_d2.get_car_data().add_distance() 97 | 98 | color1 = ff1.plotting.driver_color(input_data[3]) 99 | color2 = ff1.plotting.driver_color(input_data[4]) 100 | 101 | fig, ax = plt.subplots() 102 | ax.plot(tel_d1['Distance'], tel_d1['Speed'], color = color1, label = input_data[3]) 103 | ax.plot(tel_d2['Distance'], tel_d2['Speed'], color = color2, label = input_data[4]) 104 | ax.set_xlabel('Distance (m)') 105 | ax.set_ylabel('Speed (km/h)') 106 | ax.legend() 107 | plt.suptitle(f"Fastest Lap Comparison \n" f"{race.event.year} {race.event['EventName']} {input_data[2]}") 108 | 109 | img_path = os.getcwd() + (f'/formula/plot/{input_data[5]}.png') 110 | plt.savefig(img_path, dpi = 700) 111 | 112 | 113 | # compares the sector speeds for each driver, and generates a map of the circuit, with color coded sectors for the fastest driver. 114 | # returns a saved version of the generated plot 115 | def plot_fastest_sectors(race, input_data): 116 | plt.clf() 117 | laps = race.laps 118 | drivers = [input_data[3].split()[0], input_data[4].split()[0]] 119 | telemetry = pd.DataFrame() 120 | 121 | # list of each driver 122 | for driver in drivers: 123 | driver_laps = laps.pick_driver(driver) 124 | 125 | # gets telemetry data for each driver on each lap 126 | for lap in driver_laps.iterlaps(): 127 | driver_telemtry = lap[1].get_telemetry().add_distance() 128 | driver_telemtry['Driver'] = driver 129 | driver_telemtry['Lap'] = lap[1]['LapNumber'] 130 | 131 | telemetry = telemetry.append(driver_telemtry) 132 | 133 | # keeping important columns 134 | telemetry = telemetry[['Lap', 'Distance', 'Driver', 'Speed', 'X', 'Y']] 135 | 136 | # creating minisectors 137 | total_minisectors = 25 138 | telemetry['Minisector'] = pd.cut(telemetry['Distance'], total_minisectors, labels = False) + 1 139 | 140 | average_speed = telemetry.groupby(['Lap', 'Minisector', 'Driver'])['Speed'].mean().reset_index() 141 | 142 | # calls function to returns fastest driver in each sector 143 | best_sectors = get_sectors(average_speed, input_data) 144 | best_sectors = best_sectors[['Driver', 'Minisector']].rename(columns = {'Driver': 'fastest_driver'}) 145 | 146 | # merges telemetry df with minisector df 147 | telemetry = telemetry.merge(best_sectors, on = ['Minisector']) 148 | telemetry = telemetry.sort_values(by = ['Distance']) 149 | 150 | telemetry.loc[telemetry['fastest_driver'] == input_data[3].split()[0], 'fastest_driver_int'] = 1 151 | telemetry.loc[telemetry['fastest_driver'] == input_data[4].split()[0], 'fastest_driver_int'] = 2 152 | 153 | # gets x,y data for a single lap. useful for drawing circuit. 154 | # x,y values can be inconsistent, causing strange behavior. 155 | single_lap = telemetry.loc[telemetry['Lap'] == int(input_data[6])] 156 | lap_x = np.array(single_lap['X'].values) 157 | lap_y = np.array(single_lap['Y'].values) 158 | 159 | # points and segments for drawing lap 160 | points = np.array([lap_x, lap_y]).T.reshape(-1, 1, 2) 161 | segments = np.concatenate([points[:-1], points[1:]], axis=1) 162 | 163 | # grabs which driver (1/2) is fastest # POTENTIAL PROBLEM, ENSURE THIS IS BEST SECTOR DATA 164 | which_driver = single_lap['fastest_driver_int'].to_numpy().astype(float) 165 | 166 | # getting colormap for two drivers 167 | color1 = ff1.plotting.driver_color(input_data[3]) 168 | color2 = ff1.plotting.driver_color(input_data[4]) 169 | color1 = matplotlib.colors.to_rgb(color1) 170 | color2 = matplotlib.colors.to_rgb(color2) 171 | colors = [color1, color2] 172 | cmap = matplotlib.colors.ListedColormap(colors) 173 | 174 | lc_comp = LineCollection(segments, norm = plt.Normalize(1, cmap.N), cmap = cmap) 175 | lc_comp.set_array(which_driver) 176 | lc_comp.set_linewidth(2) 177 | 178 | plt.rcParams['figure.figsize'] = [6.25, 4.70] 179 | plt.suptitle(f"Average Fastest Sectors Lap {input_data[6]}\n" f"{race.event.year} {race.event['EventName']} {input_data[2]}") #edit 180 | plt.gca().add_collection(lc_comp) 181 | plt.axis('equal') 182 | plt.tick_params(labelleft=False, left=False, labelbottom=False, bottom=False) 183 | 184 | legend_lines = [Line2D([0], [0], color = color1, lw = 1), 185 | Line2D([0], [0], color = color2, lw = 1)] 186 | 187 | plt.legend(legend_lines, [input_data[3], input_data[4]]) 188 | 189 | img_path = os.getcwd() + (f'/formula/plot/{input_data[5]}.png') 190 | plt.savefig(img_path, dpi = 200) 191 | 192 | # plots a speed, throttle, brake, rpm, gear, and drs comparison for both drivers 193 | # returns a saved version of the generated plot 194 | def plot_full_telemetry(race, input_data): # speed, throttle, brake, rpm, gear, drs 195 | plt.clf() 196 | d1 = input_data[3].split()[0] 197 | d2 = input_data[4].split()[0] 198 | 199 | fastest_d1 = race.laps.pick_driver(d1).pick_fastest() 200 | fastest_d2 = race.laps.pick_driver(d2).pick_fastest() 201 | 202 | tel_d1 = fastest_d1.get_car_data().add_distance() 203 | tel_d1['Brake'] = tel_d1['Brake'].astype(int) 204 | tel_d2 = fastest_d2.get_car_data().add_distance() 205 | tel_d2['Brake'] = tel_d2['Brake'].astype(int) 206 | 207 | delta_time, ref_tel, compare_tel = utils.delta_time(fastest_d1, fastest_d2) 208 | 209 | telem_data_combined = [tel_d1, tel_d2] 210 | colors = [ff1.plotting.driver_color(input_data[3]), ff1.plotting.driver_color(input_data[4])] 211 | 212 | fig, ax = plt.subplots(6) 213 | for telem, color in zip(telem_data_combined, colors): 214 | ax[0].axhline(0, color = 'White', linewidth = .50) 215 | ax[0].plot(ref_tel['Distance'], delta_time, color = color, linewidth = .75) 216 | ax[1].plot(telem['Distance'], telem['Speed'], color = color, linewidth = .75) 217 | ax[2].plot(telem['Distance'], telem['Throttle'], color = color, linewidth = .75) 218 | ax[3].plot(telem['Distance'], telem['Brake'], color = color, linewidth = .75) # might have to convert to binary 219 | ax[4].plot(telem['Distance'], telem['RPM'], color = color, linewidth = .75) 220 | ax[5].plot(telem['Distance'], telem['nGear'], color = color, linewidth = .75) 221 | 222 | ax[0].set(ylabel = 'Delta (s)') 223 | ax[1].set(ylabel = 'Speed') 224 | ax[2].set(ylabel = 'Throttle') 225 | ax[3].set(ylabel = 'Brake') 226 | ax[4].set(ylabel = 'RPM') 227 | ax[5].set(ylabel = 'Gear') 228 | 229 | plt.suptitle(f"Fastest Lap Telemetry - {input_data[3]} vs {input_data[4]} \n {race.event.year} {race.event['EventName']} {input_data[2]}") 230 | 231 | legend_lines = [Line2D([0], [0], color = colors[0], lw = 1), 232 | Line2D([0], [0], color = colors[1], lw = 1)] 233 | 234 | ax[0].legend(legend_lines, [input_data[3], input_data[4]], loc = 'lower right', prop={'size': 5}) 235 | 236 | img_path = os.getcwd() + (f'/formula/plot/{input_data[5]}.png') 237 | plt.savefig(img_path, dpi = 200) 238 | -------------------------------------------------------------------------------- /gui.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import shutil 4 | import threading 5 | import pandas as pd 6 | from random import randint 7 | from PyQt5 import QtGui 8 | from PyQt5.QtCore import QTimer 9 | from PyQt5.QtGui import QPixmap 10 | from PyQt5.QtWidgets import QComboBox, QApplication, QWidget, QVBoxLayout, QHBoxLayout,QLabel, QPushButton, QProgressBar, QMessageBox 11 | 12 | # create directories 13 | CWD = os.getcwd() 14 | if not os.path.exists(CWD + '/cache'): 15 | cur_dir = CWD + '/' 16 | main_dirs = ['cache', 'plot'] 17 | for i in main_dirs: 18 | os.makedirs(cur_dir + i) 19 | 20 | # imports script.py, used for creating plots 21 | import script 22 | 23 | # paths for race data 24 | events = pd.read_csv(CWD + '/formula/data/events.csv') 25 | drivers = pd.read_csv(CWD + '/formula/data/drivers.csv') 26 | race_laps = pd.read_csv(CWD + '/formula/data/laps.csv') 27 | placeholder_path = CWD + '/formula/img/placeholder.png' 28 | 29 | # active race years 30 | year = events.columns 31 | year = year[1:len(year)].to_list() 32 | year.insert(0, 'Select Year') 33 | 34 | # values for dropdown lables 35 | driver_name = drivers 36 | location = ['Select Location'] 37 | session = ['Race', 'Qualifying', 'FP1', 'FP2', 'FP3']# 'Sprint Qualifying', 'Sprint' : removed until data is consistent 38 | driver_name = ['Select Driver'] 39 | analysis_type = ['Lap Time', 'Fastest Lap', 'Fastest Sectors', 'Full Telemetry'] 40 | 41 | # stylesheet for progress bar 42 | StyleSheet = ''' 43 | #RedProgressBar { 44 | min-height: 12px; 45 | max-height: 12px; 46 | border-radius: 2px; 47 | border: .5px solid #808080;; 48 | } 49 | #RedProgressBar::chunk { 50 | border-radius: 2px; 51 | background-color: #DC0000; 52 | opacity: 1; 53 | } 54 | .warning-text { 55 | color:#DC0000 56 | } 57 | ''' 58 | 59 | # defines progressbar 60 | class ProgressBar(QProgressBar): 61 | def __init__(self, *args, **kwargs): 62 | super(ProgressBar, self).__init__(*args, **kwargs) 63 | self.setValue(0) 64 | if self.minimum() != self.maximum(): 65 | self.timer = QTimer(self, timeout=self.onTimeout) 66 | self.timer.start(randint(1, 3) * 1000) 67 | 68 | # timeout isn't currently necessary, but useful if behavior needs to change (if you want bar to appear for specific time) 69 | def onTimeout(self): 70 | if self.value() >= 100: 71 | self.timer.stop() 72 | self.timer.deleteLater() 73 | del self.timer 74 | return 75 | self.setValue(self.value() + 1) 76 | 77 | # main gui window 78 | class MainWindow(QWidget): 79 | def __init__(self): 80 | super().__init__() 81 | self.initUI() 82 | self.UIComponents() 83 | 84 | # initialize main window 85 | def initUI(self): 86 | self.setFixedSize(880, 525) 87 | self.move(200, 100) 88 | self.setWindowTitle('Formula 1 Telemetry Analytics') 89 | self.setWindowIcon(QtGui.QIcon(CWD + '/formula/img/f1.png')) 90 | 91 | # creates and places all window compenenets, including listeners 92 | def UIComponents(self): 93 | options_layout = QVBoxLayout() 94 | img_layout = QHBoxLayout() 95 | img_layout.addLayout(options_layout) # two layouts to allow split screen view 96 | 97 | self.drop_year = QComboBox() 98 | self.drop_grand_prix = QComboBox() 99 | self.drop_session = QComboBox() 100 | self.drop_driver1 = QComboBox() 101 | self.drop_driver2 = QComboBox() 102 | self.drop_analysis = QComboBox() 103 | self.lap_number = QComboBox() 104 | 105 | # not working properly 106 | self.warning_box = QMessageBox(self) 107 | self.warning_box.setWindowTitle('Error!') 108 | self.warning_box.setText('Select a valid race year.') 109 | self.warning_box.setDefaultButton(QMessageBox.Ok) 110 | 111 | label_year = QLabel(' Year: ') 112 | label_prix = QLabel(' Grand Prix Location: ') 113 | label_session = QLabel(' Session: ') 114 | label_d1 = QLabel(' Driver 1: ') 115 | label_d2 = QLabel(' Driver 2: ') 116 | label_analysis = QLabel(' Analysis Type: ') 117 | 118 | self.run_button = QPushButton('Run Analysis') 119 | self.save_button = QPushButton('Save Plot to Desktop') 120 | 121 | self.pbar = ProgressBar(self, minimum=0, maximum=0, textVisible=False, 122 | objectName="RedProgressBar") 123 | 124 | self.drop_year.addItems(year) 125 | self.drop_grand_prix.addItems(location) 126 | self.drop_session.addItems(session) 127 | self.drop_driver1.addItems(driver_name) 128 | self.drop_driver2.addItems(driver_name) 129 | self.drop_analysis.addItems(analysis_type) 130 | 131 | options_layout.addWidget(label_year) 132 | options_layout.addWidget(self.drop_year) 133 | options_layout.addWidget(label_prix) 134 | options_layout.addWidget(self.drop_grand_prix) 135 | options_layout.addWidget(label_session) 136 | options_layout.addWidget(self.drop_session) 137 | options_layout.addWidget(label_d1) 138 | options_layout.addWidget(self.drop_driver1) 139 | options_layout.addWidget(label_d2) 140 | options_layout.addWidget(self.drop_driver2) 141 | options_layout.addWidget(label_analysis) 142 | options_layout.addWidget(self.drop_analysis) 143 | options_layout.addWidget(self.lap_number) 144 | self.lap_number.hide() 145 | options_layout.addWidget(self.pbar) 146 | self.pbar.hide() 147 | options_layout.addWidget(self.run_button) 148 | options_layout.addWidget(self.save_button) 149 | self.save_button.hide() 150 | 151 | options_layout.addStretch() # compacts all widgets 152 | 153 | self.drop_year.currentTextChanged.connect(self.update_lists) # listens for change in year 154 | self.run_button.clicked.connect(self.thread_script) # listens for run analysis button press 155 | self.save_button.clicked.connect(self.save_plot) # listens for save button press 156 | self.drop_analysis.currentTextChanged.connect(self.add_laps) # listens to add lap selection 157 | self.drop_grand_prix.currentTextChanged.connect(self.update_laps) # updates lap number based on grand prix selection 158 | 159 | self.img_plot = QLabel() 160 | self.img_plot.setPixmap(QPixmap(placeholder_path).scaledToWidth(625)) # could increase scale to improve readability of high dpi images 161 | img_layout.addWidget(self.img_plot) 162 | 163 | self.setLayout(img_layout) 164 | 165 | # returns list of user selections (year, location, driver 1, driver 2, analysis type) 166 | def current_text(self): 167 | input_data = [] 168 | text = self.drop_year.currentText() 169 | input_data.append(text) 170 | text = self.drop_grand_prix.currentText() 171 | input_data.append(text) 172 | text = self.drop_session.currentText() 173 | input_data.append(text) 174 | text = self.drop_driver1.currentText() 175 | input_data.append(text) 176 | text = self.drop_driver2.currentText() 177 | input_data.append(text) 178 | text = self.drop_analysis.currentText() 179 | input_data.append(text) 180 | text = self.lap_number.currentText() 181 | input_data.append(text) 182 | return input_data 183 | 184 | # displays requested analysis plot, returned from script.py 185 | def display_plot(self, plot_path): 186 | self.img_plot.setPixmap(QPixmap(plot_path).scaledToWidth(625)) 187 | 188 | # saves currently displayed plot to user's desktop as .png 189 | def save_plot(self): 190 | desktop_path = os.path.join(os.path.join(os.environ['USERPROFILE']), 'Desktop') # may need to adjust for mac 191 | shutil.copy(self.plot_path, desktop_path) 192 | 193 | # adds option to select the lap number for sector analysis 194 | def add_laps(self): 195 | if self.drop_analysis.currentText() == 'Fastest Sectors': 196 | self.lap_number.show() 197 | else: 198 | self.lap_number.hide() 199 | 200 | # updates total number of laps depending on grand prix selection 201 | def update_laps(self): 202 | if self.drop_grand_prix.currentText() != '': 203 | self.lap_number.clear() 204 | lap_val = ['Select Lap'] 205 | race = self.drop_grand_prix.currentText() 206 | total_laps = race_laps.loc[race_laps.event == race, 'laps'].values[0] 207 | lap_val.extend(range(1, total_laps + 1)) 208 | lap_val = map(str, lap_val) 209 | self.lap_number.addItems(lap_val) 210 | 211 | # starts new thread for script.py operation so gui.py does not freeze 212 | def thread_script(self): 213 | thread_script = threading.Thread(target = self.button_listen) 214 | thread_script.start() 215 | 216 | # activates on analysis button press, gets input_data, runs script.py, adjusts gui. checks if year value is valid 217 | def button_listen(self): 218 | input_data = self.current_text() 219 | if input_data[0] == 'Select Year': 220 | self.run_button.setText('Run Analysis (Select Valid Year)') 221 | else: 222 | self.run_button.setText('Running . . .') 223 | self.save_button.hide() 224 | self.pbar.show() 225 | script.get_race_data(input_data) 226 | self.plot_path = os.getcwd() + (f'/formula/plot/{input_data[5]}.png') 227 | self.display_plot(self.plot_path) 228 | self.pbar.hide() 229 | self.run_button.setText('Run New Analysis') 230 | self.save_button.show() 231 | 232 | # updates all selection lists based on the year selected. drivers/race locations change each year 233 | def update_lists(self): 234 | sel_year = self.drop_year.currentText() 235 | if sel_year != 'Select Year': 236 | self.drop_grand_prix.clear() 237 | self.drop_driver1.clear() 238 | self.drop_driver2.clear() 239 | self.drop_grand_prix.addItems(events[str(sel_year)].dropna().to_list()) 240 | self.drop_driver1.addItems(drivers[str(sel_year)].dropna().to_list()) 241 | self.drop_driver2.addItems(drivers[str(sel_year)].dropna().to_list()) 242 | 243 | if __name__ == '__main__': 244 | app = QApplication(sys.argv) 245 | app.setStyleSheet(StyleSheet) 246 | mw = MainWindow() 247 | mw.show() 248 | sys.exit(app.exec_()) 249 | --------------------------------------------------------------------------------