# Data Source : https://api.openf1.org/v1/laps?driver_number=3&csv=true

library(readr)
library(dplyr)
## Warning: package 'dplyr' was built under R version 4.3.3
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(stringr)
library(shiny)
## Warning: package 'shiny' was built under R version 4.3.3
library(plotly)
## Loading required package: ggplot2
## 
## Attaching package: 'plotly'
## The following object is masked from 'package:ggplot2':
## 
##     last_plot
## The following object is masked from 'package:stats':
## 
##     filter
## The following object is masked from 'package:graphics':
## 
##     layout
library(tidyr)
## Warning: package 'tidyr' was built under R version 4.3.3
library(lme4)
## Warning: package 'lme4' was built under R version 4.3.3
## Loading required package: Matrix
## 
## Attaching package: 'Matrix'
## The following objects are masked from 'package:tidyr':
## 
##     expand, pack, unpack
## Warning in check_dep_version(): ABI version mismatch: 
## lme4 was built with Matrix ABI version 1
## Current Matrix ABI version is 0
## Please re-install lme4 from source or restore original 'Matrix' package
library(ggplot2)
library(scales)
## 
## Attaching package: 'scales'
## The following object is masked from 'package:readr':
## 
##     col_factor
library(corrplot)
## corrplot 0.92 loaded
library(caret)
## Loading required package: lattice
library(gridExtra)
## 
## Attaching package: 'gridExtra'
## The following object is masked from 'package:dplyr':
## 
##     combine
df <- read_csv("Verstappen.csv")
## Rows: 4930 Columns: 24
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr   (6): meeting_key, segments_sector_1, segments_sector_2, segments_secto...
## dbl  (16): driver_number, duration_sector_1, duration_sector_2, duration_sec...
## lgl   (1): is_pit_out_lap
## dttm  (1): date_start
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
newdf <- df %>%
  filter(
    `session key name` == "Race"
  )
View(newdf)
# Data Cleaning
df <- df %>%
  filter(!is.na(date_start))


df <- df %>%
  filter(
    !is.na(duration_sector_1) & str_trim(duration_sector_1)!= "",
    !is.na(duration_sector_2) & str_trim(duration_sector_2) != "",
    !is.na(duration_sector_3) & str_trim(duration_sector_3) != "",
  )

df <- df %>%
  filter(
    !is.na(i1_speed),
    !is.na(i2_speed),
    !is.na(st_speed),
  )

