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
46 changes: 28 additions & 18 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,44 @@ NFL data analytics and fantasy football tools.
## Stack

- **Language:** Python
- **Data acquisition:** `nfl_data_py` (nflverse parquet files from GitHub Releases — free, no auth)
- **Data processing:** `polars` (preferred) or `pandas`
- **Data acquisition:** `nflreadpy` (nflverse parquet files from GitHub Releases — free, no auth)
- **Data processing:** `polars`
- **TUI:** `textual`
- **HTTP (if needed):** `httpx`

## Data Source

All data comes from [nflverse](https://github.com/nflverse/nflverse-data) via `nfl_data_py`.
The loaders in `src/ffb/data/loader.py` call these importers:
All data comes from [nflverse](https://github.com/nflverse/nflverse-data) via `nflreadpy`,
nflverse's own polars-native package. The loaders in `src/ffb/data/loader.py` call these:

```python
import nfl_data_py as nfl

nfl.import_pbp_data([2024, 2025]) # play-by-play with EPA
nfl.import_snap_counts([2024, 2025]) # snap counts
nfl.import_weekly_data([2024, 2025]) # weekly player stats
nfl.import_seasonal_rosters([2024, 2025]) # rosters
nfl.import_injuries([2024, 2025]) # injury reports
nfl.import_schedules([2024, 2025]) # schedules
nfl.import_ids() # pfr_id ↔ gsis_id crosswalk
import nflreadpy as nfl

nfl.load_pbp([2024, 2025]) # play-by-play with EPA
nfl.load_snap_counts([2024, 2025]) # snap counts
nfl.load_player_stats([2024, 2025]) # weekly player stats
nfl.load_rosters([2024, 2025]) # rosters
nfl.load_injuries([2024, 2025]) # injury reports
nfl.load_schedules([2024, 2025]) # schedules
nfl.load_ff_playerids() # pfr_id ↔ gsis_id crosswalk
```

`nfl_data_py` publishes further importers the loaders do not call, among them
`import_ngs_data(stat_type="receiving")` for Next Gen Stats and `import_contracts()`.
`nflreadpy` publishes further loaders the ones here do not call, among them
`load_nextgen_stats()`, `load_depth_charts()` and `load_contracts()`.

nflverse publishes weekly player stats as one release asset per season, so a season in
progress can have no asset at all. `load_weekly_stats` derives those weeks from
play-by-play rather than dropping the season.
A loader reads one release asset per season and raises on the first it cannot read, so
`_by_season` asks for one season at a time. It raises `ValueError` before any request
for a season outside the range its release covers, and that ceiling moves with the
calendar independently of `seasons.season_for`.

Every loader returns a polars frame under the column names the release carries.
`loader` renames the two that disagree with the frames they are joined to — the roster
`gsis_id` and the weekly `team` — and takes `position` off the rosters rather than off
the weekly asset, which labels a fullback FB.

The weekly stats asset updates through a season rather than landing complete at the
end of one, so a season being played is read from it. `load_weekly_stats` reconstructs
a season the asset does not carry at all from play-by-play rather than dropping it.

## Repo

Expand Down
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ filters by position, team, season and as-of week.

## Stack

- **Data:** [nflverse](https://github.com/nflverse/nflverse-data) via `nfl_data_py` (free, no auth)
- **Data:** [nflverse](https://github.com/nflverse/nflverse-data) via `nflreadpy` (free, no auth)
- **Processing:** `polars`
- **TUI:** `textual`

Expand Down Expand Up @@ -130,7 +130,7 @@ the first download succeeds, so the wait falls once per dataset season and then
for six hours.

**Watching a load.** The loading pane names what is downloading and counts elapsed time as
`MM:SS`, updated once a second. `nfl_data_py` downloads a release asset in one blocking call
`MM:SS`, updated once a second. A release asset downloads in one blocking call
and reports no progress, so elapsed time is the only honest readout — there is no percentage
or bar.

Expand All @@ -150,15 +150,17 @@ again.

**A season a load does not obtain.** The season label rolls over on 1 September, while
nflverse publishes a season's play-by-play, snap counts, weekly stats and injuries only
once its games have been played — so for the opening days of September every tool asks for
a season the source answers `HTTP Error 404` for. That season contributes no rows and the
seasons beside it render: each loader resolves its season list one season at a time, so a
season that does not resolve costs only itself.
once its games are under way — so for the opening days of September every tool asks for a
season the source has no asset for. That season contributes no rows and the seasons beside
it render: each loader resolves its season list one season at a time, so a season that does
not resolve costs only itself.

A transport failure costs the same season the same way, and reaches the loaders as the same
exception. Nothing below the loaders distinguishes the two, so nothing reports which one
happened; a season that disappears on one load and returns on the next was a failed
transfer, and one that stays absent is waiting on the source.
transfer, and one that stays absent is waiting on the source. A season a loader refuses by
range is dropped the same way, and the ceiling it checks against is not the cut the season
label turns on.

The drop reaches the interface, because every screen builds its season filter from the
seasons its data carries rows for. The absent season is absent from the filter, and the
Expand All @@ -170,7 +172,8 @@ earlier than it can rank one — the season being played is the season a trade i
and the chart says what it is still short of rather than falling back to a finished season.

Nothing negative is cached, so the season joins the filter on the first load that obtains
it. The price is one failed request per absent asset per load.
it. The price is one refused request per absent asset per load, and none at all for a
season a loader declines before asking.

**`Failed to load data: No snap counts obtained for season 2026`.** Every season the tool
asked for dropped, so the load has nothing to render. The message names the dataset and the
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ no per-game rows for.
helper. `force_refresh` skips the cache read, not the write, which is precisely what the
Refresh button promises. A loader taking a season list resolves it through `_by_season`, one
season and one cache key per season, and concatenates what came back. That split is what
lets a published season render beside an unpublished one: `nfl_data_py` satisfies a
lets a published season render beside an unpublished one: an nflreadpy loader satisfies a
multi-season request only once every season in it has been read, so a single 404 inside a
batched request costs every season beside it. A request that resolves no season at all
raises, naming the seasons and the dataset, rather than handing an engine a frame with no
Expand Down
5 changes: 1 addition & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description = "NFL data analytics and fantasy football tools (ffb)"
license = "MIT"
requires-python = ">=3.12"
dependencies = [
"nfl-data-py>=0.3.0",
"nflreadpy==0.1.5",
"polars>=1.0",
"pyarrow>=23.0.1",
"textual>=3.0",
Expand All @@ -29,9 +29,6 @@ dev = [
"pytest-cov>=7.1.0",
]

[tool.uv]
constraint-dependencies = ["pandas>=2.0"]

[tool.ruff]
target-version = "py312"
line-length = 100
Expand Down
Loading
Loading