Skip to content

mortality_distribution

MortalityDistribution

Bases: Updateable

Source code in emod_api/demographics/mortality_distribution.py
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
class MortalityDistribution(Updateable):
    def __init__(self,
                 ages_years: List[float],
                 mortality_rate_matrix: Union[List[List[float]], List[float]],
                 calendar_years: List[float] = None):
        """
        A natural mortality distribution for one gender in units of "annual death rate for an individual". If the
        distribution is time-dependent, pass in a list of times (calendar_years).

        The MortalityDistribution provides a rate (probability) at which each agent will die naturally on any given
        model day given their current age. EMOD uses double linear interpolation (bilinear) of an agent's age and the
        current calendar year to determine the exact probability of their death.
        - See https://www.wikihow.com/Do-a-Double-Linear-Interpolation

        Mortality at any age or any year that preceeds or exceeds the supplied data will be equal to the value at
        the nearest age and/or timepoint of supplied data.

        Args:
            ages_years: (List[float]) A list of ages (in years) that mortality data will be provided for. Must be a
                list of monotonically increasing floats within range 0 <= age <= 200 .
            mortality_rate_matrix: (List[List[float]] or List[float]) A 2-d grid of mortality rates in units of
                "annual death rate for an individual". The first data dimension (index) is by age, the second data
                dimension is by calendar year. For M ages (in years) and N calendars years, the dimensionality of this
                matrix must be MxN . Alternately, a 1-d array of mortality rates may be given and will be interpreted
                as a time-independent "for all time" distribution. This option is only available if the calendar_years
                argument is not used.
            calendar_years: (List[float]) (optional) A list of times (in calendar years) that mortality data will be
                provided for. Must be a list of monotonically increasing floats within range 1900 <= year <= 2200 .
                If not provided, a default single calendar year (1900) will be used that effectively means
                "for all time".

        Example:
            ages_years: [0, 10, 20, 50, 100]   # M ages
            calendar_years: [1950, 1970, 1990] # N times. If not supplied, one time "forever" is used and N below is 1.
            mortality_rate_matrix: dimensionality: MxN
                [[0.2,  0.15, 0.1 ],  # These are mortality rates at age 0, the three time points above
                 [0.12, 0.08, 0.06],  # These are mortality rates at age 10
                 [0.05, 0.03, 0.01],  # These are mortality rates at age 20
                 [0.15, 0.1,  0.05],  # These are mortality rates at age 50
                 [0.3,  0.25, 0.2 ]]  # These are mortality rates at age 100

            Mortality at age 5 at 1960, bilinearly interpolated (shown in steps):
                0.2 + (1960-1950) * ((0.15-0.2) / (1970-1950)) = 0.175 (age 0 mortality rate at 1960)
                0.12 + (1960-1950) * ((0.08-0.12) / (1970-1950)) = 0.1 (age 10 mortality rate at 1960)
                0.175 + (5-0) * ((0.1-0.175) / (10-0)) = 0.1375 (age 5 mortality rate at 1960)
            Mortality at age 5 at 2100 (beyond supplied times), bilinearly interpolated: (shown in steps)
                (compute the value at the closest time possible, 1970, then report it for year 2100)
                0.1 (age 0 mortality rate at 1970 (also 2100))
                0.06 (age 10 mortality rate at 1970 (also 2100))
                0.1 + (5-0) * ((0.06-0.1) / (10-0)) = 0.08 (age 5 mortality rate at 1970 (also 2100))
        """
        super().__init__()
        self.ages_years = ages_years

        if calendar_years is None:
            self.calendar_years = [self._default_calendar_year]
            # Here we convert a 1-d array of values to a (trivial) 2-d array. Only allowed if time not passed.
            if check_dimensionality(data=mortality_rate_matrix, dimensionality=1) is True:
                mortality_rate_matrix = [[item] for item in mortality_rate_matrix]
        else:
            self.calendar_years = calendar_years
        self.mortality_rate_matrix = mortality_rate_matrix

        # This will convert the object to a mortality dictionary and then validate it reporting object-relevant messages
        self._validate(distribution_dict=self.to_dict(validate=False), source_is_dict=False)

    @property
    def _population_groups(self):
        return [self.ages_years, self.calendar_years]

    @classmethod
    def _rate_scale_factor(cls):
        return 1 / 365.0  # convert from per-year to per-day

    @classmethod
    def _rate_scale_units(cls):
        return "annual death rate for an individual"

    @property
    def _default_calendar_year(self):
        return 1900

    @classmethod
    def _axis_names(cls):
        return ['age', 'year']

    @classmethod
    def _axis_scale_factors(cls):
        return [365.0, 1]

    def to_dict(self, validate: bool = True) -> Dict:
        distribution_dict = {
            'AxisNames': self._axis_names(),
            'AxisScaleFactors': self._axis_scale_factors(),
            'PopulationGroups': self._population_groups,
            'ResultScaleFactor': self._rate_scale_factor(),
            'ResultUnits': self._rate_scale_units(),
            'ResultValues': self.mortality_rate_matrix
        }
        if validate:
            self._validate(distribution_dict=distribution_dict, source_is_dict=False)
        return distribution_dict

    @classmethod
    def from_dict(cls, distribution_dict: Dict):
        cls._validate(distribution_dict=distribution_dict, source_is_dict=True)
        return cls(ages_years=distribution_dict['PopulationGroups'][0],
                   mortality_rate_matrix=distribution_dict['ResultValues'],
                   calendar_years=distribution_dict['PopulationGroups'][1])

    # True means message relevant to verifying a mortality dictionary, False means messages relevant to verifying an obj
    _validation_messages = {
        'fixed_value_check': {
            True: "key: %s value: %s does not match expected value: %s",
            False: None  # These are all properties of the obj and cannot be made invalid
        },
        'population_group_length_check': {
            True: "PopulationGroups expected to be a 2-d array of floats. The first dimension length must be two, but "
                  "is length %d",
            False: None  # This is a property of the obj and cannot be made invalid
        },
        'data_dimensionality_check': {
            True: "ResultValues is expected to be a 2-d matrix of data but it is not.",
            False: "mortality_rate_matrix has an improper dimensionality. It must be a 2-d matrix if calendar_years is "
                   "given. If calendar_years is NOT given, it MAY be a 1-d list of values."
        },
        'data_dimensionality_check_dim0': {
            True: "ResultValues first dimension length %d does not match the PopulationGroups[0] age bin count %d",
            False: "mortality_rate_matrix first dimension length: %d does not match the ages_years length: %d"
        },
        'data_dimensionality_check_dim1': {
            True: "ResultValues second dimension length %d does not match the PopulationGroups[1] time bin count: %d",
            False: "mortality_rate_matrix second dimension length: %d does not match the calendar_years length: %d"
        },
        'age_range_check': {
            True: "PopulationGroups[0] age values must be: 0 <= age <= 200 in years",
            False: "All ages_years values must be: 0 <= age <= 200 in years"
        },
        'time_range_check': {
            True: "PopulationGroups[1] time values must be: 1900 <= time <= 2200 calendar year",
            False: "All calendar_years values must be: 1900 <= time <= 2200 calendar year"
        },
        'age_monotonicity_check': {
            True: "PopulationGroups[0] ages in years must monotonically increase but do not, index: %d value: %s",
            False: "ages_years values must monotonically increase but do not, index: %d value: %s"
        },
        'time_monotonicity_check': {
            True: "PopulationGroups[1] times in calendar years must monotonically increase but do not, index: %d value: %s",
            False: "calendar_years values must monotonically increase but do not, index: %d value: %s"
        }
    }

    @classmethod
    def _validate(cls, distribution_dict: Dict, source_is_dict: bool):
        """
        Validate a MortalityDistribution in dict form

        Args:
            distribution_dict: (dict) the mortality dict to validate
            source_is_dict: (bool) If true, report dict-relevant error messages. If false, report obj-relevant messages.

        Returns:
            Nothing
        """
        if source_is_dict is True:
            expected_values = {
                'AxisNames': cls._axis_names(),
                'AxisScaleFactors': cls._axis_scale_factors(),
                'ResultScaleFactor': cls._rate_scale_factor(),
                'ResultUnits': cls._rate_scale_units()
            }
            for key, expected_value in expected_values.items():
                value = distribution_dict[key]
                if value != expected_value:
                    message = cls._validation_messages['fixed_value_check'][source_is_dict] % (key, value, expected_value)
                    raise demog_ex.InvalidFixedValueException(message)

        # ensure the data table is MxN for the population groups == [M, N]
        population_groups = distribution_dict['PopulationGroups']
        data_table = distribution_dict['ResultValues']
        if source_is_dict is True:
            if len(population_groups) != 2:
                message = cls._validation_messages['population_group_length_check'][source_is_dict] % (len(population_groups))
                raise demog_ex.InvalidPopulationGroupLengthException(message)

        # ensure the data table has the correct dimensionality. It must be 2-d.
        is_2d = check_dimensionality(data=data_table, dimensionality=2)
        if not is_2d:
            message = cls._validation_messages['data_dimensionality_check'][source_is_dict]
            raise demog_ex.InvalidDataDimensionality(message)

        # continue checking dimension lengths
        ages = population_groups[0]
        times = population_groups[1]
        n_ages = len(ages)
        n_times = len(times)
        if len(data_table) != n_ages:
            message = cls._validation_messages['data_dimensionality_check_dim0'][source_is_dict] % (len(data_table), n_ages)
            raise demog_ex.InvalidDataDimensionDim0Exception(message)
        for i in range(len(data_table)):
            if len(data_table[i]) != n_times:
                message = cls._validation_messages['data_dimensionality_check_dim1'][source_is_dict] % (len(data_table[i]), n_times)
                raise demog_ex.InvalidDataDimensionDim1Exception(message)

        # ensure the age and time lists are ascending and in reasonable ranges
        if any([(age < 0) or (age > 200) for age in ages]):
            message = cls._validation_messages['age_range_check'][source_is_dict]
            raise demog_ex.AgeOutOfRangeException(message)
        if any([(time < 1900) or (time > 2200) for time in times]):
            message = cls._validation_messages['time_range_check'][source_is_dict]
            raise demog_ex.TimeOutOfRangeException(message)

        for i in range(1, len(ages)):
            if ages[i] - ages[i - 1] <= 0:
                message = cls._validation_messages['age_monotonicity_check'][source_is_dict] % (i, ages[i])
                raise demog_ex.NonMonotonicAgeException(message)
        for i in range(1, len(times)):
            if times[i] - times[i - 1] <= 0:
                message = cls._validation_messages['time_monotonicity_check'][source_is_dict] % (i, times[i])
                raise demog_ex.NonMonotonicTimeException(message)

