Skip to content

association 4.1.0

Choose a tag to compare

@jeffknupp jeffknupp released this 18 Sep 05:16
· 1205 commits to master since this release
  • The new pin rewriting refused its own first real bump, and now does not. Its
    post-rewrite check greps the whole tree for the old pin, so it fired on a
    comment inside bump_version.py - and would have fired on CHANGES.md at
    the next release, because a released entry quoting the tag it shipped under
    is a stale pin on purpose. The check honors the same exclusions the
    discovery does, and a test covers the case.
  • The version directives on this release's new public symbols name 4.1.0, not
    4.0.2. Five agents working in parallel were each told 4.0.2, correct for a
    release of fixes; GET /api/coverage is a new endpoint, which makes this a
    minor bump. Corrected in the pre-release audit, which is what that audit is
    for - and the pinned install commands were rewritten by the bump script
    itself this time (#60), rather than by hand minutes before the tag.
  • The web page now reports GET /api/coverage, what the warehouse actually
    holds grouped into the three tiers a question can land in
    - box score, box
    score plus play-by-play, and both of those plus NetPoints - and the page
    renders it as a row of pills under the header. This replaces /api/health's
    single min/max season as the page's only claim about coverage, which read as
    "the whole span is answerable" when only its narrowest table was (#71): a
    2016 shot chart and a 2016 NetPoints fingerprint looked equally reasonable to
    ask for, and only one of them was. Each tier's floor is read straight from
    association.nba.coverage.COVERAGE - the same enforced table a template's
    own refusal reads - rather than recomputed from row counts, so the page and
    a refusal cannot say two different things about the same season; only how
    far a tier's data currently reaches (last_season) is counted live, since no
    floor records that. A season inside a tier's range but declared partial
    there (2002's play-by-play, 2002-2003's shot chart) is marked rather than
    shown as uniformly whole, and 1993 - ESPN's phantom copy of 1994 - is
    excluded from the box score tier's range rather than offered.
  • A shooting-percentage leaderboard's qualifier now scales to a shortened
    season instead of applying an 82-game-calibrated floor flat.
    ts_pct
    (550 true-shooting attempts), efg_pct (480 field-goal attempts) and
    fg_pct (400 field-goal attempts) were measured against the warehouse: at
    the flat floors, 2020 qualified 157 players and 2021 155, against 174-184
    in every full season measured (2019, 2022-2026), and the 66-game 2012
    lockout season qualified 127 - a published rule scales per team game, so
    this was a stricter qualifier than the one it claimed to be, not a missing
    one. LeaderboardMetric.scales_with_schedule marks the three floors this
    applies to; leaderboard.default_min_sample scales them from the season's
    own MEDIAN real_games team-game count (72 for 2020 and 2021, 66 for
    2012), rounding half up, and falls back to the flat floor when
    real_games is unavailable. Re-measured: 2020 moves to 171-183 across the
    three metrics, 2021 to 176-220, 2012 to 176-211, and every full season is
    unchanged (proven identical, not just close, since the scaling factor at
    82/82 is 1). LeaderboardResult.min_sample_applied - already wired into
    the answer text - now names the real, scaled number rather than the flat
    one. three_pt_pct and ft_pct share the identical shape and are not
    scaled yet - see ISSUES.md.
  • NetPoints per-date rows that fail the exact display-name match now keep
    the source name instead of dropping it.
    parse_net_points_daily and
    parse_net_points_daily_players used to write athlete_id=None with
    nothing else on the row, so a query grouping by (event_id, athlete_id)
    counted every unmatched row on a date as a duplicate of every other -
    2,190 rows in net_points_player_game and 64,210 in
    net_points_player_game_fingerprint, measured 2026-09-18. Both tables now
    carry display_name on every row (matched or not), and
    net_points_player_game also carries NBA.com's own nba_player_id
    (plyrID in the source), where the file provides one (ISSUES.md #22).
  • A NetPoints display name shared by exactly two locally-known athlete ids
    is no longer dropped unconditionally.
    Pipeline._name_to_athlete_id used
    to treat any name with more than one match as permanently ambiguous, which
    meant every one of the 8 players ESPN files under two athlete_ids in the
    same box score (#87) had zero rows in net_points_player_game,
    net_points_player_game_fingerprint and net_points_player_fingerprint
    for their entire career - Corey Brewer, 985 player_box_stats rows over
    2008-2020, had no per-game NetPoints for a single one of them. A new
    Pipeline._resolve_duplicate_athlete_pairs applies the same proof
    fetch/repairs/duplicate_athletes.py uses to merge those ids at load time -
    the pair appears in the SAME team's box score for the SAME game - straight
    off the player_box_stats Parquet tree at fetch time, and resolves the name
    to the established id when that proof holds. Measured against the live
    warehouse: all 8 known pairs now resolve, to the identical id
    player_box_stats_deduped already treats as canonical, and none of the
    other 13 names shared by exactly two ids (real different people, #21) picked
    up a false match (ISSUES.md #101).
  • scripts/bump_version.py rewrites the pinned install commands itself, so
    a release can no longer ship instructions that install an older one. The pins
    said v1.4.0 through three later releases, 3.0.0 shipped still pointing at
    v2.2.0, and 4.0.0 and 4.0.1 were only right because a human edited them
    minutes before each bump. The script discovers them with git grep rather
    than a hardcoded list (a list of two files is what let docs/usage.rst rot
    unnoticed), refuses to run if a pin names neither the current nor the new
    version, and asserts no old pin survives the rewrite. It skips CHANGES.md,
    tests/ and scripts/, which hold the same pattern as release history,
    fixtures and the regex itself - scanning them refused a real bump, which a
    test against this repository now catches.
  • scripts/bump_version.py now rewrites the install-command pins itself
    (#60).
    Because PyPI is unreachable, README.md, docs/installation.rst
    and docs/usage.rst pin a release tag
    (git+https://github.com/jeffknupp/association@vX.Y.Z), and nothing rewrote
    them automatically - they said v1.4.0 through three later releases, and
    3.0.0 shipped still pointing at v2.2.0. The bump script now finds every
    such pin with git grep (not a fixed file list, so a pin added to a new doc
    is covered the same way), rewrites @v<current> to @v<new> in the same
    run as the version bump, refuses outright if a pin names a version that is
    neither the current one nor the new one, and asserts no @v<current> pin
    survives anywhere in the tree afterward. docs/releasing.rst now describes
    this instead of telling a human to update the pins by hand.
  • A retired player's question that names no season no longer defaults to
    the current one and stops there.
    player_stat, single_game_high,
    game_log, player_netpoints and shot_chart all read a missing season
    slot as "now", and a retired player's "now" is empty - "Allen Iverson's
    points" answered "Allen Iverson has no 2026 regular season numbers in the
    warehouse", true and about a year nobody asked for. The refusal now
    redirects to what the warehouse actually holds for him when the season was
    defaulted rather than named: "... He last appears in 2010. The warehouse
    holds his 1997-2010 regular seasons; name one, or ask for his career." Never
    substitutes an answer, only names where to ask again - the same discipline
    entities.suggest_players already follows for a near-miss name. A season
    the question names outright keeps its plain refusal, because that answer is
    correct as given: _Span now carries a defaulted flag from _span_of so
    every reader downstream can tell the two cases apart. player_netpoints and
    shot_chart do the same from their own local flag, since neither is built
    on _Span; neither offers "or ask for his career", since neither template
    has a career span to redirect to. (#18)