22  Multinomial Logit

22.1 The Inverse-Logit Function

Suppose \(y_i \in \{0, 1\}\). Using logistic regression, we model \(\Pr(y_i = 1)\) as a function of covariates, so that

\[ \Pr(Y_i = 1) = \frac{\exp(X_i \beta)}{1 + \exp(X_i \beta)}. \]

The function \(f(z) = \frac{e^z}{1 + e^z}\) is called the inverse-logit function and maps any real number \(X_i \beta\) into a probability between 0 and 1.

Because the two probabilities must sum to one:

\[ \Pr(Y_i = 0) = 1 - \Pr(Y_i = 1) = \frac{1}{1 + \exp(X_i \beta)}. \]

22.2 The Softmax Function

The softmax generalizes the inverse-logit—it takes \(J\) real inputs (i.e., from \(\mathbb{R}^J\)) and scales them to sum to one.

\[ \text{softmax}(z_j) = \frac{\exp(z_j)}{\sum_{k=1}^{J} \exp(z_k)}, \qquad j = 1,\ldots,J. \] Because the \(J\) outputs must sum to one, it is natural to interpret the outputs as probabilities.

The OJS widget below allows you to experiment with \(J = 4\) inputs and see how the outputted probabilities change.

22.3 Adding covariates

Now suppose the outcome takes \(J\) possible values:

\[ y_i \in \{1, 2, \ldots, J\}. \]

Example 1. We might label vote choice in the US as (1) abstain, (2) Republican, (3) Democrat, or (4) other.

Example 2. We might label coup attempts in a given country-year as (0) none attempted, (1) failed attempt, or (2) successful attempt.

For each category \(j\), define a linear predictor \(\eta_{ij} = X_i \beta_j\). Here, each \(\beta_j\) is a vector of coefficients.

We can use the softmax function to convert these linear predictors into probabilities that sum to one.

\[ \Pr(Y_i = j) = \frac{\exp(\eta_{ij})}{\sum_{k=1}^{J} \exp(\eta_{ik})}, \qquad j = 1, \ldots, J. \]

This is a generalization of the inverse-logit to \(J\) categories. If \(J = 2\), the softmax reduces to the inverse-logit.

However, this model is not identified because adding any constant to each of the \(\eta_{ij}\) produces the same probabilities.

Example Inputs

Input \(z_j\) Softmax \(p_j\)
\(-1\) 0.0303
\(1\) 0.2242
\(2\) 0.6095
\(0.5\) 0.1360

Adding a Constant (\(+2\))

New Input \(z_j + 2\) Softmax \(p_j\)
\(-1 + 2 = 1\) 0.0303
\(1 + 2 = 3\) 0.2242
\(2 + 2 = 4\) 0.6095
\(0.5 + 2 = 2.5\) 0.1360

Adding a constant to all inputs leaves the softmax unchanged. This is why the multinomial logit model requires an identification constraint.

To identify the model, we set \(\beta_J = 0\), so that \(\eta_{iJ} = 0\). Then for \(j = 1, \ldots, J-1\), we have

\[ \Pr(Y_i = j) = \frac{\exp(X_i \beta_j)} {1 + \sum_{k=1}^{J-1} \exp(X_i \beta_k)}. \]

And for the “baseline” category \(J\), we have

\[ \Pr(Y_i = J) = \frac{1} {1 + \sum_{k=1}^{J-1} \exp(X_i \beta_k)}. \]

22.4 Example: The 1992 presidential vote

  • Data: 1992 ANES Time Series Study (Miller et al. 2016); the same respondents as the Bush approval example in the ordered logit chapter.
  • Outcome: vote_1992, the reported presidential vote: Bush, Clinton, or Perot.
  • Predictors:
    • party_id: party identification, from −3 (strong Democrat) through 0 (independent) to 3 (strong Republican).
    • economy: evaluation of the national economy compared with a year ago, from 1 (much better) to 5 (much worse).
  • [CTK: 592 voters complete on these variables]
Miller, Warren E., Donald R. Kinder, Steven J. Rosenstone, and University of Michigan. Institute for Social Research. American National Election Studies. 2016. “ANES 1992 Time Series Study.” Inter-university Consortium for Political; Social Research [distributor]. https://doi.org/10.3886/ICPSR06067.v3.
  • [CTK: provenance — Adolph’s Problem Set 4 (Adolph 2025); same file as the ordered example; Perot as the reason a nominal model is needed]
  • Adolph, Christopher. 2025. “Problem Set 4: Modeling Presidential Approval with Ordered Probit.” POLS/CSSS 510: Maximum Likelihood Methods for the Social Sciences, University of Washington, Fall Quarter 2025. https://faculty.washington.edu/cadolph/mle/510hw4.pdf.

22.4.1 multinom()

