dcmbench 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. dcmbench/__init__.py +25 -0
  2. dcmbench/adapters/__init__.py +107 -0
  3. dcmbench/adapters/biogeme_adapter.py +487 -0
  4. dcmbench/adapters/biogeme_vot_adapter.py +385 -0
  5. dcmbench/adapters/enhanced_biogeme_adapter.py +218 -0
  6. dcmbench/adapters/factory.py +678 -0
  7. dcmbench/adapters/pytorch_adapter.py +514 -0
  8. dcmbench/adapters/pytorch_model.py +770 -0
  9. dcmbench/adapters/sklearn_adapter.py +554 -0
  10. dcmbench/adapters/sklearn_model.py +606 -0
  11. dcmbench/adapters/universal_biogeme_adapter.py +1324 -0
  12. dcmbench/adapters/universal_biogeme_adapter_backup.py +894 -0
  13. dcmbench/datasets/__init__.py +64 -0
  14. dcmbench/datasets/dataset_loader.py +387 -0
  15. dcmbench/datasets/metadata.json +29 -0
  16. dcmbench/datasets/simple_fetcher.py +84 -0
  17. dcmbench/model_benchmarker/__init__.py +7 -0
  18. dcmbench/model_benchmarker/benchmarker.py +403 -0
  19. dcmbench/model_benchmarker/model_interface.py +124 -0
  20. dcmbench/model_specifications.py +559 -0
  21. dcmbench/utils/__init__.py +91 -0
  22. dcmbench/utils/biogeme_wrapper.py +245 -0
  23. dcmbench/utils/individual_parameters.py +488 -0
  24. dcmbench/utils/metrics.py +482 -0
  25. dcmbench/utils/model_extensions.py +359 -0
  26. dcmbench/utils/policy_analysis.py +845 -0
  27. dcmbench/utils/preprocessing.py +318 -0
  28. dcmbench/utils/sensitivity_analysis.py +873 -0
  29. dcmbench/utils/vot_analysis.py +799 -0
  30. dcmbench/utils/vot_systematic.py +271 -0
  31. dcmbench/utils/vot_visualization.py +565 -0
  32. dcmbench-0.1.2.dist-info/METADATA +196 -0
  33. dcmbench-0.1.2.dist-info/RECORD +36 -0
  34. dcmbench-0.1.2.dist-info/WHEEL +5 -0
  35. dcmbench-0.1.2.dist-info/licenses/LICENSE +21 -0
  36. dcmbench-0.1.2.dist-info/top_level.txt +1 -0
