2021 Formula One Championship Title Fight Analysis

Marco Hui

2022-07-13

Introduction

The 2021 Formula One season will be remembered for the close world championship title fight between Max Verstappen and Lewis Hamilton. The two drivers traded blows throughout the year, before Verstappen ultimately took the crown during the last race. In doing so, Verstappen became the first ever dutch driver and the first non-Mercedes driver in the turbo-hybrid era to win the World Championship.

To gain a deeper understanding and visualisation of how the championship battle between the two played out, we performed a bit of data exploration on the results, race by race. Data was scraped from the archives of the official Formula One website, and scripts to do so can be found on the github repository.

Data Preparation

We begin by loading in the necessary packages, data sets, plot themes, etc. This preparation work is important because it simplifies much of the code later on.

# load packages
library(readr)
library(tidyverse)
library(zoo)
library(lubridate)
library(reshape2)
library(knitr)
library(kableExtra)
library(CGPfunctions)
# load data sets from csv files
race_results <- read_csv('/Users/marcohui/Desktop/2021titlefight/f1-2021-titlefight/data/results2021.csv')
starting_grid <- read_csv('/Users/marcohui/Desktop/2021titlefight/f1-2021-titlefight/data/starts2021.csv')
fastest_laps <- read_csv('/Users/marcohui/Desktop/2021titlefight/f1-2021-titlefight/data/flaps2021.csv')
sprint_results <- read_csv('/Users/marcohui/Desktop/2021titlefight/f1-2021-titlefight/data/sprints2021.csv')
qualis <- read_csv('/Users/marcohui/Desktop/2021titlefight/f1-2021-titlefight/data/qualis2021.csv')
gp_names <- read_csv('/Users/marcohui/Desktop/2021titlefight/f1-2021-titlefight/data/names2021.csv')

# factorise gp_names based on race calendar order
gp_names <- gp_names %>% 
  mutate(gp_name=factor(gp_name,levels=c('Bahrain','Emilia Romagna','Portugal','Spain',
                                         'Monaco','Azerbaijan','France','Styria',
                                         'Austria','Great Britain','Hungary',
                                         'Belgium','Netherlands','Italy',
                                         'Russia','Turkey','United States',
                                         'Mexico','Brazil','Qatar','Saudi Arabia','Abu Dhabi')))

# add unique gp_name column to all data sets
race_results <- race_results %>% left_join(gp_names,by='race_num')
starting_grid <- starting_grid %>% left_join(gp_names,by='race_num')
fastest_laps <- fastest_laps %>% left_join(gp_names,by='race_num')
sprint_results <- sprint_results %>% left_join(gp_names,by='race_num')
qualis <- qualis %>% left_join(gp_names,by='race_num')
# create tibble for teammate pairings
teammates <- tribble(
  ~No,~TeammateNo,
  33,11, 11,33, # Red Bull 
  44,77, 77,44, # Mercedes
  55,16, 16,55, # Ferrari
  4,3, 3,4,     # Mclaren
  14,31, 31,14, # Alpine
  10,22, 22,10, # AlphaTauri
  18,5, 5,18,   # Aston Martin
  63,6, 6,63,   # Williams
  7,99, 99,7,   # Alfa Romeo
  47,9, 9,47    # Haas
)
# store constructor colour hex codes in variables
alfa_clr='#900000'
alpha_clr='#2B4562'
alpine_clr='#0090FF'
aston_clr='#006F62'
ferrari_clr='#DC0000'
haas_clr='#FFFFFF'
mclaren_clr='#FF8700'
mercedes_clr='#00D2BE'
redbull_clr='#0600EF'
williams_clr='#005AFF'
# create plot theme
hui_ggplot <- theme(plot.title = element_text(face='bold',size=22,vjust=1),
                   axis.title.x = element_blank(),
                   axis.title.y = element_blank(),
                   text = element_text(color='white'),
                   axis.text = element_text(color='white'),
                   legend.position='right',
                   legend.background = element_rect(fill='#1e2226',colour='#1e2226'),
                   legend.key = element_rect(fill='#1e2226',colour='#1e2226'),
                   plot.background = element_rect(fill='#1e2226',colour='#1e2226'),
                   panel.background = element_rect(fill='#1e2226',colour='#1e2226'),
                   panel.grid.major = element_line(colour="grey27"),
                   panel.grid.major.x = element_blank(),
                   panel.grid.minor = element_blank(),
                   panel.border = element_blank())

Verstappen and Hamilton Topping the Charts

Overall, the two-way dogfight between Verstappen and Hamilton is immediately evident in several summary statistics. The drivers eclipse the rest in number of wins, number of podiums, and average finishing positions. As expected from the eventual world champion, Verstappen edges out slightly in all categories.

Number of Wins

A Grand Prix victory in Formula One grants the driver 25 championship points, making it an important contributor to title bids. In the 2021 season, six different drivers across four constructors achieved a victory. Predictably from the championship contenders, Verstappen and Hamilton top the charts with 10 and 8 wins respectively, significantly more than the others.

race_results %>% 
  filter(Pos==1) %>%
  group_by(Driver,Car) %>%
  summarise(numWins = n()) %>%
  arrange(-numWins) %>% 
  mutate(Driver=str_sub(Driver,-3,-1)) %>%
  
  ggplot() + 
  geom_bar(aes(x=reorder(Driver,-numWins),y=numWins,fill=Car),stat='identity') +
  labs(title='Number of Grand Prix Wins by Driver',
       fill='Constructor') +
  scale_y_continuous(breaks=c(0,2,4,6,8,10),expand=c(0,0)) +
  scale_fill_manual(values=c(alpine_clr,mclaren_clr,mercedes_clr,redbull_clr)) +
  hui_ggplot

Number of Podiums

An impressive thirteen drivers from eight constructors achieved a podium finish in 2021. This reflects a much tighter grid where many can challenge for top positions compared to years past. Again, Verstappen and Hamilton recorded the significantly more podium finishes than the rest, at 18 and 17 each. Often benefiting from the high performing Mercedes car, Valterri Bottas rounds out the top three with 11 himself.

