"""Generation of models estimated with samples of alternatives
:author: Michel Bierlaire
:date: Fri Sep 22 12:14:59 2023
"""
import logging
import copy
from biogeme.models import loglogit
from biogeme.expressions import (
Variable,
Expression,
BelongsTo,
ConditionalTermTuple,
ConditionalSum,
exp,
log,
logzero,
)
from biogeme.nests import NestsForNestedLogit
from .sampling_context import SamplingContext, LOG_PROBA_COL, MEV_WEIGHT, CNL_PREFIX
logger = logging.getLogger(__name__)
[docs]
class GenerateModel:
"""Class in charge of generating the biogeme expression for the
loglikelihood function
"""
def __init__(self, context: SamplingContext):
"""Constructor
:param context: contains all the information that is needed to
perform the sampling of alternatives.
"""
self.context = context
self.utility_function = context.utility_function
self.total_sample_size = context.total_sample_size
self.attributes = context.attributes
self.mev_prefix = context.mev_prefix
self.utilities = {
alt_id: self.generate_utility(prefix="", suffix=f"_{alt_id}")
for alt_id in range(self.total_sample_size)
}
if self.context.second_partition is None:
self.mev_utilities = {
alt_id: self.utilities[alt_id]
for alt_id in range(1, self.total_sample_size)
}
else:
self.mev_utilities = {
alt_id: self.generate_utility(
prefix=self.mev_prefix, suffix=f"_{alt_id}"
)
for alt_id in range(self.context.second_sample_size)
}
[docs]
def generate_utility(self, prefix: str, suffix: str) -> Expression:
"""Generate the utility function for one alternative
:param prefix: prefix to add to the attributes
:param suffix: suffix to add to the attributes
"""
copy_utility = copy.deepcopy(self.utility_function)
copy_utility.rename_elementary(self.attributes, suffix=suffix, prefix=prefix)
return copy_utility
[docs]
def get_logit(self) -> Expression:
"""Returns the expression for the log likelihood of the logit model"""
corrected_utilities = {
i: utility - Variable(f"{LOG_PROBA_COL}_{i}")
for i, utility in self.utilities.items()
}
return loglogit(corrected_utilities, None, 0)
[docs]
def get_nested_logit(self, nests: NestsForNestedLogit) -> Expression:
"""Returns the expression for the log likelihood of the nested logit model
:param nests: A tuple containing as many items as nests.
Each item is also a tuple containing two items:
- an object of type biogeme.expressions.expr.Expression representing
the nest parameter,
- a list containing the list of identifiers of the alternatives
belonging to the nest.
Example::
nesta = MUA ,[1, 2, 3]
nestb = MUB ,[4, 5, 6]
nests = nesta, nestb
"""
dict_of_mev_sums = {}
# We first compute the MEV partial sum for each nest
for nest in nests:
mu_param = nest.nest_param
list_of_alternatives = nest.list_of_alternatives
self.context.check_valid_alternatives(set(list_of_alternatives))
# We first build the MEV term using a sample of
# alternatives. To build this term, we iterate from 1, as
# we ignore the chosen alternative.
list_of_terms = []
for i, utility in self.mev_utilities.items():
alternative_id = Variable(
f"{self.mev_prefix}{self.context.id_column}_{i}"
)
belong_to_nest = BelongsTo(alternative_id, set(list_of_alternatives))
weight = Variable(f"{self.mev_prefix}{MEV_WEIGHT}_{i}")
the_term = ConditionalTermTuple(
condition=belong_to_nest, term=weight * exp(mu_param * utility)
)
list_of_terms.append(the_term)
dict_of_mev_sums[tuple(list_of_alternatives)] = ConditionalSum(
list_of_terms
)
# We now add all relevant MEV terms to the utilities
dict_of_mev_terms = {}
for i, the_utility in self.utilities.items():
alternative_id = Variable(f"{self.context.id_column}_{i}")
list_of_terms = []
for nest in nests:
mu_param = nest.nest_param
mev_sum = dict_of_mev_sums[tuple(nest.list_of_alternatives)]
mev_term = (mu_param - 1.0) * the_utility + (
(1.0 / mu_param) - 1.0
) * log(mev_sum)
belong_to_nest = BelongsTo(
alternative_id, set(nest.list_of_alternatives)
)
the_term = ConditionalTermTuple(condition=belong_to_nest, term=mev_term)
list_of_terms.append(the_term)
dict_of_mev_terms[i] = ConditionalSum(list_of_terms)
corrected_utilities = {
key: util - Variable(f"{LOG_PROBA_COL}_{key}") + dict_of_mev_terms[key]
for key, util in self.utilities.items()
}
return loglogit(corrected_utilities, None, 0)
[docs]
def get_cross_nested_logit(self) -> Expression:
"""Returns the expression for the log likelihood of the nested logit model"""
nests = self.context.cnl_nests
# We compute the MEV partial sum for each nest
dict_of_mev_sums = {}
for nest in nests:
mu_param = nest.nest_param
# We first build the MEV term using a sample of
# alternatives. To build this term, we iterate from 1, as
# we ignore the chosen alternative.
list_of_terms = []
for i, utility in self.mev_utilities.items():
alpha = Variable(f"{self.mev_prefix}{CNL_PREFIX}{nest.name}_{i}")
weight = Variable(f"{self.mev_prefix}{MEV_WEIGHT}_{i}")
the_term = ConditionalTermTuple(
condition=alpha != 0.0,
term=weight * alpha**mu_param * exp(mu_param * utility),
)
list_of_terms.append(the_term)
dict_of_mev_sums[nest.name] = ConditionalSum(list_of_terms)
# We now add all relevant MEV terms to the utilities
dict_of_mev_terms = {}
for i, the_utility in self.utilities.items():
list_of_terms = []
for nest in nests:
alpha = Variable(f"{self.mev_prefix}{CNL_PREFIX}{nest.name}_{i}")
mu_param = nest.nest_param
mev_sum = dict_of_mev_sums[nest.name] ** ((1.0 / mu_param) - 1.0)
mev_term = (
alpha**mu_param * exp((mu_param - 1) * the_utility) * mev_sum
)
the_term = ConditionalTermTuple(condition=alpha != 0.0, term=mev_term)
list_of_terms.append(the_term)
dict_of_mev_terms[i] = logzero(ConditionalSum(list_of_terms))
corrected_utilities = {
key: util - Variable(f"{LOG_PROBA_COL}_{key}") + dict_of_mev_terms[key]
for key, util in self.utilities.items()
}
return loglogit(corrected_utilities, None, 0)