Jovian
Sign In

70 Years of Formula 1 - An Exploratory Data Analysis

Formula 1, the pinnacle of motorsports, is celebrating its 70th anniversary this year. From the British Grand Prix in 1950, this sport has not just enthralled racing fans around the world but also has been a platform for innovations and cutting edge technological developments. Being an F1 fan myself, I thought I would do my first data analysis project on the 70-year history of the sport! Thanks to Ergast Developer API and their well-kept database images for making this happen. Here, we will be using their data up to the 2020 Russian GP held on 27th September. This project is a part of the course Data Analysis with Python: Zero to Pandas hosted by Jovian. I will be using Numpy and Pandas libraries in Python to do some primary data exploration on the driver and constructor records and a few other aspects of F1. Matplotlib and Seaborn libraries are used for plotting and visualisations.

How to run the code

This is an executable Jupyter notebook hosted on Jovian.ml, a platform for sharing data science projects. You can run and experiment with the code in a couple of ways: using free online resources (recommended) or on your own computer.

Option 1: Running using free online resources (1-click, recommended)

The easiest way to start executing this notebook is to click the "Run" button at the top of this page, and select "Run on Binder". This will run the notebook on mybinder.org, a free online service for running Jupyter notebooks. You can also select "Run on Colab" or "Run on Kaggle".

Option 2: Running on your computer locally
  1. Install Conda by following these instructions. Add Conda binaries to your system PATH, so you can use the conda command on your terminal.

  2. Create a Conda environment and install the required libraries by running these commands on the terminal:

conda create -n zerotopandas -y python=3.8 
conda activate zerotopandas
pip install jovian jupyter numpy pandas matplotlib seaborn opendatasets --upgrade
  1. Press the "Clone" button above to copy the command for downloading the notebook, and run it on the terminal. This will create a new directory and download the notebook. The command will look something like this:
jovian clone notebook-owner/notebook-id
  1. Enter the newly created directory using cd directory-name and start the Jupyter notebook.
jupyter notebook

You can now access Jupyter's web interface by clicking the link that shows up on the terminal or by visiting http://localhost:8888 on your browser. Click on the notebook file (it has a .ipynb extension) to open it.

Importing Libraries

We will first import the Python libraries required in this notebook.

In [1]:
# Importing python libraries
from urllib.request import urlretrieve
import os
import pandas as pd
import numpy as np
In [2]:
# Importing plotting and visualisation libraries
import matplotlib.pyplot as plt
import matplotlib
import seaborn as sns
%matplotlib inline

sns.set_style('darkgrid')
matplotlib.rcParams['font.size'] = 14
matplotlib.rcParams['figure.figsize'] = (9, 5)
matplotlib.rcParams['figure.facecolor'] = '#00000000'

Downloading the Dataset

For easy access to the CSV files and its schema from Ergast (updated till 27/09/2020), they are hosted as repositories in GitHub. The following variables give the paths to each of the files.

In [3]:
# URLs to fetch data
url_circuits = 'https://gist.githubusercontent.com/harikanth-m/f15b47aeab0d75c128824d0f72821db1/raw/circuits.csv'
url_const_results = 'https://gist.githubusercontent.com/harikanth-m/f41f068dca8a309c81a04c6a45fecf58/raw/constructor_results.csv'
url_const_standings = 'https://gist.githubusercontent.com/harikanth-m/ccf0bd917f013d34928ae8c9b695059c/raw/constructor_standings.csv'
url_const = 'https://gist.githubusercontent.com/harikanth-m/f128413f4bff5c3c901b9a8c599a78b9/raw/constructors.csv'
url_driver_standings = 'https://gist.githubusercontent.com/harikanth-m/fcdb3e6650972ee6f0db07288074b0b7/raw/driver_standings.csv'
url_drivers = 'https://gist.githubusercontent.com/harikanth-m/d37be6c8c871300822bf8676e2206d9b/raw/drivers.csv'
url_lap_times = 'https://gist.githubusercontent.com/harikanth-m/61770032a57dd013651163142589b2bc/raw/lap_times.csv'
url_pit_stops = 'https://gist.githubusercontent.com/harikanth-m/45d94ae5a8b8e7f0cace662cc7942d04/raw/pit_stops.csv'
url_qualifying = 'https://gist.githubusercontent.com/harikanth-m/08f55f6687874c5a0bf01790c477e4bc/raw/qualifying.csv'
url_races = 'https://gist.githubusercontent.com/harikanth-m/9382cffceb1264e246fae76e4989cc31/raw/races.csv'
url_results = 'https://gist.githubusercontent.com/harikanth-m/6b1b6e36d3709abf3b97fc703303471b/raw/results.csv'
url_status = 'https://gist.githubusercontent.com/harikanth-m/c1bb17485523243929c57f4f13d4fcba/raw/status.csv'
url_schema = 'https://gist.githubusercontent.com/harikanth-m/71f7fd19c8afba09ad99664ece517268/raw/f1db_schema.txt'