View(df)
unique(df$`meeting key name`)
##  [1] "Hungary"              "Belgium"              "Netherlands"         
##  [4] "United States"        "Mexico"               "Brazil"              
##  [7] "United Arab Emirates" "Bahrain"              "Saudi Arabia"        
## [10] "Australia"            "Japan"                "China"               
## [13] "Italy"                "Monaco"               "Canada"              
## [16] "Spain"                "Austria"              "United Kingdom"      
## [19] "Azerbaijan"           "Singapore"
# Structure
str(newdf)
## spc_tbl_ [1,764 × 24] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ date_start       : POSIXct[1:1764], format: "2023-07-23 13:03:06" "2023-07-23 13:04:44" ...
##  $ driver_number    : num [1:1764] 3 3 3 3 3 3 3 3 3 3 ...
##  $ duration_sector_1: num [1:1764] 40.9 31.1 30.9 30.5 30.5 ...
##  $ duration_sector_2: num [1:1764] 32.7 32.3 31.7 31.6 31.4 ...
##  $ duration_sector_3: num [1:1764] 24.8 24.5 24.4 24.5 24.4 ...
##  $ i1_speed         : num [1:1764] 276 271 269 274 278 275 277 276 278 276 ...
##  $ i2_speed         : num [1:1764] 227 225 227 227 228 231 230 228 231 231 ...
##  $ is_pit_out_lap   : logi [1:1764] FALSE FALSE FALSE FALSE FALSE FALSE ...
##  $ lap_duration     : num [1:1764] 98.5 87.9 86.9 86.6 86.2 ...
##  $ lap_number       : num [1:1764] 1 2 3 4 5 6 7 8 9 10 ...
##  $ meeting_key      : chr [1:1764] "1215" "1215" "1215" "1215" ...
##  $ segments_sector_1: chr [1:1764] "[2049, 2049, 2049, 2049, 2049, 2049, 2049]" "[None, 2049, 2049, 2049, 2049, 2048, 2048]" "[None, 2049, 2049, 2049, 2048, 2048, 2048]" "[None, 2049, 2048, 2049, 2049, 2049, 2048]" ...
##  $ segments_sector_2: chr [1:1764] "[2049, 2049, 2049, 2049, 2049, 2049, 2049, 2049]" "[2048, 2049, 2048, 2049, 2049, 2049, 2048, 2049]" "[2048, 2049, 2049, 2049, 2049, 2049, 2049, 2049]" "[2049, 2049, 2048, 2048, 2049, 2048, 2049, 2049]" ...
##  $ segments_sector_3: chr [1:1764] "[2049, 2049, 2049, 2049, 0]" "[2048, 2049, 2049, 2048, 2049]" "[2049, 2049, 2049, 2048]" "[2049, 2048, 2048, 2048]" ...
##  $ session_key      : num [1:1764] 9133 9133 9133 9133 9133 ...
##  $ st_speed         : num [1:1764] 265 290 NA 304 301 NA NA 302 300 299 ...
##  $ meeting key name : chr [1:1764] "Hungary" "Hungary" "Hungary" "Hungary" ...
##  $ session key name : chr [1:1764] "Race" "Race" "Race" "Race" ...
##  $ year             : num [1:1764] 2023 2023 2023 2023 2023 ...
##  $ Win              : num [1:1764] 0 0 0 0 0 0 0 0 0 0 ...
##  $ Total km         : num [1:1764] 4.38 4.38 4.38 4.38 4.38 ...
##  $ Sector 1 km      : num [1:1764] 1.85 1.85 1.85 1.85 1.85 1.85 1.85 1.85 1.85 1.85 ...
##  $ Sector 2 km      : num [1:1764] 1.43 1.43 1.43 1.43 1.43 1.43 1.43 1.43 1.43 1.43 ...
##  $ Sector 3 km      : num [1:1764] 1.1 1.1 1.1 1.1 1.1 1.1 1.1 1.1 1.1 1.1 ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   date_start = col_datetime(format = ""),
##   ..   driver_number = col_double(),
##   ..   duration_sector_1 = col_double(),
##   ..   duration_sector_2 = col_double(),
##   ..   duration_sector_3 = col_double(),
##   ..   i1_speed = col_double(),
##   ..   i2_speed = col_double(),
##   ..   is_pit_out_lap = col_logical(),
##   ..   lap_duration = col_double(),
##   ..   lap_number = col_double(),
##   ..   meeting_key = col_character(),
##   ..   segments_sector_1 = col_character(),
##   ..   segments_sector_2 = col_character(),
##   ..   segments_sector_3 = col_character(),
##   ..   session_key = col_double(),
##   ..   st_speed = col_double(),
##   ..   `meeting key name` = col_character(),
##   ..   `session key name` = col_character(),
##   ..   year = col_double(),
##   ..   Win = col_double(),
##   ..   `Total km` = col_double(),
##   ..   `Sector 1 km` = col_double(),
##   ..   `Sector 2 km` = col_double(),
##   ..   `Sector 3 km` = col_double()
##   .. )
##  - attr(*, "problems")=<externalptr>
# Dimensions
dim(newdf)
## [1] 1764   24
# Variable names
names(newdf)
##  [1] "date_start"        "driver_number"     "duration_sector_1"
##  [4] "duration_sector_2" "duration_sector_3" "i1_speed"         
##  [7] "i2_speed"          "is_pit_out_lap"    "lap_duration"     
## [10] "lap_number"        "meeting_key"       "segments_sector_1"
## [13] "segments_sector_2" "segments_sector_3" "session_key"      
## [16] "st_speed"          "meeting key name"  "session key name" 
## [19] "year"              "Win"               "Total km"         
## [22] "Sector 1 km"       "Sector 2 km"       "Sector 3 km"
# Data types
glimpse(newdf)
## Rows: 1,764
## Columns: 24
## $ date_start         <dttm> 2023-07-23 13:03:06, 2023-07-23 13:04:44, 2023-07-…
## $ driver_number      <dbl> 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, …
## $ duration_sector_1  <dbl> 40.936, 31.082, 30.874, 30.520, 30.494, 30.445, 30.…
## $ duration_sector_2  <dbl> 32.713, 32.294, 31.653, 31.638, 31.370, 31.473, 31.…
## $ duration_sector_3  <dbl> 24.834, 24.522, 24.351, 24.475, 24.372, 24.283, 24.…
## $ i1_speed           <dbl> 276, 271, 269, 274, 278, 275, 277, 276, 278, 276, N…
## $ i2_speed           <dbl> 227, 225, 227, 227, 228, 231, 230, 228, 231, 231, 2…
## $ is_pit_out_lap     <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FA…
## $ lap_duration       <dbl> 98.483, 87.898, 86.878, 86.633, 86.236, 86.201, 86.…
## $ lap_number         <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, …
## $ meeting_key        <chr> "1215", "1215", "1215", "1215", "1215", "1215", "12…
## $ segments_sector_1  <chr> "[2049, 2049, 2049, 2049, 2049, 2049, 2049]", "[Non…
## $ segments_sector_2  <chr> "[2049, 2049, 2049, 2049, 2049, 2049, 2049, 2049]",…
## $ segments_sector_3  <chr> "[2049, 2049, 2049, 2049, 0]", "[2048, 2049, 2049, …
## $ session_key        <dbl> 9133, 9133, 9133, 9133, 9133, 9133, 9133, 9133, 913…
## $ st_speed           <dbl> 265, 290, NA, 304, 301, NA, NA, 302, 300, 299, 285,…
## $ `meeting key name` <chr> "Hungary", "Hungary", "Hungary", "Hungary", "Hungar…
## $ `session key name` <chr> "Race", "Race", "Race", "Race", "Race", "Race", "Ra…
## $ year               <dbl> 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 202…
## $ Win                <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
## $ `Total km`         <dbl> 4.381, 4.381, 4.381, 4.381, 4.381, 4.381, 4.381, 4.…
## $ `Sector 1 km`      <dbl> 1.85, 1.85, 1.85, 1.85, 1.85, 1.85, 1.85, 1.85, 1.8…
## $ `Sector 2 km`      <dbl> 1.43, 1.43, 1.43, 1.43, 1.43, 1.43, 1.43, 1.43, 1.4…
## $ `Sector 3 km`      <dbl> 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1.1, 1…
# Summary statistics
summary(newdf)
##    date_start                     driver_number duration_sector_1
##  Min.   :2023-07-23 13:03:06.13   Min.   :3     Min.   :17.18    
##  1st Qu.:2024-03-02 15:43:10.75   1st Qu.:3     1st Qu.:24.11    
##  Median :2024-06-23 13:06:50.65   Median :3     Median :28.88    
##  Mean   :2024-08-31 22:19:19.94   Mean   :3     Mean   :28.48    
##  3rd Qu.:2024-09-15 11:39:57.11   3rd Qu.:3     3rd Qu.:31.21    
##  Max.   :2026-06-14 14:35:13.49   Max.   :3     Max.   :96.40    
##                                                 NA's   :14       
##  duration_sector_2 duration_sector_3    i1_speed        i2_speed  
##  Min.   :17.54     Min.   :16.99     Min.   : 76.0   Min.   : 53  
##  1st Qu.:29.75     1st Qu.:23.75     1st Qu.:214.0   1st Qu.:209  
##  Median :33.19     Median :26.32     Median :269.0   Median :256  
##  Mean   :34.51     Mean   :28.44     Mean   :256.1   Mean   :247  
##  3rd Qu.:39.64     3rd Qu.:32.24     3rd Qu.:287.0   3rd Qu.:278  
##  Max.   :73.46     Max.   :75.06     Max.   :345.0   Max.   :346  
##  NA's   :5         NA's   :7         NA's   :280     NA's   :5    
##  is_pit_out_lap   lap_duration       lap_number    meeting_key       
##  Mode :logical   Min.   :  70.43   Min.   : 1.00   Length:1764       
##  FALSE:1711      1st Qu.:  82.01   1st Qu.:15.00   Class :character  
##  TRUE :53        Median :  86.88   Median :30.00   Mode  :character  
##                  Mean   :  93.02   Mean   :30.69                     
##                  3rd Qu.:  98.59   3rd Qu.:45.00                     
##                  Max.   :1679.11   Max.   :76.00                     
##                  NA's   :7                                           
##  segments_sector_1  segments_sector_2  segments_sector_3   session_key   
##  Length:1764        Length:1764        Length:1764        Min.   : 9133  
##  Class :character   Class :character   Class :character   1st Qu.: 9472  
##  Mode  :character   Mode  :character   Mode  :character   Median : 9539  
##                                                           Mean   : 9801  
##                                                           3rd Qu.: 9606  
##                                                           Max.   :11307  
##                                                                          
##     st_speed     meeting key name   session key name        year     
##  Min.   : 38.0   Length:1764        Length:1764        Min.   :2023  
##  1st Qu.:290.0   Class :character   Class :character   1st Qu.:2024  
##  Median :303.0   Mode  :character   Mode  :character   Median :2024  
##  Mean   :298.7                                         Mean   :2024  
##  3rd Qu.:315.0                                         3rd Qu.:2024  
##  Max.   :355.0                                         Max.   :2026  
##  NA's   :141                                                         
##       Win            Total km      Sector 1 km     Sector 2 km   
##  Min.   :0.0000   Min.   :3.337   Min.   :1.200   Min.   :1.150  
##  1st Qu.:0.0000   1st Qu.:4.361   1st Qu.:1.650   1st Qu.:1.550  
##  Median :0.0000   Median :5.278   Median :1.850   Median :1.750  
##  Mean   :0.3203   Mean   :5.077   Mean   :1.858   Mean   :1.753  
##  3rd Qu.:1.0000   3rd Qu.:5.513   3rd Qu.:2.100   3rd Qu.:1.900  
##  Max.   :1.0000   Max.   :7.004   Max.   :2.300   Max.   :2.650  
##                                                                  
##   Sector 3 km   
##  Min.   :0.990  
##  1st Qu.:1.260  
##  Median :1.560  
##  Mean   :1.465  
##  3rd Qu.:1.620  
##  Max.   :2.050  
## 
# Count missing values
colSums(is.na(newdf))
##        date_start     driver_number duration_sector_1 duration_sector_2 
##                 0                 0                14                 5 
## duration_sector_3          i1_speed          i2_speed    is_pit_out_lap 
##                 7               280                 5                 0 
##      lap_duration        lap_number       meeting_key segments_sector_1 
##                 7                 0                 0                 2 
## segments_sector_2 segments_sector_3       session_key          st_speed 
##                 2                 3                 0               141 
##  meeting key name  session key name              year               Win 
##                 0                 0                 0                 0 
##          Total km       Sector 1 km       Sector 2 km       Sector 3 km 
##                 0                 0                 0                 0
sum(duplicated(newdf))
## [1] 0
table(newdf$Win)
## 
##    0    1 
## 1199  565
prop.table(table(newdf$Win))
## 
##         0         1 
## 0.6797052 0.3202948
ggplot(newdf,
       aes(x=factor(Win),
           fill=factor(Win)))+

