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.
- dcmbench/__init__.py +25 -0
- dcmbench/adapters/__init__.py +107 -0
- dcmbench/adapters/biogeme_adapter.py +487 -0
- dcmbench/adapters/biogeme_vot_adapter.py +385 -0
- dcmbench/adapters/enhanced_biogeme_adapter.py +218 -0
- dcmbench/adapters/factory.py +678 -0
- dcmbench/adapters/pytorch_adapter.py +514 -0
- dcmbench/adapters/pytorch_model.py +770 -0
- dcmbench/adapters/sklearn_adapter.py +554 -0
- dcmbench/adapters/sklearn_model.py +606 -0
- dcmbench/adapters/universal_biogeme_adapter.py +1324 -0
- dcmbench/adapters/universal_biogeme_adapter_backup.py +894 -0
- dcmbench/datasets/__init__.py +64 -0
- dcmbench/datasets/dataset_loader.py +387 -0
- dcmbench/datasets/metadata.json +29 -0
- dcmbench/datasets/simple_fetcher.py +84 -0
- dcmbench/model_benchmarker/__init__.py +7 -0
- dcmbench/model_benchmarker/benchmarker.py +403 -0
- dcmbench/model_benchmarker/model_interface.py +124 -0
- dcmbench/model_specifications.py +559 -0
- dcmbench/utils/__init__.py +91 -0
- dcmbench/utils/biogeme_wrapper.py +245 -0
- dcmbench/utils/individual_parameters.py +488 -0
- dcmbench/utils/metrics.py +482 -0
- dcmbench/utils/model_extensions.py +359 -0
- dcmbench/utils/policy_analysis.py +845 -0
- dcmbench/utils/preprocessing.py +318 -0
- dcmbench/utils/sensitivity_analysis.py +873 -0
- dcmbench/utils/vot_analysis.py +799 -0
- dcmbench/utils/vot_systematic.py +271 -0
- dcmbench/utils/vot_visualization.py +565 -0
- dcmbench-0.1.2.dist-info/METADATA +196 -0
- dcmbench-0.1.2.dist-info/RECORD +36 -0
- dcmbench-0.1.2.dist-info/WHEEL +5 -0
- dcmbench-0.1.2.dist-info/licenses/LICENSE +21 -0
- 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
|