Introduction

Have NHL goaltenders gotten bigger over time? Does being taller actually translate to better performance?

In this code-through, we will use historical NHL goaltender data to explore how goalie size has changed over time and whether height is associated with save percentage. We will learn how to import, manipulate, visualize, and analyze data using R.


What You’ll Learn

By the end of this tutorial, you will be able to:

  • Import Excel datasets using read_excel().
  • Inspect datasets using head() and names().
  • Create new variables using basic arithmetic.
  • Visualize data using ggplot2.
  • Combine datasets using left_join().
  • Convert character variables into numeric variables.
  • Run and interpret basic linear regressions using lm().


About the Data

This tutorial uses two datasets containing historical NHL goaltender information:

  • GoalieBio.xlsx: Contains biographical information, including height, weight, and first NHL season.
  • GoalieStats.xlsx: Contains career statistics, including games played, wins, save percentage, and goals-against average.

Goaltenders with fewer than 10 career NHL games played were excluded from the original Excel files. This helps reduce the influence of extremely small career samples.


Getting to Know Our Data

Before we can analyze NHL goaltenders, we first need to import our data sets into R and understand how the information is organized.

For this tutorial, we will use the read_excel() function from the readxl package to import our two Excel files.

Make sure that GoalieBio.xlsx and GoalieStats.xlsx are saved in the same folder as your R Markdown document.

goalie.bio <- read_excel("GoalieBio.xlsx")
goalie.stats <- read_excel("GoalieStats.xlsx")


Inspecting the Data

After importing our data, we should examine the variables in each data set.

The names() function returns the column names, helping us identify which variables we can use in our analysis.

The head() function displays the first six rows of a data set, allowing us to preview how the information is stored.

names(goalie.bio)
##  [1] "Player"     "S/C"        "DOB"        "Birth City" "S/P"       
##  [6] "Ctry"       "Ntnlty"     "Ht"         "Wt"         "Draft Yr"  
## [11] "Round"      "Overall"    "1st Season" "HOF"        "GP"        
## [16] "W"          "L"          "T"          "OT"         "SO"
names(goalie.stats)
##  [1] "Player" "S/C"    "GP"     "GS"     "W"      "L"      "T"      "OT"    
##  [9] "SA"     "Svs"    "GA"     "Sv%"    "GAA"    "TOI"    "SO"     "G"     
## [17] "A"      "P"      "PIM"
head(goalie.bio)
head(goalie.stats)

The bio data set contains information such as player height (Ht), weight (Wt), and first NHL season (1st Season).

The stats data set contains performance variables such as games played (GP), career save percentage (Sv%), and goals-against average (GAA).


Preparing the Data

Before we begin analyzing goaltender size and performance, we need to combine our data sets and ensure that the variables are formatted appropriately.

Combining the Data Sets

Biographical and statistical information is currently stored in two separate data sets. Both data sets contain a Player variable that identifies each goaltender, allowing us to combine them using left_join() from the dplyr package.

goalies <- left_join(goalie.bio, goalie.stats, by = "Player")

names(goalies)
##  [1] "Player"     "S/C.x"      "DOB"        "Birth City" "S/P"       
##  [6] "Ctry"       "Ntnlty"     "Ht"         "Wt"         "Draft Yr"  
## [11] "Round"      "Overall"    "1st Season" "HOF"        "GP.x"      
## [16] "W.x"        "L.x"        "T.x"        "OT.x"       "SO.x"      
## [21] "S/C.y"      "GP.y"       "GS"         "W.y"        "L.y"       
## [26] "T.y"        "OT.y"       "SA"         "Svs"        "GA"        
## [31] "Sv%"        "GAA"        "TOI"        "SO.y"       "G"         
## [36] "A"          "P"          "PIM"

The left_join() function matches observations using a shared variable, which we specify with by = "Player".

Because goalie.bio is the first data set, R keeps its observations and adds matching information from goalie.stats.

We saved the result as goalies and used names() to inspect it.

Creating a Debut Year Variable

To examine historical trends, we need to identify the year each goaltender entered the NHL.

We can use class() and head() to examine how the 1st Season variable is stored.

class(goalies$`1st Season`)
## [1] "numeric"
head(goalies$`1st Season`)
## [1] 19721973 20002001 19321933 20122013 20132014 20022003

The class() function tells us that 1st Season is stored as a numeric variable and that each season is represented by an eight digit number. For example, the 1972–1973 NHL season appears as 19721973.

To make these values more useful for examining trends over time, we can extract the starting year using floor().

goalies$start.year <- floor(goalies$`1st Season` / 10000)

head(goalies$start.year)
## [1] 1972 2000 1932 2012 2013 2002

By dividing the original season value by 10,000, we move the starting year into the whole number portion of the result.

19721973 / 10000 = 1972.1973

The floor() function rounds a number down to the nearest whole number, giving us 1972. We save these values in a new column called start.year using the $ operator.

Cleaning the Save Percentage Variable

Now, we need to prepare our career save percentage (Sv%) variable. We’ll use this as our primary measure of goaltender performance. Let’s examine how it is stored in R.

As before, we can use class() to identify its data type and head() to preview its values.

class(goalies$`Sv%`)
## [1] "character"
head(goalies$`Sv%`)
## [1] "0.877"               "0.91200000000000003" "--"                 
## [4] "0.90700000000000003" "0.91300000000000003" "0.91200000000000003"

Although save percentage appears to contain numerical values, class() reveals that R recognizes the variable as character data.

