File generalized_forecasting.py

Michel Bierlaire, EPFL Fri Jul 25 2025, 17:05:32

Forecasting with a MDCEV model and the “generalized translated utility” specification.

Example: generalized translated utility
Forecasting observation 0 / 2 [10 draws]
============ Comparison ===================
Brute force: {1: '29.4', 2: '426', 3: '34.2', 4: '10.9'} objective 180, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '29.4', 2: '426', 3: '34', 4: '10.9'} objective 180, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '36.2', 2: '407', 3: '42.6', 4: '14.1'} objective 180, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '36.2', 2: '407', 3: '42.6', 4: '14.1'} objective 180, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '14.1', 2: '427', 3: '43.6', 4: '15.1'} objective 199, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '14.1', 2: '427', 3: '44', 4: '15'} objective 199, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '16.7', 2: '437', 3: '33', 4: '13.7'} objective 183, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '16.7', 2: '437', 3: '33', 4: '13.7'} objective 183, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '34.9', 2: '429', 3: '26.3', 4: '10.1'} objective 169, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '35', 2: '429', 3: '26.3', 4: '10.1'} objective 169, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '16.5', 2: '406', 3: '57.8', 4: '20.2'} objective 195, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '16.4', 2: '406', 3: '57.7', 4: '20.2'} objective 195, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '10.6', 2: '452', 3: '30.4', 4: '7.34'} objective 221, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '10.6', 2: '452', 3: '30.4', 4: '7.34'} objective 221, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '8.84', 2: '436', 3: '47.8', 4: '7.77'} objective 239, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '8.84', 2: '436', 3: '47.8', 4: '7.76'} objective 239, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '21.2', 2: '432', 3: '35', 4: '12.1'} objective 171, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '21.2', 2: '432', 3: '35', 4: '12.2'} objective 171, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '34.5', 2: '378', 3: '76.7', 4: '10.4'} objective 200, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '34.6', 2: '378', 3: '77.3', 4: '10.4'} objective 200, constraint 500, choice set {1, 2, 3, 4}
Forecasting observation 1 / 2 [10 draws]
============ Comparison ===================
Brute force: {1: '8.17', 2: '455', 3: '31.5', 4: '5.67'} objective 244, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '8.09', 2: '455', 3: '31.7', 4: '5.66'} objective 244, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '24.3', 2: '401', 3: '61.1', 4: '13.9'} objective 177, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '24.3', 2: '401', 3: '61.1', 4: '13.9'} objective 177, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '37.5', 2: '411', 3: '42.1', 4: '9.52'} objective 210, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '37.5', 2: '411', 3: '42.1', 4: '9.5'} objective 210, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '12.1', 2: '443', 3: '33.7', 4: '10.8'} objective 206, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '12.1', 2: '443', 3: '33.7', 4: '10.8'} objective 206, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '26.2', 2: '415', 3: '46.4', 4: '12.3'} objective 176, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '26.2', 2: '415', 3: '46.4', 4: '12.3'} objective 176, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '51.5', 2: '378', 3: '60.6', 4: '9.91'} objective 195, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '51.4', 2: '378', 3: '60.6', 4: '9.9'} objective 195, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '9.94', 2: '452', 3: '32.5', 4: '5.42'} objective 248, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '9.94', 2: '452', 3: '32.5', 4: '5.42'} objective 248, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '19.9', 2: '450', 3: '16.3', 4: '14.1'} objective 273, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '19.9', 2: '450', 3: '16.3', 4: '14.1'} objective 273, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '26.4', 2: '423', 3: '45', 4: '5.76'} objective 210, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '26.4', 2: '423', 3: '44.8', 4: '5.77'} objective 210, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '31.7', 2: '353', 3: '107', 4: '8.95'} objective 237, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '31.6', 2: '353', 3: '107', 4: '9.06'} objective 237, constraint 500, choice set {1, 2, 3, 4}
Forecasting observation 0 / 2 [2000 draws]
Forecasting observation 1 / 2 [2000 draws]
Execution time for 2000 draws with brute force algorithm: 20.5 seconds
Forecasting observation 0 / 2 [2000 draws]
Forecasting observation 1 / 2 [2000 draws]
Execution time for 2000 draws with analytical algorithm: 0.694 seconds
                  1            2            3            4
count  2.000000e+03  2000.000000  2000.000000  2000.000000
mean   2.439792e+01   426.929193    35.366062    13.306824
std    2.538291e+01    35.357368    16.587736     7.251341
min    3.891492e-16    81.458056     0.000000     1.295123
25%    1.171063e+01   410.720424    24.893909     8.972792
50%    1.841752e+01   430.479544    33.310344    11.952136
75%    2.867985e+01   449.590603    43.615841    15.749595
max    3.735747e+02   498.346029   148.925689   107.930952
                 1            2            3            4