Let's make a new directory to download the data files.

In [4]:
# Creating a new directory for the data files
os.makedirs('./data', exist_ok=True)

We will now download the files to this directory using urlretrieve function.

In [5]:
# Downloading the csv files
urlretrieve(url_circuits, './data/circuits.csv')
urlretrieve(url_const_results, './data/constructor_results.csv')
urlretrieve(url_const_standings, './data/constructor_standings.csv')
urlretrieve(url_const, './data/constructors.csv')
urlretrieve(url_driver_standings,'./data/driver_standings.csv')
urlretrieve(url_drivers, './data/drivers.csv')
urlretrieve(url_lap_times, './data/lap_times.csv')
urlretrieve(url_pit_stops, './data/pit_stops.csv')
urlretrieve(url_qualifying, './data/qualifying.csv')
urlretrieve(url_races, './data/races.csv')
urlretrieve(url_results, './data/results.csv')
urlretrieve(url_status, './data/status.csv')
urlretrieve(url_schema, './data/f1db_schema.txt');

The following code checks if the files have been to the directory.

In [6]:
os.listdir('data')
Out[6]:
['constructor_results.csv',
 'drivers.csv',
 'constructors.csv',
 'status.csv',
 'lap_times.csv',
 'qualifying.csv',
 'f1db_schema.txt',
 'driver_standings.csv',
 'races.csv',
 'pit_stops.csv',
 'circuits.csv',
 'results.csv',
 'constructor_standings.csv']

We can look at the schema text file to get information about the database and the contents of each of the CSV files.