dcmbench/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ # dcmbench/__init__.py
2
+
3
+ from .datasets import DatasetLoader
4
+ from . import datasets
5
+ from . import model_benchmarker
6
+ from .model_specifications import (
7
+ fetch_model_spec,
8
+ build_model_from_spec,
9
+ model_to_spec,
10
+ list_available_specs,
11
+ validate_model_spec
12
+ )
13
+ # Do not import models to avoid circular imports
14
+ # from . import models
15
+
16
+ __all__ = [
17
+ 'DatasetLoader',
18
+ 'datasets',
19
+ 'model_benchmarker',
20
+ 'fetch_model_spec',
21
+ 'build_model_from_spec',
22
+ 'model_to_spec',
23
+ 'list_available_specs',
24
+ 'validate_model_spec'
25
+ ]
@@ -0,0 +1,107 @@
1
+ """
2
+ DCMBench Adapters Module
3
+
4
+ This module provides adapter classes that make models from different frameworks
5
+ compatible with DCMBench benchmarking by implementing the required prediction interface.
6
+ Adapters allow users to create models using their preferred framework and then
7
+ use them with DCMBench without writing boilerplate prediction code.
8
+ """
9
+
10
+ import logging
11
+
12
+ # Set up logging
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Import base interfaces
16
+ from ..model_benchmarker.model_interface import PredictionInterface
17
+
18
+
19
+ # Register Biogeme adapters - always available
20
+ from .biogeme_adapter import (
21
+ BiogemeModelAdapter,
22
+ BiogemeMultinomialLogitAdapter,
23
+ BiogemeNestedLogitAdapter,
24
+ BiogemeModelFromSpecAdapter
25
+ )
26
+
27
+
28
+ # Import enhanced adapters
29
+ try:
30
+ from .enhanced_biogeme_adapter import EnhancedBiogemeAdapter
31
+ from .biogeme_vot_adapter import BiogemeVOTAdapter
32
+ _has_enhanced_adapters = True
33
+ except ImportError:
34
+ logger.info("Enhanced adapters not available")
35
+ _has_enhanced_adapters = False
36
+
37
+ # Biogeme adapters - always available
38
+ logger.info("Biogeme adapters available")
39
+
40
+ # PyTorch adapters if available
41
+ try:
42
+ from .pytorch_adapter import (
43
+ PyTorchModelAdapter,
44
+ PyTorchMultinomialLogitAdapter
45
+ )
46
+ _has_pytorch = True
47
+ logger.info("PyTorch adapters available")
48
+ except ImportError:
49
+ logger.info("PyTorch not available, skipping PyTorch adapters")
50
+ _has_pytorch = False
51
+
52
+ # scikit-learn adapters if available
53
+ try:
54
+ from .sklearn_adapter import (
55
+ SklearnModelAdapter,
56
+ LogisticRegressionAdapter
57
+ )
58
+ _has_sklearn = True
59
+ logger.info("scikit-learn adapters available")
60
+ except ImportError:
61
+ logger.info("scikit-learn not available, skipping scikit-learn adapters")
62
+ _has_sklearn = False
63
+
64
+
65
+ # Define __all__ with essential components
66
+ __all__ = [
67
+ # Base interfaces
68
+ 'PredictionInterface',
69
+
70
+ # Factory function
71
+ 'create_model_adapter',
72
+
73
+ # Biogeme adapters - always available
74
+ 'BiogemeModelAdapter',
75
+ 'BiogemeMultinomialLogitAdapter',
76
+ 'BiogemeNestedLogitAdapter',
77
+ 'BiogemeModelFromSpecAdapter',
78
+
79
+ ]
80
+
81
+ # Add enhanced adapters to __all__ if available
82
+ if _has_enhanced_adapters:
83
+ __all__.extend([
84
+ 'EnhancedBiogemeAdapter',
85
+ 'BiogemeVOTAdapter'
86
+ ])
87
+
88
+ # Add PyTorch adapters to __all__ if available
89
+ if _has_pytorch:
90
+ __all__.extend([
91
+ 'PyTorchModelAdapter',
92
+ 'PyTorchMultinomialLogitAdapter'
93
+ ])
94
+
95
+ # Add scikit-learn adapters to __all__ if available
96
+ if _has_sklearn:
97
+ __all__.extend([
98
+ 'SklearnModelAdapter',
99
+ 'LogisticRegressionAdapter'
100
+ ])
101
+
102
+ # Log available frameworks
103
+ if _has_pytorch:
104
+ logger.debug("PyTorch adapters available")
105
+ if _has_sklearn:
106
+ logger.debug("scikit-learn adapters available")
107
+ logger.debug("Biogeme adapters available")
@@ -0,0 +1,487 @@
1
+ """
2
+ BiogemeModelAdapter module for DCMBench.
3
+
4
+ This module provides adapter classes that make Biogeme models compatible with DCMBench
5
+ benchmarking by implementing the required prediction interface. It allows users to
6
+ create models using standard Biogeme syntax and then use them with DCMBench without
7
+ writing boilerplate prediction code.
8
+ """
9
+
10
+ import pandas as pd
11
+ import numpy as np
12
+ from typing import Dict, Union, Optional, Any, List, Tuple
13
+
14
+ import biogeme.database as db
15
+ import biogeme.biogeme as bio
16
+ from biogeme import models
17
+ from biogeme.expressions import Expression
18
+
19
+ from ..model_benchmarker.model_interface import PredictionInterface
20
+
21
+
22
+ class BiogemeModelAdapter:
23
+ """
24
+ Adapter class that makes Biogeme models compatible with DCMBench benchmarking.
25
+
26
+ This class takes an estimated Biogeme model along with its utility functions
27
+ and availability conditions, and implements the prediction interface required
28
+ by the DCMBench benchmarker. This allows users to create models using standard
29
+ Biogeme syntax and then use them with DCMBench without writing boilerplate code.
30
+
31
+ Parameters
32
+ ----------
33
+ biogeme_model : bio.BIOGEME
34
+ The Biogeme model object
35
+ utility_functions : Dict[Union[str, int], Expression]
36
+ Dictionary mapping alternative IDs to utility expressions
37
+ availability : Dict[Union[str, int], Expression]
38
+ Dictionary mapping alternative IDs to availability expressions
39
+ nests : Optional[Any], default=None
40
+ Nesting structure for nested logit models
41
+ results : Optional[Any], default=None
42
+ Pre-estimated model results. If None, estimation will be performed
43
+ name : Optional[str], default=None
44
+ Custom name for the model
45
+ choice_column : str, default="CHOICE"
46
+ Name of the column containing the chosen alternatives
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ biogeme_model: bio.BIOGEME,
52
+ utility_functions: Dict[Union[str, int], Expression],
53
+ availability: Dict[Union[str, int], Expression],
54
+ nests: Optional[Any] = None,
55
+ results: Optional[Any] = None,
56
+ name: Optional[str] = None,
57
+ choice_column: str = "CHOICE"
58
+ ):
59
+ self.biogeme_model = biogeme_model
60
+ self.utility_functions = utility_functions
61
+ self.availability = availability
62
+ self.nests = nests
63
+ self.choice_column = choice_column
64
+ self.name = name or "BiogemeModel"
65
+
66
+ # Store the database for later use
67
+ self.database = biogeme_model.database
68
+ self.data_raw = self.database.data.copy() if hasattr(self.database, 'data') else None
69
+
70
+ # Get or estimate results
71
+ if results is None:
72
+ self.results = self.biogeme_model.estimate()
73
+ else:
74
+ self.results = results
75
+
76
+ # Extract model statistics
77
+ self._extract_model_statistics()
78
+
79
+ def _extract_model_statistics(self):
80
+ """Extract key statistics from Biogeme results."""
81
+ if self.results is None:
82
+ return
83
+
84
+ try:
85
+ stats = self.results.getGeneralStatistics()
86
+ self.final_ll = stats['Final log likelihood'][0]
87
+ self.null_ll = stats['Null log likelihood'][0]
88
+ self.rho_squared = stats['Rho-square for the null model'][0]
89
+ self.rho_squared_bar = stats['Rho-square-bar for the null model'][0]
90
+ self.n_parameters = len(self.results.getBetaValues())
91
+ except (KeyError, AttributeError) as e:
92
+ # Handle the case where some statistics may not be available
93
+ self.final_ll = None
94
+ self.null_ll = None
95
+ self.rho_squared = None
96
+ self.rho_squared_bar = None
97
+ self.n_parameters = None
98
+
99
+ def predict_probabilities(self, data: pd.DataFrame) -> pd.DataFrame:
100
+ """
101
+ Predict choice probabilities for each alternative.
102
+
103
+ Parameters
104
+ ----------
105
+ data : pd.DataFrame
106
+ Input data for prediction
107
+
108
+ Returns
109
+ -------
110
+ pd.DataFrame
111
+ DataFrame with columns for each alternative and rows for each observation,
112
+ containing the predicted probability of each alternative for each observation.
113
+ Column names match the alternative identifiers.
114
+ """
115
+ if self.results is None:
116
+ raise RuntimeError("Model must be estimated before predicting")
117
+
118
+ # Determine if we need to create a new database for prediction
119
+ is_same_data = False
120
+ if self.data_raw is not None:
121
+ # Check if data is the same as what was used for estimation
122
+ if data is self.data_raw or data.equals(self.data_raw):
123
+ is_same_data = True
124
+
125
+ # Create choice column with integer type if needed
126
+ if self.choice_column in data.columns and data[self.choice_column].dtype == object:
127
+ data_copy = data.copy()
128
+ data_copy[self.choice_column] = data_copy[self.choice_column].astype(int)
129
+ else:
130
+ data_copy = data
131
+
132
+ # Create a new database if needed
133
+ if not is_same_data:
134
+ prediction_db = db.Database('prediction', data_copy)
135
+ else:
136
+ prediction_db = self.database
137
+
138
+ # Get estimated parameters
139
+ betas = self.results.getBetaValues()
140
+
141
+ # Prepare simulation expressions
142
+ simulate = {}
143
+ alternatives = list(self.utility_functions.keys())
144
+
145
+ # Calculate probabilities for each alternative
146
+ for i, alt in enumerate(alternatives):
147
+ if self.nests is None:
148
+ # MNL probabilities
149
+ prob_expr = models.logit(self.utility_functions, self.availability, alt)
150
+ else:
151
+ # NL probabilities
152
+ prob_expr = models.nested(self.utility_functions, self.availability, self.nests, alt)
153
+
154
+ # Add to simulation dictionary
155
+ simulate[f'Prob_{alt}'] = prob_expr
156
+
157
+ # Setup and run simulation
158
+ biogeme = bio.BIOGEME(prediction_db, simulate)
159
+ biogeme.modelName = "prediction"
160
+ simulatedValues = biogeme.simulate(betas)
161
+
162
+ # Convert to DataFrame with the right column names
163
+ result = pd.DataFrame()
164
+ for alt in alternatives:
165
+ alt_str = str(alt)
166
+ result[alt_str] = simulatedValues[f'Prob_{alt}']
167
+
168
+ return result
169
+
170
+ def predict_choices(self, data: pd.DataFrame) -> pd.Series:
171
+ """
172
+ Predict the most likely choice for each observation.
173
+
174
+ Parameters
175
+ ----------
176
+ data : pd.DataFrame
177
+ Input data for prediction
178
+
179
+ Returns
180
+ -------
181
+ pd.Series
182
+ Series containing the predicted choice for each observation.
183
+ Values are the alternative identifiers.
184
+ """
185
+ # Get probabilities and select most likely alternative
186
+ probs = self.predict_probabilities(data)
187
+
188
+ # Make sure we return choices as strings
189
+ return probs.idxmax(axis=1)
190
+
191
+ def calculate_choice_accuracy(self, data: Optional[pd.DataFrame] = None) -> Dict[str, float]:
192
+ """
193
+ Calculate choice prediction accuracy and market shares.
194
+
195
+ Parameters
196
+ ----------
197
+ data : pd.DataFrame, optional
198
+ Data to calculate metrics for. If None, uses the data from initialization.
199
+
200
+ Returns
201
+ -------
202
+ Dict[str, float]
203
+ Dictionary with accuracy metrics including choice_accuracy and market_share_accuracy
204
+ """
205
+ # Use provided data or fall back to original data
206
+ if data is None:
207
+ if self.data_raw is None:
208
+ raise ValueError("No data available for accuracy calculation")
209
+ data = self.data_raw
210
+
211
+ # Get choice column
212
+ if self.choice_column not in data.columns:
213
+ raise ValueError(f"Choice column '{self.choice_column}' not found in data")
214
+
215
+ # Get actual choices
216
+ actual_choices = data[self.choice_column]
217
+
218
+ # Calculate predicted choices
219
+ predicted_choices = self.predict_choices(data)
220
+ if isinstance(predicted_choices, pd.Series):
221
+ # Convert string indices to integers if needed
222
+ predicted_choices = predicted_choices.astype(int)
223
+
224
+ # Calculate choice accuracy
225
+ choice_accuracy = (actual_choices == predicted_choices).mean()
226
+
227
+ # Calculate market shares
228
+ actual_shares = actual_choices.value_counts(normalize=True).to_dict()
229
+
230
+ # Calculate predicted market shares
231
+ probs = self.predict_probabilities(data)
232
+ predicted_shares = {int(col): probs[col].mean() for col in probs.columns}
233
+
234
+ # Calculate market share accuracy
235
+ total_abs_error = sum(
236
+ abs(actual_shares.get(int(alt), 0) - predicted_shares.get(int(alt), 0))
237
+ for alt in set(actual_shares.keys()) | set(predicted_shares.keys())
238
+ )
239
+ market_share_accuracy = 1 - (total_abs_error / 2) # Divide by 2 to normalize
240
+
241
+ # Store for later use
242
+ self.choice_accuracy = choice_accuracy
243
+ self.market_share_accuracy = market_share_accuracy
244
+ self.actual_shares = actual_shares
245
+ self.predicted_shares = predicted_shares
246
+
247
+ # Calculate Value of Time (VOT) if possible
248
+ vot = None
249
+ if self.results is not None:
250
+ try:
251
+ params = self.results.get_beta_values()
252
+ # Look for standard time and cost parameters
253
+ time_params = [k for k in params.keys() if 'TIME' in k.upper() or 'TT' in k.upper()]
254
+ cost_params = [k for k in params.keys() if 'COST' in k.upper() or 'CO' in k.upper()]
255
+
256
+ # Try to find B_TIME and B_COST specifically
257
+ b_time = None
258
+ b_cost = None
259
+
260
+ if 'B_TIME' in params:
261
+ b_time = params['B_TIME']
262
+ elif time_params:
263
+ b_time = params[time_params[0]]
264
+
265
+ if 'B_COST' in params:
266
+ b_cost = params['B_COST']
267
+ elif cost_params:
268
+ b_cost = params[cost_params[0]]
269
+
270
+ if b_time is not None and b_cost is not None and b_cost != 0:
271
+ # VOT = (B_TIME / B_COST) * 60
272
+ vot = (b_time / b_cost) * 60
273
+ self.value_of_time = {
274
+ 'value': vot,
275
+ 'time_coefficient': b_time,
276
+ 'cost_coefficient': b_cost,
277
+ 'units': 'CHF/h'
278
+ }
279
+ except Exception as e:
280
+ # If VOT calculation fails, we continue without it
281
+ pass
282
+
283
+ result = {
284
+ 'choice_accuracy': choice_accuracy,
285
+ 'market_share_accuracy': market_share_accuracy,
286
+ 'actual_shares' : actual_shares,
287
+ 'predicted_shares' : predicted_shares
288
+ }
289
+
290
+ if hasattr(self, 'value_of_time'):
291
+ result['value_of_time'] = self.value_of_time
292
+
293
+ return result
294
+
295
+ def get_metrics(self) -> Dict[str, Any]:
296
+ """
297
+ Return model metrics for benchmarking.
298
+
299
+ Returns
300
+ -------
301
+ Dict[str, Any]
302
+ Dictionary of model metrics including log-likelihood, rho-squared values,
303
+ parameter counts, and market shares if available.
304
+ """
305
+ if self.results is None:
306
+ raise RuntimeError("Model must be estimated before getting metrics")
307
+
308
+ metrics = {
309
+ 'n_parameters': self.n_parameters,
310
+ 'final_ll': self.final_ll,
311
+ 'null_ll': self.null_ll,
312
+ 'rho_squared': self.rho_squared,
313
+ 'rho_squared_bar': self.rho_squared_bar
314
+ }
315
+
316
+ # Include accuracy metrics if available
317
+ if hasattr(self, 'choice_accuracy'):
318
+ metrics['choice_accuracy'] = self.choice_accuracy
319
+
320
+ if hasattr(self, 'market_share_accuracy'):
321
+ metrics['market_share_accuracy'] = self.market_share_accuracy
322
+
323
+ if hasattr(self, 'actual_shares'):
324
+ metrics['actual_shares'] = self.actual_shares
325
+
326
+ if hasattr(self, 'predicted_shares'):
327
+ metrics['predicted_shares'] = self.predicted_shares
328
+
329
+ # Include Value of Time if available
330
+ if hasattr(self, 'value_of_time'):
331
+ metrics['value_of_time'] = self.value_of_time['value']
332
+ metrics['vot_details'] = self.value_of_time
333
+
334
+ # Calculate market shares if not already calculated
335
+ if not hasattr(self, 'actual_shares') and self.data_raw is not None:
336
+ try:
337
+ accuracy_metrics = self.calculate_choice_accuracy()
338
+ metrics.update(accuracy_metrics)
339
+ except Exception as e:
340
+ # If calculation fails, continue without these metrics
341
+ pass
342
+
343
+ return metrics
344
+
345
+ def get_parameter_values(self) -> Dict[str, float]:
346
+ """
347
+ Get the estimated parameter values.
348
+
349
+ Returns
350
+ -------
351
+ Dict[str, float]
352
+ Dictionary of parameter names and values
353
+ """
354
+ if self.results is None:
355
+ raise RuntimeError("Model must be estimated before getting parameter values")
356
+
357
+ return self.results.getBetaValues()
358
+
359
+ def get_model_name(self) -> str:
360
+ """
361
+ Get the model name.
362
+
363
+ Returns
364
+ -------
365
+ str
366
+ Model name
367
+ """
368
+ return self.name
369
+
370
+
371
+ class BiogemeMultinomialLogitAdapter(BiogemeModelAdapter):
372
+ """
373
+ Adapter specifically for Multinomial Logit models.
374
+
375
+ This is a convenience class that sets nests=None to ensure MNL probabilities.
376
+
377
+ Parameters are the same as BiogemeModelAdapter.
378
+ """
379
+
380
+ def __init__(
381
+ self,
382
+ biogeme_model: bio.BIOGEME,
383
+ utility_functions: Dict[Union[str, int], Expression],
384
+ availability: Dict[Union[str, int], Expression],
385
+ results: Optional[Any] = None,
386
+ name: Optional[str] = None,
387
+ choice_column: str = "CHOICE"
388
+ ):
389
+ super().__init__(
390
+ biogeme_model=biogeme_model,
391
+ utility_functions=utility_functions,
392
+ availability=availability,
393
+ nests=None, # Explicitly set to None for MNL
394
+ results=results,
395
+ name=name or "BiogemeMultinomialLogit",
396
+ choice_column=choice_column
397
+ )
398
+
399
+
400
+ class BiogemeNestedLogitAdapter(BiogemeModelAdapter):
401
+ """
402
+ Adapter specifically for Nested Logit models.
403
+
404
+ This class requires a nesting structure and ensures NL probabilities are used.
405
+
406
+ Parameters are the same as BiogemeModelAdapter with nests required.
407
+ """
408
+
409
+ def __init__(
410
+ self,
411
+ biogeme_model: bio.BIOGEME,
412
+ utility_functions: Dict[Union[str, int], Expression],
413
+ availability: Dict[Union[str, int], Expression],
414
+ nests: Any, # Required for NL
415
+ results: Optional[Any] = None,
416
+ name: Optional[str] = None,
417
+ choice_column: str = "CHOICE"
418
+ ):
419
+ if nests is None:
420
+ raise ValueError("Nesting structure is required for Nested Logit models")
421
+
422
+ super().__init__(
423
+ biogeme_model=biogeme_model,
424
+ utility_functions=utility_functions,
425
+ availability=availability,
426
+ nests=nests,
427
+ results=results,
428
+ name=name or "BiogemeNestedLogit",
429
+ choice_column=choice_column
430
+ )
431
+
432
+
433
+ class BiogemeModelFromSpecAdapter(BiogemeModelAdapter):
434
+ """
435
+ Adapter for models created from model specifications.
436
+
437
+ This class extends BiogemeModelAdapter to work specifically with models
438
+ generated from JSON specifications through the ModelSpecLoader.
439
+
440
+ Parameters
441
+ ----------
442
+ model : Any
443
+ The model object created from a specification
444
+ spec : Dict[str, Any]
445
+ The model specification data
446
+ """
447
+
448
+ def __init__(
449
+ self,
450
+ model: Any,
451
+ spec: Dict[str, Any]
452
+ ):
453
+ # Extract relevant components from the specification
454
+ utility_functions = getattr(model, 'formulas', {})
455
+ availability = getattr(model, 'availability_conditions', {})
456
+ nests = getattr(model, 'nests', None) if hasattr(model, 'nests') else None
457
+ results = getattr(model, 'results', None)
458
+ choice_column = spec.get('choice_column', 'CHOICE')
459
+ name = spec.get('name', 'SpecBasedModel')
460
+
461
+ # Get the Biogeme model if available
462
+ biogeme_model = getattr(model, 'biogeme_instance', None)
463
+
464
+ # If no Biogeme model instance is available, create one
465
+ if biogeme_model is None:
466
+ # Try to get the database
467
+ database = getattr(model, 'database', None)
468
+ if database is None:
469
+ raise ValueError("Model must have a database attribute")
470
+
471
+ # Create a dummy Biogeme model just for the adapter
472
+ biogeme_model = bio.BIOGEME(database, None)
473
+
474
+ # Initialize the parent class
475
+ super().__init__(
476
+ biogeme_model=biogeme_model,
477
+ utility_functions=utility_functions,
478
+ availability=availability,
479
+ nests=nests,
480
+ results=results,
481
+ name=name,
482
+ choice_column=choice_column
483
+ )
484
+
485
+ # Store the original model and specification
486
+ self.original_model = model
487
+ self.spec = spec