12. Mixture of logit with panel data

Bayesian estimation of a mixture of logit models. The datafile is organized as panel data. Note that, with Bayesian estimation, there is no need to calculate a Monte-Carlo integration.

Michel Bierlaire, EPFL Thu Nov 20 2025, 14:50:04

from IPython.core.display_functions import display

import biogeme.biogeme_logging as blog
from biogeme.bayesian_estimation import (
    BayesianResults,
    FigureSize,
    generate_html_file as generate_bayesian_html_file,
    get_pandas_estimated_parameters,
)
from biogeme.biogeme import BIOGEME
from biogeme.expressions import Beta, DistributedParameter, Draws
from biogeme.filenames import get_new_file_name
from biogeme.models import loglogit

See the data processing script: Panel data preparation for Swissmetro.

from swissmetro_panel import (
    CAR_AV_SP,
    CAR_CO_SCALED,
    CAR_TT_SCALED,
    CHOICE,
    SM_AV,
    SM_COST_SCALED,
    SM_TT_SCALED,
    TRAIN_AV_SP,
    TRAIN_COST_SCALED,
    TRAIN_TT_SCALED,
    database,
)

logger = blog.get_screen_logger(level=blog.INFO)
logger.info('Example b12_panel.py')
Example b12_panel.py

The scale parameters must stay away from zero. We define a small but positive lower bound

POSITIVE_LOWER_BOUND = 1.0e-5

Parameters to be estimated.

b_cost = Beta('b_cost', 0, None, 0, 0)

Define a random parameter, normally distributed across individuals, designed to be used for Monte-Carlo simulation.

b_time = Beta('b_time', 0, None, 0, 0)
b_time_s = Beta('b_time_s', 1, POSITIVE_LOWER_BOUND, None, 0)
b_time_eps = Draws('b_time_eps', 'NORMAL')
b_time_eps.set_draw_per_individual()
b_time_rnd = DistributedParameter('b_time_rnd', b_time + b_time_s * b_time_eps)

We do the same for the constants, to address serial correlation.

asc_car = Beta('asc_car', 0, None, None, 0)
asc_car_s = Beta('asc_car_s', 1, POSITIVE_LOWER_BOUND, None, 0)
asc_car_eps = Draws('asc_car_eps', 'NORMAL')
asc_car_eps.set_draw_per_individual()
asc_car_rnd = DistributedParameter('asc_car_rnd', asc_car + asc_car_s * asc_car_eps)

asc_train = Beta('asc_train', 0, None, None, 0)
asc_train_s = Beta('asc_train_s', 1, POSITIVE_LOWER_BOUND, None, 0)
asc_train_eps = Draws('asc_train_eps', 'NORMAL')
asc_car_eps.set_draw_per_individual()
asc_train_rnd = DistributedParameter(
    'asc_train_rnd', asc_train + asc_train_s * asc_train_eps
)

asc_sm = Beta('asc_sm', 0, None, None, 0)
asc_sm_s = Beta('asc_sm_s', 1, POSITIVE_LOWER_BOUND, None, 0)
asc_sm_eps = Draws('asc_sm_eps', 'NORMAL')
asc_sm_eps.set_draw_per_individual()
asc_sm_rnd = DistributedParameter('asc_sm_rnd', asc_sm + asc_sm_s * asc_sm_eps)

Definition of the utility functions.

v_train = asc_train_rnd + b_time_rnd * TRAIN_TT_SCALED + b_cost * TRAIN_COST_SCALED
v_swissmetro = asc_sm_rnd + b_time_rnd * SM_TT_SCALED + b_cost * SM_COST_SCALED
v_car = asc_car_rnd + b_time_rnd * CAR_TT_SCALED + b_cost * CAR_CO_SCALED

Associate utility functions with the numbering of alternatives.

v = {1: v_train, 2: v_swissmetro, 3: v_car}

Associate the availability conditions with the alternatives.

av = {1: TRAIN_AV_SP, 2: SM_AV, 3: CAR_AV_SP}

Conditional on the random parameters, the likelihood of one observation is given by the logit model (called the kernel).

log_probability_one_observation = loglogit(v, av, CHOICE)

As the objective is to illustrate the syntax, we calculate the Monte-Carlo approximation with a small number of draws.

the_biogeme = BIOGEME(
    database,
    log_probability_one_observation,
    warmup=5000,
    bayesian_draws=5000,
    chains=4,
)
the_biogeme.model_name = 'b12_panel'
Biogeme parameters read from biogeme.toml.

Estimate the parameters.

try:
    results = BayesianResults.from_netcdf(
        filename=f'saved_results/{the_biogeme.model_name}.nc'
    )
    html_filename = get_new_file_name(the_biogeme.model_name, "html")
    generate_bayesian_html_file(
        filename=html_filename,
        estimation_results=results,
        figure_size=FigureSize.LARGE,
    )
    print(f'{html_filename} generated')

except FileNotFoundError:
    results = the_biogeme.bayesian_estimation()
