DEV Community

Matvey
Matvey

Posted on

269 of 280 MLB Injury Rows Described Teams With No Games Left

I run a visa agency in Bali. Scrapers are the other half of my week, and one of mine reads the ESPN injury report for the NFL, NBA, MLB, NHL and WNBA as flat rows — player, status, body part, expected return, the analyst note.

Last night's scheduled run did the obvious thing: all four major leagues, nothing filtered.

{ "mode": "injuries", "leagues": ["nfl", "nba", "mlb", "nhl"] }
Enter fullscreen mode Exit fullscreen mode

3.2 seconds, 696 rows, about 17 cents. Then I sorted by team to see who was worst hit, and the top of my table was this:

mlb  San Francisco Giants   22
mlb  Seattle Mariners       16
mlb  Athletics              14
mlb  Baltimore Orioles      14
mlb  Boston Red Sox         14
mlb  Kansas City Royals     14
mlb  Toronto Blue Jays      13
nfl  Philadelphia Eagles    12
nfl  San Francisco 49ers    12
Enter fullscreen mode Exit fullscreen mode

It is 20 October. Every one of those seven baseball teams is finished for the year. The most injured team in my dataset cannot field a line-up because there is no game to field one for.

Where the rows came from

league rows share
MLB 280 40%
NFL 198 28%
NHL 128 18%
NBA 90 13%

Baseball supplied two rows in five. So I asked the same Actor the other question — not who is hurt, but who is playing — with games mode and a seven-day window:

{ "mode": "games", "leagues": ["mlb"], "daysAhead": 7, "includePlayers": false }
Enter fullscreen mode Exit fullscreen mode

Four rows came back.

Tampa Bay Rays at Milwaukee Brewers   2026-10-24  out 6 / 5
Tampa Bay Rays at Milwaukee Brewers   2026-10-25  out 6 / 5
Milwaukee Brewers at Tampa Bay Rays   2026-10-27  out 5 / 6
Milwaukee Brewers at Tampa Bay Rays   2026-10-28  out 5 / 6
Enter fullscreen mode Exit fullscreen mode

Two teams. The World Series. Of the 280 MLB injury rows, 11 belong to those two clubs — 3.9%. The other 269 describe players on teams whose next game is in April.

Nothing is broken here. ESPN serves a league-wide injury list and answers exactly that. The mistake was mine, for treating row count as a signal. In baseball the injured list is a roster mechanism: a 60-day IL slot frees a spot and nobody clears it when the season ends. 170 of my 280 MLB rows were 60-Day-IL. The ranking I built measured paperwork.

The notes are not the same age

Each row carries reportedAt, the timestamp on the note. I measured how old they were at the moment of the sweep.

league median note age updated in last 24h older than 30 days
NFL 0 days 148 of 198 0
NBA 2 days 35 of 90 5
NHL 8 days 32 of 128 2
MLB 36 days 0 of 280 176

The oldest row in the sweep was reported on 9 February. Three leagues out of four hand you a live document; the fourth hands you an archive, and both arrive in the same array with the same field names.

The NFL number is not ESPN being diligent about football — it is the league's own rule that clubs file a participation report on practice days, so the feed is regenerated daily by obligation. Nobody imposes that on the others.

A nasty detail: for the two MLB teams that are still playing, the median note is 65 days old, worse than the league as a whole. Anyone who got hurt recently and could come back already has; what survives in October is the long-term list.

status is four vocabularies in one column

Twelve distinct values appeared across the sweep, and they do not overlap the way you would hope.

NFL   Out · Injured Reserve · Questionable · Doubtful · Suspension
NBA   Day-To-Day · Out
NHL   Injured Reserve · Day-To-Day · Out
MLB   60-Day-IL · 15-Day-IL · 10-Day-IL · 7-Day IL · Out · Day-To-Day · suspension · paternity
Enter fullscreen mode Exit fullscreen mode

Out is the only value present in all four, which makes status === "Out" the filter everybody writes first. Here is what it actually catches, against the count of players who genuinely will not take the field:

league status === "Out" not playing at all caught
NFL 96 149 64%
NBA 35 35 100%
NHL 13 115 11%
MLB 25 268 9%

Nine per cent. A hockey player on injured reserve and a pitcher on the 60-day list are as absent as absent gets, and the obvious filter drops both. To do this by hand you need the whole alternation:

const OUT = /^(out|injured reserve|suspension|not with team|60-day il|10-day il|15-day il|7-day il|il)$/i;
Enter fullscreen mode Exit fullscreen mode

Which is why outOnly: true exists in the input form — it is this regex, applied over both status and statusType, and the reason it checks both is punctuation. The same designation is spelled two ways in two fields of the same row:

{ "status": "60-Day-IL", "statusType": "60-day IL", "statusAbbreviation": "IL60" }
Enter fullscreen mode Exit fullscreen mode