race_results %>%
  filter(Pos==1|Pos==2|Pos==3) %>%
  group_by(Driver,Car) %>%
  summarise(numPodiums = n()) %>%
  arrange(-numPodiums) %>%
  mutate(Driver=str_sub(Driver,-3,-1)) %>%
  
  ggplot() +
  geom_bar(aes(x=reorder(Driver,-numPodiums),y=numPodiums,fill=Car),stat='identity') +
  labs(title='Number of Grand Prix Podiums by Driver',
       fill='Constructor') +
  scale_y_continuous(breaks=c(0,2,4,6,8,10,12,14,16,18,20),expand=c(0,0)) +
  scale_fill_manual(values=c(alpha_clr,alpine_clr,aston_clr,
                             ferrari_clr,mclaren_clr,mercedes_clr,
                             redbull_clr,williams_clr)) +
  hui_ggplot

Average Finishing Position

This metric takes the average of every finishing race result for a driver (retirements and disqualifications excluded). Unsurprisingly, Verstappen and Hamilton tend to finish much higher up the order. However, the difference between the two are marginal at at 2.65 and 2.76. It is interesting to note that several poor results dragged Red Bull Racing’s Sergio Perez to below both Ferrari drivers Carlos Sainz and Charles Leclerc.

race_results %>%
  # disregard non-finishing positions
  filter(Pos!='NC' & Pos!='DQ') %>%
  mutate(Pos=as.numeric(Pos)) %>%
  group_by(Driver) %>%
  summarise(avgFinPos=round(mean(Pos),3)) %>%
  arrange(avgFinPos) %>% 
  
  kbl(col.names=c('Driver','Average Finishing Position'),
      align='lc') %>%
  kable_material_dark() %>%
  row_spec(c(1,2),background='lightyellow',color='black')
Driver Average Finishing Position
Max Verstappen VER 2.650
Lewis Hamilton HAM 2.762
Valtteri Bottas BOT 5.000
Carlos Sainz SAI 6.500
Charles Leclerc LEC 6.600
Sergio Perez PER 6.600
Lando Norris NOR 6.619
Pierre Gasly GAS 7.737
Daniel Ricciardo RIC 8.095
Fernando Alonso ALO 9.050
Esteban Ocon OCO 9.158
Lance Stroll STR 10.789
Sebastian Vettel VET 11.053
Yuki Tsunoda TSU 11.722
Kimi Räikkönen RAI 12.500
George Russell RUS 12.833
Antonio Giovinazzi GIO 12.905
Robert Kubica KUB 14.500
Nicholas Latifi LAT 15.105
Mick Schumacher MSC 16.474
Nikita Mazepin MAZ 17.812

Note: calculations exclude sprint qualifying results

Change in Position

A look into how the drivers moved up and down the race order throughout races.

Average Position Gained or Lost

A slopegraph below marks the average start position and average end position for each driver across every race they competed in. Like before, Verstappen and Hamilton feature at the top for both statistics, starting and ending high up the grid overall. The visual suggests that both generally move down the grid over the course of a race. And while Verstappen tends to start races in better positions, he has a lower end position on average.

However, it is important to note the limitations of this metric as an indicator of performance. Because they are means that factor in non-finishing positions, the numbers can be significantly skewed by outliers. In other words, a few poor results by a driver through no fault of their own (such as retirements) can heavily drag down their statistic. Additionally, gaining positions on average does not signal better performances because drivers who often start at the front, like Verstappen and Hamilton, have much fewer opportunities to gain positions; whereas hypothetically, a driver starting from last has 19 cars to overtake.

# set slopegraph colours
cols <- c('VER'='#0600EF','HAM'='#00D2BE',
          'PER'='gray','BOT'='gray','LEC'='gray','SAI'='gray','NOR'='gray','RIC'='gray',
          'VET'='gray','STR'='gray','GAS'='gray','TSU'='gray','MSC'='gray','MAZ'='gray',
          'RUS'='gray','LAT'='gray','ALO'='gray','OCO'='gray','RAI'='gray','GIO'='gray','KUB'='gray')

# simplify race_results table
race_results_s <- race_results %>% 
  select(Driver,gp_name,Pos) %>%
  rename(endPos=Pos)

# simplify starting_grid table
starting_grid_s <- starting_grid %>% 
  select(Driver,gp_name,Pos) %>%
  rename(startPos=Pos)

startend <- race_results_s %>% 
  left_join(starting_grid_s,by=c('Driver','gp_name')) %>%
  # replace non-numeric values with 21 
  mutate(endPos=replace(endPos,endPos=='NC','21'), # DNF/DNS
         endPos=replace(endPos,endPos=='DQ','21'), # disqualification
         startPos=replace(startPos,is.na(startPos),'21'), # pit lane start
         endPos=as.numeric(endPos),
         startPos=as.numeric(startPos))

startend %>%
  group_by(Driver) %>%
  summarise(avgStartPos=round(mean(startPos),3),
            avgEndPos=round(mean(endPos),3)) %>%
  mutate(Driver=str_sub(Driver,-3,-1)) %>%
  melt() %>%
  
  newggslopegraph(Times=variable,
                  Measurement=value,
                  Grouping=Driver,
                  ReverseYAxis=TRUE,
                  Title='Average Start and End Position by Driver',
                  SubTitle=element_blank(),
                  Caption=element_blank(),
                  TitleTextSize=20,
                  XTextSize=15,
                  DataTextSize=5,
                  LineColor=cols)

Note: pit lane starts, retirements, and disqualifications are considered 21st position.

Position Gained or Lost by Race

We then plot the positions gained and lost race by race for Verstappen and Hamilton. Seeing how the two drivers gain or lose positions at each Grand Prix gives us a more detailed understanding that a summary statistic cannot.

Counting the vertical lines tells us that Hamilton maintained his position in a race 12 times this season, which is more often than Verstappen did at 10 times. At 6 and 7, both moved up in races similar number of times. It is also interesting to see how Verstappen and Hamilton lost out greatly in the Italy and Azerbaijan GP.

