Estimation and simulation of a nested logit model

We estimate a nested logit model, and we perform simulation using the estimated model.

Michel Bierlaire, EPFL Sat Jun 28 2025, 16:08:07

from IPython.core.display_functions import display

import biogeme.biogeme_logging as blog
from biogeme.biogeme import BIOGEME
from biogeme.calculator import get_value_c
from biogeme.data.optima import read_data
from biogeme.models import lognested
from biogeme.results_processing import get_pandas_estimated_parameters
from scenarios import scenario

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

Obtain the specification for the default scenario. The definition of the scenarios is available in Specification of a nested logit model.

V, nests, choice, _ = scenario()

The choice model is a nested logit, with availability conditions For estimation, we need the log of the probability.

log_probability = lognested(util=V, availability=None, nests=nests, choice=choice)

Get the database

database = read_data()

Create the Biogeme object for estimation.

the_biogeme = BIOGEME(database, log_probability)
the_biogeme.model_name = 'b02estimation'
Biogeme parameters read from biogeme.toml.

Estimate the parameters. Perform bootstrapping.

results = the_biogeme.estimate(run_bootstrap=True)
*** Initial values of the parameters are obtained from the file __b02estimation.iter
Parameter values restored from __b02estimation.iter
Starting values for the algorithm: {'beta_time_fulltime': -1.5964670520847424, 'beta_time_other': -0.5526862214775381, 'beta_cost': -0.719072321557382, 'mu_no_car': 1.5237160520807354, 'asc_sm': 0.06300034044143561, 'beta_dist_male': -0.6876768914763282, 'beta_dist_female': -0.8321525469977931, 'beta_dist_unreported': -0.7047372090923671, 'asc_car': 0.2586072914689233}
As the model is rather complex, we cancel the calculation of second derivatives. If you want to control the parameters, change the algorithm from "automatic" to "simple_bounds" in the TOML file.
Optimization algorithm: hybrid Newton/BFGS with simple bounds [simple_bounds]
** Optimization: BFGS with trust region for simple bounds
Optimization algorithm has converged.
Relative gradient: 4.478787282617695e-06
Cause of termination: Relative gradient = 4.5e-06 <= 6.1e-06
Number of function evaluations: 1
Number of gradient evaluations: 1
Number of hessian evaluations: 0
Algorithm: BFGS with trust region for simple bound constraints
Number of iterations: 0
Optimization time: 0:00:00.466837
Calculate second derivatives and BHHH
Re-estimate the model 100 times for bootstrapping