To perform calculations using this variable, we need to convert it into numeric data using the as.numeric() function. We’ll also run class() and head() again to confirm that the variable is now numeric.

goalies$`Sv%` <- as.numeric(goalies$`Sv%`)

class(goalies$`Sv%`)
## [1] "numeric"
head(goalies$`Sv%`)
## [1] 0.877 0.912    NA 0.907 0.913 0.912

Note, values that cannot be converted into numbers, such as “–”, are replaced with NA, indicating a missing value.

Visualizing Relationships

Now that our data is prepared, we can use ggplot2 to explore relationships between goaltender size, debut year, and performance.

Goaltender Size Over Time

We can create a scatterplot to examine whether goaltender height has changed over time. The ggplot() function specifies our data and variables, while geom_jitter() plots individual observations with slight adjustments to reduce overlapping points.

ggplot(goalies, aes(x = start.year, y = Ht)) +
  geom_jitter(width = 0.2, height = 0) +
  geom_smooth(method = "lm")

The points represent individual goaltenders, while geom_smooth(method = "lm") adds a linear trend line. The upward trend suggests that NHL goaltenders have generally gotten taller over time.

Height and Save Percentage

Next, we can examine whether taller goaltenders tend to have higher career save percentages.

Using the same functions, we can create a scatterplot comparing height (Ht) with career save percentage (Sv%).

ggplot(goalies, aes(x = Ht, y = `Sv%`)) +
  geom_jitter(width = 0.2, height = 0) +
  geom_smooth(method = "lm")

The trend line slopes upward, suggesting a positive relationship between goaltender height and career save percentage.

However, this graph alone cannot tell us how strong that relationship is or whether other factors might help explain it.

Exploring Regression

Our visualizations suggest that taller goaltenders tend to have higher career save percentages. However, we can use statistical analysis to better understand this relationship.

Linear Regression

The lm() function estimates the relationship between a dependent variable and one or more independent variables.

First, we will create a simple regression model using career save percentage as our dependent variable and height as our independent variable.

model1 <- lm(`Sv%` ~ Ht, data = goalies)

summary(model1)
## 
## Call:
## lm(formula = `Sv%` ~ Ht, data = goalies)
## 
## Residuals:
##       Min        1Q    Median        3Q       Max 
## -0.075571 -0.009361  0.002348  0.011607  0.031655 
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept) 0.7028407  0.0188908   37.20   <2e-16 ***
## Ht          0.0026127  0.0002604   10.03   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.01613 on 530 degrees of freedom
##   (62 observations deleted due to missingness)
## Multiple R-squared:  0.1596, Adjusted R-squared:  0.158 
## F-statistic: 100.7 on 1 and 530 DF,  p-value: < 2.2e-16

We save the regression as model1, then use summary() to examine its results.

The height coefficient is approximately 0.00261. This suggests that each additional inch of height is associated with a 0.00261 increase in career save percentage, or about 0.26 percentage points.

The model has an R-squared value of approximately 0.16, meaning height alone explains about 16% of the variation in career save percentage among goaltenders included in the model.

Accounting for Debut Year

Because goaltender performance may also differ across historical eras, we can add debut year (start.year) to our regression model.

This allows us to examine the relationship between height and save percentage while holding debut year constant.

model2 <- lm(`Sv%` ~ Ht + start.year, data = goalies)

summary(model2)
## 
## Call:
## lm(formula = `Sv%` ~ Ht + start.year, data = goalies)
## 
## Residuals:
##       Min        1Q    Median        3Q       Max 
## -0.066828 -0.008787  0.002058  0.010208  0.036380 
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept) 8.251e-02  8.433e-02   0.978 0.328368    
## Ht          1.093e-03  3.196e-04   3.419 0.000676 ***
## start.year  3.662e-04  4.864e-05   7.529 2.23e-13 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.01535 on 529 degrees of freedom
##   (62 observations deleted due to missingness)
## Multiple R-squared:  0.2409, Adjusted R-squared:  0.2381 
## F-statistic: 83.95 on 2 and 529 DF,  p-value: < 2.2e-16

After adding debut year, the height coefficient decreases from approximately 0.00261 to 0.00109.

This means that each additional inch of height is now associated with an increase of approximately 0.11 percentage points in career save percentage, holding debut year constant.

The model’s R-squared also increases from approximately 0.16 to 0.24, indicating that including debut year improves the model’s explanatory power.

These results suggest that historical era helps explain part of the observed relationship between height and save percentage.

Although height remains positively associated with save percentage, we cannot conclude that being taller directly causes better goaltender performance.

Conclusion

So, does size matter?

Our analysis suggests that NHL goaltenders have generally gotten taller over time and that taller goaltenders tend to have higher career save percentages.

However, after accounting for debut year in our regression model, the relationship between height and save percentage becomes smaller. This suggests that historical era helps explain some of the relationship we initially observed.

While these results demonstrate an association between height and performance, they do not establish that taller goaltenders are inherently better. Other factors, such as changes in equipment, playing styles, and goaltender techniques, may also influence performance.

Throughout this tutorial, we used R to import and combine data sets, create and clean variables, visualize relationships, and estimate linear regression models.

These techniques can be applied to many other data sets.

Thanks for reading!


Further Resources

The following R packages were used in this tutorial:


Works Cited

This code through references and cites the following sources:

National Hockey League. (n.d.). NHL goaltender statistics [Data set]. NHL.com. Retrieved October 9, 2026, from https://www.nhl.com/stats/goalies

The data were obtained from the National Hockey League’s official statistics database and downloaded into two Excel files for this tutorial.