__init__(ages_years, mortality_rate_matrix, calendar_years=None)

A natural mortality distribution for one gender in units of "annual death rate for an individual". If the distribution is time-dependent, pass in a list of times (calendar_years).

The MortalityDistribution provides a rate (probability) at which each agent will die naturally on any given model day given their current age. EMOD uses double linear interpolation (bilinear) of an agent's age and the current calendar year to determine the exact probability of their death. - See https://www.wikihow.com/Do-a-Double-Linear-Interpolation

Mortality at any age or any year that preceeds or exceeds the supplied data will be equal to the value at the nearest age and/or timepoint of supplied data.

Parameters:

Name Type Description Default
ages_years List[float]

(List[float]) A list of ages (in years) that mortality data will be provided for. Must be a list of monotonically increasing floats within range 0 <= age <= 200 .

required
mortality_rate_matrix Union[List[List[float]], List[float]]

(List[List[float]] or List[float]) A 2-d grid of mortality rates in units of "annual death rate for an individual". The first data dimension (index) is by age, the second data dimension is by calendar year. For M ages (in years) and N calendars years, the dimensionality of this matrix must be MxN . Alternately, a 1-d array of mortality rates may be given and will be interpreted as a time-independent "for all time" distribution. This option is only available if the calendar_years argument is not used.

