File translated_forecasting.py

Michel Bierlaire, EPFL Fri Jul 25 2025, 17:34:35

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

Example: translated utility
Forecasting observation 0 / 2 [10 draws]
============ Comparison ===================
Brute force: {1: '10.8', 2: '461', 3: '18.9', 4: '8.99'} objective 57, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '10.8', 2: '461', 3: '18.9', 4: '8.99'} objective 57, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '18.7', 2: '409', 3: '64.9', 4: '7.83'} objective 59.5, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '18.7', 2: '409', 3: '64.9', 4: '7.83'} objective 59.5, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '19', 2: '280', 3: '188', 4: '12.5'} objective 49.9, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '19', 2: '281', 3: '188', 4: '12.5'} objective 49.9, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '21.6', 2: '378', 3: '89.8', 4: '10.8'} objective 46.9, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '21.5', 2: '378', 3: '89.8', 4: '10.8'} objective 46.9, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '19.3', 2: '392', 3: '74.8', 4: '13.9'} objective 50.2, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '19.3', 2: '392', 3: '74.9', 4: '13.9'} objective 50.2, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '6.32', 2: '464', 3: '23.4', 4: '6.64'} objective 61.2, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '6.33', 2: '464', 3: '23.4', 4: '6.63'} objective 61.2, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '22.8', 2: '387', 3: '68.4', 4: '22.1'} objective 53.1, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '22.8', 2: '387', 3: '68.4', 4: '22.1'} objective 53.1, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '17.2', 2: '357', 3: '112', 4: '13.7'} objective 42.6, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '17.2', 2: '357', 3: '112', 4: '13.7'} objective 42.6, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '30.1', 2: '386', 3: '69.7', 4: '13.7'} objective 60.8, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '30.1', 2: '386', 3: '69.7', 4: '13.7'} objective 60.8, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '21.1', 2: '340', 3: '129', 4: '10.1'} objective 45.4, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '21.1', 2: '340', 3: '129', 4: '10.1'} objective 45.4, constraint 500, choice set {1, 2, 3, 4}
Forecasting observation 1 / 2 [10 draws]
============ Comparison ===================
Brute force: {1: '15', 2: '396', 3: '80.6', 4: '8.81'} objective 55.6, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '15', 2: '396', 3: '80.6', 4: '8.81'} objective 55.6, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '33.8', 2: '386', 3: '65.9', 4: '14.1'} objective 57.9, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '33.8', 2: '386', 3: '65.9', 4: '14.1'} objective 57.9, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '21.4', 2: '338', 3: '132', 4: '9.15'} objective 51.1, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '21.4', 2: '338', 3: '132', 4: '9.15'} objective 51.1, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '23', 2: '408', 3: '59.4', 4: '9.98'} objective 60.8, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '23', 2: '408', 3: '59.4', 4: '9.98'} objective 60.8, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '18.7', 2: '377', 3: '96.1', 4: '8.1'} objective 51.9, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '18.7', 2: '377', 3: '96.1', 4: '8.1'} objective 51.9, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '10.4', 2: '358', 3: '123', 4: '7.77'} objective 75, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '10.4', 2: '358', 3: '123', 4: '7.77'} objective 75, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '51', 2: '331', 3: '108', 4: '9.75'} objective 53, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '51', 2: '331', 3: '108', 4: '9.75'} objective 53, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '10', 2: '374', 3: '107', 4: '8.96'} objective 66.6, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '10', 2: '374', 3: '107', 4: '8.96'} objective 66.6, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '14.9', 2: '431', 3: '48', 4: '6.41'} objective 49.7, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '14.9', 2: '431', 3: '48', 4: '6.41'} objective 49.7, constraint 500, choice set {1, 2, 3, 4}
============ Comparison ===================
Brute force: {1: '22.5', 2: '306', 3: '162', 4: '8.79'} objective 54.1, constraint 500, choice set {1, 2, 3, 4}
Analytical:  {1: '22.5', 2: '306', 3: '162', 4: '8.8'} objective 54.1, 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: 23.7 seconds
Forecasting observation 0 / 2 [2000 draws]
Forecasting observation 1 / 2 [2000 draws]
Execution time for 2000 draws with analytical algorithm: 7.71 seconds
                 1            2            3            4
count  2000.000000  2000.000000  2000.000000  2000.000000
mean     23.856767   388.697342    74.688829    12.757062
std      20.531247    63.731952    55.735182     6.590852
min       0.000000    22.118463     0.000000     0.675360
25%      11.528252   359.322784    39.741335     8.675068
50%      18.642639   398.644268    62.150961    11.582239
75%      29.488143   432.074054    93.784235    15.143709
max     221.664145   499.324640   464.909860    82.829552
                 1            2            3            4
count  2000.000000  2000.000000  2000.000000  2000.000000
mean     23.853599   388.700106    74.688515    12.757779
std      20.521467    63.731895    55.745170     6.591058
min       0.000000    22.120534     0.000000     0.677372
25%      11.534255   359.337212    39.739389     8.673727
50%      18.639718   398.642888    62.120339    11.581488
75%      29.483047   432.064261    93.784689    15.143665
max     222.336089   499.322628   464.904336    82.828284
                 1            2            3            4
count  2000.000000  2000.000000  2000.000000  2000.000000
mean     28.037198   364.133335    98.278747     9.550721
std      27.100350    70.633492    62.732507     4.762774
min       0.000000    15.159529     0.000000     0.000000
25%      13.621909   327.506189    55.631287     6.542072
50%      21.266371   373.970464    86.753305     8.734572
75%      33.748347   412.836198   124.552781    11.426031
max     336.666716   500.000000   473.077968    58.253860
                 1            2            3            4
count  2000.000000  2000.000000  2000.000000  2000.000000
mean     28.031984   364.141193    98.275643     9.551180
std      27.074216    70.626515    62.736180     4.763061
min       0.000000    15.158661     0.000000     0.000000
25%      13.619524   327.505067    55.646424     6.546172
50%      21.269834   373.964635    86.730665     8.734749
75%      33.748929   412.837953   124.552700    11.430938
max     336.663843   500.000000   473.077960    58.254075

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 process_data import database
from translated_specification import the_translated

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

result_file = 'saved_results/translated.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_translated.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_translated.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_translated.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 = 2000

# %
# We generate the draws
epsilons = [
    np.random.gumbel(
        loc=0, scale=1, size=(number_of_draws, the_translated.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_translated.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_translated.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 32.871 seconds)

Gallery generated by Sphinx-Gallery