Every March, the NBA sells you a "playoff race." So I replayed all 1,230 regular-season games of 2023-24 in date order, rebuilt the standings at the end of every night, and asked a colder question: on each of the season's 158 game days, how many of the eventual top-eight-by-record teams already held a top-eight seat? The answer embarrasses the word "race." Thirteen of the sixteen seats were in their final hands on October 26 — the first day every team had a record. The East's eight names were fixed for good on December 16, with an average of 57 games still left on every Eastern schedule. And from January 10 to the finish, fifteen of sixteen seats were settled; the entire remaining season was an argument over one Western chair, which changed hands four times in the final nine days and wasn't resolved until April 12 — the second-to-last night of the season.

What "decided" means here — and what it doesn't

First, the honest framing, because the sloppy version of this headline would be fabrication. I am not measuring "the playoff field." Since 2021, seeds 7 through 10 settle two playoff spots on the court in the play-in, so a top-eight record doesn't guarantee a playoff berth and a ninth-place record doesn't preclude one. I am also not modeling the league's real seeding tiebreakers — head-to-head results, division records, conference records, the whole season-series rulebook I audited separately. What I track is the cleanest object the game log supports: each conference's top 8 by current winning percentage, snapshotted at the end of every date with games. When two teams sat on identical win% at the 8/9 line — it happened on 33 of the 158 days (15 East, 18 West) — I broke the tie by cumulative point differential, then alphabetically, and I'll show below that no tiebreak choice moves the headline dates by more than one day.

Two more ground rules. Conference membership is hardcoded, not computed: the same fifteen teams sat in each conference all season, which is stable public record, not a statistic. And "decided" is strictly hindsight: the date after which the set of eight names never changed again. Nobody in the Cleveland front office knew on December 16 that the East was finished — this is a replay audit of when the standings stopped arguing, not clinch-math about when they had to.

The bookkeeping: the bundled game log has 1,231 rows, one of which — the December 9 In-Season Tournament final, Pacers 109 at Lakers 123 — does not count in the standings, so it is dropped. The remaining 1,230 games put all 30 teams at exactly 82, and the replayed final records match the bundled ratings file for all 30 teams, which is the audit passing itself. The replay starts on October 26, the first date every team had played at least once.

Opening week already knew most of the answer

Here is the full-season distribution of how many of the sixteen eventual top-eight teams held a seat on a given night:

Seats already in final handsGame daysShare of 158
11 of 1663.8%
12 of 16148.9%
13 of 1685.1%
14 of 162213.9%
15 of 1610566.5%
16 of 1631.9%

The floor of that table is the astonishing row: the standings never disagreed with April by more than five seats, and the low-water mark of 11 was touched on just six scattered nights between October 29 and November 15. The seats being squatted on that first low night, October 29, tell you exactly what early standings are made of: the East's top eight contained Detroit, Charlotte, and Washington — which is to say the 14-win Pistons and the 15-win Wizards, the two worst teams of the season, both briefly "playoff teams" by record. By November 1 the East was still missing three of its eventual eight (New York, Miami, and Cleveland were outside; Atlanta, Brooklyn, and Detroit were in). October standings are noise stacked on uneven early slates — but the noise burned off fast: the map first reached 15 of 16 on November 29, and from there only single-seat arguments remained.

The churn was real but shallow. Twenty-seven of the 30 teams spent at least one day inside a top eight — every single Eastern team did, including six days of Pistons and two of Wizards — while Memphis, Portland, and Utah are the only teams that never visited. At the other end, five teams never spent a day outside: Boston, Milwaukee, Indiana, Denver, Dallas. Oklahoma City missed exactly one day, November 3. The membership at the top was never really the question; the question was always the last chair or two.

Two-panel chart of the 2023-24 NBA season replayed date by date. Left panel: step lines from October 26 to April 14 showing how many of each conference's eventual top-eight-by-record teams already held a top-eight seat. The East line starts at 6 of 8, bottoms at 5 in late October and early November, first touches 8 on November 19, wobbles between 7 and 8 in mid-December, and holds 8 from December 16 to the end. The West line starts at 7, bottoms at 5 from November 10 to 16, runs at 6 or 7 through early January, holds 7 from January 10, spikes to 8 on April 6 for a single day, falls back, and locks at 8 on April 12. Dashed vertical lines mark the two lock dates. Right panel: horizontal timeline bars showing days spent inside the West's top eight: Sacramento 146 of 158 game days but out at the end, Phoenix 125 and in, the Lakers 43 across scattered stints and in, Houston 38 and out, Golden State 20 and out.
Left: how many of the final eight-by-record teams already held a seat, by conference — the East flatlines at 8 from December 16, the West not until April 12. Right: the five teams that ever contested the West's last chairs. Source: bundled data_layer/nba_home_results.csv — 1,231 rows, with the 2023-12-09 In-Season Tournament final excluded as non-regular-season, leaving 1,230 games — audited against data_layer/nba_ratings.csv; conference membership hardcoded as public record. Chart by charts/chart_top_eight_lock.py.

The East was over by mid-December

The East's set first matched its final form on November 19. From there to the lock it slipped off-final exactly three times, one seat at a time: Atlanta over Cleveland for the night of November 25, Brooklyn over Miami on December 8, and Brooklyn over Cleveland from December 12 through 15 — on the 15th the Nets sat 13-11 (.542) to Cleveland's 13-12 (.520). Then came December 16, the last night the Eastern picture ever changed: Cleveland beat Atlanta 127-119 to go 14-12 (.538) while Brooklyn lost 124-120 at Golden State to fall 13-12 (.520). Cavaliers in, Nets out, and the eight names — Boston, New York, Milwaukee, Cleveland, Indiana, Philadelphia, Orlando, Miami — never moved again for the 120 calendar days of season that remained.

On the night it locked, Eastern teams had played between 24 and 27 games, 25 on average — 57 games of theoretical race left per team with nothing left to race for at the cut. And the final margin explains why: eighth-place Miami finished 46-36 (.561) and ninth-place Chicago 39-43 (.476), seven full games of daylight. The East spent 129 of the season's 158 game days already in its final configuration. Its December wobble was the whole drama.

The West's one-seat war

The West is the reason this article isn't boring. Its set was messier early — as low as 5 of 8 from November 10 to 16 — and its mid-season subplot was Houston: the Rockets spent 38 days inside the top eight across November, December, and early January, trading the last chair with Phoenix over and over. January 9 was Houston's final day inside. On January 10 the Suns passed them for good at 19-18 (.514) to Houston's 18-18 (.500), and the Western membership question collapsed to a single seat.

From January 10 onward there were 88 game days. The East was in final form on all 88. The West was one seat off on 85 of them — and that one seat is the best story in the file. Sacramento held a top-eight seat on 146 of the season's 158 game days, including every single game day from November 10 through April 5, and finished outside. The Lakers, who ended up owning the chair, spent just 43 days inside all season, flipping in and out 14 times — the most volatile membership in the league. A seat isn't won by occupying it; it's won by holding it on the last day it's contested.

15 of 16 Seats of the final top-eight-by-record map that were already settled by January 10. The last one changed hands four times in the season's final nine days: Kings to Lakers to Kings to Warriors to Lakers.

Worked example: the last chair, night by night

Here is the West's eighth seat over the final ten days, straight from the game log — the season's entire remaining suspense, in one table:

DateChairRecordWhat moved it
Apr 5Kings44-33Kings lose 101-100 at Boston, Lakers idle — both .571, Kings seated on point differential
Apr 6Lakers45-33Lakers beat Cleveland 116-97; first day all season the West's eight matched its final form
Apr 7Kings45-33Minnesota beats the Lakers 127-117; Kings win 107-77 at Brooklyn
Apr 9-10Kings45-34Kings lose at Oklahoma City, Lakers lose to Golden State — nothing moves
Apr 11Warriors45-35Warriors beat Portland, Pelicans beat the Kings, Lakers idle: a genuine three-way tie at .5625, seated by point differential
Apr 12Lakers46-35Lakers win 123-120 at Memphis; Phoenix edges Sacramento 108-107; New Orleans beats Golden State 114-109. Last change of the season
Apr 14Lakers47-35Lakers win at New Orleans; Kings and Warriors both win and both finish 46-36. Too late

Sit with April 12 for a second. The final alteration to the league's entire sixteen-seat map was assembled out of a three-point road win in Memphis and a one-point Kings loss to Phoenix — margins I've shown elsewhere are mostly indistinguishable from noise. The East's answer was carved in October-through-December granite; the West's was scribbled at 10:30 p.m. on the second-to-last night by a combined four points of scoreboard. Both facts are the same phenomenon at different gaps: seven games of daylight decides seats in December, and zero games of daylight doesn't decide them at all.

What this doesn't mean

  • It isn't clinching. "Locked on December 16" is a hindsight fact about the standings never re-arguing the East, not a thing anyone could have known that night. Magic-number math would put certainty far later; this measures when the argument actually ended, not when it provably had to.
  • It isn't seeding. The league breaks win% ties with head-to-head, division, and conference records; I used cumulative point differential, then team name, on the 33 of 158 days when the 8/9 cut was tied. The choice barely matters: rerun with the tiebreak reversed and the East still locks December 16, while the West's lock moves only from April 12 to April 11 — because April 11's chair really was a three-way 45-35 tie. No tiebreak rule gets the West decided before mid-April.
  • Top eight by record isn't the playoff field. The play-in settles seeds 7-10 on the court. As it happens — public record, not something this file can show — the 2023-24 play-ins advanced Philadelphia and Miami in the East and the Lakers and New Orleans in the West, exactly the eight-by-record in both conferences. That's an outcome, not a law, and it's why I count seats by record and say so.
  • One season. 2023-24 gave the East a seven-game moat and the West a photo finish. Another year could invert the conferences; the method transfers, the dates don't.
  • Snapshots are end-of-date. Standings only move on game days, so the 158 snapshots cover every date with games from October 26 (the first day all 30 teams had a record) through April 14. Membership persists across off-days, including the All-Star break.

Reproduce it

The whole audit is one pass through the game log with a running record. Core loop:

import pandas as pd
from collections import defaultdict

g = pd.read_csv("data_layer/nba_home_results.csv")
g = g[~((g.date == "2023-12-09") & (g.away_team == "Indiana Pacers")
        & (g.home_team == "Los Angeles Lakers"))]   # IST final: not regular season

EAST = {"Atlanta Hawks", "Boston Celtics", ...}      # public record, 15 teams
WEST = {...}                                          # the other 15

w, l, pdiff = defaultdict(int), defaultdict(int), defaultdict(int)
snaps = {}
for date, day in g.sort_values("date").groupby("date"):
    for _, x in day.iterrows():
        won = x.home_team if x.home_pts > x.away_pts else x.away_team
        lost = x.away_team if won == x.home_team else x.home_team
        w[won] += 1; l[lost] += 1
        pdiff[x.home_team] += x.home_pts - x.away_pts
        pdiff[x.away_team] += x.away_pts - x.home_pts
    rank = lambda ts: sorted(ts, key=lambda t: (-w[t] / (w[t] + l[t]), -pdiff[t], t))
    snaps[date] = (set(rank(EAST)[:8]), set(rank(WEST)[:8]))
# start reading from the first date every team has a record (2023-10-26),
# then compare each date's sets against snaps[final_date] ...

The lock date is the earliest date from which a conference's set equals the final set on every later date; charts/chart_top_eight_lock.py recomputes the whole replay from the raw log every time the chart is built, and asserts the replayed final records against the ratings file. Nothing is hand-entered.

Sources & method

  • Game-by-game results: bundled data_layer/nba_home_results.csv — 1,231 rows covering 2023-24, of which the 2023-12-09 In-Season Tournament final (Pacers at Lakers) is excluded as a non-regular-season game, leaving all 1,230 regular-season results. Underlying data: Basketball-Reference.
  • Audit: final records replayed from the game log match bundled data_layer/nba_ratings.csv (30 teams, final W-L) exactly, and every team lands on 82 games.
  • Conference membership (which 15 teams sit in each conference) is hardcoded as stable 2023-24 public record, not derived from the data. Rankings use win%, with ties at the 8/9 cut — 33 of 158 game days — broken by cumulative point differential, then name; lock dates are robust to reversing that tiebreak within one day. The play-in note in the limitations is public record, outside this dataset.
  • All figures computed 2026-07-24; the chart is charts/chart_top_eight_lock.py.
  • Related on this site: who wins the season series (the real first tiebreaker), audited · how much of an NBA game is luck · why early-season schedules mislead.

C. B. Zakarian

C. B. Zakarian is an independent analyst who writes about what he can measure: ball sports and the player-run economies inside Roblox. He builds every model, chart, and calculator here himself from public data, shows the working, and never invents a number. When the data can't answer a question, he says so. On NBAAnalytic, that means NBA ratings, shot charts, and stat explainers built from the league's public data. More about the methodology →