devto 2026-10-05 원문 보기 ↗
Every spring there are plenty of "how many games will each team win" projections. Most of them build a forecast for each player and add the players up by team. I had long wondered how well that actually works.
So I built one of my own and checked it against the 2026 MLB season. I froze the projections in a repository before looking at the results, so nothing could be adjusted afterwards. I have no experience inside the game; this is just an analysis of public data.
The player forecasts are based on Marcel, a deliberately simple method by Tom Tango: weight the last three seasons (newer ones more), pull the result toward league average, and adjust a little for age. It is often used as the baseline other systems are compared with. The real Marcel projects each component (home runs, walks, strikeouts, ...) separately; mine is a simplified version that applies its weights, regression, playing time and age factor to wOBA and FIP directly.
For one batter it looks like this in Python (simplified; the real code also adjusts for age and makes league runs scored and allowed match):
# h: the player's last three seasons, one row per season
# rel = that season's wOBA minus that season's league wOBA
w = h.season.map({y - 1: 5, y - 2: 4, y - 3: 3})
rel = (w * h.pa * h.rel).sum() / ((w * h.pa).sum() + 1200) # pull toward league average
pa = 0.5 * pa_last + 0.1 * pa_before + 200 # playing time
runs_above_avg = pa * rel / woba_scale # runs above an average hitter
A team's runs scored are league-average runs plus the sum of its players' runs above average; runs allowed are built the same way from the pitchers. Plate appearances and innings are scaled so each team gets 162 games' worth.
To have something to beat, I used three floors that take no work at all:
If adding up player forecasts is worth the effort, it should at least beat these three. The yardstick is the mean absolute error of wins over the 30 teams.
Before 2026 I ran the same thing on the nine seasons 2016-2025 (leaving out the 60-game 2020), projecting each season only from the seasons before it and taking the mean error over its 30 teams.
The average over the nine seasons was 8.27 wins, against 10.60 for .500, 9.67 for last season's record and 9.34 for last season's Pythagenpat. It beat the toughest floor, Pythagenpat, in 7 of 9 seasons (not in 2019 or 2025).
These nine seasons, though, are the ones I was looking at while making design choices, so they may flatter the method. Against Pythagenpat, the 95% bootstrap interval (resampling seasons) only just cleared zero, with an upper end of -0.01 to -0.05 depending on the random seed.
For 2026 I wrote the projections to a file and merged them into the repository, together with the scoring rules, before fetching the standings. I froze them after the season had ended, but the projections only use information from before opening day; the point is that the method cannot be adjusted to fit the result. At scoring time I checked that the frozen files had not changed by a byte.
The mean error was 7.92 wins, a little below all three floors. But the 95% intervals from resampling the 30 teams cross zero for every floor (against Pythagenpat: -0.80 wins, -2.82 to +1.36). By the rule set in advance, that is "indistinguishable".
A version that stretches the projections away from .500 by a factor of 1.25 (fitted on the past seasons) did slightly worse in 2026, at 8.16.
Projected wins across, actual wins up; the closer to the dashed line, the better. MIL was projected for 84 and won 103, TB 80 and 98, SF 83 and 65, ATH 80 and 64. The correlation of projected and actual wins was 0.48 (0.63 over the nine past seasons).
The 30 projected and actual win totals on one line: the projections sit between 67 and 93 wins, while the actual records ran from 58 to 103. Marcel pulls every player toward league average, so the teams built from them are pulled toward .500 too.
A miss splits into two parts: the runs scored and allowed were projected wrong, or the team won more or fewer games than its runs suggest. The second part comes from things like close games, and no player forecast can remove it. So I asked how well the Pythagorean formula does if the actual runs scored and allowed were known.
With the actual runs, the average miss is still 3.94 wins. The gap between the projected wins and the wins implied by the actual runs, which is the runs projection being off, averages 8.06 (runs scored were off by a standard deviation of 56, runs allowed by 68).
For the four big misses, I split the miss into those two parts. Blue is the runs projection (projected wins minus wins implied by actual runs), gray is record versus runs (wins implied by actual runs minus actual wins); together they make the miss.
Almost all of MIL's 19-win miss came from runs. TB's 10 wins from runs were topped up by 8 wins of beating its runs. ATH's runs projection was off by 21 wins, but it won 5 more than its runs suggest, shrinking the miss to 16. SF's 11 wins from runs were compounded by losing 6 more than its runs suggest.
I split MIL, the biggest miss, by player: how many runs above average each was projected to add (or save), against what they did. Batters use wOBA and pitchers FIP, converted to runs the same way as in the projection. Projected PA and innings are after scaling the roster to 162 games. Only players on the opening-day 40-man roster with a projection are shown.
Jacob Misiorowski (pitcher) was projected for 97 innings and about 5 runs saved; he threw 169 innings and saved about 38. With few innings behind him, his forecast was pulled hard toward average and his innings were estimated low. Jake Bauers (batter) was projected for 309 PA at about league average and had 574 PA and about 31 runs above average. Better rates and more playing time both played a part.
MIL scored 93 more runs than projected and allowed 96 fewer, about 190 runs in all. The projected batters beating their forecasts (+19 projected, +71 actual) account for 52 of them, the projected pitchers (by FIP, +46 to +70) for 24: about 40% together. The other 60% includes fielding, baserunning and batted-ball outcomes that FIP and wOBA do not see, and players without a projection; pitchers with no forecast threw 33% of MIL's innings.
The players without a projection are those not on the opening-day 40-man roster and rookies with no MLB history. I took their share of all 2026 plate appearances and innings.
They took 20% of plate appearances and 27% of innings. The projection assumes the projected players fill that time in proportion. Across the 30 teams, though, the correlation between a team's miss and its unprojected players' total WAR was a weak -0.27, with no clear link to the big misses.
What surprised me most is that a gap of under one win cannot be seen in one season. A method can win over past seasons and still not show it in a single year of scoring, so comparing projection systems probably takes several years side by side. If I change anything next, it will be how rookies and new arrivals are handled, again frozen in advance, for 2027.
Code, the frozen projections and the scoring rules are in mlb-data-pipeline, forecast/team_wins.
fetch.py: opening-day 40-man rosters and standings from the MLB Stats APIproject.py: Marcel forecasts and team winsscore.py: floors and scoringPREREG.md: the scoring rules written before the answer was knownRESULTS.md: the 2026 result