In [7]:
schema = open('./data/f1db_schema.txt', 'r')
print(schema.read())
+----------------------+ | Tables_in_f1db | +----------------------+ | circuits | | constructorResults | | constructorStandings | | constructors | | driverStandings | | drivers | | lapTimes | | pitStops | | qualifying | | races | | results | | seasons | | status | +----------------------+ circuits.csv +------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | circuitId | int(11) | NO | PRI | NULL | auto_increment | | circuitRef | varchar(255) | NO | | | | | name | varchar(255) | NO | | | | | location | varchar(255) | YES | | NULL | | | country | varchar(255) | YES | | NULL | | | lat | float | YES | | NULL | | | lng | float | YES | | NULL | | | alt | int(11) | YES | | NULL | | | url | varchar(255) | NO | UNI | | | +------------+--------------+------+-----+---------+----------------+ constructor_results.csv +----------------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------------------+--------------+------+-----+---------+----------------+ | constructorResultsId | int(11) | NO | PRI | NULL | auto_increment | | raceId | int(11) | NO | | 0 | | | constructorId | int(11) | NO | | 0 | | | points | float | YES | | NULL | | | status | varchar(255) | YES | | NULL | | +----------------------+--------------+------+-----+---------+----------------+ constructor_standings.csv +------------------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------------------+--------------+------+-----+---------+----------------+ | constructorStandingsId | int(11) | NO | PRI | NULL | auto_increment | | raceId | int(11) | NO | | 0 | | | constructorId | int(11) | NO | | 0 | | | points | float | NO | | 0 | | | position | int(11) | YES | | NULL | | | positionText | varchar(255) | YES | | NULL | | | wins | int(11) | NO | | 0 | | +------------------------+--------------+------+-----+---------+----------------+ constructors.csv +----------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------------+--------------+------+-----+---------+----------------+ | constructorId | int(11) | NO | PRI | NULL | auto_increment | | constructorRef | varchar(255) | NO | | | | | name | varchar(255) | NO | UNI | | | | nationality | varchar(255) | YES | | NULL | | | url | varchar(255) | NO | | | | +----------------+--------------+------+-----+---------+----------------+ driver_standings.csv +-------------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------------+--------------+------+-----+---------+----------------+ | driverStandingsId | int(11) | NO | PRI | NULL | auto_increment | | raceId | int(11) | NO | | 0 | | | driverId | int(11) | NO | | 0 | | | points | float | NO | | 0 | | | position | int(11) | YES | | NULL | | | positionText | varchar(255) | YES | | NULL | | | wins | int(11) | NO | | 0 | | +-------------------+--------------+------+-----+---------+----------------+ drivers.csv +-------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------+--------------+------+-----+---------+----------------+ | driverId | int(11) | NO | PRI | NULL | auto_increment | | driverRef | varchar(255) | NO | | | | | number | int(11) | YES | | NULL | | | code | varchar(3) | YES | | NULL | | | forename | varchar(255) | NO | | | | | surname | varchar(255) | NO | | | | | dob | date | YES | | NULL | | | nationality | varchar(255) | YES | | NULL | | | url | varchar(255) | NO | UNI | | | +-------------+--------------+------+-----+---------+----------------+ lap_times.csv +--------------+--------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +--------------+--------------+------+-----+---------+-------+ | raceId | int(11) | NO | PRI | NULL | | | driverId | int(11) | NO | PRI | NULL | | | lap | int(11) | NO | PRI | NULL | | | position | int(11) | YES | | NULL | | | time | varchar(255) | YES | | NULL | | | milliseconds | int(11) | YES | | NULL | | +--------------+--------------+------+-----+---------+-------+ pit_stops.csv +--------------+--------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +--------------+--------------+------+-----+---------+-------+ | raceId | int(11) | NO | PRI | NULL | | | driverId | int(11) | NO | PRI | NULL | | | stop | int(11) | NO | PRI | NULL | | | lap | int(11) | NO | | NULL | | | time | time | NO | | NULL | | | duration | varchar(255) | YES | | NULL | | | milliseconds | int(11) | YES | | NULL | | +--------------+--------------+------+-----+---------+-------+ qualifying.csv +---------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------------+--------------+------+-----+---------+----------------+ | qualifyId | int(11) | NO | PRI | NULL | auto_increment | | raceId | int(11) | NO | | 0 | | | driverId | int(11) | NO | | 0 | | | constructorId | int(11) | NO | | 0 | | | number | int(11) | NO | | 0 | | | position | int(11) | YES | | NULL | | | q1 | varchar(255) | YES | | NULL | | | q2 | varchar(255) | YES | | NULL | | | q3 | varchar(255) | YES | | NULL | | +---------------+--------------+------+-----+---------+----------------+ races.csv +-----------+--------------+------+-----+------------+----------------+ | Field | Type | Null | Key | Default | Extra | +-----------+--------------+------+-----+------------+----------------+ | raceId | int(11) | NO | PRI | NULL | auto_increment | | year | int(11) | NO | | 0 | | | round | int(11) | NO | | 0 | | | circuitId | int(11) | NO | | 0 | | | name | varchar(255) | NO | | | | | date | date | NO | | 0000-00-00 | | | time | time | YES | | NULL | | | url | varchar(255) | YES | UNI | NULL | | +-----------+--------------+------+-----+------------+----------------+ results.csv +-----------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-----------------+--------------+------+-----+---------+----------------+ | resultId | int(11) | NO | PRI | NULL | auto_increment | | raceId | int(11) | NO | | 0 | | | driverId | int(11) | NO | | 0 | | | constructorId | int(11) | NO | | 0 | | | number | int(11) | YES | | NULL | | | grid | int(11) | NO | | 0 | | | position | int(11) | YES | | NULL | | | positionText | varchar(255) | NO | | | | | positionOrder | int(11) | NO | | 0 | | | points | float | NO | | 0 | | | laps | int(11) | NO | | 0 | | | time | varchar(255) | YES | | NULL | | | milliseconds | int(11) | YES | | NULL | | | fastestLap | int(11) | YES | | NULL | | | rank | int(11) | YES | | 0 | | | fastestLapTime | varchar(255) | YES | | NULL | | | fastestLapSpeed | varchar(255) | YES | | NULL | | | statusId | int(11) | NO | | 0 | | +-----------------+--------------+------+-----+---------+----------------+ seasons.csv +-------+--------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------+--------------+------+-----+---------+-------+ | year | int(11) | NO | PRI | 0 | | | url | varchar(255) | NO | UNI | | | +-------+--------------+------+-----+---------+-------+ status.csv +----------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------+--------------+------+-----+---------+----------------+ | statusId | int(11) | NO | PRI | NULL | auto_increment | | status | varchar(255) | NO | | | | +----------+--------------+------+-----+---------+----------------+

There are 13 files in the dataset, and various Ids connect them. We can use these Ids to join files to get data frames for our needs. The data seems pretty well organised. There is also an active community reporting bugs in the dataset. Hence, we will skip data cleaning for the time being. Necessary processing will be done on course. Now, let's dive right in!

Races

Let us start by looking at the Formula 1 races so far. For this, we will load the races.csv file as a data frame.

In [8]:
races_raw_df = pd.read_csv('data/races.csv')
races_raw_df
Out[8]:

The beginning few rows are from the 2009 season. Thus, the data is not in chronological order. Since the date column has type date (see schema), we can sort it as follows.

In [9]:
races_raw_df = races_raw_df.sort_values('date')
races_raw_df
Out[9]:

The file gives data for all the races held so far, but also has info on upcoming races scheduled for this year's championship. Let's remove those rows as these races are yet to happen. The last completed race is the Russian GP held on 27th September 2020. We will delete the future races using the date column again.