geom_bar()+

geom_text(stat="count",
          aes(label=after_stat(count)),
          vjust=-0.5)+

labs(title="Distribution of Win",
     x="Win",
     y="Count")+

theme_minimal()

lap_analysis <- df %>%
  group_by(year, `meeting key name`) %>%
  arrange(lap_number) %>%
  mutate(
    avg_lap_duration = cummean(lap_duration)
  )

# Example Lap 20:

lap20 <- lap_analysis %>%
  filter(lap_number == 20)

# Logistic Regression Model:

model1 <- glm(
  Win ~ avg_lap_duration,
  data= lap20,
  family = binomial
)

summary(model1)
## 
## Call:
## glm(formula = Win ~ avg_lap_duration, family = binomial, data = lap20)
## 
## Coefficients:
##                  Estimate Std. Error z value Pr(>|z|)
## (Intercept)      -4.44148    3.02163   -1.47    0.142
## avg_lap_duration  0.02203    0.02939    0.75    0.453
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 52.429  on 81  degrees of freedom
## Residual deviance: 51.866  on 80  degrees of freedom
## AIC: 55.866
## 
## Number of Fisher Scoring iterations: 5
# Predcit Probability

new_data <- data.frame(
  avg_lap_duration = seq(
    min(lap20$avg_lap_duration),
    max(lap20$avg_lap_duration),
    length.out = 100
  )
)