required
calendar_years List[float]

(List[float]) (optional) A list of times (in calendar years) that mortality data will be provided for. Must be a list of monotonically increasing floats within range 1900 <= year <= 2200 . If not provided, a default single calendar year (1900) will be used that effectively means "for all time".

None
Example

ages_years: [0, 10, 20, 50, 100] # M ages calendar_years: [1950, 1970, 1990] # N times. If not supplied, one time "forever" is used and N below is 1. mortality_rate_matrix: dimensionality: MxN [[0.2, 0.15, 0.1 ], # These are mortality rates at age 0, the three time points above [0.12, 0.08, 0.06], # These are mortality rates at age 10 [0.05, 0.03, 0.01], # These are mortality rates at age 20 [0.15, 0.1, 0.05], # These are mortality rates at age 50 [0.3, 0.25, 0.2 ]] # These are mortality rates at age 100

Mortality at age 5 at 1960, bilinearly interpolated (shown in steps): 0.2 + (1960-1950) * ((0.15-0.2) / (1970-1950)) = 0.175 (age 0 mortality rate at 1960) 0.12 + (1960-1950) * ((0.08-0.12) / (1970-1950)) = 0.1 (age 10 mortality rate at 1960) 0.175 + (5-0) * ((0.1-0.175) / (10-0)) = 0.1375 (age 5 mortality rate at 1960) Mortality at age 5 at 2100 (beyond supplied times), bilinearly interpolated: (shown in steps) (compute the value at the closest time possible, 1970, then report it for year 2100) 0.1 (age 0 mortality rate at 1970 (also 2100)) 0.06 (age 10 mortality rate at 1970 (also 2100)) 0.1 + (5-0) * ((0.06-0.1) / (10-0)) = 0.08 (age 5 mortality rate at 1970 (also 2100))

