Goal: Build practical intuition for multinomial logit (MNL): what it is, when to use it, how to prep data, how to estimate it using three approaches, and how to interpret results for managerial what-ifs.
Dataset:
Fishingfrommlogit(choice of fishing mode: beach, boat, charter, pier).
Approaches:nnet::multinom,VGAM::vglm,mlogit::mlogit.
Data prep:mlogit.data(shortcut), tidyversepivot_longer, classic tidyversegather/separate/spread.
MNL models the probability that a decision-maker chooses one option out of several. In marketing, think: - Brand choice, channel choice, ad click, SKU selection, service mode (here: fishing mode).
Each option \(j\) has a latent
utility \(U_{ij}\) for
person \(i\). With the logit form:
\[
P(Y_i=j) \;=\; \frac{\exp(X_{ij}\beta)}{\sum_k \exp(X_{ik}\beta)}.
\] - Coefficients are relative to a baseline
alternative (we use beach).
- Variables can be alternative-specific (e.g., the
price of the specific option) or individual-specific
(e.g., income of the chooser).
- IIA assumption: odds between any pair of alternatives
don’t depend on other options. Good for learning and often reasonable;
for close substitutes, consider nested/mixed logit.
# Optionally install packages if missing
pkgs <- c("nnet","VGAM","mlogit","lmtest","pscl","tidyverse","knitr","kableExtra")
to_install <- setdiff(pkgs, rownames(installed.packages()))
if (length(to_install)) install.packages(to_install, repos = "https://cloud.r-project.org")
library(nnet)
library(VGAM)
library(mlogit)
library(lmtest)
library(pscl)
library(tidyverse)
library(knitr); library(kableExtra)
set.seed(123) # reproducibility for multinom
knitr::opts_chunk$set(comment = NA, fig.align = "center", fig.width = 7, fig.height = 4.5, message = FALSE, warning = FALSE)
# ---- tables-helper (define kbl_safe early) ----
if (!exists("kbl_safe")) {
kbl_safe <- function(x, ...) {
tb <- knitr::kable(x, ...)
if (knitr::is_html_output()) {
out <- tryCatch(kableExtra::kable_styling(tb), error = function(e) tb)
return(out)
}
tb
}
}
Data preview (wide format):
data("Fishing", package="mlogit")
str(Fishing)
'data.frame': 1182 obs. of 10 variables:
$ mode : Factor w/ 4 levels "beach","pier",..: 4 4 3 2 3 4 1 4 3 3 ...
$ price.beach : num 157.9 15.1 161.9 15.1 106.9 ...
$ price.pier : num 157.9 15.1 161.9 15.1 106.9 ...
$ price.boat : num 157.9 10.5 24.3 55.9 41.5 ...
$ price.charter: num 182.9 34.5 59.3 84.9 71 ...
$ catch.beach : num 0.0678 0.1049 0.5333 0.0678 0.0678 ...
$ catch.pier : num 0.0503 0.0451 0.4522 0.0789 0.0503 ...
$ catch.boat : num 0.26 0.157 0.241 0.164 0.108 ...
$ catch.charter: num 0.539 0.467 1.027 0.539 0.324 ...
$ income : num 7083 1250 3750 2083 4583 ...
kbl_safe(head(Fishing))
| mode | price.beach | price.pier | price.boat | price.charter | catch.beach | catch.pier | catch.boat | catch.charter | income |
|---|---|---|---|---|---|---|---|---|---|
| charter | 157.930 | 157.930 | 157.930 | 182.930 | 0.0678 | 0.0503 | 0.2601 | 0.5391 | 7083.332 |
| charter | 15.114 | 15.114 | 10.534 | 34.534 | 0.1049 | 0.0451 | 0.1574 | 0.4671 | 1250.000 |
| boat | 161.874 | 161.874 | 24.334 | 59.334 | 0.5333 | 0.4522 | 0.2413 | 1.0266 | 3750.000 |
| pier | 15.134 | 15.134 | 55.930 | 84.930 | 0.0678 | 0.0789 | 0.1643 | 0.5391 | 2083.333 |
| boat | 106.930 | 106.930 | 41.514 | 71.014 | 0.0678 | 0.0503 | 0.1082 | 0.3240 | 4583.332 |
| charter | 192.474 | 192.474 | 28.934 | 63.934 | 0.5333 | 0.4522 | 0.1665 | 0.3975 | 4583.332 |
Interpretation: - mode: chosen alternative
(beach, boat, charter,
pier) - price.*, catch.*:
alternative-specific attributes - income:
individual-specific attribute
Goal: create long choice format = one row per
(chooser, alternative) + a logical mode flag
that equals TRUE for the chosen alternative.
mlogit.datafishing_mlogit <- mlogit.data(Fishing,
shape = "wide",
choice = "mode",
varying = 2:9, # price.* and catch.*
sep = ".")
kbl_safe(head(as.data.frame(fishing_mlogit)[, c("chid","alt","mode","price","catch","income")]))
| chid | alt | mode | price | catch | income |
|---|---|---|---|---|---|
| 1 | beach | FALSE | 157.930 | 0.0678 | 7083.332 |
| 1 | boat | FALSE | 157.930 | 0.2601 | 7083.332 |
| 1 | charter | TRUE | 182.930 | 0.5391 | 7083.332 |
| 1 | pier | FALSE | 157.930 | 0.0503 | 7083.332 |
| 2 | beach | FALSE | 15.114 | 0.1049 | 1250.000 |
| 2 | boat | FALSE | 10.534 | 0.1574 | 1250.000 |
chid: chooser id; alt: alternative;
mode: TRUE for the chosen row.mlogit() expects.pivot_longerfishing_pivot <- Fishing %>%
mutate(chid = row_number()) %>%
pivot_longer(cols = starts_with(c("price","catch")),
names_to = c(".value","alt"),
names_sep = "\\.") %>%
mutate(mode = (mode == alt))
kbl_safe(head(as.data.frame(fishing_pivot)[, c("chid","alt","mode","price","catch","income")]))
| chid | alt | mode | price | catch | income |
|---|---|---|---|---|---|
| 1 | beach | FALSE | 157.930 | 0.0678 | 7083.332 |
| 1 | pier | FALSE | 157.930 | 0.0503 | 7083.332 |
| 1 | boat | FALSE | 157.930 | 0.2601 | 7083.332 |
| 1 | charter | TRUE | 182.930 | 0.5391 | 7083.332 |
| 2 | beach | FALSE | 15.114 | 0.1049 | 1250.000 |
| 2 | pier | FALSE | 15.114 | 0.0451 | 1250.000 |
gather / separate /
spreadfishing_gather <- Fishing %>%
mutate(chid = row_number()) %>%
gather(key = "var_alt", value = "val",
starts_with("price"), starts_with("catch")) %>%
separate(var_alt, into = c("var","alt"), sep = "\\.") %>%
spread(var, val) %>%
mutate(mode = (mode == alt))
kbl_safe(head(as.data.frame(fishing_gather)[, c("chid","alt","mode","price","catch","income")]))
| chid | alt | mode | price | catch | income |
|---|---|---|---|---|---|
| 604 | beach | TRUE | 5.934 | 0.0678 | 416.6667 |
| 604 | boat | FALSE | 7.740 | 0.0014 | 416.6667 |
| 604 | charter | FALSE | 36.740 | 0.0029 | 416.6667 |
| 604 | pier | FALSE | 5.934 | 0.0789 | 416.6667 |
| 893 | beach | TRUE | 3.870 | 0.5333 | 416.6667 |
| 893 | boat | FALSE | 47.730 | 0.2413 | 416.6667 |
What we did
1. mutate(chid = row_number()) — label each chooser; tells
the model which rows form a choice set.
2. gather(...) — melt alt-specific
columns; exposes the hidden alternative
dimension.
3. separate(var_alt, c("var","alt")) — split
price.boat → var="price",
alt="boat".
4. spread(var, val) — pivot var back into
columns so each (chid, alt) row has price
and catch columns.
5. mutate(mode = (mode == alt)) — one TRUE
per chooser, marking the chosen alt.
Why we did it
- Make the alternative dimension explicit.
- Align with mlogit formula: alt-specific
(price, catch) before |;
individual-specific (income) after.
- Pedagogically clarifies what rows mean in a choice model.
Mini checks (run & inspect):
# See the stacked alternatives just after gather/separate
tmp_long <- Fishing %>%
mutate(chid = row_number()) %>%
gather("var_alt","val", starts_with("price"), starts_with("catch")) %>%
separate(var_alt, c("var","alt"), sep="\\.")
head(tmp_long)
# Exactly one chosen row per chooser?
fishing_gather %>%
group_by(chid) %>%
summarise(chosen_rows = sum(mode)) %>%
count(chosen_rows)
check1 <- all.equal(
as.data.frame(fishing_mlogit)[,c("chid","alt","mode","price","catch","income")],
as.data.frame(fishing_pivot)[,c("chid","alt","mode","price","catch","income")]
)
check2 <- all.equal(
as.data.frame(fishing_mlogit)[,c("chid","alt","mode","price","catch","income")],
as.data.frame(fishing_gather)[,c("chid","alt","mode","price","catch","income")]
)
check1; check2
[1] "Attributes: < Component \"class\": Lengths (2, 1) differ (string compare on first 1) >"
[2] "Component \"alt\": 'current' is not a factor"
[3] "Component \"mode\": 2096 element mismatches"
[4] "Component \"price\": Mean relative difference: 0.8301712"
[5] "Component \"catch\": Mean relative difference: 1.243696"
[1] "Attributes: < Component \"class\": Lengths (2, 1) differ (string compare on first 1) >"
[2] "Component \"chid\": Mean relative difference: 0.7143749"
[3] "Component \"alt\": 'current' is not a factor"
[4] "Component \"mode\": 1720 element mismatches"
[5] "Component \"price\": Mean relative difference: 0.9345275"
[6] "Component \"catch\": Mean relative difference: 1.222338"
[7] "Component \"income\": Mean relative difference: 0.6685676"
This is familiar from ML classes, but less suited to what-ifs.
wide_df <- subset(fishing_mlogit, mode == TRUE)
wide_df$alt <- relevel(wide_df$alt, ref = "beach") # baseline
table(wide_df$alt)
beach boat charter pier
134 418 452 178
nnet::multinomm_nnet <- multinom(alt ~ price + catch + income, data = wide_df, trace = FALSE)
summary(m_nnet)
Call:
multinom(formula = alt ~ price + catch + income, data = wide_df,
trace = FALSE)
Coefficients:
(Intercept) price catch income
boat 0.9880772 0.003555725 -1.2997215 6.930617e-05
charter 0.1588430 0.022700714 1.4116330 -1.788990e-04
pier 1.0836892 -0.003027237 -0.9494361 -1.286185e-04
Std. Errors:
(Intercept) price catch income
boat 1.767921e-05 0.003155494 2.992347e-06 3.161360e-05
charter 1.364347e-05 0.003160311 9.419144e-06 3.691491e-05
pier 2.304065e-05 0.003965765 5.115583e-06 4.049548e-05
Residual Deviance: 2531.988
AIC: 2555.988
Wald-style p-values (quick read; LR preferred):
coefs <- summary(m_nnet)$coefficients
ses <- summary(m_nnet)$standard.errors
pvals <- 2*(1 - pnorm(abs(coefs/ses)))
kbl_safe(round(pvals, 3))
| (Intercept) | price | catch | income | |
|---|---|---|---|---|
| boat | 0 | 0.260 | 0 | 0.028 |
| charter | 0 | 0.000 | 0 | 0.000 |
| pier | 0 | 0.445 | 0 | 0.001 |
LR test vs null:
m0_nnet <- multinom(alt ~ 1, data = wide_df, trace = FALSE)
anova(m0_nnet, m_nnet, test = "Chisq")
In-sample classification snapshot (for intuition only):
pred_nnet_class <- predict(m_nnet, type = "class")
tab_nnet <- table(True = wide_df$alt, Pred = pred_nnet_class)
acc_nnet <- mean(pred_nnet_class == wide_df$alt)
tab_nnet; acc_nnet
Pred
True beach boat charter pier
beach 0 96 38 0
boat 0 314 102 2
charter 0 138 314 0
pier 0 129 46 3
[1] 0.5338409
VGAM::vglmm_vgam <- vglm(alt ~ price + catch + income,
family = multinomial(refLevel = "beach"),
data = wide_df)
summary(m_vgam)
Call:
vglm(formula = alt ~ price + catch + income, family = multinomial(refLevel = "beach"),
data = wide_df)
Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept):1 9.880e-01 2.231e-01 4.429 9.49e-06 ***
(Intercept):2 1.589e-01 2.367e-01 0.671 0.502172
(Intercept):3 1.084e+00 2.584e-01 4.194 2.75e-05 ***
price:1 3.557e-03 3.343e-03 1.064 0.287253
price:2 2.270e-02 3.333e-03 6.811 9.71e-12 ***
price:3 -3.026e-03 4.180e-03 -0.724 0.469130
catch:1 -1.300e+00 3.406e-01 -3.816 0.000136 ***
catch:2 1.412e+00 2.760e-01 5.115 3.14e-07 ***
catch:3 -9.493e-01 4.023e-01 -2.360 0.018291 *
income:1 6.931e-05 4.239e-05 1.635 0.102074
income:2 -1.789e-04 4.891e-05 -3.658 0.000254 ***
income:3 -1.286e-04 5.514e-05 -2.332 0.019683 *
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Names of linear predictors: log(mu[,2]/mu[,1]), log(mu[,3]/mu[,1]),
log(mu[,4]/mu[,1])
Residual deviance: 2531.988 on 3534 degrees of freedom
Log-likelihood: -1265.994 on 3534 degrees of freedom
Number of Fisher scoring iterations: 5
No Hauck-Donner effect found in any of the estimates
Reference group is level 1 of the response
LR test vs null:
m0_vgam <- vglm(alt ~ 1, family = multinomial(refLevel = "beach"), data = wide_df)
VGAM::lrtest(m0_vgam, m_vgam)
Likelihood ratio test
Model 1: alt ~ 1
Model 2: alt ~ price + catch + income
#Df LogLik Df Chisq Pr(>Chisq)
1 3543 -1497.7
2 3534 -1266.0 -9 463.46 < 2.2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Interpretation note: Coefficients are
log-odds vs baseline (beach). Negative
price means higher price lowers odds of choosing that
alternative vs beach; positive catch raises odds.
(chooser, alternative))This is the econometric formulation used for market-share simulation and policy experiments.
m_mlogit <- mlogit(mode ~ price + catch | income,
data = fishing_mlogit,
reflevel = "beach")
summary(m_mlogit)
Call:
mlogit(formula = mode ~ price + catch | income, data = fishing_mlogit,
reflevel = "beach", method = "nr")
Frequencies of alternatives:choice
beach boat charter pier
0.11337 0.35364 0.38240 0.15059
nr method
7 iterations, 0h:0m:0s
g'(-H)^-1g = 1.37E-05
successive function values within tolerance limits
Coefficients :
Estimate Std. Error z-value Pr(>|z|)
(Intercept):boat 5.2728e-01 2.2279e-01 2.3667 0.0179485 *
(Intercept):charter 1.6944e+00 2.2405e-01 7.5624 3.952e-14 ***
(Intercept):pier 7.7796e-01 2.2049e-01 3.5283 0.0004183 ***
price -2.5117e-02 1.7317e-03 -14.5042 < 2.2e-16 ***
catch 3.5778e-01 1.0977e-01 3.2593 0.0011170 **
income:boat 8.9440e-05 5.0067e-05 1.7864 0.0740345 .
income:charter -3.3292e-05 5.0341e-05 -0.6613 0.5084031
income:pier -1.2758e-04 5.0640e-05 -2.5193 0.0117582 *
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Log-Likelihood: -1215.1
McFadden R^2: 0.18868
Likelihood ratio test : chisq = 565.17 (p.value = < 2.22e-16)
mode.|: alt-specific variables
(price, catch).|: individual-specific variables
(income).reflevel="beach" sets the baseline utility.LR test vs null:
m0_mlogit <- mlogit(mode ~ 1 | 1, data = fishing_mlogit, reflevel = "beach")
lmtest::lrtest(m0_mlogit, m_mlogit)
Reading the output: For each non-baseline
alternative (boat/charter/pier), you’ll see coefficients on
price and catch. Signs should match intuition;
magnitudes are in utility units (log-odds). Probability
effects depend on the full choice set (softmax).
IIA caveat: MNL assumes IIA (relative odds unaffected by presence/absence of other options). For close substitutes, consider nested or mixed logit.
Across multinom, vglm, and
mlogit, coefficients are in log-odds units
relative to baseline (beach).
Examples:
- β_price_boat = -0.05 ⇒ +1 in boat price multiplies
odds(boat vs beach) by exp(-0.05)≈0.95
(≈5% drop).
- β_catch_charter = +0.20 ⇒ +1 in catch multiplies
odds(charter vs beach) by exp(0.20)≈1.22
(≈22% increase).
Intercepts (ASCs): baseline appeal relative to
beach.
Significance: prefer LR vs null for
overall signal; use per-coefficient p-values for details.
Units: rescale (e.g., price per $10) for easier
storytelling.
# Optional: explicit odds ratios for mlogit fit
or_tab <- exp(summary(m_mlogit)$coefficients)
kbl_safe(round(or_tab, 3))
| x | |
|---|---|
| (Intercept):boat | 1.694 |
| (Intercept):charter | 5.443 |
| (Intercept):pier | 2.177 |
| price | 0.975 |
| catch | 1.430 |
| income:boat | 1.000 |
| income:charter | 1.000 |
| income:pier | 1.000 |
mlogit for marketing inference & what-ifsWhen your goal is to identify significant drivers of
choice and to run credible what-if
simulations, mlogit beats simple multi-class
classifiers:
# Packages
if (!requireNamespace("mlogit", quietly = TRUE)) install.packages("mlogit", repos = "https://cloud.r-project.org")
if (!requireNamespace("nnet", quietly = TRUE)) install.packages("nnet", repos = "https://cloud.r-project.org")
# Data
if (!exists("Fishing")) data("Fishing", package = "mlogit")
# Long choice data
if (!exists("fishing_mlogit")) {
fishing_mlogit <- mlogit::mlogit.data(Fishing, shape = "wide", choice = "mode", varying = 2:9, sep = ".")
}
# Classification data
if (!exists("wide_df")) {
wide_df <- subset(fishing_mlogit, mode == TRUE)
wide_df$alt <- stats::relevel(wide_df$alt, ref = "beach")
}
# Models
if (!exists("m_nnet")) {
m_nnet <- nnet::multinom(alt ~ price + catch + income, data = wide_df, trace = FALSE)
}
if (!exists("m_mlogit")) {
m_mlogit <- mlogit::mlogit(mode ~ price + catch | income, data = fishing_mlogit, reflevel = "beach")
}
# Baseline class shares from multinom (classification view)
p_class0 <- predict(m_nnet, type = "probs")
share_class0 <- colMeans(p_class0)
# Attempt a classification what-if: +20 boat price (only affects chosen 'boat' rows here)
wide_df_whatif <- wide_df
wide_df_whatif$price[wide_df_whatif$alt == "boat"] <- wide_df_whatif$price[wide_df_whatif$alt == "boat"] + 20
p_class1 <- predict(m_nnet, newdata = wide_df_whatif, type = "probs")
share_class1 <- colMeans(p_class1)
kbl_safe(round(rbind(Classifier_Base = share_class0,
Classifier_BoatUp = share_class1), 3))
| beach | boat | charter | pier | |
|---|---|---|---|---|
| Classifier_Base | 0.113 | 0.354 | 0.382 | 0.151 |
| Classifier_BoatUp | 0.108 | 0.345 | 0.407 | 0.140 |
Proper choice-model what-if (updates everyone):
p0_mlogit <- predict(m_mlogit, newdata = fishing_mlogit)
share0_mlogit <- colMeans(p0_mlogit)
whatif_mlogit <- fishing_mlogit
whatif_mlogit$price[whatif_mlogit$alt == "boat"] <- whatif_mlogit$price[whatif_mlogit$alt == "boat"] + 20
p1_mlogit <- predict(m_mlogit, newdata = whatif_mlogit)
share1_mlogit <- colMeans(p1_mlogit)
kbl_safe(round(rbind(mlogit_Base = share0_mlogit,
mlogit_BoatUp = share1_mlogit), 3))
| beach | boat | charter | pier | |
|---|---|---|---|---|
| mlogit_Base | 0.113 | 0.354 | 0.382 | 0.151 |
| mlogit_BoatUp | 0.123 | 0.259 | 0.454 | 0.164 |
Marketing takeaway:
mlogitis the right tool because it uses the full competitive set, keeps alt-specific vs individual-specific effects straight, and turns significance into credible share-shift predictions under price/feature changes.
Effect of raising boat price by 20 on
predicted market shares.
# Baseline market shares
p0 <- predict(m_mlogit, newdata = fishing_mlogit) # rows = obs, cols = alts
share0 <- colMeans(p0)
# Counterfactual: increase boat price by +20
whatif <- fishing_mlogit
whatif$price[whatif$alt == "boat"] <- whatif$price[whatif$alt == "boat"] + 20
p1 <- predict(m_mlogit, newdata = whatif)
share1 <- colMeans(p1)
# Compare
shares <- rbind(Baseline = share0, BoatPriceUp = share1)
kbl_safe(round(shares, 3))
| beach | boat | charter | pier | |
|---|---|---|---|---|
| Baseline | 0.113 | 0.354 | 0.382 | 0.151 |
| BoatPriceUp | 0.123 | 0.259 | 0.454 | 0.164 |
Bar plot:
barplot(t(shares), beside = TRUE, legend = TRUE,
ylab = "Market Share",
main = "What-if: Boat Price +20")
What-if: Boat Price +20 (Predicted Market Shares)
Reading: Own effect (boat share falls). Cross effects (which alts gain?) indicate substitution patterns.
Heterogeneity: interact income with
price/catch:
m_mlogit_ix <- mlogit(mode ~ price + catch + price:income + catch:income | income,
data = fishing_mlogit, reflevel = "beach")
summary(m_mlogit_ix)ASCs: add alternative-specific constants to
capture baseline appeal.
Model comparison: AIC/BIC; out-of-sample
validation by splitting choosers.
IIA discussion: when to consider nested or mixed logit.
# Average Marginal Effects (AME): nudge a variable and average Δprobabilities
ame <- function(model, data, var, delta = 1, target_alt = NULL) {
p0 <- predict(model, newdata = data)
d1 <- data
if (!is.null(target_alt)) {
d1[[var]][d1$alt == target_alt] <- d1[[var]][d1$alt == target_alt] + delta
} else {
d1[[var]] <- d1[[var]] + delta
}
p1 <- predict(model, newdata = d1)
colMeans(p1 - p0)
}
# Examples:
ame_price_boat <- ame(m_mlogit, fishing_mlogit, var = "price", delta = 1, target_alt = "boat")
ame_income_all <- ame(m_mlogit, fishing_mlogit, var = "income", delta = 1, target_alt = NULL)
kbl_safe(round(ame_price_boat, 4))
| x | |
|---|---|
| beach | 0.0006 |
| boat | -0.0050 |
| charter | 0.0037 |
| pier | 0.0007 |
kbl_safe(round(ame_income_all, 4))
| x | |
|---|---|
| beach | 0 |
| boat | 0 |
| charter | 0 |
| pier | 0 |
# Elasticities: %Δshare / %Δprice for an alt-specific price
elasticities <- function(model, data, target_alt, pct = 0.01) {
base_s <- colMeans(predict(model, newdata = data))
d_up <- data
idx <- d_up$alt == target_alt
d_up$price[idx] <- d_up$price[idx] * (1 + pct)
s_up <- colMeans(predict(model, newdata = d_up))
((s_up - base_s) / base_s) / pct
}
elas_boat <- elasticities(m_mlogit, fishing_mlogit, target_alt = "boat", pct = 0.01)
kbl_safe(round(elas_boat, 3))
| x | |
|---|---|
| beach | 0.292 |
| boat | -0.609 |
| charter | 0.376 |
| pier | 0.255 |
sessionInfo()
R version 4.4.2 (2024-10-31)
Platform: aarch64-apple-darwin20
Running under: macOS Sequoia 15.6
Matrix products: default
BLAS: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.0
locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
time zone: Australia/Melbourne
tzcode source: internal
attached base packages:
[1] splines stats4 stats graphics grDevices utils datasets
[8] methods base
other attached packages:
[1] kableExtra_1.4.0 knitr_1.49 lubridate_1.9.4 forcats_1.0.0
[5] stringr_1.5.1 dplyr_1.1.4 purrr_1.0.4 readr_2.1.5
[9] tidyr_1.3.1 tibble_3.2.1 ggplot2_3.5.2 tidyverse_2.0.0
[13] pscl_1.5.9 lmtest_0.9-40 zoo_1.8-13 mlogit_1.1-3
[17] dfidx_0.1-0 VGAM_1.1-13 nnet_7.3-20
loaded via a namespace (and not attached):
[1] sass_0.4.9 generics_0.1.3 xml2_1.3.8 stringi_1.8.4
[5] lattice_0.22-6 hms_1.1.3 digest_0.6.37 magrittr_2.0.3
[9] timechange_0.3.0 evaluate_1.0.3 grid_4.4.2 fastmap_1.2.0
[13] jsonlite_1.9.0 Formula_1.2-5 viridisLite_0.4.2 scales_1.3.0
[17] textshaping_1.0.0 jquerylib_0.1.4 Rdpack_2.6.2 cli_3.6.4
[21] rlang_1.1.5 rbibutils_2.3 munsell_0.5.1 withr_3.0.2
[25] cachem_1.1.0 yaml_2.3.10 tools_4.4.2 tzdb_0.5.0
[29] colorspace_2.1-1 vctrs_0.6.5 R6_2.6.1 lifecycle_1.0.4
[33] MASS_7.3-64 pkgconfig_2.0.3 pillar_1.10.1 bslib_0.9.0
[37] gtable_0.3.6 glue_1.8.0 systemfonts_1.2.3 statmod_1.5.0
[41] xfun_0.52 tidyselect_1.2.1 rstudioapi_0.17.1 htmltools_0.5.8.1
[45] svglite_2.2.1 rmarkdown_2.29 compiler_4.4.2