Bootstraps:   0%|          | 0/100 [00:00<?, ?it/s]

  0%|          | 0/100 [00:00<?, ?it/s]

  1%|          | 1/100 [00:03<04:57,  3.01s/it]

  3%|▎         | 3/100 [00:04<02:15,  1.40s/it]

  5%|▌         | 5/100 [00:06<01:48,  1.15s/it]

  7%|▋         | 7/100 [15:18<5:00:22, 193.79s/it]

  9%|▉         | 9/100 [15:20<3:03:42, 121.12s/it]/Users/bierlair/python_envs/venv313/lib/python3.13/site-packages/joblib/externals/loky/process_executor.py:782: UserWarning: A worker stopped while some jobs were given to the executor. This can be caused by a too short worker timeout or by a memory leak.
  warnings.warn(


 11%|█         | 11/100 [15:24<1:57:42, 79.35s/it]

 12%|█▏        | 12/100 [15:24<1:33:13, 63.56s/it]

 13%|█▎        | 13/100 [15:25<1:12:06, 49.73s/it]

 14%|█▍        | 14/100 [15:26<54:11, 37.81s/it]

 15%|█▌        | 15/100 [15:27<40:20, 28.48s/it]

 16%|█▌        | 16/100 [15:27<29:16, 20.91s/it]

 17%|█▋        | 17/100 [15:29<21:27, 15.51s/it]

 18%|█▊        | 18/100 [15:29<15:15, 11.16s/it]

 19%|█▉        | 19/100 [15:30<11:19,  8.39s/it]

 20%|██        | 20/100 [15:31<08:05,  6.07s/it]

 21%|██        | 21/100 [15:34<06:44,  5.12s/it]

 22%|██▏       | 22/100 [15:34<04:49,  3.71s/it]

 23%|██▎       | 23/100 [15:35<03:51,  3.01s/it]

 24%|██▍       | 24/100 [15:36<02:47,  2.21s/it]

 25%|██▌       | 25/100 [15:37<02:28,  1.98s/it]

 26%|██▌       | 26/100 [15:37<01:47,  1.45s/it]

 27%|██▋       | 27/100 [15:39<01:44,  1.43s/it]

 28%|██▊       | 28/100 [15:39<01:17,  1.08s/it]

 29%|██▉       | 29/100 [15:40<01:23,  1.18s/it]

 30%|███       | 30/100 [15:41<01:06,  1.05it/s]

 31%|███       | 31/100 [15:44<01:43,  1.49s/it]

 32%|███▏      | 32/100 [15:44<01:16,  1.12s/it]

 33%|███▎      | 33/100 [15:45<01:20,  1.21s/it]

 34%|███▍      | 34/100 [15:45<00:59,  1.10it/s]

 35%|███▌      | 35/100 [15:47<01:11,  1.09s/it]

 37%|███▋      | 37/100 [15:49<01:00,  1.04it/s]

 39%|███▉      | 39/100 [15:50<00:56,  1.08it/s]

 41%|████      | 41/100 [15:53<01:08,  1.15s/it]

 42%|████▏     | 42/100 [15:54<00:56,  1.02it/s]

 43%|████▎     | 43/100 [15:55<01:00,  1.06s/it]

 44%|████▍     | 44/100 [15:55<00:47,  1.17it/s]

 45%|████▌     | 45/100 [15:57<00:54,  1.00it/s]

 46%|████▌     | 46/100 [15:57<00:43,  1.25it/s]

 47%|████▋     | 47/100 [15:58<00:50,  1.05it/s]

 48%|████▊     | 48/100 [15:59<00:39,  1.33it/s]

 49%|████▉     | 49/100 [16:00<00:48,  1.06it/s]

 50%|█████     | 50/100 [16:00<00:39,  1.26it/s]

 51%|█████     | 51/100 [16:03<01:05,  1.33s/it]

 52%|█████▏    | 52/100 [16:03<00:50,  1.06s/it]

 53%|█████▎    | 53/100 [16:05<00:53,  1.13s/it]

 54%|█████▍    | 54/100 [16:05<00:40,  1.13it/s]

 55%|█████▌    | 55/100 [16:06<00:46,  1.04s/it]

 56%|█████▌    | 56/100 [16:07<00:36,  1.22it/s]

 57%|█████▋    | 57/100 [16:08<00:39,  1.08it/s]

 58%|█████▊    | 58/100 [16:08<00:32,  1.29it/s]

 59%|█████▉    | 59/100 [16:10<00:37,  1.10it/s]

 60%|██████    | 60/100 [16:10<00:30,  1.29it/s]

 61%|██████    | 61/100 [16:13<00:51,  1.32s/it]

 62%|██████▏   | 62/100 [16:13<00:41,  1.10s/it]

 63%|██████▎   | 63/100 [16:14<00:39,  1.08s/it]

 64%|██████▍   | 64/100 [16:15<00:33,  1.08it/s]

 65%|██████▌   | 65/100 [16:16<00:34,  1.03it/s]

 66%|██████▌   | 66/100 [16:16<00:28,  1.18it/s]

 67%|██████▋   | 67/100 [16:18<00:30,  1.09it/s]

 68%|██████▊   | 68/100 [16:18<00:24,  1.28it/s]

 69%|██████▉   | 69/100 [16:19<00:27,  1.11it/s]

 70%|███████   | 70/100 [16:20<00:23,  1.28it/s]

 71%|███████   | 71/100 [16:22<00:39,  1.37s/it]

 72%|███████▏  | 72/100 [16:23<00:29,  1.05s/it]

 73%|███████▎  | 73/100 [16:24<00:30,  1.13s/it]

 74%|███████▍  | 74/100 [16:24<00:23,  1.12it/s]

 75%|███████▌  | 75/100 [16:26<00:25,  1.03s/it]

 76%|███████▌  | 76/100 [16:26<00:19,  1.25it/s]

 77%|███████▋  | 77/100 [16:27<00:22,  1.02it/s]

 78%|███████▊  | 78/100 [16:27<00:15,  1.39it/s]

 79%|███████▉  | 79/100 [16:29<00:20,  1.05it/s]

 80%|████████  | 80/100 [16:29<00:15,  1.26it/s]

 81%|████████  | 81/100 [16:32<00:25,  1.36s/it]

 82%|████████▏ | 82/100 [16:32<00:18,  1.05s/it]

 83%|████████▎ | 83/100 [16:34<00:19,  1.14s/it]

 84%|████████▍ | 84/100 [16:34<00:14,  1.12it/s]

 85%|████████▌ | 85/100 [16:35<00:15,  1.01s/it]

 86%|████████▌ | 86/100 [16:36<00:11,  1.21it/s]

 87%|████████▋ | 87/100 [16:37<00:12,  1.03it/s]

 88%|████████▊ | 88/100 [16:37<00:09,  1.31it/s]

 89%|████████▉ | 89/100 [16:39<00:10,  1.05it/s]

 90%|█████████ | 90/100 [16:39<00:07,  1.26it/s]

 91%|█████████ | 91/100 [16:42<00:11,  1.33s/it]

 92%|█████████▏| 92/100 [16:42<00:08,  1.07s/it]

 93%|█████████▎| 93/100 [16:43<00:07,  1.10s/it]

 94%|█████████▍| 94/100 [16:44<00:05,  1.12it/s]

 95%|█████████▌| 95/100 [16:45<00:04,  1.01it/s]

 96%|█████████▌| 96/100 [16:46<00:03,  1.19it/s]

 97%|█████████▋| 97/100 [16:47<00:02,  1.08it/s]

 98%|█████████▊| 98/100 [16:47<00:01,  1.28it/s]

 99%|█████████▉| 99/100 [16:48<00:00,  1.09it/s]

100%|██████████| 100/100 [16:49<00:00,  1.31it/s]
100%|██████████| 100/100 [16:49<00:00, 10.09s/it]
File b02estimation~00.html has been generated.
File b02estimation~00.yaml has been generated.

Get the results in a pandas table

pandas_results = get_pandas_estimated_parameters(estimation_results=results)
display(pandas_results)
                   Name     Value  ...  Bootstrap t-stat.  Bootstrap p-value
0    beta_time_fulltime -1.596467  ...          -5.445201       5.174692e-08
1       beta_time_other -0.552686  ...          -2.037927       4.155725e-02
2             beta_cost -0.719072  ...          -5.167539       2.371969e-07
3             mu_no_car  1.523716  ...           5.342280       9.178487e-08
4                asc_sm  0.063000  ...           0.284539       7.759970e-01
5        beta_dist_male -0.687677  ...          -3.925931       8.639485e-05
6      beta_dist_female -0.832153  ...          -3.929043       8.528455e-05
7  beta_dist_unreported -0.704737  ...          -2.987710       2.810762e-03
8               asc_car  0.258607  ...           2.881082       3.963129e-03

[9 rows x 5 columns]

Simulation

simulated_choices = get_value_c(
    expression=log_probability,
    betas=results.get_beta_values(),
    database=database,
    numerically_safe=False,
    use_jit=True,
)
display(simulated_choices)
Bootstraps:   0%|          | 0/100 [16:49<?, ?it/s]
[-0.65553681 -1.42271161 -0.13348367 ... -0.37368096 -0.29814134
 -0.27030377]
loglikelihood = get_value_c(
    expression=log_probability,
    betas=results.get_beta_values(),
    database=database,
    aggregation=True,
    numerically_safe=False,
    use_jit=True,
)
print(f'Final log likelihood:     {results.final_log_likelihood}')
print(f'Simulated log likelihood: {loglikelihood}')
Final log likelihood:     -1295.1202531057634
Simulated log likelihood: -1295.1202531057634

Total running time of the script: (16 minutes 53.085 seconds)

Gallery generated by Sphinx-Gallery