library(tidyverse)
library(nnet)  # for multinom()
library(marginaleffects)

# load data; keep complete cases on the model's variables
vote <- read_csv("https://pos5747.github.io/data/bush-approval-1992.csv") |>
  select(vote_1992, party_id, economy) |>
  drop_na() |>
  mutate(vote_1992 = factor(vote_1992, levels = c("Bush", "Clinton", "Perot"))) |>
  glimpse()
Rows: 592
Columns: 3
$ vote_1992 <fct> Bush, Bush, Bush, Clinton, Clinton, Clinton, Bush, Bush, Bus…
$ party_id  <dbl> 2, 3, 3, 3, -2, -3, 2, 3, 2, 1, -3, -2, 3, 1, -1, -3, -1, -3…
$ economy   <dbl> 4, 4, 3, 3, 5, 5, 4, 2, 4, 3, 5, 4, 4, 4, 5, 5, 4, 5, 4, 5, …
# the first level (Bush) is the baseline
levels(vote$vote_1992)
[1] "Bush"    "Clinton" "Perot"  
# fit multinomial logit
fit_vote <- multinom(vote_1992 ~ party_id + economy, data = vote)
# weights:  12 (6 variable)
initial  value 650.378475 
iter  10 value 434.958942
final  value 434.953412 
converged
summary(fit_vote)
Call:
multinom(formula = vote_1992 ~ party_id + economy, data = vote)

Coefficients:
        (Intercept)   party_id   economy
Clinton   -1.977521 -1.0977031 0.5514930
Perot     -1.518696 -0.4389651 0.3361671

Std. Errors:
        (Intercept)   party_id   economy
Clinton   0.6221333 0.08564888 0.1512617
Perot     0.5848327 0.07740859 0.1437105

Residual Deviance: 869.9068 
AIC: 881.9068 
  • [CTK: two rows of coefficients, one linear predictor per non-baseline candidate; each relative to Bush]
  • [CTK: baseline first here (multinom()), last in the math above; the probabilities don’t change]
  • [CTK: signs: party_id negative for both rows (Republicans → Bush); economy positive for both (worse economy → away from Bush)]
# probability of each candidate as party ID varies
p <- predictions(fit_vote, newdata = datagrid(party_id = -3:3), type = "probs")

# plot
ggplot(p, aes(x = party_id, y = estimate, ymin = conf.low, ymax = conf.high,
              color = group, fill = group)) +
  geom_ribbon(alpha = 0.2, color = NA) +
  geom_line() +
  labs(x = "Party ID", y = "Probability", color = "Candidate", fill = "Candidate")

  • [CTK: Pr(Perot) rises then falls (about 0.08 at −3, peak about 0.28 at 0 to 1, 0.18 at 3), though both linear predictors are linear in party ID]
  • [CTK: why: Perot’s party_id coefficient (−0.44) sits between Bush’s (0) and Clinton’s (−1.10); the softmax shares probability]
  • [CTK: so the sign of a coefficient doesn’t give the direction of that category’s probability; compute probabilities]
# change in each candidate's probability as the economy goes from
# "better" (2) to "worse" (4), for a typical respondent
comparisons(fit_vote,
            variables = list(economy = c(2, 4)),
            newdata = datagrid(grid_type = "mean_or_mode"),
            type = "probs")

   Group Estimate Std. Error      z Pr(>|z|)    S   2.5 %  97.5 %
 Bush     -0.2188     0.0628 -3.487   <0.001 11.0 -0.3418 -0.0958
 Clinton   0.1799     0.0472  3.811   <0.001 12.8  0.0874  0.2724
 Perot     0.0389     0.0458  0.851    0.395  1.3 -0.0508  0.1287

Term: economy
Type: probs
Comparison: 4 - 2
  • [CTK: worse economy: Bush −0.22, Clinton +0.18, Perot +0.04 (CI includes 0)]
  • [CTK: Perot’s economy coefficient is positive (0.34), but Clinton gains most of what Bush loses]

22.5 Example: Balancing the federal budget, CCES 2012

  • Data: 2012 Cooperative Congressional Election Study, Common Content (Ansolabehere and Schaffner 2013).
  • Outcome: budget, the answer to “If the Congress were to balance the budget it would have to consider cutting defense spending, cutting domestic spending (such as Medicare and Social Security), or raising taxes to cover the deficit. What would you MOST prefer that Congress do?” Three categories: cut defense spending, cut domestic spending, or raise taxes.
  • Predictors:
    • party_id: party identification, from 1 (strong Democrat) to 7 (strong Republican).
    • female: 1 for women, 0 for men.
    • income: family income in 16 brackets, from 1 (less than $10,000) to 16 ($500,000 or more).
  • 45,237 respondents with valid answers on all four variables.