In [10]:
races_df = races_raw_df[races_raw_df.date <= '2020-09-27']
Question: How many chequered flags have been waved in Formula 1 so far?

Let's see how many F1 championship races have been completed until the Russian GP.

In [11]:
races_completed = len(races_df)
print('The number of chequered flags waved in F1 so far (until 2020 Russian GP) is {}.'.format(races_completed))
The number of chequered flags waved in F1 so far (until 2020 Russian GP) is 1028.

Now, let us visualise the trend in the number of championship races held per season calendar over the 70 years.

In [12]:
idx = races_df.groupby(['year'])['date'].transform(max) == races_df['date'] # Finds if the race index is last of the season
season_finale = races_df[idx].rename(columns = {'round' : 'tot_races'}) 
season_finale = season_finale[season_finale.year != 2020] # A new data frame with season-ending races only, excluding the 2020 season

# Plotting
plt.figure(figsize=(16, 6))

plt.plot(season_finale.year, season_finale.tot_races, 's-b')
plt.xlabel('Year')
plt.ylabel('Number of races')
plt.title("Championship races per seasons");

There were only seven races for the initial 1950 Formula1 season. Compare that to 21 championship races last year! Although most races happen in Europe, Formula 1 races in all other continents except Africa and Antarctica. Travelling is a heavy burden on both the drivers and the teams. Need not mention the amount of cargo they transport all around the world.

Circuits

Let us look at the circuits around the world that have hosted Formula 1 races. For this, we will load the circuits.csv file as a data frame.

In [13]:
circuits_raw_df = pd.read_csv('data/circuits.csv')
circuits_raw_df
Out[13]:

If you look at the data, you can find names of some circuits that haven't hosted any races yet. For example, Hanoi in Vietnam was supposed to hold a race this season. But due to the pandemic, like many other races, it had to be cancelled. To find the actual number of tracks to have hosted an F1 race, we will merge the above data frame with the one containing data of all the races.

In [14]:
host_circuits =  races_df.drop(['date', 'time', 'url', 'round'], axis = 1).rename(columns = {'name':'gp_name'}).merge(
    circuits_raw_df.drop(['lat', 'lng', 'alt', 'url'], 1).rename(columns = {'name':'cir_name'}), how = 'left')
Question: How many circuits have hosted a Formula 1 race on their tarmac?
In [15]:
tot_host_cir = host_circuits['circuitId'].nunique()
print('{} different circuits around the world have hosted a Formula 1 race on their tarmac.'.format(tot_host_cir))
73 different circuits around the world have hosted a Formula 1 race on their tarmac.

So far 73 circuits have played venues for F1 races. Autodromo Internazionale del Mugello is the latest addition to this list after it hosted the penultimate Grand Prix so far, the 2020 Tuscan GP. We will now look at the top 10 circuits to have hosted the most number of GPs.

In [16]:
# Sorting top 10 after taking count of occurrences
top_hosts = host_circuits[['cir_name']].cir_name.value_counts().reset_index().rename(
           columns={'index': 'name', 'cir_name': 'counts'}).head(10)

# Plotting
plt.figure(figsize = (12,6))

sns.barplot(top_hosts.counts, top_hosts.name)
plt.title('Top 10 F1 host circuits')
plt.xlabel('Number of races hosted')
plt.ylabel('');

With 70 races, Monza has hosted more GPs than any other circuit. This high-speed Italian racetrack has been a part of every Formula1 season, except 1980. In the second spot, we have the legendary Circuit de Monaco laid out on the streets of Monte Carlo. Apart from racing, it is famous for glitz and glamour. Silverstone and Spa are other iconic circuits that can boast more than 50 races. All these historic tacks still feature in modern F1 calendars.

We can now look at the countries to have hosted Formula 1 over the years.

In [17]:
host_countries = host_circuits[['raceId', 'year', 'country']].groupby(
    ['year','country']).size().reset_index().rename(columns = {0:'Races'})

# Plotting
plt.figure(figsize = (15,10))

sns.scatterplot(data = host_countries, x = 'year', y = 'country', s = 100, hue = 'Races', palette="deep")
plt.title('F1 host countries')
plt.xlabel('Year')
plt.ylabel('');

Formula 1 has deep roots in the European soil, and it shows. All the countries to have consistently hosted F1 races are from Europe. Italy and the UK have been part of every Formula 1 season yet. Other top host nations are Belgium and Germany. The USA is the country outside the European continent with most races. F1 has looked for newer grounds ever since its inception. From the 1970s and 1980s, countries like Canada, Brazil, Japan, Australia and Hungary are frequently visited. Some of the regular modern F1 circuits are in Bahrain, China, Singapore and UAE. Countries like Italy, Germany and the USA have hosted on multiple circuits in the same calendar year. The United States hosted three Grands Prix in 1982, one each in Long Beach, Detroit and Las Vegas. In 2020, when the organisers had to reschedule the season due to the pandemic, Silverstone in the UK and Red Bull Ring in Austria hosted two races each. Bahrain is also set to do the same, but the races will be in different layouts.