count  2000.000000  2000.000000  2000.000000  2000.000000
mean     24.384192   426.950104    35.359549    13.306155
std      25.317514    35.294916    16.592635     7.247057
min       0.000000    81.419828     0.000000     1.295016
25%      11.713627   410.828540    24.889764     8.973056
50%      18.428000   430.437278    33.315996    11.929500
75%      28.729678   449.570674    43.571832    15.739476
max     373.612178   498.346598   148.858217   107.952013
                  1            2            3            4
count  2.000000e+03  2000.000000  2000.000000  2000.000000
mean   2.751289e+01   418.312799    44.009956    10.164356
std    2.328927e+01    37.856372    21.457813     5.170982
min    7.601052e-16   110.756965     0.000000     0.000000
25%    1.347040e+01   398.061316    30.703689     6.847331
50%    2.173273e+01   422.534456    41.124187     9.315500
75%    3.405653e+01   444.519786    52.669373    12.297088
max    3.559648e+02   499.719068   256.407717    45.326247
                 1            2            3            4
count  2000.000000  2000.000000  2000.000000  2000.000000
mean     27.508135   418.335139    43.992754    10.163971
std      23.260041    37.821507    21.433937     5.170667
min       0.000000   110.851464     0.000000     0.000000
25%      13.483257   398.308127    30.761719     6.846872
50%      21.733449   422.483878    41.136792     9.310790
75%      34.060759   444.571312    52.697874    12.294282
max     355.797541   499.718958   254.564340    45.334397

import sys
import time

import numpy as np
import pandas as pd
from IPython.core.display_functions import display

import biogeme.biogeme_logging as blog
from biogeme.database import Database
from biogeme.results_processing import EstimationResults
from generalized_specification import the_generalized
from process_data import database

logger = blog.get_screen_logger(level=blog.INFO)
logger.info('Example: generalized translated utility')

result_file = 'saved_results/generalized.yaml'
try:
    results = EstimationResults.from_yaml_file(filename=result_file)
except FileNotFoundError as e:
    print(e)
    print(f'File {result_file} is missing.')
    sys.exit()

the_generalized.estimation_results = results

# %
# We apply the model only on the first two rows of the database.
two_rows_of_database: Database = database.extract_rows([0, 1])

# %
budget_in_hours = 500

# %
# # Validation

# %
# As the implementation is still experimental, we compare the result obtained by the bruteforce algorithm and
# the analytical algorithm for a few draws.

# Note that minor discrepancies between the outcome of the two algorithms are likely to occur, due to numerical
# imprecision, inevitable in finite arithmetic.

# However, if there are major differences, it should be reported.

# %
number_of_draws = 10

# %
# We generate the draws
epsilons = [
    np.random.gumbel(
        loc=0, scale=1, size=(number_of_draws, the_generalized.number_of_alternatives)
    )
    for _ in range(two_rows_of_database.num_rows())
]

# %
# We first compare the results obtained from the brute force and the analytical algorithms, for each draw.
the_generalized.validate_forecast(
    database=two_rows_of_database, total_budget=budget_in_hours, epsilons=epsilons
)

# %
# # Forecasting
# We use a larger number of draws to obtain the forecast.

# %
number_of_draws = 2_000

# %
# We generate the draws
epsilons = [
    np.random.gumbel(
        loc=0, scale=1, size=(number_of_draws, the_generalized.number_of_alternatives)
    )
    for _ in range(two_rows_of_database.num_rows())
]

# %
# First, the brute force algorithm.
start_time = time.time()
optimal_consumptions_brute_force: list[pd.DataFrame] = the_generalized.forecast(
    database=two_rows_of_database,
    total_budget=budget_in_hours,
    epsilons=epsilons,
    brute_force=True,
)
end_time = time.time()

# %
print(
    f'Execution time for {number_of_draws} draws with brute force algorithm: {end_time-start_time:.3g} seconds'
)

# %
# Then, the analytical algorithm.
start_time = time.time()
optimal_consumptions_analytical: list[pd.DataFrame] = the_generalized.forecast(
    database=two_rows_of_database,
    total_budget=budget_in_hours,
    epsilons=epsilons,
    brute_force=False,
)
end_time = time.time()

# %
print(
    f'Execution time for {number_of_draws} draws with analytical algorithm: {end_time-start_time:.3g} seconds'
)

# %
# Results for the first observation, brute force method
display(optimal_consumptions_brute_force[0].describe())

# %
# Results for the first observation, analytical method
display(optimal_consumptions_analytical[0].describe())

# %
# Results for the second observation, brute force method
display(optimal_consumptions_brute_force[1].describe())

# %
# Results for the second observation, analytical method
display(optimal_consumptions_analytical[1].describe())

Total running time of the script: (0 minutes 22.622 seconds)

Gallery generated by Sphinx-Gallery