Ansolabehere, Stephen, and Brian F. Schaffner. 2013. “CCES Common Content, 2012.” Harvard Dataverse. https://doi.org/10.7910/DVN/HQEVPK.

This example is inspired by a SAGE Research Methods Datasets guide (Reid and Allum 2020). We use exactly the same model specification as the guide. Our data set is larger than the guide’s (45,237 respondents rather than 35,759). The dataset from the guide is an extract shared with other SAGE guides, and it keeps only respondents who answered all 15 survey items in the larger extract, including questions about immigration, gun control, and race that are not in this model. Our larger dataset includes every respondent who answered the four questions in this model.

Reid, Abigail-Kate, and Nick Allum. 2020. “Learn about Multinomial Logit in Stata with Data from the Cooperative Congressional Election Study (2012).” London: SAGE Research Methods Datasets Part 1; SAGE Publications. https://doi.org/10.4135/9781529710465.
library(tidyverse)
library(nnet)  # for multinom()
library(marginaleffects)

# load data; "Raise taxes" first, so it is the baseline
cces <- read_csv("data/cces2012-budget.csv") |>
  mutate(budget = factor(budget, levels = c("Raise taxes",
                                            "Cut defense spending",
                                            "Cut domestic spending"))) |>
  glimpse()
Rows: 45,237
Columns: 4
$ budget   <fct> Cut defense spending, Cut defense spending, Raise taxes, Cut …
$ party_id <dbl> 2, 3, 2, 4, 1, 3, 3, 1, 1, 1, 3, 7, 6, 1, 1, 1, 4, 4, 1, 1, 3…
$ female   <dbl> 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1…
$ income   <dbl> 8, 8, 5, 7, 3, 11, 7, 6, 1, 10, 5, 10, 8, 13, 2, 12, 11, 10, …
# fit multinomial logit
fit_budget <- multinom(budget ~ party_id + female + income, data = cces)
# weights:  15 (8 variable)
initial  value 49697.924102 
iter  10 value 40573.505332
final  value 40539.113501 
converged
summary(fit_budget)
Call:
multinom(formula = budget ~ party_id + female + income, data = cces)

Coefficients:
                      (Intercept)   party_id      female      income
Cut defense spending    0.8627772 -0.0445021  0.06490173 -0.01869888
Cut domestic spending  -1.6914786  0.5629467 -0.32864044  0.02632282

Std. Errors:
                      (Intercept)    party_id     female      income
Cut defense spending   0.03742825 0.006677125 0.02577410 0.003938263
Cut domestic spending  0.04519703 0.007193643 0.02878465 0.004429042

Residual Deviance: 81078.23 
AIC: 81094.23 
  • Two rows of coefficients, each relative to raising taxes.
  • party_id: −0.04 for cutting defense, 0.56 for cutting domestic spending.
  • female: 0.06 for cutting defense, −0.33 for cutting domestic spending.
# probability of each choice as party ID varies
p <- predictions(fit_budget, newdata = datagrid(party_id = 1:7), type = "probs")

ggplot(p, aes(x = party_id, y = estimate, ymin = conf.low, ymax = conf.high,
              color = group, fill = group)) +
  geom_ribbon(alpha = 0.2, color = NA) +
  geom_line() +
  labs(x = "Party ID", y = "Probability", color = "Most preferred", fill = "Most preferred")

  • female at its mode (1) and income at its rounded mean (6, $50,000–59,999).
  • Strong Democrats to strong Republicans: Pr(cut domestic spending) 0.08 to 0.75; Pr(cut defense spending) 0.63 to 0.16; Pr(raise taxes) 0.29 to 0.09.
# women minus men, for an independent with income 6
comparisons(fit_budget, variables = "female", newdata = datagrid(grid_type = "mean_or_mode"), type = "probs")

                 Group Estimate Std. Error      z Pr(>|z|)     S   2.5 %
 Raise taxes             0.0216    0.00411   5.25   <0.001  22.7  0.0136
 Cut defense spending    0.0655    0.00509  12.86   <0.001 123.4  0.0556
 Cut domestic spending  -0.0872    0.00551 -15.82   <0.001 184.9 -0.0980
  97.5 %
  0.0297
  0.0755
 -0.0764

Term: female
Type: probs
Comparison: 1 - 0
  • Women: 0.07 more likely to prefer cutting defense, 0.09 less likely to prefer cutting domestic spending, 0.02 more likely to prefer raising taxes.

22.6 With brm()

22.6.1 The 1992 presidential vote

library(brms)

# fit the same model with brm(); categorical() is the multinomial logit,
# with the first level (Bush) as the baseline
fit_vote_brm <- brm(vote_1992 ~ party_id + economy,
                    data = vote,
                    family = categorical(link = "logit"),
                    backend = "cmdstanr",
                    chains = 4,
                    cores = 4,
                    seed = 1234)