Constructors

Being a technology-driven sport, constructors have a massive role in F1. A driver can only be as good as the car underneath him. F1 also runs a championship for the constructors along with the drivers' championship. Let's look at all the players that have been in this business.

In [18]:
constructors_raw = pd.read_csv('data/constructors.csv')
constructors_raw
Out[18]:

There have been 211 official entrants into Formula 1 until 2020. Technically, this list should be considered as entrants/teams and not as constructors (read more here). Teams generally change their entry names for rebranding purposes etc. For example, Torro Rosso was rebranded to AlphaTauri in 2020.

Let us count the number of race entries for each team. We will have to make use of the results.csv file for this.

In [19]:
results_raw = pd.read_csv('data/results.csv')
results_raw
Out[19]:

We will take only the relevant columns and merge it with the constructors' data frame to create a new one for the specific purpose.

In [20]:
const_inter_df1 = results_raw.groupby(['constructorId', 'raceId']).size() # Group based on constructors and race

const_inter_df2 = const_inter_df1.groupby('constructorId').count().reset_index().rename(columns = {0:'races_ent'}) # Count number of races entered by constructors

const_races_entered = const_inter_df2.merge(constructors_raw, on = 'constructorId')
const_races_entered
Out[20]:

Let's visualise the top 10 teams to have entered the most number of Grands Prix in Formula 1.

In [21]:
const_races_entered_top = const_races_entered.sort_values('races_ent',ascending = False).head(10)

# Plotting
plt.figure(figsize = (14,6))

sns.barplot(const_races_entered_top.races_ent, const_races_entered_top.name)
plt.title('Top 10 F1 constructor enteries')
plt.xlabel('Number of races entered')
plt.ylabel('');

Ferrari has entered the most number of races in Formula 1 than any other team. It was, in fact, Ferrari's 1000th race start (different from entry, see here) at the 2020 Tuscan Grand Prix. Ferrari is the only team to have participated in every season from the inception of Formula 1. McLaren is the second most old and experienced in the present lineup, followed by Williams. The only other team in this list competing in the current season is Renault. There is a discrepancy in the number of teams and their race entries when compared to the data given here. The websites possibly do not distinguish between the official entry names and the 'actual' team. For example, McLaren has five entry names (McLaren, McLaren-Ford, McLaren-Serenissima, McLaren-BRM and McLaren-Alfa Romeo) in our data, but f1-fansite.com probably consider all as one. There is a discrepancy in data between different websites too. Take a look at the Williams race start numbers shown in these sites:

Let's move on and see where all these teams are/were licensed.

In [22]:
const_country = const_races_entered[['constructorId', 'nationality']].groupby(
    'nationality').count().rename(columns = {'constructorId' : 'counts'}) # Grouping based on nationality

const_country = const_country[const_country.counts >= 10].sort_values('counts', ascending = False)
const_country.loc['Others'] = [(len(const_races_entered) - const_country.counts.sum())] # Consolidating countries with less than 10 teams into 'Others'

# Plotting pie chart
plt.figure(figsize=(12,6))
explode = np.append(np.zeros(5),0.1)  # only "explode" the Others slice
plt.axis('equal')

plt.title('Constructor Countries')
plt.pie(const_country.counts, explode=explode, labels=const_country.index, autopct='%1.1f%%',
        shadow=True, startangle=270);

It is no secret that the British have dominated the sport. Even if licensed in other countries, many teams still are based in Britain. Among the 2020 constructors, only Alfa Romeo, AlphaTauri and Ferrari do not have a factory in the UK. Surprisingly, America holds the second sport. Only one team in the present championship is registered from America (Haas F1 Team). Following them, we have the usual suspects the Italians, the Germans and the French.

Question: Licensed in how many different countries have the teams registered in Formula 1?
In [23]:
const_country_num = const_races_entered.nationality.nunique()
print('F1 teams have registered with licenses in {} different countries.'.format(const_country_num))
F1 teams have registered with licenses in 24 different countries.

Next, we will look at the number of race victories for all the constructors.

In [24]:
results_wins = results_raw[results_raw['position'] == '1'] # taking result rows with wins (position = 1)
results_wins
Out[24]:

It looks like there are more race winners than the number of races (1028)! Let's see which raceId has duplicates in the data.

In [25]:
results_wins[results_wins.duplicated(subset=['raceId'])]
Out[25]:

These races (1951 French GP, 1956 Argentine GP and 1957 British GP) had multiple drivers credited as winners while driving the same car (This kind of driver swapping is not possible in modern F1 races!). However, since the car was the same, there was only one winning constructor. Since our interest at the moment is only on constructor victories, we can drop these duplicates to form a new data frame.

