Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "main.py"]
CMD ["python", "scripts/main.py"]
94 changes: 87 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,103 @@
# 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:

```bash
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.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -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']
Expand All @@ -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(
Expand Down
7 changes: 5 additions & 2 deletions main.py → scripts/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions scatter-plot.py → scripts/scatter-plot.py
Original file line number Diff line number Diff line change
@@ -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']
Expand All @@ -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")
Expand Down
Binary file not shown.
18 changes: 18 additions & 0 deletions tests/test_project_structure.py
Original file line number Diff line number Diff line change
@@ -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}"
Loading