Show the code for the table
library(tinytable)

# put the multinom() and brm() estimates side by side
compare_mnl <- function(fit_ml, fit_brm) {
  # multinom(): ML estimates and standard errors
  ml <- broom::tidy(fit_ml) |>
    transmute(outcome = y.level,
              term = if_else(term == "(Intercept)", "Intercept", term),
              est_ml = estimate,
              se_ml = std.error)

  # brm(): posterior means and SDs; mu is brms's prefix for each outcome's
  # linear predictor, so the names look like mu<outcome, no spaces>_<term>
  bayes <- fixef(fit_brm) |>
    as_tibble(rownames = "name") |>
    separate_wider_delim(name, delim = "_", names = c("mu", "term"),
                         too_many = "merge") |>
    transmute(mu, term, est_brm = Estimate, se_brm = Est.Error)

  # match brm()'s names to multinom()'s outcome labels
  ml |>
    mutate(mu = paste0("mu", str_remove_all(outcome, "[^[:alnum:]]"))) |>
    left_join(bayes, by = c("mu", "term")) |>
    select(-mu)
}

# make a table with one block of rows per outcome
tt_mnl <- function(comparison) {
  comparison |>
    select(-outcome) |>
    as.data.frame() |>
    setNames(c("Term", "Estimate", "SE", "Estimate", "SE")) |>
    tt(notes = "multinom(): ML estimates and standard errors. brm(): posterior means and standard deviations.") |>
    format_tt(j = 2:5, digits = 3, num_fmt = "decimal") |>
    style_tt(j = 2:5, align = "r") |>
    group_tt(i = comparison$outcome,
             j = list("multinom()" = 2:3, "brm()" = 4:5))
}

compare_mnl(fit_vote, fit_vote_brm) |>
  tt_mnl()
multinom() brm()
Term Estimate SE Estimate SE
multinom(): ML estimates and standard errors. brm(): posterior means and standard deviations.
Clinton Clinton Clinton Clinton Clinton
Intercept -1.978 0.622 -1.987 0.623
party_id -1.098 0.086 -1.112 0.085
economy 0.551 0.151 0.554 0.151
Perot Perot Perot Perot Perot
Intercept -1.519 0.585 -1.539 0.591
party_id -0.439 0.077 -0.445 0.079
economy 0.336 0.144 0.341 0.146
  • [CTK: posterior means within about 0.02 of the ML estimates (e.g., Clinton party_id −1.11 vs −1.10; Perot economy 0.34 vs 0.34); default priors barely matter at N = 592]
# probability of each candidate as party ID varies, now with posterior intervals
# (for this fit, predictions() returns rowid but not the grid columns, so join them back)
grid <- datagrid(model = fit_vote_brm, party_id = -3:3)
p_brm <- predictions(fit_vote_brm, newdata = grid) |>
  left_join(select(grid, rowid, party_id), by = "rowid")

ggplot(p_brm, aes(x = party_id, y = estimate, ymin = conf.low, ymax = conf.high,
                  color = group, fill = group)) +
  geom_ribbon(alpha = 0.2, color = NA) +
  geom_line() +
  labs(x = "Party ID", y = "Probability", color = "Candidate", fill = "Candidate")

  • [CTK: same {marginaleffects} code, no type = "probs"; group column holds the candidate; the grid columns need joining back by rowid]
  • [CTK: why bother with brm(): priors, hierarchical extensions (weeks 10–11)]

22.6.2 Balancing the federal budget

# fit the same model with brm(), with "Raise taxes" as the baseline
fit_budget_brm <- brm(budget ~ party_id + female + income,
                      data = cces,
                      family = categorical(link = "logit"),
                      backend = "cmdstanr",
                      chains = 4,
                      cores = 4,
                      seed = 1234)
Show the code for the table
compare_mnl(fit_budget, fit_budget_brm) |>
  tt_mnl()
multinom() brm()
Term Estimate SE Estimate SE
multinom(): ML estimates and standard errors. brm(): posterior means and standard deviations.
Cut defense spending Cut defense spending Cut defense spending Cut defense spending Cut defense spending
Intercept 0.863 0.037 0.862 0.038
party_id -0.045 0.007 -0.045 0.006
female 0.065 0.026 0.065 0.026
income -0.019 0.004 -0.019 0.004
Cut domestic spending Cut domestic spending Cut domestic spending Cut domestic spending Cut domestic spending
Intercept -1.691 0.045 -1.693 0.045
party_id 0.563 0.007 0.563 0.007
female -0.329 0.029 -0.328 0.029
income 0.026 0.004 0.026 0.004
  • [CTK: posterior means within 0.002 of the ML estimates, and posterior SDs within 0.0003 of the standard errors; N = 45,237]