In [26]:
const_wins = results_wins.drop_duplicates(subset = ['raceId']) # Dropping duplicates

const_win_counts = const_wins[['constructorId', 'raceId']].groupby(
    'constructorId').count().reset_index().rename(columns = {'raceId':'wins'})

const_win_counts = const_win_counts.merge(constructors_raw, on = 'constructorId').sort_values('wins', ascending = False)

# Plotting
plt.figure(figsize = (16,6))

sns.barplot(const_win_counts.head(5).wins, const_win_counts.head(5).name)
plt.title('Top 5 F1 constructors with most wins')
plt.xlabel('Number of wins')
plt.ylabel('');

Ferrari has 238 race victories in their hands from their 1001 race starts, 60 more than McLaren and much ahead of others. It has to be noted that both Mercedes (as a works team) and Red Bull do not have the F1 pedigree like the other three (the Mercs raced between 1954 and 1955, though). Yet they share the top 5 spots! To get a different perspective, we can look at the data in terms of win percentage per races entered.

In [27]:
const_win_percent = const_win_counts[['constructorId', 'wins']].merge(
    const_races_entered[['races_ent', 'constructorId', 'name']], on = 'constructorId')

const_win_percent['win_percent'] = const_win_percent['wins'] * 100 / const_win_percent['races_ent'] # Adding the percentage win column

const_win_percent = const_win_percent[const_win_percent['races_ent'] > 100].sort_values(
    'win_percent', ascending = False) # Dropping constructors with less than 100 race entries

# Plotting
plt.figure(figsize=(16,5))
sns.barplot(const_win_percent.name, const_win_percent.win_percent)
plt.xticks(rotation=90)
plt.title('Constructors with high win percentage')
plt.xlabel('')
plt.ylabel('%');

Mercedes win rate is more than double that of its nearest rival, Ferrari. They have won about half of all the races they have participated in F1! Such has been their dominance in the new turbo hybrid era. Ferrari has about 25% success rate in converting race entries to victories, while its little above 20% for McLaren and Red Bull. Note that, only teams with more than 100 race starts are considered here for this analysis for a fair comparison.

We will not look at the constructor championship titles. For this, we have to load the constructor_standings.csv file to a data frame, merge it with the season_finale, and then sort and group.

In [28]:
const_st_raw = pd.read_csv('data/constructor_standings.csv')

const_st_season_end = season_finale[['raceId', 'year', 'tot_races']].merge(const_st_raw, on = 'raceId') # Constructors' standings at season end
const_champ = const_st_season_end[const_st_season_end['position'] == 1] # Sort only the champions in each year
const_champ_tot = const_champ[['constructorId', 'position']].groupby('constructorId').sum().reset_index().merge(
    constructors_raw[['name', 'constructorId']]).rename(columns={'position':'titles'}).sort_values('titles', ascending = False) # Total titles per constructor

# Plotting
plt.figure(figsize = (14,9))

sns.barplot(const_champ_tot.titles, const_champ_tot.name)
plt.title('Constructors with World Championship Titles')
plt.xlabel('Number of times crowned')
plt.ylabel('');

Ferrari has an outstanding 16 world titles on their name. Interestingly they claimed 8 of these between 1999 and 2008! Although McLaren has more race victories, Williams has one world title more compared to them. Mercedes has been crowned world champions consequently from the 2014 season, to pack a total of 6 titles. Lotus and Red Bull are other significant teams in the list, winning four championships each. Brawn GP finished the only season they competed (2009) as champions, before being bought and renamed by Mercedes.

Drivers

Drivers are the superstars of F1. They are the athletes who ultimately battle it out on the tarmac. The competition is always intense and sometimes has become fierce to produce great rivalries. Let us explore some driver stats by loading the drivers.csv file.

In [29]:
drivers_raw = pd.read_csv('data/drivers.csv')
drivers_raw
Out[29]:

There have been 848 driver entries in Formula 1 so far. Let's explore their nationalities.

Question: Find the number of countries from which there has been a driver representation in Formula 1.
In [30]:
print('There has been representation from {} nationalities in Formula1.'.format(drivers_raw.nationality.nunique()))
There has been representation from 41 nationalities in Formula1.
In [31]:
drivers_country = drivers_raw[['driverId', 'nationality']].groupby('nationality').count().rename(
    columns = {'driverId' : 'counts'}) # Grouping based on nationality

drivers_country = drivers_country[drivers_country.counts > 30].sort_values('counts', ascending = False)
drivers_country.loc['Others'] = [(len(drivers_raw) - drivers_country.counts.sum())] # Consolidating countries with less than 30 representations into 'Others'

# Plotting pie chart
plt.figure(figsize=(12,6))
explode = np.append(np.zeros(6),0.1)  # only "explode" the Others slice
plt.axis('equal')