Again, gaining positions does not necessarily indicate better performances because you cannot move up if you are already in the lead.

startend %>%
  mutate(difPos=startPos-endPos) %>%
  filter(Driver=='Lewis Hamilton HAM'|Driver=='Max Verstappen VER') %>%
  mutate(Driver=str_sub(Driver,-3,-1)) %>%
  
  ggplot() + 
  geom_bar(aes(x=gp_name,y=difPos),stat='identity',colour='white',fill='white') + 
  facet_wrap(~Driver) +
  labs(title='Number of Grand Prix Wins by Driver',
       fill='Constructor') +
  scale_y_continuous(breaks=c(-20,-15,-10,-5,0,5,10,15,20),expand=c(0,0)) +
  coord_flip() +
  hui_ggplot

Is Hamilton Worse at Converting Qualifying Performance to Race Results?

In Motorsports, performance during qualifying sessions plays a significant role in determining grid positions for the race. Therefore, a good qualifying showing that puts the car further up the pack can help a driver achieve better results on race day.

Average Qualifying Gap to Teammate

One common metric to evaluate qualifying performance for a driver is the average lap time gap to teammate. While both qualify quicker than their teammates over the course of the 2021 season, Verstappen seems to pull larger gaps. Verstappen out-qualifies teammate Perez by 0.67 seconds on average; Hamilton out-qualifies teammate Bottas by 0.35 seconds on average.

# replace DNFs and DNSs with NA values
qualis[qualis=='DNF'] <- NA
qualis[qualis=='DNS'] <- NA

qualis %>% 
  # filter out Robert Kubica, who only participated in two sessions
  filter(Driver!='Robert Kubica KUB') %>%
  left_join(teammates,by='No') %>%
  # filter out rows where driver did not set a qualifying lap time
  filter(!is.na(Q1)) %>%
  # set best time to the latest qualifying lap time
  mutate(bestTime=coalesce(Q3,Q2,Q1)) %>%
  mutate(bestTime=as.duration(ms(bestTime))) %>%
  group_by(gp_name) %>%
  mutate(mateTime=bestTime[match(TeammateNo,No)]) %>%
  ungroup() %>%
  # filter out rows where teammate did not set a qualifying lap time
  filter(mateTime>0) %>%
  group_by(Driver,Car) %>%
  summarise(avgGap=mean(bestTime-mateTime)) %>% 
  arrange(Car) %>%
  
  kbl(col.names=c('Driver','Constructor','Average Qualifying Gap to Teammate'),
      align='llc') %>%
  kable_material_dark() %>%
  row_spec(c(15,17),background='lightyellow',color='black')
Driver Constructor Average Qualifying Gap to Teammate
Antonio Giovinazzi GIO Alfa Romeo Racing Ferrari -0.2962632
Kimi Räikkönen RAI Alfa Romeo Racing Ferrari 0.2962632
Pierre Gasly GAS AlphaTauri Honda -0.7170500
Yuki Tsunoda TSU AlphaTauri Honda 0.7170500
Esteban Ocon OCO Alpine Renault -0.0081364
Fernando Alonso ALO Alpine Renault 0.0081364
Lance Stroll STR Aston Martin Mercedes 1.8265714
Sebastian Vettel VET Aston Martin Mercedes -1.8265714
Carlos Sainz SAI Ferrari 1.0493182
Charles Leclerc LEC Ferrari -1.0493182
Mick Schumacher MSC Haas Ferrari -0.9259500
Nikita Mazepin MAZ Haas Ferrari 0.9259500
Daniel Ricciardo RIC McLaren Mercedes -0.2445000
Lando Norris NOR McLaren Mercedes 0.2445000
Lewis Hamilton HAM Mercedes -0.3467273
Valtteri Bottas BOT Mercedes 0.3467273
Max Verstappen VER Red Bull Racing Honda -0.6726667
Sergio Perez PER Red Bull Racing Honda 0.6726667
George Russell RUS Williams Mercedes -0.5189545
Nicholas Latifi LAT Williams Mercedes 0.5189545

Average Qualifying Position

A more important metric to look at is the average qualifying position, which describes where a driver generally qualifies throughout the season in terms of position rather than time. This statistic is less indicative of pure driver capabilities because qualifying order is partially determined by car performance during the Grand Prix weekend; however, it is ultimately more decisive in final race results.

In this regard, Hamilton is shown to outdo Verstappen. With an average qualifying position of 2.19, Hamilton tend to qualify higher up the order compared to Verstappen. Knowing from before that Verstappen achieved higher race results throughout the season, does this imply that Hamilton is less capable of converting qualifying results into points during a Grand Prix?

qualis %>%
  # ignore non-qualifying positions
  filter(Pos!='NC' & Pos!='RT') %>%
  mutate(Pos=as.numeric(Pos)) %>%
  group_by(Driver) %>%
  summarise(avgQualiPos=round(mean(Pos),3)) %>%
  arrange(avgQualiPos) %>%
  
  kbl(col.names=c('Driver','Average Qualifying Position'),
      align='lc') %>%
  kable_material_dark() %>%
  row_spec(c(1,2),background='lightyellow',color='black')
Driver Average Qualifying Position
Lewis Hamilton HAM 2.190
Max Verstappen VER 2.773
Valtteri Bottas BOT 3.727
Sergio Perez PER 6.364
Lando Norris NOR 6.545
Charles Leclerc LEC 6.591
Pierre Gasly GAS 6.773
Carlos Sainz SAI 8.091
Daniel Ricciardo RIC 9.591
Esteban Ocon OCO 10.955
Fernando Alonso ALO 10.955
Sebastian Vettel VET 11.727
Yuki Tsunoda TSU 12.050
George Russell RUS 12.591
Lance Stroll STR 12.900
Antonio Giovinazzi GIO 13.714
Kimi Räikkönen RAI 15.700
Nicholas Latifi LAT 16.364
Mick Schumacher MSC 18.050
Robert Kubica KUB 18.500
Nikita Mazepin MAZ 19.545