Loaded NetCDF file size: 1.9 GB
load finished in 13943 ms (13.94 s)
posterior_predictive_loglike finished in 72 ms
/Users/bierlair/python_envs/venv313/lib/python3.13/site-packages/arviz/stats/stats.py:1667: UserWarning: For one or more samples the posterior variance of the log predictive densities exceeds 0.4. This could be indication of WAIC starting to fail.
See http://arxiv.org/abs/1507.04544 for details
  warnings.warn(
waic_res finished in 164 ms
waic finished in 164 ms
/Users/bierlair/python_envs/venv313/lib/python3.13/site-packages/arviz/stats/stats.py:797: UserWarning: Estimated shape parameter of Pareto distribution is greater than 0.70 for one or more samples. You should consider using a more robust model, this is because importance sampling is less likely to work well if the marginal posterior and LOO posterior are very different. This is more likely to happen with a non-robust model and highly influential observations.
  warnings.warn(
loo_res finished in 16667 ms (16.67 s)
loo finished in 16667 ms (16.67 s)
Diagnostics computation took 279.6 seconds (cached).
File b12_panel.html has been generated.
b12_panel.html generated
print(results.short_summary())
Sample size                                              6768
Sampler                                                  NUTS
Number of chains                                         4
Number of draws per chain                                5000
Total number of draws                                    20000
Acceptance rate target                                   0.9
Run time                                                 0:55:52.458498
Posterior predictive log-likelihood (sum of log mean p)  -2153.79
Expected log-likelihood E[log L(Y|θ)]                    -2335.35
Best-draw log-likelihood (posterior upper bound)         -2222.36
WAIC (Widely Applicable Information Criterion)           -2708.78
WAIC Standard Error                                      73.22
Effective number of parameters (p_WAIC)                  554.99
LOO (Leave-One-Out Cross-Validation)                     -3023.97
LOO Standard Error                                       77.65
Effective number of parameters (p_LOO)                   870.18
pandas_results = get_pandas_estimated_parameters(estimation_results=results)
display(pandas_results)


print(results.idata.posterior.dims)
print(results.idata.posterior)
          Name  Value (mean)  ...    ESS (bulk)    ESS (tail)
0    asc_train     -0.393490  ...  19678.445193  16095.817718
1       asc_sm     -0.001563  ...  19625.145156  16120.632916
2      asc_car      0.486365  ...  19680.529187  16066.934765
3  asc_train_s      2.252391  ...    701.092646   1848.611686
4       b_time     -6.146344  ...  10481.125240  14218.811677
5     b_time_s      3.858984  ...   4555.396875  10178.924520
6       b_cost     -3.906447  ...   9587.971472  11842.207973
7     asc_sm_s      1.196161  ...    445.145871    898.921184
8    asc_car_s      3.865498  ...   4070.521381  10502.559065

[9 rows x 12 columns]
FrozenMappingWarningOnValuesAccess({'chain': 4, 'draw': 5000, 'individuals': 752, 'obs': 6768})
<xarray.Dataset> Size: 5GB
Dimensions:                       (chain: 4, draw: 5000, individuals: 752,
                                   obs: 6768)
Coordinates:
  * chain                         (chain) int64 32B 0 1 2 3
  * draw                          (draw) int64 40kB 0 1 2 3 ... 4997 4998 4999
  * individuals                   (individuals) int64 6kB 0 1 2 ... 749 750 751
  * obs                           (obs) int64 54kB 0 1 2 3 ... 6765 6766 6767
Data variables: (12/22)
    asc_train                     (chain, draw) float64 160kB -1.414 ... -1.294
    asc_train_eps                 (chain, draw, individuals) float64 120MB 0....
    b_time_eps                    (chain, draw, individuals) float64 120MB 0....
    asc_sm                        (chain, draw) float64 160kB -1.196 ... -0.6504
    asc_sm_eps                    (chain, draw, individuals) float64 120MB 0....
    asc_car                       (chain, draw) float64 160kB -0.5308 ... -0.513
    ...                            ...
    b_time_rnd                    (chain, draw, obs) float64 1GB -5.053 ... 0...
    asc_sm_rnd_per_individual     (chain, draw, individuals) float64 120MB -0...
    asc_sm_rnd                    (chain, draw, obs) float64 1GB -0.565 ... -...
    asc_car_rnd_per_individual    (chain, draw, individuals) float64 120MB -2...
    asc_car_rnd                   (chain, draw, obs) float64 1GB -2.038 ... 1...
    log_like                      (chain, draw, individuals) float64 120MB -3...
Attributes: (12/15)
    created_at:                 2025-12-24T13:52:32.429170+00:00
    arviz_version:              0.22.0
    inference_library:          numpyro
    inference_library_version:  0.19.0
    sampling_time:              3291.301176
    tuning_steps:               5000
    ...                         ...
    log_like_name:              log_like
    number_of_observations:     6768
    beta_names:                 ["asc_train", "asc_train_s", "b_time", "b_tim...
    sampler:                    NUTS
    target_accept:              0.9
    run_time_seconds:           3352.458498

Total running time of the script: (5 minutes 13.379 seconds)

Gallery generated by Sphinx-Gallery