Do Bigger Skaters Translate Better in the Playoffs?

Question

The playoff-size cliché is familiar: heavier teams supposedly survive the grind, win the walls, and keep scoring when space disappears. But that claim hides two different questions:

  • Do bigger skaters score more in the playoffs?
  • Do bigger skaters lose less scoring when regular-season hockey becomes playoff hockey?

This article uses skater_playoff_statistics(), skater_statistics(), and players() to compare career regular-season scoring, career playoff scoring, and the gap between the two.

Build Player Table

We keep salary-cap-era skaters with meaningful regular-season and playoff samples. The unit is one player, not one season.

# Pull scoring and bio tables.
playoff_stats <- nhlscraper::skater_playoff_statistics()
career_stats <- nhlscraper::skater_statistics()[, c(
  'playerId',
  'rsGamesPlayed',
  'rsPoints',
  'positionCode'
)]
player_bios <- nhlscraper::players()[, c(
  'playerId',
  'playerFullName',
  'height',
  'weight'
)]

# Join player-level sources.
analysis_tbl <- merge(
  playoff_stats,
  career_stats,
  by = c('playerId', 'positionCode'),
  all.x = TRUE
)
analysis_tbl <- merge(
  analysis_tbl,
  player_bios,
  by = 'playerId',
  all.x = TRUE
)

# Keep modern skaters with stable samples.
analysis_tbl <- analysis_tbl[
  !is.na(analysis_tbl[['height']]) &
    !is.na(analysis_tbl[['weight']]) &
    analysis_tbl[['firstSeasonForGameType']] >= 20052006 &
    analysis_tbl[['gamesPlayed']] >= 20 &
    analysis_tbl[['rsGamesPlayed']] >= 200,
  ,
  drop = FALSE
]

# Fill names and compute rates.
analysis_tbl[['playerFullName']] <- ifelse(
  is.na(analysis_tbl[['playerFullName']]) |
    analysis_tbl[['playerFullName']] == '',
  paste(
    analysis_tbl[['skaterFirstName']],
    analysis_tbl[['skaterLastName']]
  ),
  analysis_tbl[['playerFullName']]
)
analysis_tbl[['regularPPG']] <-
  analysis_tbl[['rsPoints']] / analysis_tbl[['rsGamesPlayed']]
analysis_tbl[['playoffPPG']] <-
  analysis_tbl[['points']] / analysis_tbl[['gamesPlayed']]
analysis_tbl[['playoffLift']] <-
  analysis_tbl[['playoffPPG']] - analysis_tbl[['regularPPG']]
analysis_tbl[['positionBucket']] <- ifelse(
  analysis_tbl[['positionCode']] == 'D',
  'Defense',
  'Forward'
)

# Assign equal-count weight quartiles.
weight_rank <- rank(
  analysis_tbl[['weight']],
  ties.method = 'first'
) / nrow(analysis_tbl)
analysis_tbl[['weightQuartile']] <- cut(
  weight_rank,
  breaks = c(0, 0.25, 0.50, 0.75, 1),
  include.lowest = TRUE,
  labels = c('Lightest', 'Second', 'Third', 'Heaviest')
)
nrow(analysis_tbl)
#> [1] 1732

The sample has 1732 skaters. That filters out one-series mirages while still leaving enough players to compare body types.

Level Versus Translation

First, compare regular-season scoring, playoff scoring, and playoff lift by weight quartile.

# Summarize scoring by weight quartile.
quartile_summary <- aggregate(
  cbind(regularPPG, playoffPPG, playoffLift) ~ weightQuartile,
  data = analysis_tbl,
  FUN = mean
)
quartile_counts <- as.data.frame(table(analysis_tbl[['weightQuartile']]))
names(quartile_counts) <- c('weightQuartile', 'n')
quartile_summary <- merge(
  quartile_summary,
  quartile_counts,
  by = 'weightQuartile'
)
quartile_summary <- quartile_summary[
  match(levels(analysis_tbl[['weightQuartile']]), quartile_summary[['weightQuartile']]),
  c('weightQuartile', 'n', 'regularPPG', 'playoffPPG', 'playoffLift')
]
make_table(
  quartile_summary,
  caption = 'Regular-season scoring, playoff scoring, and playoff lift by weight quartile.',
  digits = 3
)
Regular-season scoring, playoff scoring, and playoff lift by weight quartile.
weightQuartile n regularPPG playoffPPG playoffLift
2 Lightest 433 0.516 0.465 -0.051
3 Second 433 0.464 0.407 -0.057
4 Third 433 0.440 0.385 -0.055
1 Heaviest 433 0.422 0.379 -0.043