Average Starting Position (After Penalties and Sprints)

No, because qualifying performance does not directly translate to starting grid position, which is a bigger influence final race positions. The starting grid takes qualifying order and adjust it based on penalties and sprint qualifying results. When examining the starting grids of races, we find that Verstappen actually starts further up front on average.

startend %>%
  group_by(Driver) %>%
  summarise(avgStartPos=round(mean(startPos),3)) %>%
  arrange(avgStartPos) %>%
  
  kbl(col.names=c('Driver','Average Starting Position'),
      align='lc') %>%
  kable_material_dark() %>%
  row_spec(c(1,2),background='lightyellow',color='black')
Driver Average Starting Position
Max Verstappen VER 2.864
Lewis Hamilton HAM 3.091
Valtteri Bottas BOT 5.591
Charles Leclerc LEC 6.455
Lando Norris NOR 6.864
Sergio Perez PER 7.091
Pierre Gasly GAS 7.318
Carlos Sainz SAI 7.909
Daniel Ricciardo RIC 9.682
Fernando Alonso ALO 10.500
Esteban Ocon OCO 10.682
Sebastian Vettel VET 11.773
George Russell RUS 13.000
Lance Stroll STR 13.091
Yuki Tsunoda TSU 13.136
Antonio Giovinazzi GIO 13.455
Kimi Räikkönen RAI 15.400
Nicholas Latifi LAT 16.182
Robert Kubica KUB 16.500
Mick Schumacher MSC 17.545
Nikita Mazepin MAZ 18.545

Number of Qualifying P1s versus Pole Positions

The difference between qualifying position and starting grid position is particularly pronounced when looking at the top positions.

Although Verstappen and Hamilton almost match each other in topping the timing sheets during qualifying sessions, Verstappen ends up starting the race in pole position significantly more often. This can be explained by Hamilton losing positions at sprint qualifying and taking an engine penalty while ahead, both of which Verstappen never did.

This is an important observation because starting from the most favourable position on the grid ultimately is the biggest contributor to race victories. According to race archives, 40% of all race victories in Formula One has been on from pole position.

poles <- starting_grid %>% 
  filter(Pos==1) %>%
  group_by(Driver) %>%
  summarise(numPoles=n()) %>%
  arrange(-numPoles)

qualip1s <- qualis %>% 
  filter(Pos==1) %>%
  group_by(Driver) %>%
  summarise(numQualiP1s=n()) %>%
  arrange(-numQualiP1s)

# table
qualip1s %>%
  left_join(poles,by='Driver') %>%
  kbl(col.names=c('Driver','Number of Qualifying P1s','Number of Pole Positions'),
      align='lcc') %>%
  kable_material_dark() %>%
  row_spec(c(1,2),background='lightyellow',color='black')
Driver Number of Qualifying P1s Number of Pole Positions
Max Verstappen VER 9 10
Lewis Hamilton HAM 7 5
Valtteri Bottas BOT 3 4
Charles Leclerc LEC 2 2
Lando Norris NOR 1 1
# plot
qualip1s %>%
  left_join(poles,by='Driver') %>%
  melt() %>%
  
  ggplot() +
  geom_bar(aes(x=reorder(Driver,-value),y=value,fill=variable),
           stat='identity',
           width=0.5,
           position='dodge') +
  labs(title='Number of Qualifying P1s and Pole Positions by Driver',
       fill='Statistic') +
  scale_y_continuous(breaks=c(0,2,4,6,8,10),expand=c(0,0)) +
  hui_ggplot

Wins from Pole

Now knowing that Hamilton has started races from pole position far less often than Verstappen did, it makes sense why why a lower number and percentage of Hamilton’s victories are won from pole position.

startend %>%
  filter(endPos=='1') %>%
  mutate(WFP=if_else(endPos==1&startPos==1,1,0)) %>%
  group_by(Driver) %>%
  summarise(numWins=n(),
            numWFP=sum(WFP),
            pcntWFP=numWFP/sum(endPos)) %>%
  arrange(-numWins) %>%
  
  kbl(col.names=c('Driver','Number of Wins','Number of Wins From Pole', 'Percent of Wins From Pole'),
      align='lccc') %>%
  kable_material_dark() %>%
  row_spec(c(1,2),background='lightyellow',color='black')
Driver Number of Wins Number of Wins From Pole Percent of Wins From Pole
Max Verstappen VER 10 7 0.700
Lewis Hamilton HAM 8 3 0.375
Daniel Ricciardo RIC 1 0 0.000
Esteban Ocon OCO 1 0 0.000
Sergio Perez PER 1 0 0.000
Valtteri Bottas BOT 1 1 1.000

Pole Conversion Rate

In fact, when Hamilton is on pole position because of a good qualifying performance, he is nearly just as likely to achieve a win compared to Verstappen. The difference between two in pole conversion rate is much lower than that in percentage of wins from poles.

startend %>%
  filter(startPos=='1') %>%
  mutate(converted=if_else(endPos==1&startPos==1,1,0)) %>%
  group_by(Driver) %>%
  summarise(numPoles=n(),
            numConverted=sum(converted),
            pcntConverted=numConverted/sum(startPos)) %>%
  arrange(-numPoles) %>%
  
  kbl(col.names=c('Driver','Number of Poles','Number of Poles Converted','Percent of Poles Converted'),
      align='lccc') %>%
  kable_material_dark() %>%
  row_spec(c(1,2),background='lightyellow',color='black')
Driver Number of Poles Number of Poles Converted Percent of Poles Converted
Max Verstappen VER 10 7 0.70
Lewis Hamilton HAM 5 3 0.60
Valtteri Bottas BOT 4 1 0.25
Charles Leclerc LEC 2 0 0.00
Lando Norris NOR 1 0 0.00

Who Has the Most Fastest Laps?

Another facet of the season to explore is fastest laps. A fast lap is important to the championship because one championship point is awarded to the driver with the fastest lap each race (but only if they finish within the top 10 positions).

Number of Fastest Laps

