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, and Oelschläger (2026) 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 (Kleiber and Zeileis 2008), the
MASS package (Venables and Ripley 2002), or
the mlogit package (Croissant 2020). 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.
The probit model introduced in the vignette Get
started with RprobitB 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[<alternative>,<alternative>], 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.
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)
#> Bayesian probit choice model
#> Formula: choice ~ x + z | 0 | 0
#> Samples: 500 retained per chain, 1 chain
#> variable dgp mean mode sd rhat ess_bulk
#> beta[x] 1.0 1.124 1.079 0.1055 1 36.2
#> beta[z] -0.5 -0.617 -0.604 0.0877 1 51.1
scale_z <- update(scale_default, scale = c(z = -1))
summary(scale_z)
#> Bayesian probit choice model
#> Formula: choice ~ x + z | 0 | 0
#> Samples: 500 retained per chain, 1 chain
#> variable dgp mean mode sd rhat ess_bulk
#> beta[x] 2 1.87 1.80 0.229 1.01 115
#> Sigma[B,B] 4 2.94 2.33 0.932 1.04 47Under 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.
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.
default_prior <- fit(
choice ~ x | 0,
dgp_parameters = list(beta = c(x = -1)),
chains = 1
)
default_prior$prior
#> $fixed_mean
#> [1] 0
#>
#> $fixed_covariance
#> [,1]
#> [1,] 10
#>
#> $error_covariance_df
#> [1] 3
#>
#> $error_covariance_scale
#> [,1]
#> [1,] 1
summary(default_prior)
#> Bayesian probit choice model
#> Formula: choice ~ x | 0 | 0
#> Samples: 500 retained per chain, 1 chain
#> variable dgp mean mode sd rhat ess_bulk
#> beta[x] -1 -1.02 -0.986 0.163 1.02 46.3
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
)
#> variable dgp default moderate tight
#> 1 beta[x] -1 -1.016823 -0.9577477 0.1240365The 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.
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.
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)
#> Bayesian probit choice model
#> Formula: choice ~ x | z | w
#> Samples: 500 retained per chain, 1 chain
#> variable dgp mean mode sd rhat ess_bulk
#> beta[x] 0.50 0.510 0.512 0.0593 1.01 87
#> beta[z_B] -0.50 -0.512 -0.527 0.0730 1.01 117
#> beta[ASC_B] 0.25 0.234 0.236 0.0680 1.01 147
#> beta[w_A] -0.50 -0.433 -0.427 0.0649 1.00 160
#> beta[w_B] 0.50 0.306 0.282 0.0702 1.00 146The 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:
base_b <- update(covariate_types, base = "B")
coef(base_b)[c("beta[x]", "beta[z_A]", "beta[ASC_A]")]
#> beta[x] beta[z_A] beta[ASC_A]
#> 0.5193362 0.5230973 -0.2385689The 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 (Kleiber and Zeileis
2008) 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.
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.
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)
#> Bayesian probit choice model
#> Formula: choice ~ wait + vcost + travel | income + size | 0
#> Samples: 100 retained per chain, 2 chains
#>
#> variable mean mode sd rhat ess_bulk
#> beta[wait] -0.04013 -0.04029 0.006254 1.027 78.3
#> beta[vcost] -0.00890 -0.00985 0.005978 0.996 168.7
#> beta[travel] -0.00219 -0.00225 0.000482 0.995 177.8
#> beta[income_bus] -0.02164 -0.02217 0.012442 1.004 160.9
#> beta[income_car] -0.00865 -0.01092 0.012088 0.994 190.2
#> beta[income_train] -0.05497 -0.05135 0.013880 1.002 157.8
#> beta[size_bus] 0.30353 0.25372 0.168444 1.008 133.8
#> beta[size_car] 0.44195 0.46840 0.141919 1.007 149.8
#> beta[size_train] 0.52959 0.53249 0.153874 1.009 150.6
#> beta[ASC_bus] -0.24083 -0.12344 0.523311 1.001 112.9
#> beta[ASC_car] -2.27621 -2.07970 0.710476 1.003 81.6
#> beta[ASC_train] 0.18447 0.32307 0.478793 0.996 135.7
#> Sigma[bus,car] 0.78141 0.76891 0.181697 1.005 86.0
#> Sigma[car,car] 1.16719 1.02865 0.364239 1.009 116.5
#> Sigma[bus,train] 0.81602 0.85507 0.175795 0.998 75.6
#> Sigma[car,train] 0.76832 0.62301 0.286075 1.002 64.1
#> Sigma[train,train] 1.21533 1.12458 0.395939 1.015 105.3The 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:
mode_effects <- interpret(travel, type = "mea")
mode_effects[mode_effects$covariate == "income", ]
#> Marginal effects at the average covariate values
#> Change in the probability of the alternative per unit of the covariate, with 95% interval
#> covariate alternative at mean sd lower upper
#> income air 21.3 0.01023 0.00382 0.00332 0.01747
#> income bus 21.3 0.00101 0.00237 -0.00327 0.00544
#> income car 21.3 0.00885 0.00355 0.00184 0.01655
#> income train 21.3 -0.02010 0.00434 -0.02873 -0.01223A higher household income lowers the probability of the train and raises the probabilities of the plane and the car.
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 (Bhat 1995).
data("ModeCanada", package = "mlogit")
head(ModeCanada)
#> # A tibble: 6 × 11
#> case alt choice dist cost ivt ovt freq income urban noalt
#> <int> <fct> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 1 train 0 83 28.2 50 66 4 45 0 2
#> 2 1 car 1 83 15.8 61 0 0 45 0 2
#> 3 2 train 0 83 28.2 50 66 4 25 0 2
#> 4 2 car 1 83 15.8 61 0 0 25 0 2
#> 5 3 train 0 83 28.2 50 66 4 70 0 2
#> 6 3 car 1 83 15.8 61 0 0 70 0 2Cost (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 16 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.
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))
#>
#> 2 3
#> 283 717The 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.
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)
#> Bayesian probit choice model
#> Formula: choice ~ cost + ivt + ovt + freq | income + urban | 0
#> Samples: 200 retained per chain, 2 chains
#>
#> variable mean mode sd rhat ess_bulk
#> beta[cost] -0.03860 -0.0401 0.00736 1.04 32.2
#> beta[ivt] -0.00608 -0.0060 0.00101 1.02 43.9
#> beta[ovt] -0.01651 -0.0176 0.00233 1.02 151.2
#> beta[freq] 0.02731 0.0270 0.00556 1.00 282.3
#> beta[income_car] -0.01895 -0.0194 0.00670 1.00 159.2
#> beta[income_train] -0.03364 -0.0286 0.00978 1.02 53.6
#> beta[urban_car] -0.47057 -0.4902 0.15469 1.01 301.3
#> beta[urban_train] 0.70098 0.6166 0.28609 1.04 48.7
#> beta[ASC_car] -0.67282 -0.7814 0.62921 1.06 29.6
#> beta[ASC_train] -0.50463 -0.5758 0.58564 1.02 34.0
#> Sigma[car,train] 0.35322 0.3501 0.16977 1.04 64.4
#> Sigma[train,train] 1.22169 0.9447 0.69708 1.04 34.9The income coefficients of car and train are negative: a higher income increases the probability of flying.
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 (Albert and Chib 1993). The following
demonstration estimates a simulated three-category model and reports the
coefficient and the free threshold beside their true values.
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)
#> Bayesian probit choice model
#> Formula: choice ~ x | 0 | 0
#> Samples: 500 retained per chain, 1 chain
#> variable dgp mean mode sd rhat ess_bulk
#> beta[x] 1 1.067 1.091 0.0768 1.04 74.7
#> gamma[2] 1 0.926 0.891 0.0624 1.05 144.1The 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
(Venables and
Ripley 2002) 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.
data("survey", package = "MASS")
levels(survey$Smoke)
#> [1] "Heavy" "Never" "Occas" "Regul"
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)
#> Bayesian probit choice model
#> Formula: Smoke ~ Age + Exer | 0 | 0
#> Samples: 500 retained per chain, 1 chain
#> variable mean mode sd rhat ess_bulk
#> beta[Age] -0.0286 -0.028 0.0058 1 144
#> beta[ExerNone] -0.1815 -0.127 0.2977 1 219
#> beta[ExerSome] -0.4658 -0.480 0.1888 1 114
#> gamma[2] 0.3777 0.401 0.0824 1 108
#> gamma[3] 0.8967 0.837 0.1332 1 155With 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.
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 explains marginal effects in more detail.
age_effects <- interpret(smoking, type = "ame")
age_effects
#> Average marginal effects on the choice probabilities
#> Change in the probability of the alternative per unit of the covariate, with 95% interval
#> covariate alternative mean sd lower upper
#> Age Never 0.00817 0.001405 0.00529 0.01078
#> Age Occas -0.00234 0.000657 -0.00370 -0.00118
#> Age Regul -0.00285 0.000693 -0.00430 -0.00162
#> Age Heavy -0.00297 0.000760 -0.00453 -0.00162One more year of age raises the probability of never smoking by about 0.8 percentage points and lowers the probabilities of the other three levels.
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.
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]"))
#> Bayesian probit choice model
#> Formula: rank ~ x | 0 | 0
#> Samples: 500 retained per chain, 1 chain
#> variable dgp mean mode sd rhat ess_bulk
#> beta[x] 1.0 1.253 1.246 0.0966 1.03 16.1
#> Sigma[C,B] 0.2 0.128 0.115 0.1690 1.03 34.2
#> Sigma[C,C] 1.0 1.690 1.585 0.4085 1.02 53.6The 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 (Fok et
al. 2012). 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.
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]
#> beta[own] beta[age.GameCube] beta[age.PC]
#> 0.841230647 0.010794947 0.054745222
#> beta[age.PSPortable] beta[age.PlayStation] beta[age.Xbox]
#> 0.003669015 0.038698335 0.021066253The 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:
platform_effects <- interpret(gaming, type = "mea")
platform_effects[platform_effects$covariate == "hours", ]
#> Marginal effects at the average covariate values
#> Change in the probability of the alternative per unit of the covariate, with 95% interval
#> covariate alternative at mean sd lower upper
#> hours GameBoy 3.88 -0.00221 0.00136 -0.00573 -0.000403
#> hours GameCube 3.88 -0.00476 0.00366 -0.01232 0.001605
#> hours PC 3.88 0.03442 0.01328 0.01232 0.058558
#> hours PSPortable 3.88 -0.00873 0.00375 -0.01562 -0.003349
#> hours PlayStation 3.88 -0.00511 0.00769 -0.01932 0.007699
#> hours Xbox 3.88 -0.01361 0.00792 -0.02724 -0.001194Heavy gamers prefer the PC: every additional weekly hour raises the probability of ranking it first by about 3.4 percentage points.
The vignette Modeling preference heterogeneity covers coefficients that differ between deciders. The vignette Posterior prediction computes predictions and marginal effects from a fitted model, and the vignette Bayesian model evaluation compares competing specifications.