--- title: "Get started with RprobitB" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Get started with RprobitB} %\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" ) ``` Discrete choice models describe how a decider selects one alternative from a finite set, for example a train connection, an electricity supplier, or a mode of transport [@Train2009]. The probability of each alternative is a function of the attributes of the alternatives and the characteristics of the decider. The coefficients of the model quantify the weight of each attribute in the decision, and ratios of coefficients quantify the trade-offs between attributes. **RprobitB** estimates discrete choice models in a Bayesian probit framework. This vignette fits a basic probit model to panel data from a stated choice experiment and introduces the posterior summaries, convergence diagnostics, and plots on which every analysis with the package relies. ## The probit model Consider $N$ deciders $n = 1, \dots, N$, each of whom chooses at $T_n$ occasions $t = 1, \dots, T_n$ one of $J$ alternatives $j = 1, \dots, J$. The probit model is a random utility model: it assigns to every alternative the latent utility $$U_{ntj} = X_{ntj}^\top \beta_n + \epsilon_{ntj}, \qquad \epsilon_{nt} = (\epsilon_{nt1}, \dots, \epsilon_{ntJ})^\top \sim \mathrm{N}(0, \Sigma),$$ where $X_{ntj}$ is the vector of $P$ covariates assigned to alternative $j$ at occasion $t$ of decider $n$, $\beta_n$ is the vector of the $P$ coefficients of decider $n$, and $\epsilon_{ntj}$ is an error term. The error vector $\epsilon_{nt}$ of an occasion is multivariate normal with mean zero and the $J \times J$ covariance matrix $\Sigma$, and it is independent across deciders and occasions. The decider chooses the alternative with the largest utility, $y_{nt} = \operatorname{argmax}_j U_{ntj}$, and only this choice $y_{nt}$ is observed, not the utilities. The choice probability of alternative $j$ is the probability that its utility exceeds the utilities of all other alternatives, $$\Pr(y_{nt} = j) = \Pr(U_{ntj} > U_{ntl} \text{ for all } l \neq j),$$ which is a function of the covariates $X_{nt}$ of the occasion, the coefficients $\beta_n$, and the covariance matrix $\Sigma$. The choice 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** removes the level by taking utility differences with respect to a base alternative and fixes the scale by restricting either one error variance or one coefficient. These two restrictions are the normalization of the model. The coefficient vector $\beta_n$ is common to all deciders unless random effects are specified, in which case the coefficients of every decider are drawn from a population distribution whose parameters are estimated. The vignette [Model specification and variants][v02] describes the normalization and the prior distribution, and the vignette [Modeling preference heterogeneity][v03] the specification of heterogeneous coefficients. Bayesian estimation of the multinomial probit model goes back to @McCulloch1994 and @Imai2005a, and @Oelschlaeger2026c gives a unified account of the heterogeneity models that **RprobitB** implements. ## A stated choice experiment on train trips In 1987, a stated choice experiment commissioned by the Dutch national railways presented 235 travelers with pairs of hypothetical train trips and asked which trip of each pair they would choose [@BenAkiva1993]. The two trips of a pair differ in price, travel time, number of changes, and comfort class, and every traveler evaluated about twelve pairs. The **mlogit** package [@Croissant2020] provides the 2929 choices as the data set `Train`. The data are in wide format: one row per choice occasion, with the attributes of trip `A` in the columns ending in `_A` and those of trip `B` in the columns ending in `_B`. Prices are recorded in cents of Dutch guilders and travel times in minutes; both are converted to euro and hours below. ```{r data} library(RprobitB) set.seed(1) data("Train", package = "mlogit") Train$price_A <- Train$price_A / 100 / 2.20371 Train$price_B <- Train$price_B / 100 / 2.20371 Train$time_A <- Train$time_A / 60 Train$time_B <- Train$time_B / 60 head(Train) ``` The model formula names the response and the covariates: ```{r formula} formula <- choice ~ price + time + change + factor(comfort) | 0 ``` All four covariates are attributes of the trips with one coefficient common to both alternatives, which places them in the first part of the formula. The second part takes characteristics of the decider and the alternative-specific constants; here it contains only the `0` that removes the constants, which is appropriate because the labels `A` and `B` of the two trips were assigned arbitrarily and carry no utility of their own. The comfort class is stored as an integer but is a categorical variable with three levels; `factor(comfort)` expands it into dummy variables, as in `lm()`. The arguments `column_decider` and `column_occasion` identify the traveler and the choice occasion. The vignette [Model specification and variants][v02] describes the three parts of the formula and the further arguments of `fit()`. ```{r fit} model <- fit( formula = formula, data = Train, column_decider = "id", column_occasion = "choiceid", iterations = 2000, warmup = 1000, chains = 2, progress = FALSE ) model ``` `fit()` estimates the model with a Gibbs sampler, a Markov chain Monte Carlo method that draws the latent utilities and the parameters in turn from their conditional posterior distributions. The posterior distribution combines the prior distribution of the parameters, described in the vignette [Model specification and variants][v02], with the likelihood of the observed choices. A chain of the sampler consists of `iterations` draws. The first `warmup` of them are discarded, because the chain first has to move from its starting values into the region of the posterior, and the remaining draws are retained as the posterior draws from which every summary below is computed. Several independent chains, set by `chains`, sample the same posterior and make it possible to check convergence, that is, whether the retained draws represent the posterior distribution. ## Posterior summaries `summary()` reports the posterior mean, mode, and standard deviation of every population-level parameter together with the convergence diagnostics of the **posterior** package [@Buerkner2026]. ```{r summary} summary(model) ``` The coefficients of price, travel time, and number of changes are negative, as expected: each of these attributes reduces the utility of a trip. Comfort is a factor whose level `0` denotes the highest class, so the negative coefficients of the dummies for levels `1` and `2` quantify the loss of utility in the lower classes. The error variance of the utility difference, `Sigma[B,B]`, is absent from the table because the default normalization fixes it to one. Because of this normalization, the coefficients are measured in units of the error standard deviation, and their absolute size has no substantive interpretation. The ratio of two coefficients is invariant to the scale and answers a substantive question: which price reduction compensates a traveler for one additional hour of travel time? `interpret()` computes such ratios for every posterior draw and expresses each effect in units of a reference effect, here the price: ```{r interpret} compensation <- interpret(model, reference = "price") compensation ``` One additional hour of travel time is compensated by a price that is about `r round(abs(compensation$mean[compensation$effect == "time"]), 1)` euro lower (at 1987 prices), and the lowest comfort class relative to the highest by a price about `r round(abs(compensation$mean[compensation$effect == "factor(comfort)2"]), 1)` euro lower. The last two columns of the summary table are the convergence diagnostics recommended by @Vehtari2021: - `rhat` compares the variance between the chains with the variance within the chains and equals one when all chains sample the same distribution. - `ess_bulk` is the effective sample size for the bulk of the posterior distribution, that is, for its central part around the median as opposed to its tails. Consecutive draws of a chain are correlated, and the effective sample size is the number of independent draws that would estimate the posterior mean or median with the same precision. A few hundred effective draws suffice for these summaries; the precision of extreme quantiles is governed by the tail effective sample size instead. The arguments `probs` and `statistics` select the columns of the table. `probs` adds posterior quantiles, and `statistics` selects the measures, among them the tail effective sample size, which governs the precision of the quantiles, and the Monte Carlo standard error of the mean, which quantifies the simulation error of the reported posterior mean. `?summary.RprobitB_fit` describes each measure. ```{r summary-statistics} summary( model, statistics = c("mean", "mcse_mean", "ess_bulk", "ess_tail"), probs = c(0.05, 0.95) ) ``` `coef()` and `confint()` return posterior means or medians and equal-tailed credible intervals. The bounds of such an interval are the posterior quantiles at `(1 - level) / 2` and `(1 + level) / 2`, so that the interval contains the parameter with posterior probability `level`. ```{r coef} coef(model) confint(model, level = 0.9) ``` `vcov()` returns the posterior covariance matrix of the parameters: ```{r vcov} round(vcov(model), 5) ``` An interval plot displays the marginal posteriors: the thick bars cover the central 50% and the thin lines the central 90% of each posterior. ```{r interval} plot(model, type = "interval") ``` ## Graphical convergence diagnostics `plot()` delegates to **bayesplot** [@Gabry2025]. Trace plots show the draws of each chain against their index, and density overlays compare the marginal posteriors of the chains; there are two chains here because `fit()` was called with `chains = 2`. For converged chains, the traces are stationary without trend, and the densities of the chains coincide. ```{r trace} plot(model, type = "trace", variables = c("beta[price]", "beta[time]")) plot(model, type = "density", variables = c("beta[price]", "beta[time]")) ``` Rank plots are a more sensitive check [@Vehtari2021]. The draws of all chains are pooled and ranked, and the histogram of the ranks is drawn for every chain. If all chains sample the same distribution, every chain receives ranks from the whole range with equal frequency, and the histograms are uniform. A chain that remains in a subregion of the posterior, for example because it has not yet left the region of its starting values, receives predominantly small or large ranks, and its histogram is peaked at one end. Autocorrelation plots show, for every chain, the correlation between draws that are $k$ iterations apart as a function of the lag $k$. Consecutive draws of a Gibbs sampler are dependent, so a chain carries less information than the same number of independent draws. The effective sample size expresses this loss in one number: it is the number of independent draws that would estimate the posterior mean with the same precision. In its classical form, it equals $S / (1 + 2 \sum_{k \geq 1} \rho_k)$ for $S$ draws with autocorrelations $\rho_k$, so the faster the autocorrelation decays to zero, the closer the effective sample size is to the number of draws. `ess_bulk` applies this formula to rank-normalized draws, which makes it robust to heavy tails [@Vehtari2021]. ```{r rank} plot(model, type = "rank", variables = c("beta[price]", "beta[time]")) plot(model, type = "acf", variables = c("beta[price]", "beta[time]")) ``` Pairs plots show the joint posterior of two parameters and reveal posterior correlations: ```{r pairs} plot(model, type = "pairs", variables = c("beta[price]", "beta[time]")) ``` ## Working with the posterior draws The retained draws are stored as a `draws_array` of the **posterior** package with dimensions iteration, chain, and variable, so that all functions of that package apply to them directly. ```{r draws} draws <- posterior::as_draws(model) dim(draws) posterior::summarise_draws(draws, "mean", "quantile2") ``` The standard accessor methods are available: `formula()` returns the fitted formula, `model.frame()` the data the model was fitted to, and `nobs()` the number of independent units in the likelihood. Here these are the observed choice occasions, because the model has one coefficient vector for all travelers. With random effects, and with the latent classes of the vignette [Modeling preference heterogeneity][v03], the choices of a traveler are dependent, and `nobs()` counts travelers. ```{r accessors} formula(model) nobs(model) head(model.frame(model)) ``` ## Parallel chains The chains of a fit are independent of each other, so **RprobitB** can run them in parallel on several cores, which reduces the computing time in proportion to the number of chains. The package does not select a parallel backend itself. The chains run through the **future** framework [@Bengtsson2021], so a plan set before fitting is used automatically. ```{r parallel, eval=FALSE} future::plan(future::multisession, workers = 4) # run chains on four cores parallel_model <- fit( formula = formula, data = Train, column_decider = "id", column_occasion = "choiceid" ) future::plan(future::sequential) # return to sequential evaluation ``` In interactive sessions, all chains report their progress through the **progressr** package [@Bengtsson2026]. ## Further reading - [Model specification and variants][v02]: the normalization, the prior distribution, the three covariate types, individual choice sets, and ordered and ranked responses. - [Modeling preference heterogeneity][v03]: random coefficients and latent classes. - [Posterior prediction][v04]: posterior predictive probabilities for the population and for individual deciders, scenarios, out-of-sample prediction, and marginal effects. - [Bayesian model evaluation][v05]: model comparison by WAIC, PSIS-LOO, and Bayes factors. [v02]: https://loelschlaeger.de/RprobitB/articles/v02_model_variants.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