
Two components of a GLM
1. Systematic component: The systematic part describes the relationship between the mean of \(Y\) and some linear combination of the predictors
2. Random component (distribution of the response): The random part describes the distribution of \(Y\) around that mean
The systematic part of the GLM describes the relationship between the mean of \(Y\) and some function of \(\beta_0 + \beta_1 X_1 + \dots + \beta_p X_p\).
Specifically, the systematic part is of the form
\[g(\mathbb{E}[Y \mid X = x]) = \beta_0 + \beta_1 X_1 + \dots + \beta_p X_p\]
where \(g\) is known as the link function.
(Recall: We cannot just simply model \(p = \beta_0 + \beta_1 X_1 + \dots + \beta_p X_p\) since the binary response can only take on values of either 0 or 1)
\[\log \left( \frac{p(x)}{1 - p(x)} \right) = \beta_0 + \beta_1 x_1 + \cdots + \beta_p x_p\]
\[\frac{p(x)}{1 - p(x)} = \exp(\beta_0 + \beta_1 x_1 + \cdots + \beta_p x_p) \]
\[p(x) = \frac{\exp(\beta_0 + \beta_1 x_1 + \cdots + \beta_p x_p)}{1 + \exp(\beta_0 + \beta_1 x_1 + \cdots + \beta_p x_p)} = \frac{1}{1 + \exp(-(\beta_0 + \beta_1 x_1 + \cdots + \beta_p x_p))}\]
(We cannot just simply model \(\lambda = \beta_0 + \beta_1 X_1 + \dots + \beta_p X_p\) since the Poisson mean parameter must be positive)
\[\log(\lambda) =\beta_0 + \beta_1 X_1 + \dots + \beta_p X_p\] \[\lambda = \exp(\beta_0 + \beta_1 X_1 + \dots + \beta_p X_p)\]
which guarantees \(\lambda >0\).
In linear regression, the distribution is normal and the domain of \(Y \mid x\) is \((-\infty,\infty)\)
What, however, happens if we know that
the domain of observed values of the response is actually \([0,\infty]\)
the observed values are discrete, with possible values \(0, 1, 2, \dots\)
The normal distribution doesn’t hold here
Which distribution could possibly govern \(Y \mid x\)?
Remember, we might not know truly how \(Y \mid x\) is distributed, but any assumption we make has to fit with the nature of the data
\(\lambda\) is both the mean and the variance of the distribution
distribution of independent event occurrences in an interval, e.g. soccer goals in a match
\(\lambda\) is the average number of the events in an interval
So, when we apply GLM in this context, we would identify the family as Poisson
But there’s another step in generalization…

Start with one predictor, linear function: \(\beta_0 + \beta_1 x\)
Range of this function: \((-\infty,\infty)\)
But for Poisson regression, \(Y\) cannot be negative,
We need to transform the linear function to be \([0,\infty)\)
(What if we use linear regression? Results may not be meaningful, e.g., we predict \({\hat Y}\) to be negative!)
There is usually no unique transformation, but rather conventional ones
For Poisson, we use the \(\log()\) function as the link function \(g()\):
\[g(\lambda \mid x) = \log(\lambda \mid x) = \beta_0 + \beta_1 x\]
(Hence we also call this a log-linear model)
Given \(Y\) with values limited to being either 0 or positive integers, with no upper bound, we
assume \(Y \mid x \sim \text{Poisson}(\lambda)\)
assume \(\lambda \mid x = e^{\beta_0 + \beta_1 x}\)
use optimization to estimate \(\beta_0\) and \(\beta_1\) by maximizing the likelihood function
\(Y \mid x\) continuous, but bounded between 0 and \(\infty\)
Application: modeling continuous, positive, and (right) skewed response
Finance: modeling insurance claim severity
Environmental science: modeling rainfall
\(Y \mid x\) continuous, but bounded between 0 and 1
Application: modeling proportions (see here for an example)
Amount of foreign aid a country receives as a percent of its GDP
Proportion of a population that received COVID-19 vaccine
Game level shot information for the NHL 2021-2022 season, via the SCORE Sports Data Repository
Rows: 2,802
Columns: 8
$ game_id <dbl> 2021020001, 2021020001, 2021020002, 2021020002, 2021020003,…
$ event_team <chr> "Pittsburgh Penguins", "Tampa Bay Lightning", "Seattle Krak…
$ shots <dbl> 58, 55, 63, 58, 55, 64, 53, 48, 61, 70, 67, 66, 58, 54, 68,…
$ avg_dist <dbl> 34.45106, 41.99500, 34.34091, 35.07234, 35.58780, 34.63137,…
$ avg_angle <dbl> 29.69149, 25.62750, 33.07727, 29.58298, 31.37805, 26.01373,…
$ goals <dbl> 6, 2, 3, 4, 1, 2, 1, 5, 2, 4, 2, 2, 5, 1, 3, 2, 6, 7, 5, 4,…
$ is_home <dbl> 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0,…
$ division <chr> "Metropolitan", "Atlantic", "Pacific", "Pacific", "Atlantic…
a <- scoring |>
mutate(goals_group = case_when(goals <2 ~ "0-1",
goals >= 2 & goals <=3 ~ "2-3",
goals == 4 ~ "4",
goals > 4 ~ "5+")) |>
ggplot(aes(x=avg_dist, color = goals_group))+
geom_density()+
theme(legend.position = "bottom")
b <- scoring |>
ggplot(aes(x=avg_dist, y = goals))+
geom_point(position = "jitter", alpha=0.5)+
geom_smooth()
library(patchwork)
a+b
What observations in the dataset correspond to predictions using only the intercept?
# A tibble: 6 × 5
term estimate std.error statistic p.value
<chr> <dbl> <dbl> <dbl> <dbl>
1 (Intercept) 1.50 0.0874 17.1 8.04e-66
2 divisionCentral 0.0132 0.0299 0.441 6.59e- 1
3 divisionMetropolitan -0.0229 0.0303 -0.756 4.50e- 1
4 divisionPacific -0.0367 0.0305 -1.20 2.29e- 1
5 is_home 0.0819 0.0215 3.80 1.44e- 4
6 avg_dist -0.0112 0.00239 -4.68 2.88e- 6
# A tibble: 6 × 7
term estimate std.error statistic p.value conf.low conf.high
<chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 (Intercept) 4.47 0.0874 17.1 8.04e-66 3.77 5.31
2 divisionCentral 1.01 0.0299 0.441 6.59e- 1 0.956 1.07
3 divisionMetropolitan 0.977 0.0303 -0.756 4.50e- 1 0.921 1.04
4 divisionPacific 0.964 0.0305 -1.20 2.29e- 1 0.908 1.02
5 is_home 1.09 0.0215 3.80 1.44e- 4 1.04 1.13
6 avg_dist 0.989 0.00239 -4.68 2.88e- 6 0.984 0.994
One foot of average shot distance further from the net is associated with the mean number of goals being multiplied by 0.988 [95% CI(0.984, 0.993)].
The expected number of goals changes (is multiplied) by a factor of 1.085 (95% CI [1.04, 1.13]) if the team is home in comparison to away teams, after accounting for shot distance and division.
Often used for testing null that
The deviance difference is a likelihood ratio statistic. Likelihood ratio statistics are asymptotically chi-square, so this test has a chi-squared distribution.
Analysis of Deviance Table
Model 1: goals ~ is_home + avg_dist
Model 2: goals ~ division + is_home + avg_dist
Resid. Df Resid. Dev Df Deviance Pr(>Chi)
1 2799 3162.6
2 2796 3159.4 3 3.2622 0.3529
A deviance test comparing our model with a reduced model fit solely with average shot distance and home advantage does not provide evidence that division is associated with number of goals scored.
Poisson regression makes 3 assumptions:
Check with empirical link plot in EDA or with residuals after fitting model.
Doesn’t apply to factor variables
library(regressinator)
scoring |>
bin_by_quantile(avg_dist, breaks = 10) |>
summarize(
mean_avg_dist = mean(avg_dist),
log_goals = empirical_link(
goals,
family = poisson(link = "log"),
n = n()
)) |>
ggplot(aes(x = mean_avg_dist, y = log_goals)) +
geom_point()+
geom_smooth() +
labs(x = "Avg shot distance", y = "Log goals")
In short, randomized quantile residuals deal with the patterns produced by discrete response variables by calculating the CDF for each observation.
Examples of when to use an offset
modeling number of arrested insurrectionists by county (population offset)
counting bird species in varies forests (size of forest or time spent birding offset)
Why not just include this as a variable?
In the situations above, interpreting with counts doesn’t make much sense
Including the variable as an offset constrains the model to predict rates rather than raw counts by fixing its coefficient to 1
For hockey data: should we include shots as a offset? Depends on what question you are trying to answer.
Using shots as an offset: Do division, home ice, and average shot distance affect a team’s goal-scoring rate per shot?
Using shots as a predictor: Holding division, home ice, and average shot distance constant, how does generating more shots affect expected goals?
# A tibble: 6 × 5
term estimate std.error statistic p.value
<chr> <dbl> <dbl> <dbl> <dbl>
1 (Intercept) 0.0694 0.0885 -30.1 1.12e-199
2 divisionCentral 1.04 0.0299 1.22 2.22e- 1
3 divisionMetropolitan 0.977 0.0303 -0.754 4.51e- 1
4 divisionPacific 0.928 0.0305 -2.44 1.46e- 2
5 is_home 1.03 0.0215 1.25 2.12e- 1
6 avg_dist 0.993 0.00243 -2.87 4.12e- 3
A 10-foot increase in average shot distance is associated with approximately a \(0.99306499^{10} = 0.933\) lower goal-scoring rate per shot, holding division and home ice constant.
Key idea: introduces another parameter such that the variance can exceed the mean
We have \(E(Y) = \lambda\) and \(\text{Var}(Y)=\lambda(1+\alpha \lambda)\)
See here for more information
Call:
MASS::glm.nb(formula = goals ~ division + is_home + avg_dist,
data = scoring, init.theta = 415.9359625, link = log)
Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) 1.49745 0.08772 17.071 < 2e-16 ***
divisionCentral 0.01320 0.03004 0.440 0.660255
divisionMetropolitan -0.02286 0.03039 -0.752 0.452029
divisionPacific -0.03669 0.03064 -1.197 0.231123
is_home 0.08185 0.02162 3.786 0.000153 ***
avg_dist -0.01119 0.00240 -4.661 3.15e-06 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Dispersion parameter for Negative Binomial(415.936) family taken to be 1)
Null deviance: 3182.3 on 2801 degrees of freedom
Residual deviance: 3138.4 on 2796 degrees of freedom
AIC: 11002
Number of Fisher Scoring iterations: 1
Theta: 416
Std. Err.: 1535
Warning while fitting theta: alternation limit reached
2 x log-likelihood: -10987.88
A zero-inflated Poisson (ZIP) model is a mixture model with two components
Logistic regression model to predict whether the response will be zero (whether 0 goals are observed) \[P(Y = 0) = \pi + (1 - \pi)e^{\lambda}\]
Poisson regression model for non-zero counts (non-zero goal values) \[P(Y = y) = (1 - \pi) \frac{\lambda^y e^{-\lambda}}{y!}, \text{ for } y = 1, 2, 3, \dots\]
Gamma regression: Strictly positive, continuous data that are right-skewed, where the variance increases with the mean.
Feel free to explore more or ask us questions!
hockey <- read_csv("https://raw.githubusercontent.com/36-SURE/2026/main/data/nhl-shots-2021.csv")
summary <- hockey |>
group_by(game_id, event_team, home_name) |>
summarize(goals_home = max(home_score), goals_away = max(away_score), shots = n(), avg_dist = mean(shot_distance, na.rm=T), avg_angle = mean(shot_angle, na.rm=T)) |>
mutate(goals = case_when(home_name == event_team ~ goals_home,
TRUE ~ goals_away),
is_home = case_when(event_team == home_name ~ 1,
TRUE ~ 0)) |>
dplyr::select(-goals_home, -goals_away, -home_name) |>
mutate(division = case_when(event_team %in% c("Boston Bruins", "Buffalo Sabres", "Detroit Red Wings", "Florida Panthers", "Montréal Canadiens", "Ottawa Senators", "Tampa Bay Lightning", "Toronto Maple Leafs") ~ "Atlantic",
event_team %in% c("Carolina Hurricanes", "Columbus Blue Jackets", "New Jersey Devils", "New York Islanders", "New York Rangers", "Philadelphia Flyers", "Pittsburgh Penguins", "Washington Capitals" ) ~ "Metropolitan",
event_team %in% c("Arizona Coyotes", "Chicago Blackhawks", "Colorado Avalanche", "Dallas Stars", "Minnesota Wild", "Nashville Predators", "St. Louis Blues", "Winnipeg Jets") ~ "Central",
event_team %in% c("Anaheim Ducks", "Calgary Flames", "Edmonton Oilers", "Los Angeles Kings", "San Jose Sharks", "Seattle Kraken", "Vancouver Canucks", "Vegas Golden Knights") ~ "Pacific"))