--- title: "Model specification and variants" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Model specification and variants} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} bibliography: ../inst/REFERENCES.bib link-citations: true --- ```{r, include=FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4, fig.align = "center" ) ``` This vignette describes how to specify a probit choice model with `fit()`. It covers the normalization of the utility scale and the prior distribution, which every fit involves, and then the model variants: the three types of covariates and the alternative-specific constants, choice sets that differ between occasions, and ordered and ranked responses. Preference heterogeneity between deciders is the subject of the vignette [Modeling preference heterogeneity][v03], and @Oelschlaeger2026c gives the methodological background. From the covariate types onward, each section first fits the variant to simulated data and then to a data set of the **AER** package [@Kleiber2008], the **MASS** package [@VenablesRipley2002], or the **mlogit** package [@Croissant2020]. Without data, `fit()` simulates the requested model before estimating it, and `summary()` prints the true values in a `dgp` column beside the posterior summaries, converted to the normalization of the fit. ```{r setup} library(RprobitB) set.seed(1) ``` ## Normalization The probit model introduced in the vignette [Get started with RprobitB][v01] assigns every alternative $j$ at occasion $t$ of decider $n$ the latent utility $U_{ntj} = X_{ntj}^\top \beta_n + \epsilon_{ntj}$ with jointly normal errors $\epsilon_{nt} \sim \mathrm{N}(0, \Sigma)$, and the decider chooses the alternative with the largest utility. The choice probability of an alternative is the probability that its utility exceeds the utilities of all others. These probabilities are invariant to adding a constant to all utilities and to multiplying all utilities by a positive number, so neither the level nor the scale of the utilities is identified. **RprobitB** therefore works with utility differences relative to a base alternative, by default the first in alphabetical order, and fixes one parameter through `scale`: - `scale = NULL` fixes the error variance of the first utility difference to one. This is the default. The covariance matrix of the error differences is reported as `Sigma[,]`, with rows and columns named by the alternatives other than the base. For three alternatives `A`, `B`, and `C` with base `A`, its entries are `Sigma[B,B]`, `Sigma[C,B]`, and `Sigma[C,C]`. - `scale = c(z = -1)` fixes the coefficient of `z` to `-1` instead, so that all other coefficients are measured in units of it. With a price coefficient fixed in this way, the other coefficients are willingness-to-pay values. The sampler draws all parameters without restriction and rescales every retained draw afterwards. Variables that the normalization fixes remain in the draws but are omitted from `summary()`, `coef()`, and `vcov()`. The following demonstration shows the effect of the normalization on the reported values. We simulate data with coefficients `1` for `x` and `-0.5` for `z` and fit them under the default scale. Simulated alternatives are labeled with capital letters unless `alternatives` names them, here `A` and `B`, and `A` is the base. `update()` then refits the same simulated data with the coefficient of `z` fixed to `-1`, so only the normalization differs between the two fits. ```{r normalization} scale_default <- fit( choice ~ x + z | 0, dgp_parameters = list(beta = c(x = 1, z = -0.5)), n_deciders = 300, chains = 1 ) summary(scale_default) scale_z <- update(scale_default, scale = c(z = -1)) summary(scale_z) ``` Under the default scale, the `dgp` column shows the coefficients as specified. `Sigma[B,B]`, the error variance of the utility difference between alternative `B` and the base `A`, is fixed to one and therefore not listed. Under the coefficient normalization, `beta[z]` is fixed to `-1` instead of its true `-0.5`, so all utilities are multiplied by `2`: the true `beta[x]` becomes `2` and the true error variance `4`. `summary()` converts the true values to the normalization of the fit, so the `dgp` column remains comparable. The convergence diagnostics of the two tables differ because `update()` runs the sampler again and because the rescaled variables are different functions of the draws: `beta[x]` is now a ratio of two coefficients, which mixes differently than a single coefficient. ## Prior distribution **RprobitB** estimates every model with a Gibbs sampler, which draws each block of parameters in turn from its conditional posterior distribution given the other parameters and the latent utilities. The prior is conjugate for every block, so each conditional posterior belongs to the same family as the prior. Fixed coefficients and class means have normal priors, covariance matrices have inverse Wishart priors, class weights have a Dirichlet prior, and the log-increments between ordered thresholds have a normal prior. The section "Prior distribution" of `?fit` lists all components with their defaults, which are weakly informative for coefficients and covariances. Single components are overridden through a named list, and the complete prior of a fit is stored in its `prior` component. The following demonstration simulates 100 deciders with a true coefficient of `-1` under the default prior and then refits the same data under two priors with mean `1`: a moderate one with variance `0.5` and a tight one with variance `0.01`. ```{r prior} default_prior <- fit( choice ~ x | 0, dgp_parameters = list(beta = c(x = -1)), chains = 1 ) default_prior$prior summary(default_prior) moderate_prior <- update( default_prior, prior = list(fixed_mean = 1, fixed_covariance = matrix(0.5)) ) tight_prior <- update( default_prior, prior = list(fixed_mean = 1, fixed_covariance = matrix(0.01)) ) data.frame( variable = "beta[x]", dgp = -1, default = coef(default_prior), moderate = coef(moderate_prior), tight = coef(tight_prior), row.names = NULL ) ``` The table compares the posterior means under the three priors with the true value. The moderate prior shifts the posterior mean only slightly towards the prior mean. The tight prior has a standard deviation of `0.1` around `1`, outweighs the data, and pulls the posterior mean far away from the true value. A prior on a coefficient is unproblematic as long as its variance reflects the actual uncertainty about the coefficient. ## Covariate types and alternative-specific constants The formula `choice ~ A | B | C` distinguishes three kinds of covariates: attributes of the alternatives with one shared coefficient (`A`), characteristics of the decider with alternative-specific coefficients (`B`), and attributes of the alternatives with alternative-specific coefficients (`C`). Alternative-specific constants are included by default and removed with `0` in the second part. The following demonstration simulates all three types at once, and `summary()` shows the posterior summaries beside the true values. ```{r covariate-types-sim} covariate_types <- fit( choice ~ x | z | w, n_deciders = 500, dgp_parameters = list(beta = c( x = 0.5, z_B = -0.5, ASC_B = 0.25, w_A = -0.5, w_B = 0.5 )), chains = 1 ) summary(covariate_types) ``` The summary lists the shared coefficient of `x`, the coefficient of `z` and the constant of alternative `B`, and the two alternative-specific coefficients of `w`, each beside its true value. `base` selects the alternative against which the alternative-specific coefficients are measured, by default the first in alphabetical order, here `A`. Switching the base to `B` changes only the parameterization, not the model: ```{r base} base_b <- update(covariate_types, base = "B") coef(base_b)[c("beta[x]", "beta[z_A]", "beta[ASC_A]")] ``` The shared coefficient of `x` is unchanged up to Monte Carlo error, while the coefficient of `z` and the constant now describe alternative `A` relative to `B` and therefore change sign. In 1987, 210 travelers between Sydney and Melbourne reported which of four modes they had taken: air, train, bus, or car. The `TravelMode` data of the **AER** package [@Kleiber2008] are in long format with one row per mode and a `"yes"`/`"no"` indicator of the chosen mode, which `fit()` expects as a logical or `0`/`1` variable. Terminal waiting time (`wait`), in-vehicle cost (`vcost`), and travel time (`travel`) vary across modes and enter as type `A`. Household income and the size of the traveling party describe the traveler and enter as type `B`, with the alternative-specific constants included by default. Cost and income are converted from Australian dollars to euro. ```{r travel-formula} travel_formula <- choice ~ wait + vcost + travel | income + size ``` The first alternative in alphabetical order, `air`, is the base, so the constants and the coefficients of income and party size describe the other modes relative to flying. ```{r travel} data("TravelMode", package = "AER") TravelMode$choice <- TravelMode$choice == "yes" TravelMode$vcost <- TravelMode$vcost / 1.6196 TravelMode$income <- TravelMode$income / 1.6196 travel <- fit( formula = travel_formula, data = TravelMode, format = "long", column_decider = "individual", column_alternative = "mode", iterations = 6000, warmup = 3000, thin = 30, chains = 2, progress = FALSE ) summary(travel) ``` The three attribute coefficients are negative: waiting, cost, and travel time reduce the utility of a mode. The income and party size coefficients are alternative-specific and have no direct interpretation on the utility scale, so `interpret(type = "mea")` computes marginal effects, the derivatives of the choice probabilities with respect to a covariate, for a traveler with average covariates: ```{r travel-income} mode_effects <- interpret(travel, type = "mea") mode_effects[mode_effects$covariate == "income", ] ``` A higher household income lowers the probability of the train and raises the probabilities of the plane and the car. ## Individual choice sets Not every alternative is available at every occasion: a traveler without a car cannot drive, and a route without a rail link has no train option. Unordered choices can therefore be made from occasion-specific subsets of the alternatives. In long format, an occasion lists only the rows of its available alternatives, and no further argument is needed. The sampler imputes the latent utilities of unavailable alternatives without restriction, so they do not affect the choice, and predictions assign them probability zero. Between Montreal and Toronto, travelers can fly, drive, or take the train, but not all modes are available on every trip. The `ModeCanada` data of the **mlogit** package cover 4324 trips in this corridor [@Bhat1995]. ```{r canada-load} data("ModeCanada", package = "mlogit") head(ModeCanada) ``` Cost (`cost`), in-vehicle time (`ivt`), out-of-vehicle time (`ovt`), and service frequency (`freq`) vary across modes; household income and the number of urban trip endpoints are trip-specific. The data also contain the bus, which was chosen on only `r sum(ModeCanada$alt == "bus" & ModeCanada$choice == 1)` trips, too few to identify its coefficients and error covariances, so the bus rows and the trips on which it was chosen are removed. Cost and income are converted from Canadian dollars to euro, and trips with a single remaining alternative are dropped. The first 1000 of the remaining trips keep the computation short. ```{r canada-data} ModeCanada$cost <- ModeCanada$cost / 1.6151 ModeCanada$income <- ModeCanada$income / 1.6151 bus_trips <- ModeCanada$case[ModeCanada$alt == "bus" & ModeCanada$choice == 1] canada_data <- ModeCanada[ ModeCanada$alt != "bus" & !(ModeCanada$case %in% bus_trips), ] set_size <- table(canada_data$case) canada_data <- canada_data[set_size[as.character(canada_data$case)] > 1, ] canada_data <- canada_data[ canada_data$case %in% unique(canada_data$case)[1:1000], ] table(table(canada_data$case)) ``` The choice sets are read from the rows of each trip. Every trip is one decider, and the model has the same structure as the travel mode model above: three attributes of type `A`, two trip characteristics of type `B`, and the constants, all relative to `air`. ```{r canada} canada <- fit( choice ~ cost + ivt + ovt + freq | income + urban, data = canada_data, format = "long", column_decider = "case", column_alternative = "alt", iterations = 6000, warmup = 3000, thin = 15, chains = 2, progress = FALSE ) summary(canada) ``` The income coefficients of car and train are negative: a higher income increases the probability of flying. ## Ordered responses Some responses are ordered levels. An ordered model has one latent utility per occasion and compares it with increasing thresholds `gamma`; the level is the interval into which the utility falls. `alternatives` gives the response levels in increasing order. Latent-variable data augmentation provides a direct Bayesian treatment of ordered probit models [@Albert1993]. The following demonstration estimates a simulated three-category model and reports the coefficient and the free threshold beside their true values. ```{r ordered-sim} ordered_sim <- fit( choice ~ x | 0, alternatives = c("low", "middle", "high"), choice_type = "ordered", n_deciders = 500, dgp_parameters = list(beta = c(x = 1), gamma = c(0, 1)), chains = 1 ) summary(ordered_sim) ``` The summary lists the coefficient and the free threshold `gamma[2]` beside their true values. The first threshold is fixed to zero and the error variance to one, which identifies the level and the scale of the utility. The `survey` data of the **MASS** package [@VenablesRipley2002] come from 237 statistics students at the University of Adelaide who reported how often they smoke, together with their age and how much they exercise. The smoking level `Smoke` is stored as a factor whose levels are in alphabetical order; `alternatives` puts them in their natural order from never to heavy. The other survey questions are not used; `fit()` ignores their missing values. ```{r ordered} data("survey", package = "MASS") levels(survey$Smoke) smoking_levels <- c("Never", "Occas", "Regul", "Heavy") smoking <- fit( Smoke ~ Age + Exer | 0, data = survey, alternatives = smoking_levels, choice_type = "ordered", column_decider = NULL, chains = 1 ) summary(smoking) ``` With four levels, the thresholds `gamma[2]` and `gamma[3]` are estimated. Each student has one latent utility, normally distributed around its systematic part, and the thresholds partition it into the four levels. The area under the density between two thresholds is the probability of that level. The figure shows the density of a student whose systematic utility is zero. ```{r ordered-figure} thresholds <- coef(smoking)[c("gamma[2]", "gamma[3]")] cuts <- c(-Inf, 0, thresholds, Inf) shades <- grey(seq(0.45, 0.9, length.out = length(smoking_levels))) utility <- seq(-3.5, 3.5, length.out = 400) plot( utility, dnorm(utility), type = "n", axes = FALSE, ylab = "", xlab = "latent utility of a student" ) for (k in seq_along(smoking_levels)) { inside <- utility >= cuts[k] & utility <= cuts[k + 1] polygon( c(max(cuts[k], -3.5), utility[inside], min(cuts[k + 1], 3.5)), c(0, dnorm(utility[inside]), 0), col = shades[k], border = NA ) } lines(utility, dnorm(utility), lwd = 2) axis(1, at = c(-3, 0, 3)) legend( "topright", legend = smoking_levels, fill = shades, border = NA, bty = "n" ) ``` A covariate shifts the density along the utility axis, so one coefficient per covariate describes its effect on all four levels. The age coefficient is negative: older students report smoking less. The factor `Exer` enters through its dummy variables relative to the students who exercise frequently. `interpret(type = "ame")` translates the age coefficient into probabilities: it differentiates the probability of each level with respect to age and averages the derivatives over the students. The vignette [Posterior prediction][v04] explains marginal effects in more detail. ```{r ordered-interpret} age_effects <- interpret(smoking, type = "ame") age_effects ``` One more year of age raises the probability of never smoking by about `r round(100 * age_effects$mean[age_effects$alternative == "Never"], 1)` percentage points and lowers the probabilities of the other three levels. ## Ranked responses Ranked data record the complete order of the alternatives. In wide format, the response columns combine the response name with each alternative, for example `rank_Xbox`. The model is the same probit model as for unordered choices, but the likelihood uses the full ordering of the utilities. The following demonstration simulates rankings of three alternatives and compares the coefficient and the free covariance parameters with the true values. ```{r ranked-sim} ranked_sim <- fit( rank ~ x | 0, choice_type = "ranked", n_deciders = 300, dgp_parameters = list( beta = c(x = 1), Sigma = rbind(c(0, 0, 0), c(0, 1, 0.2), c(0, 0.2, 1)) ), chains = 1 ) summary(ranked_sim, variables = c("beta[x]", "Sigma[C,B]", "Sigma[C,C]")) ``` The `Game` data of the **mlogit** package contain complete rankings of six gaming platforms by 91 Dutch respondents, together with whether they own each platform (`own`), their age, and their weekly gaming hours; the source study develops a rank-ordered choice model for these data [@Fok2012]. The ranks are stored in the columns `ch.Xbox`, `ch.PlayStation`, and so on, so the response in the formula is `ch` and `delimiter = "."` separates it from the alternative. ```{r ranked} data("Game", package = "mlogit") gaming <- fit( ch ~ own | age + hours, data = Game, alternatives = c( "Xbox", "PlayStation", "PSPortable", "GameCube", "GameBoy", "PC" ), choice_type = "ranked", delimiter = ".", column_decider = NULL, iterations = 1000, warmup = 500, thin = 20, chains = 2, progress = FALSE ) coef(gaming)[1:6] ``` The coefficient of `own` is positive: owning a platform raises its rank. The alternative-specific constants and the coefficients of `age` and `hours` are relative to the base alternative `GameBoy`. Which platform gains from additional gaming hours? For a ranked model, the marginal effects of `interpret()` refer to the probability of being ranked first, here for a respondent of average age who plays the average number of hours: ```{r ranked-interpret} platform_effects <- interpret(gaming, type = "mea") platform_effects[platform_effects$covariate == "hours", ] ``` ```{r ranked-numbers, include=FALSE} pc_hours <- platform_effects$mean[ platform_effects$covariate == "hours" & platform_effects$alternative == "PC" ] ``` Heavy gamers prefer the PC: every additional weekly hour raises the probability of ranking it first by about `r round(100 * pc_hours, 1)` percentage points. ## Further reading The vignette [Modeling preference heterogeneity][v03] covers coefficients that differ between deciders. The vignette [Posterior prediction][v04] computes predictions and marginal effects from a fitted model, and the vignette [Bayesian model evaluation][v05] compares competing specifications. [v01]: https://loelschlaeger.de/RprobitB/articles/v01_get_started.html [v03]: https://loelschlaeger.de/RprobitB/articles/v03_heterogeneity.html [v04]: https://loelschlaeger.de/RprobitB/articles/v04_prediction.html [v05]: https://loelschlaeger.de/RprobitB/articles/v05_model_evaluation.html ## References