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: Fishing from mlogit (choice of fishing mode: beach, boat, charter, pier).
Approaches: nnet::multinom, VGAM::vglm, mlogit::mlogit.
Data prep: mlogit.data (shortcut), tidyverse pivot_longer, classic tidyverse gather/separate/spread.


1 1. What is Multinomial Logit (MNL)?

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.


2 2. Setup

# 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


3 3. Data Preparation (3 ways)

Goal: create long choice format = one row per (chooser, alternative) + a logical mode flag that equals TRUE for the chosen alternative.

3.1 3A. Shortcut: mlogit.data

fishing_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.
  • This is exactly the structure mlogit() expects.

3.2 3B. Tidyverse: pivot_longer

fishing_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

3.3 3C. Tidyverse (classic): gather / separate / spread

fishing_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

3.3.1 3C.1 Why this older tidyverse path? (what we did & why)

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.boatvar="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)

3.4 3D. Sanity check: structures are equivalent

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"                             

4 4. “Classification” View (Only the Chosen Row per Person)

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 

4.1 4A. nnet::multinom

m_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

4.2 4B. VGAM::vglm

m_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.


5 5. “Choice Model” View (One Row per (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)
  • Left side: the choice indicator mode.
  • Before |: alt-specific variables (price, catch).
  • After |: 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.

5.1 5.1 How to Interpret the Coefficients (all three approaches)

Across multinom, vglm, and mlogit, coefficients are in log-odds units relative to baseline (beach).

  • Log-odds change: \(\Delta \log\text{-odds} = \beta \cdot \Delta x\)
  • Odds ratio (OR): \(\text{OR} = e^{\beta}\)
  • % change in odds: \(100\times(e^{\beta}-1)\%\)

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

5.2 5.2 Why mlogit for marketing inference & what-ifs

When your goal is to identify significant drivers of choice and to run credible what-if simulations, mlogit beats simple multi-class classifiers:

  1. Uses the full choice set (updates all alternatives when one attribute changes).
  2. Respects variable roles (alt-specific vs individual-specific).
  3. Doesn’t discard information (uses all rows, not just the chosen row).
  4. Managerial interpretation (coefficients → odds → predicted shares).
  5. Proper significance testing (evidence from the whole choice set).
  6. Policy-ready & extensible (ASCs, robust SEs, nested/mixed logit).

5.2.1 Guard (ensures objects exist if students jump here)

# 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")
}

5.2.2 Quick contrast demo: why classification what-ifs can mislead

# 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: mlogit is 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.


6 6. Prediction & What-If Simulation

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)

What-if: Boat Price +20 (Predicted Market Shares)

Reading: Own effect (boat share falls). Cross effects (which alts gain?) indicate substitution patterns.


7 7. Extensions & Exercises

  1. 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)
  2. ASCs: add alternative-specific constants to capture baseline appeal.

  3. Model comparison: AIC/BIC; out-of-sample validation by splitting choosers.

  4. IIA discussion: when to consider nested or mixed logit.


8 8. Summary (Talking Points)

  • MNL models one-of-many choices.
  • Choice-model view supports scenario analysis and share predictions.
  • Interpret through odds ratios; use AMEs/elasticities for probability-level stories.
  • IIA is an assumption—know when to move beyond MNL.

9 Appendix: Interpretation Tools (AMEs & Elasticities)

# 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

10 9. Reproducibility

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