Over the season, Hamilton and Verstappen achieved the fastest lap of a race the same number of times in total. When unable to fight for the lead, Mercedes strategists have many times targeted the point for fastest lap towards the end of a race as part of damage limitation. It is interesting to see this play reflected in the numbers, which show Hamilton in general achieving the fastest lap in later laps of a Grand Prix.

fastest_laps %>% 
  filter(Pos==1) %>% 
  group_by(Driver) %>%
  summarise(numFastestLaps=n(),avgLap=round(mean(Lap),3)) %>%
  arrange(-numFastestLaps) %>%
  kbl(col.names=c('Driver','Number of Fastest Laps','Average Lap'),
      align='lcc') %>%
  kable_material_dark() %>%
  row_spec(c(1,2),background='lightyellow',color='black')
Driver Number of Fastest Laps Average Lap
Lewis Hamilton HAM 6 60.000
Max Verstappen VER 6 49.833
Valtteri Bottas BOT 4 62.000
Sergio Perez PER 2 60.500
Daniel Ricciardo RIC 1 53.000
Lando Norris NOR 1 39.000
Pierre Gasly GAS 1 70.000

Note: there is no fastest lap recorded in the Belgium GP

Championship Standings

Let’s see if a broader look at the season reveals any insights about the championship title fight between Verstappen and Hamilton.

Championship Points By Driver

Earning consistently top results throughout the season, both Verstappen and Hamilton have a firm lead in the championship standings, over 150 points clear of the third highest scoring driver Valterri Bottas. Moreover, the two seem to have brought the majority of points for their respective constructors, with Verstappen at 67.5 percent and Hamilton at 63.2 percent.

# add point finishes in sprint qualifying to race_results
sprint_results_points <- sprint_results %>% filter(PTS>0) 
race_results <- rbind(race_results,sprint_results_points)

driverpoints <- race_results %>%
  group_by(Driver,Car) %>%
  summarise(driverPoints = sum(PTS))

teampoints <- race_results %>%
  group_by(Car) %>%
  summarise(teamPoints = sum(PTS))

driverpoints %>%
  left_join(teampoints,by='Car') %>%
  arrange(-driverPoints) %>%
  mutate(pcntTeamPoints=round(driverPoints/teamPoints,3)) %>%
  kbl(col.names=c('Driver','Constructor','Driver Points',
                  'Constructor Points','Percent of Constructor Points'),
      align='llccc') %>%
  kable_styling(full_width = F) %>%
  kable_material_dark() %>%
  row_spec(c(1,2),background='lightyellow',color='black')
Driver Constructor Driver Points Constructor Points Percent of Constructor Points
Max Verstappen VER Red Bull Racing Honda 395.5 585.5 0.675
Lewis Hamilton HAM Mercedes 387.5 613.5 0.632
Valtteri Bottas BOT Mercedes 226.0 613.5 0.368
Sergio Perez PER Red Bull Racing Honda 190.0 585.5 0.325
Carlos Sainz SAI Ferrari 164.5 323.5 0.509
Lando Norris NOR McLaren Mercedes 160.0 275.0 0.582
Charles Leclerc LEC Ferrari 159.0 323.5 0.491
Daniel Ricciardo RIC McLaren Mercedes 115.0 275.0 0.418
Pierre Gasly GAS AlphaTauri Honda 110.0 142.0 0.775
Fernando Alonso ALO Alpine Renault 81.0 155.0 0.523
Esteban Ocon OCO Alpine Renault 74.0 155.0 0.477
Sebastian Vettel VET Aston Martin Mercedes 43.0 77.0 0.558
Lance Stroll STR Aston Martin Mercedes 34.0 77.0 0.442
Yuki Tsunoda TSU AlphaTauri Honda 32.0 142.0 0.225
George Russell RUS Williams Mercedes 16.0 23.0 0.696
Kimi Räikkönen RAI Alfa Romeo Racing Ferrari 10.0 13.0 0.769
Nicholas Latifi LAT Williams Mercedes 7.0 23.0 0.304
Antonio Giovinazzi GIO Alfa Romeo Racing Ferrari 3.0 13.0 0.231
Mick Schumacher MSC Haas Ferrari 0.0 0.0 NaN
Nikita Mazepin MAZ Haas Ferrari 0.0 0.0 NaN
Robert Kubica KUB Alfa Romeo Racing Ferrari 0.0 13.0 0.000

Cumative Points Scored by Race by Driver

A visual on the cumulative points scored race by race illustrates how the lead changed hands a total of six times in 2021. Overall, it seems like Verstappen was ahead more often than Hamilton was.

race_points_cum <- race_results %>% 
  group_by(Driver,gp_name) %>% 
  summarise(totalPoints=sum(PTS)) %>%
  mutate(cumPoints=cumsum(totalPoints)) %>% 
  select(Driver,gp_name,cumPoints)

levels(race_points_cum$gp_name) <- c('BHR','EMI','POR','ESP','MON','AZE','FRA','STY','AUT','GBR',
                                     'HUN','BEL','NED','ITA','RUS','TUR','USA','MXC','SAP','QAT',
                                     'SAU','ABU')

# table
race_points_cum %>%
  pivot_wider(names_from=gp_name,values_from=cumPoints) %>%
  arrange(-ABU) %>%
  kbl() %>%
  kable_styling(full_width=F) %>%
  kable_material_dark() %>%
  row_spec(c(1,2),background='lightyellow',color='black')