The most important distinction is level versus translation. Lighter skaters can still post higher raw scoring rates. Bigger skaters can still translate a bit better relative to their own regular-season baseline. Those are different claims, and mixing them together is how playoff clichés become sloppy.

# Plot playoff scoring and playoff lift.
old_par <- graphics::par(no.readonly = TRUE)
graphics::par(mfrow = c(1, 2), mar = c(8, 4, 3, 1))
graphics::boxplot(
  playoffPPG ~ weightQuartile,
  data = analysis_tbl,
  col = c('#d8f3dc', '#b7e4c7', '#74c69d', '#2d6a4f'),
  border = '#1b4332',
  las = 2,
  xlab = '',
  ylab = 'Playoff Points Per Game'
)
graphics::barplot(
  quartile_summary[['playoffLift']],
  names.arg = quartile_summary[['weightQuartile']],
  col = c('#fcbf49', '#f77f00', '#d62828', '#6a4c93'),
  border = NA,
  las = 2,
  xlab = '',
  ylab = 'Playoff Lift'
)
graphics::abline(h = 0, lty = 2, col = '#495057')
Playoff scoring level and playoff lift by weight quartile.

Playoff scoring level and playoff lift by weight quartile.

graphics::par(old_par)

Every group loses scoring on average. The interesting question is how much.

Position Is Part of the Story

Weight and position are tangled. Defensemen are heavier, score less, and often play playoff minutes that are less offense-driven. Splitting forwards and defensemen helps keep the interpretation honest.

# Summarize rates by position and quartile.
position_summary <- aggregate(
  cbind(regularPPG, playoffPPG, playoffLift) ~ positionBucket + weightQuartile,
  data = analysis_tbl,
  FUN = mean
)
position_counts <- aggregate(
  playerId ~ positionBucket + weightQuartile,
  data = analysis_tbl,
  FUN = length
)
names(position_counts)[names(position_counts) == 'playerId'] <- 'n'
position_summary <- merge(
  position_summary,
  position_counts,
  by = c('positionBucket', 'weightQuartile')
)
make_table(
  position_summary,
  caption = 'Scoring translation by position family and weight quartile.',
  digits = 3
)
Scoring translation by position family and weight quartile.
positionBucket weightQuartile regularPPG playoffPPG playoffLift n
Defense Heaviest 0.309 0.280 -0.029 200
Defense Lightest 0.437 0.414 -0.023 82
Defense Second 0.376 0.333 -0.043 147
Defense Third 0.314 0.279 -0.035 150
Forward Heaviest 0.520 0.465 -0.055 233
Forward Lightest 0.534 0.477 -0.057 351
Forward Second 0.509 0.444 -0.064 286
Forward Third 0.507 0.442 -0.065 283

The split keeps the story from becoming too neat. Size alone is not the answer. Role, position, and baseline scoring level matter.

Which Players Actually Rise?

Population averages are useful, but the player list is where the question feels like hockey.