Source code in emod_api/demographics/mortality_distribution.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def __init__(self,
             ages_years: List[float],
             mortality_rate_matrix: Union[List[List[float]], List[float]],
             calendar_years: List[float] = None):
    """
    A natural mortality distribution for one gender in units of "annual death rate for an individual". If the
    distribution is time-dependent, pass in a list of times (calendar_years).

    The MortalityDistribution provides a rate (probability) at which each agent will die naturally on any given
    model day given their current age. EMOD uses double linear interpolation (bilinear) of an agent's age and the
    current calendar year to determine the exact probability of their death.
    - See https://www.wikihow.com/Do-a-Double-Linear-Interpolation

    Mortality at any age or any year that preceeds or exceeds the supplied data will be equal to the value at
    the nearest age and/or timepoint of supplied data.

    Args:
        ages_years: (List[float]) A list of ages (in years) that mortality data will be provided for. Must be a
            list of monotonically increasing floats within range 0 <= age <= 200 .
        mortality_rate_matrix: (List[List[float]] or List[float]) A 2-d grid of mortality rates in units of
            "annual death rate for an individual". The first data dimension (index) is by age, the second data
            dimension is by calendar year. For M ages (in years) and N calendars years, the dimensionality of this
            matrix must be MxN . Alternately, a 1-d array of mortality rates may be given and will be interpreted
            as a time-independent "for all time" distribution. This option is only available if the calendar_years
            argument is not used.
        calendar_years: (List[float]) (optional) A list of times (in calendar years) that mortality data will be
            provided for. Must be a list of monotonically increasing floats within range 1900 <= year <= 2200 .
            If not provided, a default single calendar year (1900) will be used that effectively means
            "for all time".

    Example:
        ages_years: [0, 10, 20, 50, 100]   # M ages
        calendar_years: [1950, 1970, 1990] # N times. If not supplied, one time "forever" is used and N below is 1.
        mortality_rate_matrix: dimensionality: MxN
            [[0.2,  0.15, 0.1 ],  # These are mortality rates at age 0, the three time points above
             [0.12, 0.08, 0.06],  # These are mortality rates at age 10
             [0.05, 0.03, 0.01],  # These are mortality rates at age 20
             [0.15, 0.1,  0.05],  # These are mortality rates at age 50
             [0.3,  0.25, 0.2 ]]  # These are mortality rates at age 100

        Mortality at age 5 at 1960, bilinearly interpolated (shown in steps):
            0.2 + (1960-1950) * ((0.15-0.2) / (1970-1950)) = 0.175 (age 0 mortality rate at 1960)
            0.12 + (1960-1950) * ((0.08-0.12) / (1970-1950)) = 0.1 (age 10 mortality rate at 1960)
            0.175 + (5-0) * ((0.1-0.175) / (10-0)) = 0.1375 (age 5 mortality rate at 1960)
        Mortality at age 5 at 2100 (beyond supplied times), bilinearly interpolated: (shown in steps)
            (compute the value at the closest time possible, 1970, then report it for year 2100)
            0.1 (age 0 mortality rate at 1970 (also 2100))
            0.06 (age 10 mortality rate at 1970 (also 2100))
            0.1 + (5-0) * ((0.06-0.1) / (10-0)) = 0.08 (age 5 mortality rate at 1970 (also 2100))
    """
    super().__init__()
    self.ages_years = ages_years

    if calendar_years is None:
        self.calendar_years = [self._default_calendar_year]
        # Here we convert a 1-d array of values to a (trivial) 2-d array. Only allowed if time not passed.
        if check_dimensionality(data=mortality_rate_matrix, dimensionality=1) is True:
            mortality_rate_matrix = [[item] for item in mortality_rate_matrix]
    else:
        self.calendar_years = calendar_years
    self.mortality_rate_matrix = mortality_rate_matrix

    # This will convert the object to a mortality dictionary and then validate it reporting object-relevant messages
    self._validate(distribution_dict=self.to_dict(validate=False), source_is_dict=False)