new_data$win_probability <- predict(
  model1,
  newdata = new_data,
  type = "response"
)

ggplot(new_data,
       aes(avg_lap_duration,
           win_probability)) +
  geom_line(size=1.2) +
  scale_y_continuous(labels=scales::percent) +
  labs(
    x="Average Lap Duration (seconds)",
    y="Probability of Finishing P1",
    title="Chance of Winning vs Average Lap Duration"
  ) +
  theme_minimal()
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

threshold <- 90


fast <- new_data %>%
  filter(avg_lap_duration < threshold) %>%
  summarise(
    avg_probability = mean(win_probability)
  )


slow <- new_data %>%
  filter(avg_lap_duration >= threshold) %>%
  summarise(
    avg_probability = mean(win_probability)
  )


drop_percentage <- 
  (slow$avg_probability - fast$avg_probability) /
  slow$avg_probability * 100


drop_percentage
## [1] 43.77042
# Distance and Pole Position

# Distance and Win Probability

df_distance <- df %>%
  mutate(
    # each completed lap equals one track length
    lap_distance_km = `Total km`
  )


df_distance <- df_distance %>%
  arrange(
    year,
    `meeting key name`,
    lap_number
  ) %>%
  group_by(
    year,
    `meeting key name`
  ) %>%
  mutate(
    cumulative_distance_km = cumsum(lap_distance_km),
    cumulative_duration_min = cumsum(lap_duration) / 60
  ) %>%
  ungroup()


# Choose distance point instead of lap number
# Example: closest point to 200 km

distance_checkpoint <- 200


checkpoint <- df_distance %>%
  group_by(
    year,
    `meeting key name`
  ) %>%
  slice_min(
    abs(cumulative_distance_km - distance_checkpoint),
    with_ties = FALSE
  ) %>%
  ungroup()


# Logistic regression

model <- glm(
  Win ~ cumulative_distance_km,
  data = checkpoint,
  family = binomial
)