# Show largest positive playoff lifts.
risers_tbl <- analysis_tbl[
  analysis_tbl[['gamesPlayed']] >= 40,
  c(
    'playerFullName',
    'positionBucket',
    'weight',
    'regularPPG',
    'playoffPPG',
    'playoffLift',
    'gamesPlayed'
  )
]
risers_tbl <- risers_tbl[order(-risers_tbl[['playoffLift']]), ]
risers_tbl <- utils::head(risers_tbl, 10)
make_table(
  risers_tbl,
  caption = 'Largest playoff scoring lifts among skaters with at least 40 playoff games.',
  digits = 3
)
Largest playoff scoring lifts among skaters with at least 40 playoff games.
playerFullName positionBucket weight regularPPG playoffPPG playoffLift gamesPlayed
8229 Daniel Brière Forward 174 0.715 1.059 0.344 68
12718 Evan Bouchard Defense 196 0.801 1.086 0.285 81
12719 Evan Bouchard Defense 196 0.801 1.086 0.285 81
10838 Jakob Silfverberg Forward 207 0.455 0.719 0.264 57
11971 Sam Bennett Forward 193 0.515 0.766 0.251 77
11968 Leon Draisaitl Forward 215 1.237 1.480 0.243 102
11969 Leon Draisaitl Forward 215 1.237 1.480 0.243 102
12284 Evan Rodrigues Forward 182 0.438 0.672 0.234 61
10827 Ryan O’Reilly Forward 207 0.729 0.961 0.232 51
12287 Evan Rodrigues Forward 182 0.438 0.667 0.228 45
# Show largest negative playoff lifts.
fallers_tbl <- analysis_tbl[
  analysis_tbl[['gamesPlayed']] >= 40,
  c(
    'playerFullName',
    'positionBucket',
    'weight',
    'regularPPG',
    'playoffPPG',
    'playoffLift',
    'gamesPlayed'
  )
]
fallers_tbl <- fallers_tbl[order(fallers_tbl[['playoffLift']]), ]
fallers_tbl <- utils::head(fallers_tbl, 10)
make_table(
  fallers_tbl,
  caption = 'Largest playoff scoring drops among skaters with at least 40 playoff games.',
  digits = 3
)
Largest playoff scoring drops among skaters with at least 40 playoff games.
playerFullName positionBucket weight regularPPG playoffPPG playoffLift gamesPlayed
8552 Martin St. Louis Forward 180 0.911 0.500 -0.411 44
11450 J.T. Miller Forward 219 0.809 0.400 -0.409 40
12289 Artemi Panarin Forward 176 1.148 0.761 -0.387 46
11173 Tyler Seguin Forward 205 0.810 0.429 -0.381 42
12600 Robert Thomas Forward 213 0.863 0.500 -0.363 52
12601 Robert Thomas Forward 213 0.863 0.500 -0.363 52
12031 Ivan Barbashev Forward 198 0.529 0.180 -0.349 50
12761 Noah Dobson Defense 200 0.585 0.250 -0.335 44
10843 John Tavares Forward 211 0.936 0.608 -0.329 51
8683 Brad Richards Forward 199 0.828 0.509 -0.319 55

These tables are deliberately humbling. Playoff translation is not a body-type sorting machine. Some lighter players hold up beautifully. Some bigger players drop. The useful signal is a tendency, not a rule.

Model the Gap

A simple model lets us ask whether height or weight carries a standalone slope once position is included.

# Fit playoff-lift model.
lift_fit <- stats::lm(
  playoffLift ~ height + weight + I(positionCode == 'D'),
  data = analysis_tbl
)
lift_fit_tbl <- as.data.frame(summary(lift_fit)$coefficients)
lift_fit_tbl[['term']] <- rownames(lift_fit_tbl)
rownames(lift_fit_tbl) <- NULL
lift_fit_tbl[['term']] <- c(
  'Intercept',
  'Height',
  'Weight',
  'Defense indicator'
)
lift_fit_tbl <- lift_fit_tbl[, c(
  'term',
  'Estimate',
  'Std. Error',
  't value',
  'Pr(>|t|)'
)]
make_table(
  lift_fit_tbl,
  caption = 'Linear model of playoff scoring lift.',
  digits = 4
)
Linear model of playoff scoring lift.
term Estimate Std. Error t value Pr(>|t|)
Intercept 0.0118 0.1182 0.0995 0.9207
Height -0.0016 0.0021 -0.7551 0.4503
Weight 0.0002 0.0003 0.7287 0.4663
Defense indicator 0.0275 0.0066 4.1803 0.0000

The plain-language result is that size is not magic. Once position is in the model, the direct height/weight slopes are not the headline. The playoff translation cliché is closer to a role-and-usage story than a simple bigger-is-better story.

What We Learned

Bigger skaters do not automatically become better playoff scorers. The lighter groups can still lead in raw playoff points per game. But the heaviest group can look slightly sturdier relative to its own regular-season scoring baseline.

That split answer is the point. nhlscraper makes it easy to combine career stats and player bios, but the interpretation still has to respect the shape of the data. A good mini research question is not always one where the cliché is true or false. Sometimes the best answer is: true in one sense, false in another.