Source code for biogeme.segmentation

"""Class that provides some automatic specification for segmented parameters

:author: Michel Bierlaire
:date: Thu Feb  2 09:42:36 2023

"""

from biogeme.expressions import Beta, bioMultSum, Variable, Numeric
import biogeme.exceptions as excep


[docs] class DiscreteSegmentationTuple: """Characterization of a segmentation"""
[docs] def __init__(self, variable, mapping, reference=None): """Ctor :param variable: socio-economic variable used for the segmentation, or its name :type variable: biogeme.expressions.Variable or str :param mapping: maps the values of the variable with the name of a category :type mapping: dict(int: str) :param reference: name of the reference category. If None, an arbitrary category is selected as reference. :type: :type reference: str :raise BiogemeError: if the name of the reference category does not appear in the list. """ self.variable = ( variable if isinstance(variable, Variable) else Variable(variable) ) self.mapping = mapping if reference is None: self.reference = next(iter(mapping.values())) elif reference not in mapping.values(): error_msg = ( f'Reference category {reference} does not appear in the list ' f'of categories: {mapping.values()}' ) raise excep.BiogemeError(error_msg) else: self.reference = reference if self.reference is None: raise excep.BiogemeError('Reference should not be None')
def __repr__(self): result = f'{self.variable.name}: [{self.mapping}] ref: {self.reference}' return result def __str__(self): result = f'{self.variable.name}: [{self.mapping}] ref: {self.reference}' return result
[docs] class OneSegmentation: """Single segmentation of a parameter"""
[docs] def __init__(self, beta, segmentation_tuple): """Ctor :param beta: parameter to be segmented :type beta: biogeme.expressions.Beta :param segmentation_tuple: characterization of the segmentation :type segmentation_tuple: DiscreteSegmentationTuple """ self.beta = beta self.variable = segmentation_tuple.variable self.reference = segmentation_tuple.reference self.mapping = { k: v for k, v in segmentation_tuple.mapping.items() if v != self.reference }
[docs] def beta_name(self, category): """Construct the name of the parameter associated with a specific category :param category: name of the category :type category: str :return: name of parameter for the category :rtype: str :raise BiogemeError: if the category is not listed in the mapping of the segmentation. """ if category not in self.mapping.values(): error_msg = ( f'Unknown category: {category}. List of known categories: ' f'{self.mapping.values()}' ) raise excep.BiogemeError(error_msg) return f'{self.beta.name}_{category}'
[docs] def beta_expression(self, category): """Constructs the expression for the parameter associated with a specific category :param category: name of the category :type category: str :return: expression of the parameter for the category :rtype: biogeme.expressions.Beta """ name = self.beta_name(category) if category == self.reference: lower_bound = self.beta.lb upper_bound = self.beta.ub else: lower_bound = None upper_bound = None return Beta( name, self.beta.initValue, lower_bound, upper_bound, self.beta.status, )
[docs] def beta_code(self, category, assignment): """Constructs the Python code for the expression of the parameter associated with a specific category :param category: name of the category :type category: str :param assignment: if True, the code includes the assigbnment to a variable. :type assignment: bool :return: the Python code :rtype: str """ if category == self.reference: lower_bound = self.beta.lb upper_bound = self.beta.ub else: lower_bound = None upper_bound = None name = self.beta_name(category) if assignment: return ( f"{name} = Beta('{name}', {self.beta.initValue}, " f"{lower_bound}, {upper_bound}, {self.beta.status})" ) return ( f"Beta('{name}', {self.beta.initValue}, {lower_bound}, " f"{upper_bound}, {self.beta.status})" )
[docs] def list_of_expressions(self): """Create a list of expressions involved in the segmentation of the parameter :return: list of expressions :rtype: list(biogeme.expressions.Expression) """ terms = [ self.beta_expression(category) * (self.variable == Numeric(value)) for value, category in self.mapping.items() ] return terms
[docs] def list_of_code(self): """Create a list of Python codes for the expressions involved in the segmentation of the parameter :return: list of codes :rtype: list(str) """ return [ ( f"{self.beta_name(category)} " f"* (Variable('{self.variable.name}') == {value})" ) for value, category in self.mapping.items() ]
[docs] class Segmentation: """Segmentation of a parameter, possibly with multiple socio-economic variables"""
[docs] def __init__(self, beta, segmentation_tuples, prefix='segmented'): """Ctor :param beta: parameter to be segmented :type beta: biogeme.expressions.Beta :param segmentation_tuples: characterization of the segmentations :type segmentation_tuples: list(DiscreteSegmentationTuple) :param prefix: prefix to be used to generated the name of the segmented parameter :type prefix: str """ self.beta = beta self.segmentations = tuple( OneSegmentation(beta, s) for s in segmentation_tuples ) self.prefix = prefix
[docs] def beta_code(self): """Constructs the Python code for the parameter :return: Python code :rtype: str """ beta_name = f"'{self.beta.name}'" return ( f'Beta({beta_name}, {self.beta.initValue}, {self.beta.lb}, ' f'{self.beta.ub}, {self.beta.status})' )
[docs] def segmented_beta(self): """Create an expressions that combines all the segments :return: combined expression :rtype: biogeme.expressions.Expression """ ref_beta = Beta( name=self.beta.name, value=self.beta.initValue, lowerbound=self.beta.lb, upperbound=self.beta.ub, status=self.beta.status, ) terms = [ref_beta] terms += [ element for s in self.segmentations for element in s.list_of_expressions() ] return bioMultSum(terms)
[docs] def segmented_code(self): """Create the Python code for an expressions that combines all the segments :return: Python code for the combined expression :rtype: str """ result = '\n'.join( [ s.beta_code(c, assignment=True) for s in self.segmentations for c in s.mapping.values() ] ) result += '\n' terms = [self.beta_code()] terms += [element for s in self.segmentations for element in s.list_of_code()] if len(terms) == 1: result += terms[0] else: joined_terms = ', '.join(terms) result += f'{self.prefix}_{self.beta.name} = bioMultSum([{joined_terms}])' return result
[docs] def segmented_beta(beta, segmentation_tuples, prefix='segmented'): """Obtain the segmented beta from a unique function call :param beta: parameter to be segmented :type beta: biogeme.expressions.Beta :param segmentation_tuples: characterization of the segmentations :type segmentation_tuples: list(DiscreteSegmentationTuple) :param prefix: prefix to be used to generated the name of the segmented parameter :type prefix: str :return: expression of the segmented beta :rtype: biogeme.expressions.Expression """ the_segmentation = Segmentation( beta=beta, segmentation_tuples=segmentation_tuples, prefix=prefix ) return the_segmentation.segmented_beta()