summary(model)
## 
## Call:
## glm(formula = Win ~ cumulative_distance_km, family = binomial, 
##     data = checkpoint)
## 
## Coefficients:
##                        Estimate Std. Error z value Pr(>|z|)
## (Intercept)            -7.08064   14.61325  -0.485    0.628
## cumulative_distance_km  0.02576    0.07334   0.351    0.725
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 24.376  on 32  degrees of freedom
## Residual deviance: 24.145  on 31  degrees of freedom
## AIC: 28.145
## 
## Number of Fisher Scoring iterations: 6
# Prediction curve

prediction <- data.frame(
  cumulative_distance_km = seq(
    min(checkpoint$cumulative_distance_km, na.rm=TRUE),
    max(checkpoint$cumulative_distance_km, na.rm=TRUE),
    length.out = 100
  )
)


prediction$win_probability <- predict(
  model,
  newdata = prediction,
  type = "response"
)


# Plot

ggplot(
  prediction,
  aes(
    x = cumulative_distance_km,
    y = win_probability
  )
)+
geom_line(
  linewidth = 1.2
)+
scale_y_continuous(
  labels = scales::percent
)+
labs(
  title="Probability of Finishing P1 vs Distance Completed",
  x="Race Distance Completed (km)",
  y="Probability of P1"
)+
theme_minimal()

Since there are 3158 records with 0 and 619 records with 1, how about if I remove all the races except for Grand Prix:

table(newdf$Win)
## 
##    0    1 
## 1199  565

Checking the curve now:

# Distance and Pole Position

# Distance and Win Probability

df_distance <- newdf %>%
  mutate(
    # each completed lap equals one track length
    lap_distance_km = `Total km`
  )


df_distance <- df_distance %>%
  arrange(
    year,
    `meeting key name`,
    lap_number
  ) %>%
  group_by(
    year,
    `meeting key name`
  ) %>%
  mutate(
    cumulative_distance_km = cumsum(lap_distance_km),
    cumulative_duration_min = cumsum(lap_duration) / 60
  ) %>%
  ungroup()


# Choose distance point instead of lap number
# Example: closest point to 200 km

distance_checkpoint <- 200


checkpoint <- df_distance %>%
  group_by(
    year,
    `meeting key name`
  ) %>%
  slice_min(
    abs(cumulative_distance_km - distance_checkpoint),
    with_ties = FALSE
  ) %>%
  ungroup()


# Logistic regression

mod2 <- glm(
  Win ~ cumulative_distance_km,
  data = checkpoint,
  family = binomial
)


summary(mod2)
## 
## Call:
## glm(formula = Win ~ cumulative_distance_km, family = binomial, 
##     data = checkpoint)
## 
## Coefficients:
##                         Estimate Std. Error z value Pr(>|z|)
## (Intercept)             0.140806   1.447643   0.097    0.923
## cumulative_distance_km -0.004502   0.007549  -0.596    0.551
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 38.191  on 29  degrees of freedom
## Residual deviance: 37.839  on 28  degrees of freedom
## AIC: 41.839
## 
## Number of Fisher Scoring iterations: 4
# Prediction curve

prediction <- data.frame(
  cumulative_distance_km = seq(
    min(checkpoint$cumulative_distance_km, na.rm=TRUE),
    max(checkpoint$cumulative_distance_km, na.rm=TRUE),
    length.out = 100
  )
)


prediction$win_probability <- predict(
  mod2,
  newdata = prediction,
  type = "response"
)


# Plot

ggplot(
  prediction,
  aes(
    x = cumulative_distance_km,
    y = win_probability
  )
)+
geom_line(
  linewidth = 1.2
)+
scale_y_continuous(
  labels = scales::percent
)+
labs(
  title="Probability of Finishing P1 vs Distance Completed",
  x="Race Distance Completed (km)",
  y="Probability of P1"
)+
theme_minimal()

View(newdf)

The chart looks ok now… with increasing distance, the probability of winning decreases:

Now Let’s check with Lap Duration:

#-------------------------------------------------
# Create cumulative distance and elapsed race time
#-------------------------------------------------

df_distance <- newdf %>%
  mutate(
    lap_distance_km = `Total km`
  ) %>%
  arrange(
    year,
    `meeting key name`,
    lap_number
  ) %>%
  group_by(
    year,
    `meeting key name`
  ) %>%
  mutate(
    cumulative_distance_km = cumsum(lap_distance_km),

    # Sector times are in seconds
    cumulative_duration_min =
      cumsum(
        duration_sector_1 +
        duration_sector_2 +
        duration_sector_3
      ) / 60
  ) %>%
  ungroup()

#-------------------------------------------------
# Logistic Regression
#-------------------------------------------------

mod2 <- glm(
  Win ~ cumulative_distance_km +
    cumulative_duration_min,
  data = df_distance,
  family = binomial
)

