Playoffs
Summary
Most analytic surfaces on the site silently exclude playoff matches at the compute layer (form, splits, xPts, SQI, predictions, standings) so that derived metrics aren't distorted by tiny post-season samples. Elo, by contrast, walks every match including playoffs. The result is that pre-this-feature, the only place a user could see playoff performance was the per-match pages and a "PO" badge on match cards.
The Playoffs page at /analysis/playoffs is the inverse of every
other analysis page: it surfaces only playoff matches and aggregates
them into team records, an Elo-during-playoffs view, championship
counts, top performers, and the full results book.
Data
- Source tables:
matches(filtered tois_playoff = 1) for results, scores, and finals/titles attribution.team_elo_historyjoined tomatchesso playoff status is read frommatches.is_playoff(the source of truth) rather than the snapshot column on Elo history. The Elo historyis_playoffcolumn lags any playoff backfill until the next Elo recompute, so the join keeps the page accurate without forcing a recompute.match_players(joined tomatchesonmatch_idand filtered to playoff matches) for per-player goals/assists/minutes/appearances.
- Filter:
is_playoff = 1plus, for the records computation,home_goals IS NOT NULL AND away_goals IS NOT NULLso unplayed fixtures don't inflate played/PPG. - Time range: 2019 onward. Every CPL season has at least one playoff match (final-only in 2019, full Island Games tournament in 2020, semis + finals from 2021 on).
- Sample size: 32 playoff matches across 7 seasons as of writing (2-7 per season). Numbers are small. The page is meant to be a record book + run-summary, not an inferential statistical surface.
Choices
Records
- Aggregates W/D/L/GF/GA/GD/CS/Pts/PPG over all playoff matches in
scope (single season when
season=N, all seasons when omitted). seasons_in_playoffs= count of distinct seasons in which the franchise appeared in any playoff match. Always 1 in per-season scope (hidden in the UI for that scope to reduce noise).finals= number of seasons in which the franchise played in the championship-deciding match.titles= number of seasons in which the franchise won that match. Both are derived from a single rule: the chronologically latest playoff match of the season (date DESC, id DESCtiebreak) is treated as the final. Both teams in that match get +1 final; the winner gets +1 title.- Sorted by points desc, GD desc, GF desc, team name.
Elo deltas
- Per-season scope: reports the team's Elo entering its first
playoff match of the season (
elo_beforeof the earliest row) and its Elo after the last playoff match (elo_afterof the latest row).delta = elo_after - elo_before. Captures the "playoff run" swing. - All-time scope: sums per-match Elo changes (
elo_after - elo_before) across every playoff match in the franchise's history. Captures whether playoff performance has been a net builder or destroyer of the franchise's rating across its full run, without conflating it with regular-season movement.elo_before/elo_afterare null because they aren't meaningful when summed across separate playoff runs in different seasons. - Sorted by delta desc.
Leaderboards
- Aggregates
match_players.stats_jsonfor matches whereis_playoff = 1. Reads the SDP per-matchgoalsandassistsstat keys directly so totals reconcile with the season pages, scoped to playoff matches. - Goal-attribution caveat: own goals are credited to the conceding
team in match scores but never appear in the scorer's
goalsstat, so the sum across the leaderboard can be one or two below the scoreline-derived goal total in seasons with own goals. goal_contributions= goals + assists.teamis the player's most-frequent (team_id, season) combination within scope, then resolved era-correctly.- Each category returns the top 10 with
min_value=1so leaderboards don't fill up with players who hold all-zeros across the metric.
Champions strip (frontend-derived)
- The "Champions" panel on the all-time view is derived client-side
from the same results list, applying the same final-detection rule
as the backend (
groupBy(season).firston a season-DESC, date-DESC feed). Saves an extra endpoint and keeps the rule in lockstep with the records computation.
Validation
- All 7 known CPL champions are correctly attributed by the last-match-winner rule (Forge 2019, 2020, 2022; Pacific 2021; Cavalry 2023, 2024, 2025).
- All-time records spot-check against publicly available CPL playoff histories: Forge leads in playoff appearances (7), finals (6), and titles (4), matching the league record.
- Elo joins via
matches.is_playoffrather thanteam_elo_history.is_playoff. A regression test zeroes the Elo history flag and asserts deltas still surface, guarding against a class of stale-derived-data bug we've hit elsewhere. - Leaderboards exclude regular-season matches by joining to
matches.is_playoff = 1. A regression test seeds a 5-goal regular-season match for the same player and asserts it does not appear in the playoff totals. - Era-correct naming: a 2019-scoped query returns "York9 FC", an all-time query returns the franchise's current name.
Failure modes
- Two-leg finals (2019, 2021): only the second leg is treated as "the final" by the last-match rule. In both years the same two teams played both legs and the aggregate winner also won the second leg, so titles and finals attribution still come out correct. A future format with a 2-leg final where the trailing team wins the second leg but loses on aggregate would be misattributed; if this becomes a real possibility, replace the rule with a bracket/round table.
- Own goals: the goals leaderboard under-counts total scoreline goals by the number of own goals in the period. This is intentional (own goals shouldn't be credited to a player) but worth noting if a user tries to reconcile player goals to scoreline totals.
- Per-match stat coverage: the
goalsandassistsstat keys inmatch_players.stats_jsonrely on SDP populating them. Older seasons have full coverage today thanks to the data-quality backfill, but a player who appears in a playoff match without a stats blob (extremely rare in current data) would show appearances/minutes frommatch_playersbut zero goals/assists. - Tiny samples: per-season records are computed over 1-3 matches per team. Sort-by-PPG can put a team that won a single match above one that won 6 of 8. The default sort is by raw points to mitigate.
What would make it better
- A real bracket / round table so seeding, semi vs final, and away-goals tiebreaks can render cleanly instead of being inferred from chronology.
- Player playoff percentiles — currently the Player Profiles page
treats
season=Nas all-competitions. A dedicated playoff percentile cohort (or at minimum a "Playoff" tab on the player profile) would let a user see whether top performers in playoffs deviate from their regular-season rates. - Toggle on existing pages for "include playoffs" so users can re-run form/splits/xPts/SQI including post-season matches and see how the picture shifts.
- A playoff vs regular-season delta column on the team records table (this team's regular-season PPG vs their all-time playoff PPG) to quantify the "play big in October" effect.