plt.title('Driver Nationalities')
plt.pie(drivers_country.counts, explode=explode, labels=drivers_country.index, autopct='%1.1f%%',
        shadow=True, startangle=270);

Similar to constructors, the British has dominated in driver representation in F1. Here too, surprisingly, Americans came second. Not many American drivers are involved in F1 in recent years. Their last representation was in 2015. And again the Italians, the French and the Germans share the other top spots along with the Brazilians. There is a significant share, however, from the other 35 nationalities.

Let's build a new consolidated data frame from the CSV files which will contain all the statistics for the drivers. For fulfilling that, we first have to define a few functions which will count the number of race wins, podiums, fastest laps and pole positions from the results_raw data frame.

In [32]:
def podium_counter(pos):
    if pos in ['1', '2', '3']:
        return True
    else:
        return False

def win_counter(pos):
    if pos == '1':
        return True
    else:
        return False
    
def pole_counter(grid):
    if grid == 1:
        return True
    else:
        return False
In [33]:
results_copy = results_raw[['raceId', 'driverId', 'grid', 'position', 'points', 'laps', 'rank']].copy().rename(
    columns = {'rank':'fastlap'})

# Adding podium, win, pole and fastest lap columns
results_copy['podium'] = results_copy.position.apply(podium_counter)
results_copy['win'] = results_copy.position.apply(win_counter)
results_copy['pole'] = results_copy.grid.apply(pole_counter)
results_copy['fastestLap'] = results_copy.fastlap.apply(win_counter)

# Gouping with respect to drivers applying different rules for different columns
driver_stats_1 = results_copy.drop(columns = ['position', 'grid', 'fastlap']).groupby('driverId')
func_dic = {'raceId':'count', 'points':'sum', 'laps':'sum', 'podium':'sum', 'win':'sum', 'pole':'sum', 'fastestLap':'sum'}
driver_stats_1 = driver_stats_1.aggregate(func_dic).reset_index().rename(columns = {'raceId':'races'})
In [34]:
# Getting driver championship data
driver_st_raw = pd.read_csv('data/driver_standings.csv')
driver_st_season_end = season_finale[['raceId', 'year', 'tot_races']].merge(driver_st_raw, on = 'raceId')
driver_champ = driver_st_season_end[driver_st_season_end['position'] == 1]
driver_champ_tot = driver_champ[['driverId', 'position']].groupby('driverId').sum().reset_index().merge(
    drivers_raw[['forename', 'surname', 'driverId']]).rename(
    columns={'position':'titles'}).sort_values('titles', ascending = False)

# Merging everthing to a single dataframe
driver_stats = driver_stats_1.merge(driver_champ_tot[['driverId', 'titles']], how = 'left').fillna(0)
driver_stats = drivers_raw[['driverId', 'forename', 'surname', 'nationality']].merge(driver_stats, on = 'driverId')
driver_stats
Out[34]:

It will be nice to have a function which will take in the driver name as input and display all their stats.

In [35]:
# Funtion to display driver stats
def get_driver_stats():
    try:
        f_n, s_n = input("Enter driver name (as Forename Surname): ").split()
        df = driver_stats.loc[(driver_stats['forename'] == f_n) & (driver_stats['surname'] == s_n)].squeeze()
        return print('''Name: {} {}
Nationality: {}
Stats
Races Entered:\t \t{}
Drivers' Titles:\t{:.0f}
Race Wins:\t \t{}
Pole Positions:\t \t{}
Podiums:\t \t{}
Total Points:\t \t{}
Fastest Laps:\t \t{}
Laps Raced:\t \t{}'''.format(df.forename, df.surname, df.nationality, df.races, df.titles,
          df.win, df.pole, df.podium, df.points, df.fastestLap, df.laps))
    
    except TypeError:
        print('No driver found! Make sure the first letters are capital.')
        
    except ValueError:
        print('Please enter both forename and surname.')

Let's try the function on some drivers!

In [36]:
get_driver_stats()
Enter driver name (as Forename Surname): Daniel Ricciardo Name: Daniel Ricciardo Nationality: Australian Stats Races Entered: 181 Drivers' Titles: 0 Race Wins: 7 Pole Positions: 3 Podiums: 29 Total Points: 1103.0 Fastest Laps: 14 Laps Raced: 9809
In [37]:
get_driver_stats()
Enter driver name (as Forename Surname): Juan Fangio Name: Juan Fangio Nationality: Argentine Stats Races Entered: 58 Drivers' Titles: 5 Race Wins: 24 Pole Positions: 29 Podiums: 35 Total Points: 279.14 Fastest Laps: 0 Laps Raced: 3387