summary(mod2)
## 
## Call:
## glm(formula = Win ~ cumulative_distance_km + cumulative_duration_min, 
##     family = binomial, data = df_distance)
## 
## Coefficients:
##                          Estimate Std. Error z value Pr(>|z|)    
## (Intercept)             -1.076735   0.112792  -9.546   <2e-16 ***
## cumulative_distance_km  -0.054457   0.005203 -10.466   <2e-16 ***
## cumulative_duration_min  0.181338   0.016317  11.113   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 1683.4  on 1373  degrees of freedom
## Residual deviance: 1465.5  on 1371  degrees of freedom
##   (390 observations deleted due to missingness)
## AIC: 1471.5
## 
## Number of Fisher Scoring iterations: 5
#-------------------------------------------------
# Prediction Grid for Heatmap
#-------------------------------------------------

prediction <- expand.grid(
  cumulative_distance_km = seq(
    min(df_distance$cumulative_distance_km, na.rm = TRUE),
    max(df_distance$cumulative_distance_km, na.rm = TRUE),
    length.out = 100
  ),

  cumulative_duration_min = seq(
    min(df_distance$cumulative_duration_min, na.rm = TRUE),
    max(df_distance$cumulative_duration_min, na.rm = TRUE),
    length.out = 100
  )
)

prediction$win_probability <- predict(
  mod2,
  newdata = prediction,
  type = "response"
)

#-------------------------------------------------
# Heatmap
#-------------------------------------------------

ggplot(
  prediction,
  aes(
    x = cumulative_distance_km,
    y = cumulative_duration_min,
    fill = win_probability
  )
) +
  geom_tile() +
  scale_fill_gradient(
    low = "#132B43",
    high = "#56B1F7",
    labels = scales::percent
  ) +
  scale_x_continuous(
    limits = c(0, 200),
    breaks = seq(0, 200, by = 50)
  ) +
  labs(
    title = "Probability Max is P1 by Distance and Race Time",
    x = "Cumulative Distance Completed (km)",
    y = "Elapsed Race Time (minutes)",
    fill = "P1 Probability"
  ) +
  theme_minimal()
## Warning: Removed 7200 rows containing missing values or values outside the scale range
## (`geom_tile()`).

##################################################
# EXPLORATORY DATA ANALYSIS (EDA)
##################################################

# Load libraries
library(dplyr)
library(ggplot2)
library(corrplot)

##################################################
# 1. Dataset Overview
##################################################