Driver BHR EMI POR ESP MON AZE FRA STY AUT GBR HUN BEL NED ITA RUS TUR USA MXC SAP QAT SAU ABU
Max Verstappen VER 18 43 61 80 105 105 131 156 182 185 187 199.5 224.5 226.5 244.5 262.5 287.5 312.5 332.5 351.5 369.5 395.5
Lewis Hamilton HAM 25 44 69 94 101 101 119 138 150 177 195 202.5 221.5 221.5 246.5 256.5 275.5 293.5 318.5 343.5 369.5 387.5
Valtteri Bottas BOT 16 16 32 47 47 47 59 74 92 108 108 108.0 123.0 141.0 151.0 177.0 185.0 185.0 203.0 203.0 218.0 226.0
Sergio Perez PER 10 10 22 32 44 69 84 96 104 104 104 104.0 108.0 118.0 120.0 135.0 150.0 165.0 178.0 190.0 190.0 190.0
Carlos Sainz SAI 4 14 14 20 38 42 42 50 60 68 83 83.5 89.5 97.5 112.5 116.5 122.5 130.5 139.5 145.5 149.5 164.5
Lando Norris NOR 12 27 37 41 56 66 76 86 101 113 113 113.0 114.0 132.0 139.0 145.0 149.0 150.0 151.0 153.0 154.0 160.0
Charles Leclerc LEC 8 20 28 40 40 52 52 58 62 80 80 82.0 92.0 104.0 104.0 116.0 128.0 138.0 148.0 152.0 158.0 159.0
Daniel Ricciardo RIC 6 14 16 24 24 26 34 34 40 50 50 56.0 56.0 83.0 95.0 95.0 105.0 105.0 105.0 105.0 115.0 115.0
Pierre Gasly GAS 0 6 7 8 16 31 37 37 39 39 50 54.0 66.0 66.0 66.0 74.0 74.0 86.0 92.0 92.0 100.0 110.0
Fernando Alonso ALO 0 1 5 5 5 13 17 19 20 26 38 38.0 46.0 50.0 58.0 58.0 58.0 60.0 62.0 77.0 77.0 81.0
Esteban Ocon OCO 0 2 8 10 12 12 12 12 12 14 39 42.0 44.0 45.0 45.0 46.0 46.0 46.0 50.0 60.0 72.0 74.0
Sebastian Vettel VET 0 0 0 0 10 28 30 30 30 30 30 35.0 35.0 35.0 35.0 35.0 36.0 42.0 42.0 43.0 43.0 43.0
Lance Stroll STR 1 5 5 5 9 9 10 14 14 18 18 18.0 18.0 24.0 24.0 26.0 26.0 26.0 26.0 34.0 34.0 34.0
Yuki Tsunoda TSU 2 2 2 2 2 8 8 9 9 10 18 18.0 18.0 18.0 18.0 18.0 20.0 20.0 20.0 20.0 20.0 32.0
George Russell RUS 0 0 0 0 0 0 0 0 0 0 4 13.0 13.0 15.0 16.0 16.0 16.0 16.0 16.0 16.0 16.0 16.0
Kimi Räikkönen RAI 0 0 0 0 0 1 1 1 1 1 2 2.0 NA NA 6.0 6.0 6.0 10.0 10.0 10.0 10.0 10.0
Nicholas Latifi LAT 0 0 0 0 0 0 0 0 0 0 6 7.0 7.0 7.0 7.0 7.0 7.0 7.0 7.0 7.0 7.0 7.0
Antonio Giovinazzi GIO 0 0 0 0 1 1 1 1 1 1 1 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 3.0 3.0
Mick Schumacher MSC 0 0 0 0 0 0 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Nikita Mazepin MAZ 0 0 0 0 0 0 0 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
Robert Kubica KUB NA NA NA NA NA NA NA NA NA NA NA NA 0.0 0.0 NA NA NA NA NA NA NA NA
# plot
race_points_cum %>% 
  filter(Driver=='Max Verstappen VER'|Driver=='Lewis Hamilton HAM') %>% 
  
  ggplot() + 
  geom_line(aes(x=gp_name,y=cumPoints,colour=Driver,group=Driver)) +
  labs(title='Cumulative Points Scored by Race by Driver') +
  scale_y_continuous(breaks=c(0,50,100,150,200,250,300,350,400),expand=c(0,0)) +
  scale_colour_manual(values=c(mercedes_clr,redbull_clr)) +
  hui_ggplot

Points Scored by Race by Driver

A breakdown of points scored per Grand Prix allows for a more detailed view of how the drivers performed throughout the season. For instance, the troughs on the dark blue line see Verstappen scoring minimal points in Azerbaijan, Great Britain, Hungary, and Italy mostly due to retirements or technical issues. The extended peak from France to Austria represents a particular run of good form.

On the light blue line, the highest point at the middle shows Lewis Hamilton achieving more points than any other weekend due to a second place finish at the sprint qualifying and a victory at the race. The three race peak covering Sao Paolo, Qatar, and Saudi Arabia portrays a slew of good results that helped him catch up to Verstappen in the championship. The datapoints in the middle section represents several mediocre results.

On balance, Verstappen achieves better results more often than Hamilton during the 2021 season. However, his consistently good showings are counteracted by a couple very poor finishes. So although Hamilton does not win as much as Verstappen, he is able to stay in the title fight because his lows are not as low. Also unsurprisingly, whenever one wins, the other usually picks up a second place finish.

race_points <- race_results %>% 
  group_by(Driver,gp_name) %>% 
  summarise(totalPoints=sum(PTS)) %>%
  select(Driver,gp_name,totalPoints)

levels(race_points$gp_name) <- c('BHR','EMI','POR','ESP','MON','AZE','FRA','STY','AUT','GBR',
                                     'HUN','BEL','NED','ITA','RUS','TUR','USA','MXC','SAP','QAT',
                                     'SAU','ABU')

# table
race_points %>% 
  pivot_wider(names_from=gp_name,values_from=totalPoints) %>%
  arrange(-ABU) %>%
  kbl() %>%
  kable_styling(full_width = F) %>%
  kable_material_dark() %>%
  row_spec(c(1,2),background='lightyellow',color='black')
