REST API for NHL statistics analysis built with Python, FastAPI, PostgreSQL, and Docker.
- Docker Desktop installed and running
# 1. Start container
docker compose up -d --build
# 2. Wait 30 seconds for database initialization
sleep 30
# 3. Upload data
curl -X POST "http://localhost:8000/upload-csv/" \
-F "file=@./data/data_dump.csv"
# 4. Verify
curl "http://localhost:8000/health"API Documentation: http://localhost:8000/docs
Upload NHL statistics CSV file to database.
curl -X POST "http://localhost:8000/upload-csv/" \
-F "file=@./data/data_dump.csv"Response:
{
"message": "Successfully uploaded 951 records",
"columns": ["player_id", "player", "team", "position", "games_played", "goals", "assists", "points", "penalty_minutes", "shots", "shooting_pct", "season"],
"sample_data": [
{
"player_id": 8481720,
"player": "Nick Abruzzese",
"team": "TOR",
"goals": 0,
"games_played": 2
}
]
}Players with most goals in specified season.
curl "http://localhost:8000/top-scorers?season=2022-2023&limit=5"Response:
[
{
"player": "Connor McDavid",
"team": "EDM",
"goals": 64,
"games_played": 82,
"goals_per_game": 0.78
},
{
"player": "David Pastrnak",
"team": "BOS",
"goals": 61,
"games_played": 82,
"goals_per_game": 0.74
},
{
"player": "Mikko Rantanen",
"team": "COL",
"goals": 55,
"games_played": 82,
"goals_per_game": 0.67
}
]Players with most penalty minutes.
curl "http://localhost:8000/penalty-leaders?season=2022-2023&limit=5"Response:
[
{
"player": "Arber Xhekaj",
"team": "MTL",
"penalty_minutes": 81,
"games_played": 44,
"pim_per_game": 1.84
},
{
"player": "Matt Rempe",
"team": "NYR",
"penalty_minutes": 71,
"games_played": 17,
"pim_per_game": 4.18
}
]Team aggregated statistics (goals and shots).
curl "http://localhost:8000/team-stats?season=2022-2023"Response:
[
{
"team": "ANA",
"total_goals": 219,
"total_shots": 2467,
"total_goals_without_multi_team_players": 188,
"total_shots_without_multi_team_players": 2111
},
{
"team": "ARI",
"total_goals": 251,
"total_shots": 2352,
"total_goals_without_multi_team_players": 183,
"total_shots_without_multi_team_players": 1582
}
]Players who changed teams during the season.
curl "http://localhost:8000/team-changes?season=2022-2023&limit=5"Response:
[
{
"player": "Vladimir Tarasenko",
"teams": "NYR,OTT,STL",
"team_count": 3
},
{
"player": "Noel Acciari",
"teams": "STL,TOR",
"team_count": 2
},
{
"player": "Tanner Jeannot",
"teams": "NSH,TBL",
"team_count": 2
}
]Active roster for a specific team.
curl "http://localhost:8000/team-roster/TOR?season=2022-2023"Response:
[
{
"player": "Auston Matthews",
"position": "C",
"goals": 40,
"assists": 46,
"points": 86,
"games_played": 74,
"shots": 348
},
{
"player": "William Nylander",
"position": "R",
"goals": 40,
"assists": 47,
"points": 87,
"games_played": 82,
"shots": 299
},
{
"player": "Mitchell Marner",
"position": "R",
"goals": 29,
"assists": 70,
"points": 99,
"games_played": 82,
"shots": 234
}
]All endpoints accept optional ?season=2022-2023 parameter (defaults to 2022-2023).
Tech Stack: FastAPI, PostgreSQL, Docker, SQLAlchemy, Pandas
Key Decisions:
- SQL queries separated into
sql_queries/*.sqlfiles for maintainability - PostgreSQL for robust statistical aggregations
- Single-table schema for MVP simplicity
- Dynamic season parameter for multi-season analysis
Data Processing:
- Cleans and normalizes CSV column names
- Handles NULL values and multi-team players
- Converts season format:
20222023→2022-2023
Team Statistics Accuracy: The /team-stats endpoint excludes players who changed teams mid-season to avoid double-counting their statistics. The CSV format "STL,TOR" doesn't specify which goals were scored with which team. Current implementation prioritizes completeness over accuracy by including two separate totals columns (goals & shots; with and without multi team players).
Production Solution: Would require game-by-game data or NHL API integration to accurately split traded players' statistics between teams.
nhl-stats-api/
├── main.py # FastAPI application
├── docker-compose.yml # Container orchestration
├── Dockerfile # Python container
├── requirements.txt # Dependencies
├── sql_queries/ # SQL query files
│ ├── query_loader.py
│ └── *.sql
└── data/
└── data_dump.csv
# Test all endpoints
curl "http://localhost:8000/health"
curl "http://localhost:8000/top-scorers?limit=3"
curl "http://localhost:8000/penalty-leaders?limit=3"
curl "http://localhost:8000/team-stats"
curl "http://localhost:8000/team-changes?limit=5"
curl "http://localhost:8000/team-roster/TBL"Run the included demo script for a guided walkthrough of all endpoints:
chmod +x demo.sh
./demo.sh
The demo script will:
- Check API health status
- Display top 5 goal scorers
- Show team statistics with goals and shots
- List penalty leaders
- Identify players who changed teams
- Display a specific team roster (Tampa Bay)
- Provide an overall season summary
Each demo pauses between sections, allowing you to review the results before continuing.
Stop API: docker compose down
Breakdown:
- Docker/Environment Setup (~45 min): Initial Docker Compose configuration, understanding container networking and health checks
- FastAPI Development (~90 min): Endpoint implementation, request/response handling, automatic documentation setup
- Database Design & SQL (~60 min): Schema decisions, writing queries with Postgres-specific functions, handling NULL values
- Data Processing (~45 min): CSV parsing with Pandas, column mapping, multi-team player logic
- Testing & Documentation (~45 min): Manual endpoint testing, README writing, deployment verification
Docker Compose (New):
- First time orchestrating containerized applications
- Learned about best practices (example: health checks)
- Debugged file naming issue (docker-compose.yml vs docker_compose.yml)
FastAPI (Familiar, but mostly new):
- Coming from other Python frameworks, FastAPI's automatic OpenAPI docs were impressive
- Type hints and Pydantic validation were intuitive
- Async capabilities available but not required for this use case
Python - (Familiar, but seriously expanded):
- While familiar with Python, this project pushed me to explore more advanced patterns
- Implemented custom utility classes (QueryLoader) with proper encapsulation
- Leveraged lambda functions (new) and Pandas apply() for data transformations
Challenge 1: Multi-Team Players
- Issue: Players traded mid-season appear as
"STL,TOR"in a single field - Initial Approach: Tried to split and aggregate normally, realized this double-counted statistics
- Solution: For team stats, created two totals columns (with and without multi team players) in favor of functionality.
- Learning: Sometimes data constraints require choosing between completeness and accuracy
Challenge 2: Dynamic SQL Query Loading
- Issue: Wanted to separate SQL from Python code for maintainability
- Solution: Built
QueryLoaderutility class to read.sqlfiles at runtime - Trade-off: Queries are cached in memory, requiring container restart for changes
- Benefit: Much cleaner separation of concerns, easier to review SQL independently
Challenge 3: Season Parameter Design
- Issue: Initially hardcoded
"2022-2023"in all queries - Improvement: Made it a parameter with sensible default
- Implementation: Added
season: str = "2022-2023"to all endpoints - Result: API is now future-proof for multi-season data
Challenge 4: CSV Column Variations
- Issue: Source CSV had inconsistent naming (spaces, capitalization, empty columns)
- Solution: Created flexible column mapping dictionary and strip/normalize logic
- Edge Case: Handled string
"None"values that should be NULL
Challenge 5: Docker Deployment Testing
- Issue: Needed to ensure it works on reviewer's machine
- Solution: Tested by extracting to fresh directory and running from scratch
- Discovery: Important to include data file in submission and document exact startup steps