str(df)
## spc_tbl_ [3,777 × 24] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ date_start       : POSIXct[1:3777], format: "2023-07-21 11:42:59" "2023-07-21 15:02:41" ...
##  $ driver_number    : num [1:3777] 3 3 3 3 3 3 3 3 3 3 ...
##  $ duration_sector_1: num [1:3777] 31.6 29.3 48.8 29.2 48.7 ...
##  $ duration_sector_2: num [1:3777] 42.1 29 51.4 28.5 48.5 ...
##  $ duration_sector_3: num [1:3777] 43.9 22.8 43 23.2 36.6 ...
##  $ i1_speed         : num [1:3777] 242 275 176 276 149 165 273 181 230 272 ...
##  $ i2_speed         : num [1:3777] 141 243 141 246 159 213 246 171 197 246 ...
##  $ is_pit_out_lap   : logi [1:3777] FALSE FALSE FALSE FALSE FALSE FALSE ...
##  $ lap_duration     : num [1:3777] 117.5 81 143.2 80.9 133.7 ...
##  $ lap_number       : num [1:3777] 4 2 3 4 5 6 7 8 9 10 ...
##  $ meeting_key      : chr [1:3777] "1215" "1215" "1215" "1215" ...
##  $ segments_sector_1: chr [1:3777] "[2048, 2048, 2048, 2048, 2048, 2048, 2048]" "[2049, 2049, 2049, 2049, 2049]" "[2048, 2048, 2048, 2048, 2048]" "[2049, 2049, 2048, 2049, 2049]" ...
##  $ segments_sector_2: chr [1:3777] "[2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048]" "[2049, 2049, 2049, 2049, 2049, 2049, 2049, 2049]" "[2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048]" "[2049, 2049, 2049, 2049, 2049, 2049, 2049, 2049]" ...
##  $ segments_sector_3: chr [1:3777] "[2048, 2048, 2048, 2064, 2064]" "[2049, 2049, 2049, 2049, 2049]" "[2048, 2048, 2048, 2048, 2048]" "[2049, 2048, 2048, 2048]" ...
##  $ session_key      : num [1:3777] 9223 9127 9127 9127 9127 ...
##  $ st_speed         : num [1:3777] 294 295 276 296 276 246 295 282 251 294 ...
##  $ meeting key name : chr [1:3777] "Hungary" "Hungary" "Hungary" "Hungary" ...
##  $ session key name : chr [1:3777] "Practice 1" "Practice 2" "Practice 2" "Practice 2" ...
##  $ year             : num [1:3777] 2023 2023 2023 2023 2023 ...
##  $ Win              : num [1:3777] 0 0 0 0 0 0 0 0 0 0 ...
##  $ Total km         : num [1:3777] 4.38 4.38 4.38 4.38 4.38 ...
##  $ Sector 1 km      : num [1:3777] 1.85 1.85 1.85 1.85 1.85 1.85 1.85 1.85 1.85 1.85 ...
##  $ Sector 2 km      : num [1:3777] 1.43 1.43 1.43 1.43 1.43 1.43 1.43 1.43 1.43 1.43 ...
##  $ Sector 3 km      : num [1:3777] 1.1 1.1 1.1 1.1 1.1 1.1 1.1 1.1 1.1 1.1 ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   date_start = col_datetime(format = ""),
##   ..   driver_number = col_double(),
##   ..   duration_sector_1 = col_double(),
##   ..   duration_sector_2 = col_double(),
##   ..   duration_sector_3 = col_double(),
##   ..   i1_speed = col_double(),
##   ..   i2_speed = col_double(),
##   ..   is_pit_out_lap = col_logical(),
##   ..   lap_duration = col_double(),
##   ..   lap_number = col_double(),
##   ..   meeting_key = col_character(),
##   ..   segments_sector_1 = col_character(),
##   ..   segments_sector_2 = col_character(),
##   ..   segments_sector_3 = col_character(),
##   ..   session_key = col_double(),
##   ..   st_speed = col_double(),
##   ..   `meeting key name` = col_character(),
##   ..   `session key name` = col_character(),
##   ..   year = col_double(),
##   ..   Win = col_double(),
##   ..   `Total km` = col_double(),
##   ..   `Sector 1 km` = col_double(),
##   ..   `Sector 2 km` = col_double(),
##   ..   `Sector 3 km` = col_double()
##   .. )
##  - attr(*, "problems")=<externalptr>
summary(df)
##    date_start                     driver_number duration_sector_1
##  Min.   :2023-07-21 11:42:59.66   Min.   :3     Min.   :16.41    
##  1st Qu.:2024-03-02 15:55:01.44   1st Qu.:3     1st Qu.:25.65    
##  Median :2024-06-23 13:03:14.41   Median :3     Median :29.69    
##  Mean   :2024-10-25 20:21:20.10   Mean   :3     Mean   :31.14    
##  3rd Qu.:2026-02-13 09:06:21.36   3rd Qu.:3     3rd Qu.:33.58    
##  Max.   :2026-06-26 16:05:59.23   Max.   :3     Max.   :97.24    
##  duration_sector_2 duration_sector_3    i1_speed        i2_speed    
##  Min.   :17.54     Min.   :16.41     Min.   : 54.0   Min.   : 42.0  
##  1st Qu.:30.74     1st Qu.:23.86     1st Qu.:206.0   1st Qu.:195.0  
##  Median :36.63     Median :27.59     Median :237.0   Median :240.0  
##  Mean   :37.44     Mean   :29.97     Mean   :238.7   Mean   :234.7  
##  3rd Qu.:42.81     3rd Qu.:34.15     3rd Qu.:281.0   3rd Qu.:272.0  
##  Max.   :88.49     Max.   :98.03     Max.   :343.0   Max.   :346.0  
##  is_pit_out_lap   lap_duration      lap_number     meeting_key       
##  Mode :logical   Min.   : 65.29   Min.   :  1.00   Length:3777       
##  FALSE:3563      1st Qu.: 83.00   1st Qu.:  8.00   Class :character  
##  TRUE :214       Median : 95.36   Median : 17.00   Mode  :character  
##                  Mean   : 98.56   Mean   : 22.84                     
##                  3rd Qu.:108.28   3rd Qu.: 31.00                     
##                  Max.   :200.88   Max.   :137.00                     
##  segments_sector_1  segments_sector_2  segments_sector_3   session_key   
##  Length:3777        Length:3777        Length:3777        Min.   : 9127  
##  Class :character   Class :character   Class :character   1st Qu.: 9472  
##  Mode  :character   Mode  :character   Mode  :character   Median : 9540  
##                                                           Mean   : 9965  
##                                                           3rd Qu.:11235  
##                                                           Max.   :11469  
##     st_speed     meeting key name   session key name        year     
##  Min.   : 38.0   Length:3777        Length:3777        Min.   :2023  
##  1st Qu.:272.0   Class :character   Class :character   1st Qu.:2024  
##  Median :296.0   Mode  :character   Mode  :character   Median :2024  
##  Mean   :282.7                                         Mean   :2024  
##  3rd Qu.:312.0                                         3rd Qu.:2026  
##  Max.   :355.0                                         Max.   :2026  
##       Win            Total km      Sector 1 km     Sector 2 km   
##  Min.   :0.0000   Min.   :3.337   Min.   :1.200   Min.   :1.150  
##  1st Qu.:0.0000   1st Qu.:4.361   1st Qu.:1.650   1st Qu.:1.550  
##  Median :0.0000   Median :5.412   Median :1.850   Median :1.750  
##  Mean   :0.1639   Mean   :5.084   Mean   :1.862   Mean   :1.744  
##  3rd Qu.:0.0000   3rd Qu.:5.513   3rd Qu.:2.100   3rd Qu.:1.900  
##  Max.   :1.0000   Max.   :7.004   Max.   :2.300   Max.   :2.650  
##   Sector 3 km   
##  Min.   :0.990  
##  1st Qu.:1.260  
##  Median :1.560  
##  Mean   :1.477  
##  3rd Qu.:1.620  
##  Max.   :2.050
dim(df)
## [1] 3777   24
names(df)
##  [1] "date_start"        "driver_number"     "duration_sector_1"
##  [4] "duration_sector_2" "duration_sector_3" "i1_speed"         
##  [7] "i2_speed"          "is_pit_out_lap"    "lap_duration"     
## [10] "lap_number"        "meeting_key"       "segments_sector_1"
## [13] "segments_sector_2" "segments_sector_3" "session_key"      
## [16] "st_speed"          "meeting key name"  "session key name" 
## [19] "year"              "Win"               "Total km"         
## [22] "Sector 1 km"       "Sector 2 km"       "Sector 3 km"
##################################################
# 2. Missing Values
##################################################