Driver BHR EMI POR ESP MON AZE FRA STY AUT GBR HUN BEL NED ITA RUS TUR USA MXC SAP QAT SAU ABU
Max Verstappen VER 18 25 18 19 25 0 26 25 26 3 2 12.5 25 2 18 18 25 25 20 19 18 26
Lewis Hamilton HAM 25 19 25 25 7 0 18 19 12 27 18 7.5 19 0 25 10 19 18 25 25 26 18
Carlos Sainz SAI 4 10 0 6 18 4 0 8 10 8 15 0.5 6 8 15 4 6 8 9 6 4 15
Yuki Tsunoda TSU 2 0 0 0 0 6 0 1 0 1 8 0.0 0 0 0 0 2 0 0 0 0 12
Pierre Gasly GAS 0 6 1 1 8 15 6 0 2 0 11 4.0 12 0 0 8 0 12 6 0 8 10
Valtteri Bottas BOT 16 0 16 15 0 0 12 15 18 16 0 0.0 15 18 10 26 8 0 18 0 15 8
Lando Norris NOR 12 15 10 4 15 10 10 10 15 12 0 0.0 1 18 7 6 4 1 1 2 1 6
Fernando Alonso ALO 0 1 4 0 0 8 4 2 1 6 12 0.0 8 4 8 0 0 2 2 15 0 4
Esteban Ocon OCO 0 2 6 2 2 0 0 0 0 2 25 3.0 2 1 0 1 0 0 4 10 12 2
Charles Leclerc LEC 8 12 8 12 0 12 0 6 4 18 0 2.0 10 12 0 12 12 10 10 4 6 1
Antonio Giovinazzi GIO 0 0 0 0 1 0 0 0 0 0 0 0.0 0 0 0 0 0 0 0 0 2 0
Daniel Ricciardo RIC 6 8 2 8 0 2 8 0 6 10 0 6.0 0 27 12 0 10 0 0 0 10 0
George Russell RUS 0 0 0 0 0 0 0 0 0 0 4 9.0 0 2 1 0 0 0 0 0 0 0
Kimi Räikkönen RAI 0 0 0 0 0 1 0 0 0 0 1 0.0 NA NA 4 0 0 4 0 0 0 0
Lance Stroll STR 1 4 0 0 4 0 1 4 0 4 0 0.0 0 6 0 2 0 0 0 8 0 0
Mick Schumacher MSC 0 0 0 0 0 0 0 0 0 0 0 0.0 0 0 0 0 0 0 0 0 0 0
Nicholas Latifi LAT 0 0 0 0 0 0 0 0 0 0 6 1.0 0 0 0 0 0 0 0 0 0 0
Nikita Mazepin MAZ 0 0 0 0 0 0 0 0 0 0 0 0.0 0 0 0 0 0 0 0 0 0 0
Sebastian Vettel VET 0 0 0 0 10 18 2 0 0 0 0 5.0 0 0 0 0 1 6 0 1 0 0
Sergio Perez PER 10 0 12 10 12 25 15 12 8 0 0 0.0 4 10 2 15 15 15 13 12 0 0
Robert Kubica KUB NA NA NA NA NA NA NA NA NA NA NA NA 0 0 NA NA NA NA NA NA NA NA
# plot
race_points %>% 
  filter(Driver=='Max Verstappen VER'|Driver=='Lewis Hamilton HAM') %>%
  
  ggplot() + 
  geom_line(aes(x=gp_name,y=totalPoints,colour=Driver,group=Driver)) +
  labs(title='Points Scored by Race by Driver') +
  scale_y_continuous(breaks=c(0,1,2,4,6,8,10,12,15,18,25,27),expand=c(0,0)) +
  scale_colour_manual(values=c(mercedes_clr,redbull_clr)) +
  hui_ggplot

3 Race Rolling Average Points by Driver

Form is temporary but it can nevertheless be analysed. To study how performance fluctuated across the season between Verstappen and Hamilton, we plot a 3 race rolling average for points scored. This graph more clearly identifies the peak of Verstappen’s form at the Styrian GP right before the midway point. It also shows a subsequent dip bottoming out at the Hungarian GP before rising back up by the Mexican GP. In comparison, Hamilton generally did not achieve a run of form as high. He delivered results during Verstappen’s drop-off midseason, but really peaked only over the last few races, especially Qatar.

race_points_roll3 <- race_points %>% 
  mutate(totalPoints_roll3=rollmean(totalPoints,k=3,fill=NA)) %>%
  select(Driver,gp_name,totalPoints_roll3)

levels(race_points_roll3$gp_name) <- c('BHR','EMI','POR','ESP','MON','AZE','FRA','STY','AUT','GBR',
                                     'HUN','BEL','NED','ITA','RUS','TUR','USA','MXC','SAP','QAT',
                                     'SAU','ABU')
# table
race_points_roll3 %>% 
  pivot_wider(names_from=gp_name,values_from=totalPoints_roll3) %>%
  arrange(-ABU) %>%
  kbl() %>%
  kable_styling(full_width = F) %>%
  kable_material_dark() %>%
  row_spec(c(1,2),background='lightyellow',color='black')
