diff --git a/Dockerfile b/Dockerfile index f69a089..fb37cf5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,4 +7,4 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD ["python", "main.py"] +CMD ["python", "scripts/main.py"] diff --git a/README.md b/README.md index 9e1c91d..71306cb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,72 @@ -# f1-data-analysis -Using Pandas in Python, I analyzed massive amounts of F1 race data to figure out the team with the highest average places gained from start to finish. +# F1 Data Analysis: Constructor Racecraft Performance -## Run with Docker +This project analyzes historical Formula 1 race result data to evaluate constructor-level racecraft through **Average Positions Gained (APG)**, defined as: + +`APG = grid position - final position` + +A higher APG indicates that, on average, a team improves more positions between the race start and finish. + +## Project Goals + +- Quantify which constructors gain the most positions during races. +- Compare APG against sample size (total race entries) to highlight variance in small datasets. +- Provide both static and interactive visualizations for exploratory analysis. + +## Repository Structure + +```text +. +├── data/ +│ ├── constructors.csv +│ └── results.csv +├── scripts/ +│ ├── main.py +│ ├── scatter-plot.py +│ └── interactive-scatter-plot.py +├── Dockerfile +└── requirements.txt +``` + +## Data Sources + +- `constructors.csv` and `results.csv` were acquired from the Kaggle dataset: + https://www.kaggle.com/datasets/rohanrao/formula-1-world-championship-1950-2020 + +## Analysis Walkthrough + +- Presentation with methodology and process: + https://docs.google.com/presentation/d/1bZhT_yq7-mdMf9Ao8VaFRqklGW-47c8zOK8FQtB8lGk/edit?usp=sharing + +## Scripts + +- `scripts/main.py` + Core APG analysis with console output and a bar chart of top teams. + +- `scripts/scatter-plot.py` + Static scatter plot showing APG vs total entries (log-scaled x-axis). + +- `scripts/interactive-scatter-plot.py` + Interactive Plotly scatter plot for deeper exploratory analysis. + +## How to Run + +### Option 1: Local Python Environment + +1. Install dependencies: + ```bash + pip install -r requirements.txt + ``` +2. Run the primary analysis: + ```bash + python scripts/main.py + ``` +3. Run visualization scripts: + ```bash + python scripts/scatter-plot.py + python scripts/interactive-scatter-plot.py + ``` + +### Option 2: Docker Build the image: @@ -9,15 +74,30 @@ Build the image: docker build -t f1-data-analysis . ``` -Run the default analysis script (`main.py`): +Run the default analysis script: ```bash docker run --rm -it -v "$(pwd):/app" f1-data-analysis ``` -Run a different script if needed: +Run specific scripts: ```bash -docker run --rm -it -v "$(pwd):/app" f1-data-analysis python scatter-plot.py -docker run --rm -it -v "$(pwd):/app" f1-data-analysis python interactive-scatter-plot.py +docker run --rm -it -v "$(pwd):/app" f1-data-analysis python scripts/scatter-plot.py +docker run --rm -it -v "$(pwd):/app" f1-data-analysis python scripts/interactive-scatter-plot.py ``` + +## Methodology Summary + +1. Load race results and keep only constructor ID, grid position, and final classified position. +2. Compute per-race positions gained for each constructor entry. +3. Aggregate APG by constructor and compute total race entries. +4. Merge constructor metadata for readable team names. +5. Apply entry-count thresholds where appropriate to reduce small-sample distortion. +6. Compare central trend and variance through ranked tables and scatter plots. + +## Key Interpretation Notes + +- Teams with very few race entries can produce extreme APG values that are not stable estimates. +- Entry thresholds and log-scaled visualizations are used to make comparisons more statistically meaningful. +- APG captures race-day progression, not overall team performance across qualifying, reliability, or points systems. diff --git a/constructors.csv b/data/constructors.csv similarity index 100% rename from constructors.csv rename to data/constructors.csv diff --git a/results.csv b/data/results.csv similarity index 100% rename from results.csv rename to data/results.csv diff --git a/interactive-scatter-plot.py b/scripts/interactive-scatter-plot.py similarity index 85% rename from interactive-scatter-plot.py rename to scripts/interactive-scatter-plot.py index 20e459f..d0fdd60 100644 --- a/interactive-scatter-plot.py +++ b/scripts/interactive-scatter-plot.py @@ -1,6 +1,10 @@ import plotly.express as px import pandas as pd -f1 = pd.read_csv('results.csv') +from pathlib import Path + +DATA_DIR = Path(__file__).resolve().parents[1] / "data" + +f1 = pd.read_csv(DATA_DIR / 'results.csv') f1 = f1[['constructorId', 'grid', 'positionOrder']].copy() f1.columns = ['teamId', 'startPos', 'endPos'] f1['positionsGained'] = f1['startPos'] - f1['endPos'] @@ -9,7 +13,7 @@ team_entries = f1['teamId'].value_counts().reset_index() team_entries.columns = ['teamId', 'totalEntries'] plot_data = pd.merge(total_teams, team_entries, on='teamId', how='left') -team_names = pd.read_csv('constructors.csv') +team_names = pd.read_csv(DATA_DIR / 'constructors.csv') plot_data = pd.merge(plot_data, team_names[['constructorId', 'name']], left_on='teamId', right_on='constructorId', how='left') fig = px.scatter( diff --git a/main.py b/scripts/main.py similarity index 91% rename from main.py rename to scripts/main.py index a68b0d5..aa94a6f 100644 --- a/main.py +++ b/scripts/main.py @@ -2,9 +2,12 @@ import pandas as pd import matplotlib.pyplot as plt import seaborn as sns +from pathlib import Path + +DATA_DIR = Path(__file__).resolve().parents[1] / "data" # APG - average positions gained -f1 = pd.read_csv('results.csv') # reading results csv file into Pandas dataframe +f1 = pd.read_csv(DATA_DIR / 'results.csv') # reading results csv file into Pandas dataframe f1 = f1[['constructorId', 'grid', 'positionOrder']] # only using the three columns we need f1.columns = ['teamId', 'startPos', 'endPos'] # renaming columns f1['positionsGained'] = f1['startPos'] - f1['endPos'] # creating new column with positions gained @@ -19,7 +22,7 @@ if min_entries_required <= total_teams['totalEntries'].max(): # checking that the requirement is within limits sat_teams = total_teams[total_teams['totalEntries'] >= min_entries_required].reset_index(drop = True) # creating new dataframe with only teams that satisfy the entries requirement - team_names = pd.read_csv('constructors.csv') # reading constructors csv file into Pandas dataframe + team_names = pd.read_csv(DATA_DIR / 'constructors.csv') # reading constructors csv file into Pandas dataframe sat_teams = pd.merge(sat_teams, team_names[['constructorId', 'name', 'nationality']], left_on = 'teamId', right_on = 'constructorId', how = 'left') # combining team names and nationalities with the main dataframe sat_teams = sat_teams[['teamId', 'name', 'nationality', 'APG', 'totalEntries']] # selecting and reordering columns ht_apg = round(sat_teams['APG'].max(), 2) # finding the APG of the team with the highest APG and rounding for ease of viewing diff --git a/scatter-plot.py b/scripts/scatter-plot.py similarity index 89% rename from scatter-plot.py rename to scripts/scatter-plot.py index 60cf5e4..b16af88 100644 --- a/scatter-plot.py +++ b/scripts/scatter-plot.py @@ -1,7 +1,11 @@ import pandas as pd import matplotlib.pyplot as plt import seaborn as sns -f1 = pd.read_csv('results.csv') +from pathlib import Path + +DATA_DIR = Path(__file__).resolve().parents[1] / "data" + +f1 = pd.read_csv(DATA_DIR / 'results.csv') f1 = f1[['constructorId', 'grid', 'positionOrder']].copy() f1.columns = ['teamId', 'startPos', 'endPos'] f1['positionsGained'] = f1['startPos'] - f1['endPos'] @@ -10,7 +14,7 @@ team_entries = f1['teamId'].value_counts().reset_index() team_entries.columns = ['teamId', 'totalEntries'] plot_data = pd.merge(total_teams, team_entries, on='teamId', how='left') -team_names = pd.read_csv('constructors.csv') +team_names = pd.read_csv(DATA_DIR / 'constructors.csv') plot_data = pd.merge(plot_data, team_names[['constructorId', 'name']], left_on='teamId', right_on='constructorId', how='left') plt.figure(figsize=(11, 6)) sns.set_theme(style="whitegrid") diff --git a/tests/__pycache__/test_project_structure.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_project_structure.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000..c7686d6 Binary files /dev/null and b/tests/__pycache__/test_project_structure.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/test_project_structure.py b/tests/test_project_structure.py new file mode 100644 index 0000000..540d133 --- /dev/null +++ b/tests/test_project_structure.py @@ -0,0 +1,18 @@ +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +def test_expected_project_files_exist() -> None: + required_files = [ + "data/constructors.csv", + "data/results.csv", + "scripts/main.py", + "scripts/scatter-plot.py", + "scripts/interactive-scatter-plot.py", + "requirements.txt", + ] + + missing_files = [path for path in required_files if not (REPO_ROOT / path).is_file()] + assert not missing_files, f"Missing expected files: {missing_files}"