colSums(is.na(df))
##        date_start     driver_number duration_sector_1 duration_sector_2 
##                 0                 0                 0                 0 
## duration_sector_3          i1_speed          i2_speed    is_pit_out_lap 
##                 0                 0                 0                 0 
##      lap_duration        lap_number       meeting_key segments_sector_1 
##                 0                 0                 0                 0 
## segments_sector_2 segments_sector_3       session_key          st_speed 
##                 0                 0                 0                 0 
##  meeting key name  session key name              year               Win 
##                 0                 0                 0                 0 
##          Total km       Sector 1 km       Sector 2 km       Sector 3 km 
##                 0                 0                 0                 0
##################################################
# 3. Duplicate Rows
##################################################

sum(duplicated(df))
## [1] 0
##################################################
# 4. Target Variable (Win)
##################################################

table(df$Win)
## 
##    0    1 
## 3158  619
prop.table(table(df$Win))
## 
##         0         1 
## 0.8361133 0.1638867
ggplot(df, aes(x = factor(Win))) +
  geom_bar(fill = "steelblue") +
  labs(title = "Distribution of Win",
       x = "Win",
       y = "Count") +
  theme_minimal()

##################################################
# 5. Histograms
##################################################

ggplot(df, aes(lap_duration)) +
  geom_histogram(fill="steelblue", bins=30) +
  theme_minimal()

ggplot(df, aes(duration_sector_1)) +
  geom_histogram(fill="steelblue", bins=30) +
  theme_minimal()

ggplot(df, aes(duration_sector_2)) +
  geom_histogram(fill="steelblue", bins=30) +
  theme_minimal()

ggplot(df, aes(duration_sector_3)) +
  geom_histogram(fill="steelblue", bins=30) +
  theme_minimal()

ggplot(df, aes(st_speed)) +
  geom_histogram(fill="steelblue", bins=30) +
  theme_minimal()

##################################################
# 6. Boxplots by Win
##################################################

ggplot(df,
       aes(factor(Win),
           lap_duration,
           fill=factor(Win)))+
  geom_boxplot()+
  theme_minimal()

ggplot(df,
       aes(factor(Win),
           st_speed,
           fill=factor(Win)))+
  geom_boxplot()+
  theme_minimal()

ggplot(df,
       aes(factor(Win),
           duration_sector_1,
           fill=factor(Win)))+
  geom_boxplot()+
  theme_minimal()

##################################################
# 7. Correlation Matrix
##################################################

numeric_df <- df %>%
  select(where(is.numeric))

cor_matrix <- cor(numeric_df,
                  use="complete.obs")
## Warning in cor(numeric_df, use = "complete.obs"): the standard deviation is
## zero
corrplot(cor_matrix,
         method="color",
         type="upper")

##################################################
# 8. Lap Time vs Lap Number
##################################################

ggplot(df,
       aes(lap_number,
           lap_duration,
           color=factor(Win)))+
  geom_smooth(se=FALSE)+
  theme_minimal()
## `geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'

##################################################
# 9. Speed vs Lap Number
##################################################

ggplot(df,
       aes(lap_number,
           st_speed,
           color=factor(Win)))+
  geom_smooth(se=FALSE)+
  theme_minimal()
## `geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'

##################################################
# 10. Win Percentage by Year
##################################################

ggplot(df,
       aes(factor(year),
           fill=factor(Win)))+
  geom_bar(position="fill")+
  labs(y="Proportion")+
  theme_minimal()