Driver BHR EMI POR ESP MON AZE FRA STY AUT GBR HUN BEL NED ITA RUS TUR USA MXC SAP QAT SAU ABU
Antonio Giovinazzi GIO NA 0.0000000 0.000000 0.3333333 0.3333333 0.3333333 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.000000 0.0000000 0.0000000 0.0000000 0.0000000 0.000000 0.6666667 0.6666667 NA
Carlos Sainz SAI NA 4.6666667 5.333333 8.0000000 9.3333333 7.3333333 4.0000000 6.0000000 8.6666667 11.0000000 7.8333333 7.1666667 4.8333333 9.666667 9.0000000 8.3333333 6.0000000 7.6666667 7.666667 6.3333333 8.3333333 NA
Charles Leclerc LEC NA 9.3333333 10.666667 6.6666667 8.0000000 4.0000000 6.0000000 3.3333333 9.3333333 7.3333333 6.6666667 4.0000000 8.0000000 7.333333 8.0000000 8.0000000 11.3333333 10.6666667 8.000000 6.6666667 3.6666667 NA
Daniel Ricciardo RIC NA 5.3333333 6.000000 3.3333333 3.3333333 3.3333333 3.3333333 4.6666667 5.3333333 5.3333333 5.3333333 2.0000000 11.0000000 13.000000 13.0000000 7.3333333 3.3333333 3.3333333 0.000000 3.3333333 3.3333333 NA
Esteban Ocon OCO NA 2.6666667 3.333333 3.3333333 1.3333333 0.6666667 0.0000000 0.0000000 0.6666667 9.0000000 10.0000000 10.0000000 2.0000000 1.000000 0.6666667 0.3333333 0.3333333 1.3333333 4.666667 8.6666667 8.0000000 NA
Fernando Alonso ALO NA 1.6666667 1.666667 1.3333333 2.6666667 4.0000000 4.6666667 2.3333333 3.0000000 6.3333333 6.0000000 6.6666667 4.0000000 6.666667 4.0000000 2.6666667 0.6666667 1.3333333 6.333333 5.6666667 6.3333333 NA
George Russell RUS NA 0.0000000 0.000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 1.3333333 4.3333333 4.3333333 3.6666667 1.000000 1.0000000 0.3333333 0.0000000 0.0000000 0.000000 0.0000000 0.0000000 NA
Kimi Räikkönen RAI NA 0.0000000 0.000000 0.0000000 0.3333333 0.3333333 0.3333333 0.0000000 0.0000000 0.3333333 0.3333333 1.6666667 NA NA 1.3333333 1.3333333 1.3333333 1.3333333 1.333333 0.0000000 0.0000000 NA
Lance Stroll STR NA 1.6666667 1.333333 1.3333333 1.3333333 1.6666667 1.6666667 1.6666667 2.6666667 1.3333333 1.3333333 0.0000000 2.0000000 2.000000 2.6666667 0.6666667 0.6666667 0.0000000 2.666667 2.6666667 2.6666667 NA
Lando Norris NOR NA 12.3333333 9.666667 9.6666667 9.6666667 11.6666667 10.0000000 11.6666667 12.3333333 9.0000000 4.0000000 0.3333333 6.3333333 8.666667 10.3333333 5.6666667 3.6666667 2.0000000 1.333333 1.3333333 3.0000000 NA
Lewis Hamilton HAM NA 23.0000000 23.000000 19.0000000 10.6666667 8.3333333 12.3333333 16.3333333 19.3333333 19.0000000 17.5000000 14.8333333 8.8333333 14.666667 11.6666667 18.0000000 15.6666667 20.6666667 22.666667 25.3333333 23.0000000 NA
Max Verstappen VER NA 20.3333333 20.666667 20.6666667 14.6666667 17.0000000 17.0000000 25.6666667 18.0000000 10.3333333 5.8333333 13.1666667 13.1666667 15.000000 12.6666667 20.3333333 22.6666667 23.3333333 21.333333 19.0000000 21.0000000 NA
Mick Schumacher MSC NA 0.0000000 0.000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.000000 0.0000000 0.0000000 0.0000000 0.0000000 0.000000 0.0000000 0.0000000 NA
Nicholas Latifi LAT NA 0.0000000 0.000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 2.0000000 2.3333333 2.3333333 0.3333333 0.000000 0.0000000 0.0000000 0.0000000 0.0000000 0.000000 0.0000000 0.0000000 NA
Nikita Mazepin MAZ NA 0.0000000 0.000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.000000 0.0000000 0.0000000 0.0000000 0.0000000 0.000000 0.0000000 0.0000000 NA
Pierre Gasly GAS NA 2.3333333 2.666667 3.3333333 8.0000000 9.6666667 7.0000000 2.6666667 0.6666667 4.3333333 5.0000000 9.0000000 5.3333333 4.000000 2.6666667 2.6666667 6.6666667 6.0000000 6.000000 4.6666667 6.0000000 NA
Robert Kubica KUB NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA NA
Sebastian Vettel VET NA 0.0000000 0.000000 3.3333333 9.3333333 10.0000000 6.6666667 0.6666667 0.0000000 0.0000000 1.6666667 1.6666667 1.6666667 0.000000 0.0000000 0.3333333 2.3333333 2.3333333 2.333333 0.3333333 0.3333333 NA
Sergio Perez PER NA 7.3333333 7.333333 11.3333333 15.6666667 17.3333333 17.3333333 11.6666667 6.6666667 2.6666667 0.0000000 1.3333333 4.6666667 5.333333 9.0000000 10.6666667 15.0000000 14.3333333 13.333333 8.3333333 4.0000000 NA
Valtteri Bottas BOT NA 10.6666667 10.333333 10.3333333 5.0000000 4.0000000 9.0000000 15.0000000 16.3333333 11.3333333 5.3333333 5.0000000 11.0000000 14.333333 18.0000000 14.6666667 11.3333333 8.6666667 6.000000 11.0000000 7.6666667 NA
Yuki Tsunoda TSU NA 0.6666667 0.000000 0.0000000 2.0000000 2.0000000 2.3333333 0.3333333 0.6666667 3.0000000 3.0000000 2.6666667 0.0000000 0.000000 0.0000000 0.6666667 0.6666667 0.6666667 0.000000 0.0000000 4.0000000 NA
# plot
race_points_roll3 %>% 
  filter(Driver=='Max Verstappen VER'|Driver=='Lewis Hamilton HAM') %>% 
  
  ggplot() + 
  geom_line(aes(x=gp_name,y=totalPoints_roll3,colour=Driver,group=Driver)) +
  labs(title='3 Race Rolling Average Points by Driver') +
  scale_y_continuous(breaks=c(0,1,2,4,6,8,10,12,15,18,25,27),expand=c(0,0)) +
  scale_colour_manual(values=c(mercedes_clr,redbull_clr)) +
  hui_ggplot

Some Key Takeaways

  • Verstappen has the most wins, most podiums, and highest average race finish position
  • Hamilton tends to take fastest laps towards the end of a race as part of damage limitation
  • Verstappen achieved better results more often than Hamilton
  • Hamilton kept the championship fight alive with consistency (his lows were not as low) and a much greater points haul in the Great Britain GP
  • Verstappen was in peak form during the few races before (France, Styria, Austria) and after (Turkey, USA, Mexico) the mid season
  • Hamilton was in peak form during the last few races of the season (Sao Paolo, Qatar, Saudi Arabia)