Hyphen in one, space in the other. And the casing flips by league: MLB sends status: "suspension" lowercase where the NFL sends "Suspension". If you are matching strings, match the normalised abbreviation or let the Actor do it.

Empty fields that are empty by league, not by chance

{
  "league": "nhl", "team": "Anaheim Ducks", "player": "Ian Moore",
  "status": "Injured Reserve", "injuryType": "Lower Body",
  "bodyLocation": null, "detail": "Not Specified", "side": "Not Specified",
  "returnDate": "2026-11-08"
}
Enter fullscreen mode Exit fullscreen mode

Two things to read off that row.

bodyLocation is null for every MLB and NHL row — 408 of 696, 59% of the sweep. It is populated only for the NFL and NBA. Any pipeline that groups by bodyLocation silently becomes a football-and-basketball pipeline.

NHL injuryType is vague on purpose. Lower Body (33), Upper Body (28) and Undisclosed (23) are 84 of 128 hockey rows — two thirds. That is the sport's culture, not a parsing failure; teams do not disclose, especially in the playoffs. Baseball is the opposite: Elbow alone was 87 of 280 rows and detail: "Surgery" was 130. Same column, different information content.

75% of the raw feed is not injuries

I ran the NFL alone with ESPN's roster notes switched on:

{ "mode": "injuries", "leagues": ["nfl"], "includeActive": true }
Enter fullscreen mode Exit fullscreen mode

800 rows. 603 of them had status: "Active" — and of those 603, zero had an injuryType, zero had a returnDate. They are news items about healthy players:

{ "player": "Chad Ryland", "status": "Active", "injuryType": null, "returnDate": null,
  "shortComment": "Ryland converted all six of his field-goal attempts during Arizona's win over the Rams on Sunday." }
Enter fullscreen mode Exit fullscreen mode

A perfect kicking day is not an injury. Those rows are dropped by default, which is also what makes the remaining ones clean: returnDate was filled on 197 of 197 real NFL injury rows and on 695 of 696 rows across the whole four-league sweep. My own README had been claiming "roughly four rows in ten" carry a return date — a number from an older measurement that the feed has long since outgrown. I corrected it off the run above.

What I do instead now

Ask the schedule, not the league. Games mode joins the injury list to the fixtures, so every row is a game that exists:

curl -X POST "https://api.apify.com/v2/acts/lergassy~sports-injuries-api/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"mode":"games","leagues":["nfl"],"daysAhead":7,"includePlayers":false}'
Enter fullscreen mode Exit fullscreen mode

Two seconds, one row per fixture:

{
  "game": "New England Patriots at Chicago Bears",
  "startTime": "2026-10-23T00:15Z", "venue": "Soldier Field",
  "homeInjuredCount": 7, "homeOutCount": 5,
  "awayInjuredCount": 8, "awayOutCount": 4,
  "homeOutPlayers": [
    { "player": "T.J. Edwards", "position": "LB", "injuryType": "Hamstring", "returnDate": "2026-10-22" },
    { "player": "Montez Sweat", "position": "DE", "injuryType": "Suspension", "returnDate": "2026-11-02" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

A league with no games produces no rows, so the 269 phantom baseball rows cannot reach your model in the first place. The Actor is Sports Injury Report API; if you are pairing this with prices, the odds live in a separate Actor and join on team name plus start time.

One honest caveat: that compact out-player object carries the body part and the return date but not the status, so a day-to-day knock and a 60-day IL case look alike inside homeOutPlayers. Set includePlayers: true if you need to tell them apart.

When this is the wrong tool

  • You need injury history. Every run is a snapshot; ESPN does not serve the past. Schedule it and keep your own rows.
  • You need beat-reporter speed. reportedAt is ESPN's daily cycle. News forty minutes before kick-off hits Twitter first.
  • You want one cross-sport health number per team. After everything above, there isn't one. Compare NFL teams to NFL teams.

The general lesson is older than sports data. When one endpoint covers several populations, the field names stay the same and the meaning does not. Count your nulls per group before you trust a column, and count your rows per group before you trust a ranking.

Top comments (2)

Collapse
 
double_chen_70da460344c73 profile image
Double CHEN •

The 65-day median for the two active MLB teams shows why capture time cannot stand in for reportedAt. For a scheduled BrowserAct Cloud Bot, I would keep both fields and check freshness per league before joining fixtures. Re-fetching an old note must not make it fresh again.

Collapse
 
sportsapis profile image
SportsAPIs.dev •

The note-age table is the useful part here. Same field names, and a median note age anywhere from 0 to 36 days depending on the league.

I'd join the injury rows to each team's next game date before ranking anything. Then a 60-Day-IL row on a team with no games until April drops out on its own