There are some discrepancies in the data, for example in the number of fastest laps, especially with the old drivers. Due to time constraints, they are not addressed at the moment. You may check the stats for your favourite driver and compare the numbers with data here.

In [ ]:
get_driver_stats()
In [ ]:
get_driver_stats()

Let's find out which driver has most experience if F1 racing.

Question: Who is the most experienced driver in terms of races entered?
In [38]:
print('The most experienced driver in terms of races entered is {} {} with {} race entries.'.format(
    driver_stats.sort_values('races').tail(1).squeeze().forename, 
    driver_stats.sort_values('races').tail(1).squeeze().surname, 
    driver_stats.sort_values('races').tail(1).squeeze().races))
The most experienced driver in terms of races entered is Rubens Barrichello with 326 race entries.
Question: In terms of laps raced, who has more experience than other drivers?
In [39]:
print('In terms of laps raced, the most experienced driver is {} {} with {} laps.'.format(
    driver_stats.sort_values('laps').tail(1).squeeze().forename, 
    driver_stats.sort_values('laps').tail(1).squeeze().surname, 
    driver_stats.sort_values('laps').tail(1).squeeze().laps))
In terms of laps raced, the most experienced driver is Kimi Räikkönen with 17115 laps.

Now, let's pick up five drivers who are considered to be among the greatest of all time and compare their stats. I'm choosing Hamilton, Schumacher, Senna, Prost and Fangio for the list.

In [40]:
top_drivers = driver_stats[(driver_stats['surname'].isin(['Schumacher', 'Hamilton', 'Prost', 'Senna', 'Fangio']))
            & (driver_stats['forename'].isin(['Michael', 'Lewis', 'Alain', 'Ayrton', 'Juan']))]

top_drivers = top_drivers.drop(['driverId', 'forename', 'nationality', 'points', 'laps', 'fastestLap']
                        , axis = 1).set_index('surname')
top_drivers = top_drivers.rename(columns = 
                                 {'races':'Races', "podium":'Podiums', 'win':'Wins', 'pole':'Poles', 'titles':'Titles'})

# Plotting
sns.set_style('dark')

top_drivers.plot(secondary_y = 'Titles', kind = 'bar', figsize=(14,7), title = 'Comparing driver stats', 
                 xlabel = '', rot = 0, grid = True);

These drivers belong to different eras of Formula 1. Lewis Hamilton belongs to the modern era, while Michael Schumacher was prominent in the 1990s-2000s. Ayrton Senna and Alain Prost raced in the 80s and early 90s. Juan Manuel Fangio belonged to the initial period of F1, the 1950s. His legacy is evident in the stats. His record hold of five world titles remained unchallenged until Schumacher broke it in the 2000s. Fangio has raced much fewer races compared to others but has a win ratio close to 50%. 'The Professor' Prost has an incredible four world titles in his name. Senna is considered as one of the most influential drivers of all time. He was very quick on race tracks, evident from his tally of pole positions. Senna would have clinched more titles if his life wasn't taken in a fatal accident at Imola in 1994. Michael's numbers in F1 is outstanding. He has an incredible seven world titles, 91 wins, 155 podiums and 68 pole positions. But Lewis Hamilton seems to be on the way to cross all these numbers. In terms of poles and podiums, Lewis is already in front of Michael. He is just one short in the number of victories and is on course of winning another world title this year to level with Michael.

For our next visualisation, we will choose one of the unique records Lewis holds. He has won at least one Grand Prix every year since his debut in 2007. Let's examine his wins across the years.

In [41]:
dri_year_end_st = season_finale[['raceId', 'year']].merge(driver_st_raw)
lewis_year_end_st = dri_year_end_st[dri_year_end_st['driverId'] == 1]

# Plotting
sns.set_style('darkgrid')
plt.figure(figsize=(14, 6))

plt.bar(lewis_year_end_st.year, lewis_year_end_st.wins)
plt.xlabel('Year')
plt.ylabel('Wins')
plt.title("Lewis' year-round wins");

Lewis almost won a world title in his rookie season in 2007, before clinching his maiden title in 2008. From 2007 to 2012 shows his tally of wins with McLaren. He joined Mercedes in 2013 before the start of the turbo-hybrid era. His first year with the Merc was probably the least highlight of his career with just one win. But he didn't have to look back after that, winning almost half of all races in every following season.

Conclusion and Future Work

We explored through some of the fundamental aspects of Formula 1 like the drivers, constructors, circuits etc. For a first independent data exploration, this is comprehensive. Yet, there are many possibilities and opportunities in this data set. I hope to expand on these, sometime in future

References

In [42]:
project_name = "zerotopandas-course-project-70-years-of-f1"
In [43]:
!pip install jovian --upgrade -q
In [44]:
import jovian
In [ ]:
jovian.commit(project=project_name)
[jovian] Attempting to save notebook..
In [ ]: