set.seed(0)
n <- 500
X <- rnorm(n)
p_true <- 1 / (1 + exp(-(2 + 3 * X))) # P(Y = 1 | X)
Y <- as.integer(runif(n) <= p_true)
sim <- tibble(X, Y)
mean(Y)## [1] 0.722
Lecture 9: Logistic Regression
24 September 2026
| Question | Y = 1 | Predictors X |
|---|---|---|
| Will this customer subscribe? | Subscribes | Browsing history, past purchases |
| Will this borrower repay the loan? | Repays | Income, credit history, loan terms |
| Will this user click the ad? | Clicks | Query, device, time of day |
| Is this email spam? | Spam | Word and character indicators |
| Which of two chatbot answers wins? | Model A wins | Which two models were compared |
For a binary outcome, \mathbf{E}[Y \mid X] = \mathbf{P}(Y = 1 \mid X). A prediction model for Y is a model for a probability, so its predictions must stay inside [0, 1].
glm() maximizes and how to read its fit statistics.Additional derivations and code follow the main lesson.
Run the examples from the lectures/ directory.
| File | Role |
|---|---|
data/spam.csv |
4,601 emails with 57 features and a spam label |
data/tripadvisor.RData |
Adjective counts and 1 to 5 star ratings for hotel reviews |
data/train_small.csv |
57,477 Chatbot Arena battles between 64 language models |
train_small.csv is 184 MB because it stores full prompts and responses. We read only the five columns we need.
Keep the linear index, change what it predicts
For Y \in \{0, 1\},
\mathbf{E}[Y \mid X] = 1 \times \mathbf{P}(Y = 1 \mid X) + 0 \times \mathbf{P}(Y = 0 \mid X) = \mathbf{P}(Y = 1 \mid X).
The linear probability model (LPM) applies the regression anyway:
\mathbf{P}(Y = 1 \mid X) = X'\beta.
## [1] 0.722
Drawing U \sim \text{Uniform}(0, 1) and setting Y = \mathbf{1}\{U \le p\} gives \mathbf{P}(Y = 1 \mid X) = p exactly.
The true probability is an S-shaped function of X: below 0.02 for X < -2, above 0.97 for X > 0.5.
A generalized linear model (GLM) keeps the linear index X'\beta but passes it through a mean function G:
\mathbf{E}[Y \mid X] = G(X'\beta), \qquad G: \mathbb{R} \to [0, 1].
The usual choice for binary Y is the logistic function
G(z) = \frac{\exp(z)}{1 + \exp(z)} = \frac{1}{1 + \exp(-z)}.
It is increasing, symmetric around G(0) = 0.5, and never reaches 0 or 1.
Strictly, the link is G^{-1}, which maps the mean back to the index; we work with G because it is what produces predictions.

Choosing the normal CDF for G gives the probit model. Its coefficients live on a different scale (appendix).
The GLM with the logistic link is the logistic regression model, or logit:
\mathbf{P}(Y = 1 \mid X) = \frac{\exp(X'\beta)}{1 + \exp(X'\beta)}.
family = "binomial" selects the logistic link by default. Last time we called glm() with its default Gaussian family, which reproduces OLS.plogis() computes G in R, and qlogis() its inverse.
Both models use the same right-hand side, ~ X. The logit’s predictions approach 0 and 1 smoothly; the LPM line crosses both bounds.
| Outcome | Family in glm() |
Mean function G (inverse link) | glm() link g = G^{-1} |
Model |
|---|---|---|---|---|
| Continuous | gaussian (default) |
identity, G(z) = z | identity |
OLS |
| Binary | binomial |
logistic CDF | logit |
Logit |
| Binary | binomial(link = "probit") |
normal CDF \Phi | probit |
Probit |
| Count | poisson |
exponential e^z | log |
Poisson regression |
A GLM has three parts: a distribution for Y, the linear index X'\beta, and the mean function G with \mathbf{E}[Y \mid X] = G(X'\beta). R names each family by the link g = G^{-1}, which maps the mean back to the index; for Poisson, G = \exp and the link is \log.
glm() fits all of them with the same formula interface, so everything we learned about formulas, factors and interactions in the linear regression lecture carries over.
An agent chooses option 1 over option 0 when its utility is higher. Write the utility difference as
U_1 - U_0 = X'\beta + \varepsilon,
with X'\beta the systematic part and \varepsilon everything the econometrician does not observe. Then
\mathbf{P}(Y = 1 \mid X) = \mathbf{P}(\varepsilon > -X'\beta \mid X) = 1 - F(-X'\beta) = F(X'\beta),
where F is the CDF of \varepsilon and the last step uses symmetry of F around zero.
| Distribution of \varepsilon | Model |
|---|---|
| Logistic | Logit, F(z) = \exp(z)/(1 + \exp(z)) |
| Standard normal | Probit, F(z) = \Phi(z) |
\beta measures how X shifts the relative utility of the two options. The same idea extends to several alternatives (multinomial logit) and underlies structural models in IO, labor and public economics.
Since p = e^{z}/(1 + e^{z}) implies 1 - p = 1/(1 + e^{z}), the ratio p/(1 - p) = e^{z}. With z = X'\beta,
\log\left(\frac{\mathbf{P}(Y = 1 \mid X)}{\mathbf{P}(Y = 0 \mid X)}\right) = X'\beta = \beta_1 X_1 + \dots + \beta_d X_d.
The ratio \mathbf{P}(Y = 1 \mid X) / \mathbf{P}(Y = 0 \mid X) is the odds of Y = 1.
| Probability | Odds | Log-odds |
|---|---|---|
| 0.25 | 1/3 | -1.10 |
| 0.50 | 1 | 0 |
| 0.75 | 3 | 1.10 |
| 0.90 | 9 | 2.20 |
The logit is a linear regression for the log-odds. That is what makes its coefficients interpretable.
Odds. A one-unit increase in X_j multiplies the odds by \exp(\beta_j), a percentage change of
\big(\exp(\beta_j) - 1\big) \times 100\%,
the same arithmetic as for a logged outcome in linear regression, applied to the odds instead of Y. This effect is the same for every observation.
Probability. The slope of \mathbf{P}(Y = 1 \mid X = x) with respect to x_j is the marginal effect
\frac{\partial\, G(x'\beta)}{\partial x_j} = \beta_j\, G(x'\beta)\,\big(1 - G(x'\beta)\big),
which depends on where x is: largest when G(x'\beta) = 0.5, near zero in the tails.
| x | p | marginal_effect |
|---|---|---|
| -1.5 | 0.074 | 0.215 |
| -1.0 | 0.278 | 0.630 |
| -0.5 | 0.649 | 0.716 |
| 1.0 | 0.995 | 0.015 |
The average marginal effect over the sample is 0.298, close to the LPM slope of 0.289: the LPM can approximate the average effect well even when its individual predictions are poor (code in the appendix).
A logit for loan repayment gives \hat\beta_{\text{income}} = 0.7 per $10,000 of annual income.
What glm() maximizes, and how to read its output
OLS minimizes the sum of squared residuals, which summary() of a Gaussian glm() reports as the residual deviance:
\text{deviance} = \sum_{i=1}^n (Y_i - X_i'\hat\beta)^2.
The null deviance is the same quantity for a model with only an intercept. Since \hat\alpha = \bar Y minimizes \sum_i (Y_i - \alpha)^2,
\text{null deviance} = \sum_{i=1}^n (Y_i - \bar Y)^2.
These are the familiar pieces of the R^2:
R^2 = 1 - \frac{\text{deviance}}{\text{null deviance}} = 1 - \frac{\text{sum of squared residuals}}{\text{total sum of squares}}.
glm() reports both in summary()), so R^2 = (30079 - 13975)/30079 = 0.54. Unobserved factors still drive much of the variation in weekly sales.Deviance is what OLS minimizes and how we measure its fit. To fit the logit we need the general version of the same idea: the likelihood.
The likelihood is the probability of the outcomes we observed, under the model, given the covariates. With independent observations \{(y_i, x_i)\}_{i=1}^n,
\mathbf{P}(Y_1 = y_1, \dots, Y_n = y_n \mid X_1 = x_1, \dots, X_n = x_n) = \prod_{i=1}^n \mathbf{P}(Y_i = y_i \mid X_i = x_i).
Take Y_i \mid X_i \sim \mathcal{N}(X_i'\beta, \sigma^2), equivalently Y_i = X_i'\beta + \varepsilon_i with \varepsilon_i \mid X_i \sim \mathcal{N}(0, \sigma^2). The likelihood is
\prod_{i=1}^n \phi(y_i;\, x_i'\beta, \sigma^2) = (2\pi\sigma^2)^{-n/2} \exp\left(-\frac{1}{2\sigma^2}\sum_{i=1}^n (y_i - x_i'\beta)^2\right),
so the log-likelihood is
\log L(\beta, \sigma^2) = -\frac{n}{2}\log(2\pi\sigma^2) - \frac{1}{2\sigma^2}\sum_{i=1}^n (y_i - x_i'\beta)^2.
For any \sigma^2, maximizing over \beta means minimizing the deviance. OLS is the maximum likelihood estimator of the Gaussian linear model: likelihood and deviance are mirror images.
Write p_i(\beta) = \mathbf{P}(Y_i = 1 \mid X_i = x_i) = \exp(x_i'\beta)/(1 + \exp(x_i'\beta)). Observation i contributes p_i if y_i = 1 and 1 - p_i if y_i = 0:
L(\beta) = \prod_{i=1}^n p_i(\beta)^{y_i}\,\big(1 - p_i(\beta)\big)^{1 - y_i}.
Taking logs, and using \log p_i = x_i'\beta - \log(1 + e^{x_i'\beta}) and \log(1 - p_i) = -\log(1 + e^{x_i'\beta}),
\log L(\beta) = \sum_{i=1}^n \Big[ y_i\, x_i'\beta - \log\big(1 + \exp(x_i'\beta)\big) \Big].
No closed-form maximizer exists. glm() climbs to \hat\beta by iteratively reweighted least squares, which is the “Fisher Scoring iterations” line at the end of summary().
| X | Y | p_hat | contribution |
|---|---|---|---|
| 1.263 | 1 | 0.998 | 0.998 |
| -0.326 | 1 | 0.761 | 0.761 |
| 1.330 | 1 | 0.998 | 0.998 |
Each observation contributes the predicted probability of what actually happened: \hat p_i when y_i = 1 and 1 - \hat p_i when y_i = 0. A perfect fit would give every contribution 1, and likelihood 1.

glm() estimate.For any likelihood model define
\text{deviance} = -2 \log L(\hat\beta).
summary() calls this the residual deviance. The null deviance is -2\log L for the intercept-only model, whose fitted probability is \bar Y for everyone.A probability becomes a classification only after we choose a threshold
The logit gives \hat p(x) = \widehat{\mathbf{P}}(Y = 1 \mid X = x). A classifier commits to a label:
\hat Y = \mathbf{1}\{\hat p(X) > t\}.
Which t? Choose it to minimize the expected prediction error
EPE = \mathbf{E}\big[L(Y, \hat Y)\big],
where the loss L(Y, \hat Y) says how bad it is to predict \hat Y when the truth is Y:
\begin{bmatrix} L(0,0) & L(0,1) \\ L(1,0) & L(1,1) \end{bmatrix} = \begin{bmatrix} 0 & L(0,1) \\ L(1,0) & 0 \end{bmatrix}.
L(0,1) is the cost of a false positive (predict 1, truth 0) and L(1,0) the cost of a false negative. Correct guesses cost nothing.
Condition on X = x and write p(x) = \mathbf{P}(Y = 1 \mid X = x). The expected loss of each decision is
\hat Y = 1: \; L(0,1)\,\big(1 - p(x)\big), \qquad \hat Y = 0: \; L(1,0)\, p(x).
The Bayes classifier picks the cheaper one:
\hat Y_{\text{Bayes}}(x) = \mathbf{1}\big\{L(1,0)\, p(x) > L(0,1)\,\big(1 - p(x)\big)\big\} = \mathbf{1}\left\{p(x) > \frac{L(0,1)}{L(0,1) + L(1,0)}\right\}.
| Loss | Threshold |
|---|---|
| 0-1 loss, L(0,1) = L(1,0) = 1 | t = 0.5: predict the more probable class |
| False positive twice as costly as false negative | t = 2/3 |
| False negative twice as costly as false positive | t = 1/3 |
In practice we plug in \hat p(x) from the logit. The threshold encodes the relative costs of the two mistakes.
For a given threshold, cross-tabulate truth against prediction:
| Predicted 0 | Predicted 1 | |
|---|---|---|
| Actual 0 | true negatives (TN) | false positives (FP) |
| Actual 1 | false negatives (FN) | true positives (TP) |
| Measure | Definition | Question it answers |
|---|---|---|
| Accuracy | (TP + TN)/n | How often is the label right? |
| Sensitivity (recall) | TP/(TP + FN) | Share of actual positives we catch |
| Specificity | TN/(TN + FP) | Share of actual negatives we clear |
| Precision | TP/(TP + FP) | Share of flagged cases that are right |
Sensitivity estimates \mathbf{P}(\hat Y = 1 \mid Y = 1) and specificity \mathbf{P}(\hat Y = 0 \mid Y = 0). Accuracy flatters a model when one class dominates: predicting the majority class for everyone already scores its share.
Lowering t raises sensitivity and lowers specificity. The receiver operating characteristic (ROC) curve plots the pairs
\big(1 - \text{specificity},\ \text{sensitivity}\big)
as t moves from 1 down to 0.

The AUC compresses the curve into one number.
Use the AUC to compare models. Use the cost ratio to choose the threshold you deploy.
A hospital screens patients for a disease with a logit and a threshold t.
Your inbox runs a logistic regression
Y_i = 1 if a human labeled email i as spam, 0 otherwise. X_i describes the email: 48 word indicators, 6 character indicators and 3 measures of capital-letter runs.
## [1] 4601 58
## word_free word_george char_dollar capital_run_length_total spam
## 1 1 0 0 278 1
## 2 1 0 1 1028 1
## 3 1 0 1 2259 1
##
## 0 1
## 2788 1813
Emails 1, \dots, n have been labeled by a person. The task: predict whether a new email is spam from its features.
## Estimate Std. Error z value Pr(>|z|)
## word_free 1.543 0.179 8.621 0
## word_george -5.780 0.758 -7.623 0
## word_hp -3.604 0.387 -9.316 0
## char_dollar 1.871 0.207 9.016 0
## char_exclaim 1.343 0.146 9.220 0
~ . puts every other column of the data set on the right-hand side, so the model has 57 slope coefficients. summary(logit) prints them all; the table shows five.free, $ and ! raise the log-odds of spam, while george and hp lower them.z value column is the estimate divided by its standard error## word_george word_free
## -99.69108 367.72294
Cross-check against the raw counts:
## george
## spam 0 1
## 0 2016 772
## 1 1805 8
Only 8 of the 1,813 spam emails contain “george”, against 772 of the 2,788 legitimate ones.
## 1 12
## 0.8839073 0.8772593
## [1] 1 1
type = "response" returns \hat p = G(x'\hat\beta). The default, type = "link", returns the log-odds x'\hat\beta.newdata.## null residual minus_2_loglik
## 6170.153 1548.660 1548.660
## [1] 0.7490079
summary(logit) reports the same two deviances at the bottom of its output.## Predicted
## Actual 0 1
## 0 2669 119
## 1 167 1646
| accuracy | sensitivity | specificity | precision |
|---|---|---|---|
| 0.938 | 0.908 | 0.957 | 0.933 |
The filter catches 91% of spam and wrongly flags 4% of legitimate mail, in sample.
| threshold | sensitivity | specificity | accuracy |
|---|---|---|---|
| 0.1 | 0.990 | 0.761 | 0.851 |
| 0.3 | 0.949 | 0.915 | 0.928 |
| 0.5 | 0.908 | 0.957 | 0.938 |
| 0.7 | 0.846 | 0.976 | 0.925 |
| 0.9 | 0.702 | 0.991 | 0.877 |
A legitimate email lost in the spam folder usually costs more than a spam email that gets through, so a provider might set t above 0.5: at t = 0.9 only 1% of legitimate mail is flagged, while 30% of spam gets through.
roc_hand is one confusion matrix.
pROC computes the curve and the arearoc() evaluates every distinct threshold in the predicted probabilities; coords(roc_spam, "all") returns the same sensitivity and specificity pairs we computed by hand, and plot(roc_spam) draws the curve.Predicting a high rating from the adjectives a review uses
## train test adjectives
## 169987 40000 7573
## [1] "dgCMatrix"
## attr(,"package")
## [1] "Matrix"
## [1] 1 5 4 5 4 5 4 5 2 5
y is its 1 to 5 star rating and X counts how many times the review uses each of 7,573 adjectives.## 6 x 5 sparse Matrix of class "dgCMatrix"
## Terms
## Docs arrogant decided large minor beat
## 1 1 . . . .
## 2 . . . . .
## 3 . . . . .
## 4 . 1 3 . .
## 5 . . 1 1 .
## 6 . . . . .
## [1] 0.003047276
## [1] "55.8 Mb"
dgCMatrix format from lecture 8 stores the whole matrix in 56 MB.## great good one nice just clean
## 0.9082459 0.6806815 0.5764323 0.5167630 0.4968086 0.3836058
## [1] 0.9429552
## [1] 123
## [1] "169.9 Mb"
great, clean, friendly, disappointed and so on.glm() needs an ordinary data frame, which is why we convert after pruning.keep columns are used for the 40,000 test reviews, which play no role in estimation.summary(hotel_model) prints 124 rows, so we look at the extremes instead.
Enthusiastic words raise the log-odds; lukewarm words such as “ok” and “average” lower them as much as “bad” does.
## FALSE TRUE
## 0.5226444 0.8874854

Reviews that really are 4+ get a median predicted probability of 0.89, the rest 0.52. The classes overlap, so no threshold separates them perfectly.
## train test
## 0.8625054 0.8584706
## accuracy_at_0.5 always_4plus
## 0.830125 0.741075

The Bradley-Terry model is a logit in disguise
Teams, players, products or chatbots meet in pairs, and we observe who wins. The Bradley-Terry model gives each competitor i a rating \beta_i and sets
\log \frac{\mathbf{P}(i \text{ beats } j)}{1 - \mathbf{P}(i \text{ beats } j)} = \beta_i - \beta_j, \qquad\text{so}\qquad \mathbf{P}(i \text{ beats } j) = \frac{e^{\beta_i - \beta_j}}{1 + e^{\beta_i - \beta_j}} = \frac{e^{\beta_i}}{e^{\beta_i} + e^{\beta_j}}.
Index each match by the pair (i, j) and occasion t, with m competitors in total:
Then x_{ijt}'\beta = \beta_i - \beta_j, and the model reads \text{logit}\,\mathbf{P}(y_{ijt} = 1) = x_{ijt}'\beta: a logit with no intercept.
| Match | y | A | B | C |
|---|---|---|---|---|
| A vs B, A wins | 1 | 1 | -1 | 0 |
| B vs C, C wins | 0 | 0 | 1 | -1 |
| A vs C, A wins | 1 | 1 | 0 | -1 |
| C vs B, C wins | 1 | 0 | -1 | 1 |
Stacking the n matches gives y \in \mathbb{R}^n and X \in \mathbb{R}^{n \times m} with one +1 and one -1 in each row.
Adding the same constant to every \beta_i leaves every difference \beta_i - \beta_j, and so every probability, unchanged. Each row of X sums to zero, so X has rank at most m - 1.
Fix the level by setting \beta_m = 0 for one reference competitor. Dropping column m,
X\beta = X_{-m}\beta_{-m}, \qquad \beta_{-m} \in \mathbb{R}^{m - 1},
and the MLE over \beta_{-m} is the constrained MLE. Every other rating is then measured relative to competitor m.
Extensions fit in the same regression:
In reinforcement learning from human feedback (RLHF), annotators compare two responses to the same prompt and pick the better one.
We turn the idea around: instead of scoring responses, rate the models themselves from head-to-head battles.

## [1] 57477 5
## # A tibble: 4 × 5
## model_a model_b winner_model_a winner_model_b winner_tie
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 gpt-4-1106-preview gpt-4-0613 1 0 0
## 2 koala-13b gpt-4-0613 0 1 0
## 3 gpt-3.5-turbo-0613 mistral-medium 0 0 1
## 4 llama-2-13b-chat mistral-7b-instruct 1 0 0
col_select skips the prompt and response text, so the 184 MB file loads in a fraction of a second. Each row is one battle: the two models, and which one the user preferred.
## # A tibble: 1 × 3
## a_wins b_wins ties
## <dbl> <dbl> <dbl>
## 1 0.349 0.342 0.309
## # A tibble: 4 × 2
## model n
## <chr> <int>
## 1 gpt-4-1106-preview 7387
## 2 gpt-3.5-turbo-0613 7083
## 3 gpt-4-0613 6165
## 4 claude-2.1 5583
Wins split evenly between the two positions and 31% of battles end in a tie. Appearances per model range from 100 to more than 7,000.
## # A tibble: 6 × 3
## model battles win_rate
## <chr> <int> <dbl>
## 1 gpt-4-1106-preview 5360 0.760
## 2 gpt-4-0125-preview 798 0.747
## 3 gpt-3.5-turbo-0314 968 0.735
## 4 gpt-4-0314 2923 0.682
## 5 claude-1 2792 0.626
## 6 qwen1.5-72b-chat 370 0.581
gpt-3.5-turbo-0314 sits third with a 73.5% win rate. Did it face the same opponents as the GPT-4 models?models <- sort(unique(c(dat$model_a, dat$model_b)))
dat_noties <- dat %>%
filter(winner_tie == 0) %>%
mutate(model_a = factor(model_a, levels = models),
model_b = factor(model_b, levels = models))
x <- model.matrix(~ model_a - 1, data = dat_noties) -
model.matrix(~ model_b - 1, data = dat_noties)
colnames(x) <- str_remove(colnames(x), "^model_a")
dat_noties[1:2, 1:2]## # A tibble: 2 × 2
## model_a model_b
## <fct> <fct>
## 1 gpt-4-1106-preview gpt-4-0613
## 2 koala-13b gpt-4-0613
## gpt-4-1106-preview gpt-4-0613 koala-13b
## 1 1 -1 0
## 2 0 -1 1
model.matrix(~ model_a - 1) gives one indicator column per model, the vector e_i; subtracting the model_b version gives e_i - e_j. Shared levels keep the 64 columns aligned.
glm()baseline <- colnames(x)[ncol(x)]
x <- x[, -ncol(x)] # drop one column: its rating is 0
bt <- glm(dat_noties$winner_model_a ~ x - 1, family = "binomial")
ratings <- tibble(model = c(str_remove(names(coef(bt)), "^x"), baseline),
rating = c(coef(bt), 0)) %>%
arrange(desc(rating)) %>%
mutate(bt_rank = row_number())
head(ratings, 6)## # A tibble: 6 × 3
## model rating bt_rank
## <chr> <dbl> <int>
## 1 gpt-4-0125-preview 1.76 1
## 2 gpt-4-1106-preview 1.73 2
## 3 gpt-4-0314 1.11 3
## 4 gpt-4-0613 0.954 4
## 5 qwen1.5-72b-chat 0.897 5
## 6 mistral-medium 0.861 6
- 1 removes the intercept, and the dropped reference model (zephyr-7b-beta) has rating 0 by construction.qwen1.5-72b-chat and mistral-medium.| model | battles | win_rate | raw_rank | rating | bt_rank | opponent_rating |
|---|---|---|---|---|---|---|
| gpt-4-1106-preview | 5360 | 0.76 | 1 | 1.73 | 2 | 0.54 |
| claude-2.1 | 3969 | 0.43 | 36 | 0.57 | 13 | 0.87 |
| gpt-3.5-turbo-0314 | 968 | 0.73 | 3 | 0.43 | 22 | -0.76 |
| koala-13b | 1102 | 0.51 | 20 | -0.54 | 52 | -0.58 |
gpt-3.5-turbo-0314 drops from 3rd to 22nd: its opponents were the weakest in the data, with an average rating of -0.76.claude-2.1 rises from 36th to 13th: it was matched mostly against strong models.
The two measures agree broadly, but models that met weak opponents sit below the cloud and models that met strong ones sit above it.
## gpt-4-0125-preview gpt-4-1106-preview gpt-4-0314
## gpt-4-0125-preview 0.50 0.51 0.66
## gpt-4-1106-preview 0.49 0.50 0.65
## gpt-4-0314 0.34 0.35 0.50
## gpt-4-1106-preview
## 0.7696009
gpt-4-0314 about two thirds of the time.gpt-3.5-turbo-0613, gpt-4-1106-preview is predicted to win 77% of decided battles.## Estimate Std. Error z value Pr(>|z|)
## 0.025 0.011 2.303 0.021
gpt-4-0613 as the reference model (rating 0). Which ratings change, and which predicted probabilities?ratings, compute the probability that claude-2.1 beats mixtral-8x7b-instruct-v0.1.A binary outcome is a probability model. The logit keeps the linear index X'\beta and maps it into [0, 1]; coefficients act on the log-odds.
Likelihood generalizes least squares. OLS is the Gaussian MLE, the logit maximizes the Bernoulli likelihood, and deviance = -2\log L replaces the sum of squared residuals.
Probabilities become decisions through a threshold. The threshold encodes the cost ratio; the ROC curve and the AUC evaluate the ranking without one.
The same glm() call scales from 2 to 124 coefficients. Sparse storage, a held-out test set and an out-of-sample AUC keep large problems honest.
Pairwise comparisons are a logit with a special design. The Bradley-Terry model turns match outcomes into ratings, from chess to chatbots.
pROC, GLM families in R.Optional material for reference and practice
The classical linear model “Y \sim \mathcal{N}(X'\beta, \sigma^2)” bundles three choices: a random component Y \sim \mathcal{N}(\mu, \sigma^2), a systematic component \eta = X'\beta, and the link \mu = \eta.
A GLM changes the first and the third:
For the Bernoulli, \theta = \log\frac{\mu}{1 - \mu} is the log-odds, so the logit is the canonical link: the natural parameter itself is linear in X. Likelihood computations and inference stay tractable across the whole family.
## (Intercept) X
## logit 2.184580 3.140629
## probit 1.253479 1.813304
## ratio 1.742814 1.731993
## [1] 0.01556418
## average_marginal_effect lpm_slope
## 0.2975717 0.2893704
For a binary regressor, report the average discrete change instead: predict with the indicator switched on and off for every observation and average the difference.
## [1] 0.09421097
Switching “free” on raises the predicted probability of spam by 9.4 percentage points on average.

ties <- dat %>%
filter(winner_tie == 1) %>%
mutate(model_a = factor(model_a, levels = models),
model_b = factor(model_b, levels = models))
x_tie <- model.matrix(~ model_a - 1, data = ties) - model.matrix(~ model_b - 1, data = ties)
colnames(x_tie) <- str_remove(colnames(x_tie), "^model_a")
x_tie <- x_tie[, colnames(x)]
X_all <- rbind(x, x_tie, x_tie) # each tie enters twice ...
y_all <- c(dat_noties$winner_model_a, rep(1, nrow(ties)), rep(0, nrow(ties)))
w_all <- c(rep(1, nrow(x)), rep(0.5, 2 * nrow(ties))) # ... with weight one half
bt_ties <- glm(y_all ~ X_all - 1, family = "binomial", weights = w_all)A tie is entered as a win and a loss for the first model, each with weight one half. The likelihood then counts half a win and half a loss for every tied battle.
## # A tibble: 5 × 2
## model rating
## <chr> <dbl>
## 1 gpt-4-1106-preview 1.20
## 2 gpt-4-0125-preview 1.19
## 3 gpt-4-0314 0.780
## 4 gpt-4-0613 0.674
## 5 qwen1.5-72b-chat 0.605
## [1] 0.9992817
Run once in your R environment if needed:
Data files, all read from lectures/data/:
spam.csv, a 0/1 version of the UCI Spambase data;tripadvisor.RData, from github.com/yanxht/TripAdvisorData;train_small.csv, the training file of the Chatbot Arena human preference data.Every example in this deck runs offline once the files are in place.
Is the outcome a probability? Then model the log-odds, maximize the likelihood, and decide with a threshold that reflects the costs.