diff --git a/resources/contraception/ResourceFile_Contraception/Discontinuation_ByAge.csv b/resources/contraception/ResourceFile_Contraception/Discontinuation_ByAge.csv index c4b70b9182..7e9d1b59c7 100644 --- a/resources/contraception/ResourceFile_Contraception/Discontinuation_ByAge.csv +++ b/resources/contraception/ResourceFile_Contraception/Discontinuation_ByAge.csv @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3b8d2085d10f1680cf7d521ba415777b194ef26a964c14bca473cbdd76c7982 -size 770 +oid sha256:3bb6850fe017a2d1a6fb3430eb8fd6c0591da0539dd1786a153f0889a731fef8 +size 1474 diff --git a/resources/contraception/ResourceFile_Contraception/Initiation_ByAge.csv b/resources/contraception/ResourceFile_Contraception/Initiation_ByAge.csv index e1bf896912..8c80cb871b 100644 --- a/resources/contraception/ResourceFile_Contraception/Initiation_ByAge.csv +++ b/resources/contraception/ResourceFile_Contraception/Initiation_ByAge.csv @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb59b96ca917f218d48b349790092ea04d612c3545669c219ae25a65930a38d1 -size 810 +oid sha256:af364b872a188337427e286a8c3a2b3615f0290437e7693c0d09210d517245e8 +size 1485 diff --git a/resources/contraception/ResourceFile_Contraception/Pregnancy_NotUsing_HIVeffect.csv b/resources/contraception/ResourceFile_Contraception/Pregnancy_NotUsing_HIVeffect.csv index bd377c879b..3af330ada6 100644 --- a/resources/contraception/ResourceFile_Contraception/Pregnancy_NotUsing_HIVeffect.csv +++ b/resources/contraception/ResourceFile_Contraception/Pregnancy_NotUsing_HIVeffect.csv @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6848831b52ab636cf78cd6c3ddc999ad01a46d9d5471987d0e406c479f75ef0 -size 263 +oid sha256:179dd16b27262395c54ffbdff9048cbd67215c5b788cd8c2932e132cc608a3b2 +size 1155 diff --git a/resources/contraception/ResourceFile_Contraception/Pregnancy_NotUsing_In_2010.csv b/resources/contraception/ResourceFile_Contraception/Pregnancy_NotUsing_In_2010.csv index c05c8a2649..f75833601b 100644 --- a/resources/contraception/ResourceFile_Contraception/Pregnancy_NotUsing_In_2010.csv +++ b/resources/contraception/ResourceFile_Contraception/Pregnancy_NotUsing_In_2010.csv @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57d060bdd1a4ad87630853291de818144eeb15c5d2a5ba54a4d2507b31f879ff -size 509 +oid sha256:07eb50237174b7ef3d522972f66a4cdbc905832afc206f67cdfb06a6cae3f79e +size 1401 diff --git a/resources/contraception/ResourceFile_Contraception/simplified_labour_parameters.csv b/resources/contraception/ResourceFile_Contraception/simplified_labour_parameters.csv index 38c6f58d9e..641a32a143 100644 --- a/resources/contraception/ResourceFile_Contraception/simplified_labour_parameters.csv +++ b/resources/contraception/ResourceFile_Contraception/simplified_labour_parameters.csv @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71c1d00707d77dcba8ec87532a9ac3f122cb4bbb2c6cd473d57c0157259b16e3 -size 95 +oid sha256:51abe8f3bdeea948ae1842c12463bd26434fb20ab31490d7967ddcde7d6ced62 +size 171 diff --git a/resources/contraception/ResourceFile_ContraceptionAnalysisParams.csv b/resources/contraception/ResourceFile_ContraceptionAnalysisParams.csv index 9e13abd6f0..8b331dc242 100644 --- a/resources/contraception/ResourceFile_ContraceptionAnalysisParams.csv +++ b/resources/contraception/ResourceFile_ContraceptionAnalysisParams.csv @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfadf013d51552ec90b5d03881f09dd8fad1cae28a2dc9ba6116568fcc5fa564 -size 306 +oid sha256:3ee24fb3e65e2dfcfb5c75d23a73def66f9a0279e68598bd06988bf0d0c60cab +size 420 diff --git a/resources/contraception/ResourceFile_ContraceptionParams.csv b/resources/contraception/ResourceFile_ContraceptionParams.csv index ac1caab585..26fbfc2adb 100644 --- a/resources/contraception/ResourceFile_ContraceptionParams.csv +++ b/resources/contraception/ResourceFile_ContraceptionParams.csv @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14205ecc544760aebeff19cdcd23f9a8a3df653b293e1186f48fe17206556069 -size 620 +oid sha256:201c2a740f5a7e2c870d4758bf21015102796e6a7183f6c0e11cbae15e8264bb +size 2076 diff --git a/src/tlo/methods/contraception.py b/src/tlo/methods/contraception.py index 6f56a55cc7..6f7b315c1d 100644 --- a/src/tlo/methods/contraception.py +++ b/src/tlo/methods/contraception.py @@ -102,7 +102,38 @@ class Contraception(Module): " if False: FP interventions (pop & ppfp) are not simulated."), 'interventions_start_date': Parameter( Types.DATE, "The date since which the FP interventions (pop & ppfp) are implemented, if at all (ie, if " - "use_interventions==True") + "use_interventions==True"), + + 'min_age_contraception': Parameter( + Types.INT, "Minimum age for contraception use."), + 'max_age_contraception': Parameter( + Types.INT, "Maximum age for contraception use."), + 'sterilization_age_limit': Parameter( + Types.INT, "Minimum age for sterilization procedures."), + + 'polling_frequency_months': Parameter( + Types.INT, "Frequency in months for contraception main polling event."), + 'min_simulation_year': Parameter( + Types.INT, "Minimum year for simulation time trends."), + 'max_simulation_year': Parameter( + Types.INT, "Maximum year for simulation time trends."), + 'transition_year': Parameter( + Types.INT, "Transition year for time trend calculations."), + 'initiation_trend_rate_pre_transition': Parameter( + Types.REAL, "Annual rate of increase in contraception initiation before transition year."), + 'initiation_trend_rate_post_transition': Parameter( + Types.REAL, "Annual rate of increase in contraception initiation after transition year."), + 'discontinuation_trend_rate_pre_transition': Parameter( + Types.REAL, "Annual rate of decrease in contraception discontinuation before transition year."), + 'discontinuation_trend_rate_post_transition': Parameter( + Types.REAL, "Annual rate of decrease in contraception discontinuation after transition year."), + 'age_modification_factors': Parameter( + Types.LIST, "Age-specific modification factors for time trends by age groups defined " + "in parameter age_ranges."), + 'reference_year': Parameter( + Types.INT, "Reference year for time trend calculations."), + 'age_ranges': Parameter( + Types.LIST, "Age range labels for demographic analysis (e.g., ['15-19', '20-24', ...]).") } all_contraception_states = { @@ -225,7 +256,8 @@ def initialise_population(self, population): # 2) Assign contraception method # Select females aged 15-49 from population, for current year - females1549 = df.is_alive & (df.sex == 'F') & df.age_years.between(15, 49) + females1549 = df.is_alive & (df.sex == 'F') & df.age_years.between( + self.parameters['min_age_contraception'], self.parameters['max_age_contraception'] - 1) p_method = self.processed_params['initial_method_use'] df.loc[females1549, 'co_contraception'] = df.loc[females1549, 'age_years'].apply( lambda _age_years: self.rng.choice(p_method.columns, p=p_method.loc[_age_years]) @@ -319,7 +351,7 @@ def process_params(self): processed_params = dict() def expand_to_age_years(values_by_age_groups, ages_by_year): - _d = dict(zip(['15-19', '20-24', '25-29', '30-34', '35-39', '40-44', '45-49'], values_by_age_groups)) + _d = dict(zip(self.parameters['age_ranges'], values_by_age_groups)) return np.array( [_d[self.sim.modules['Demography'].AGE_RANGE_LOOKUP[_age_year]] for _age_year in ages_by_year] ) @@ -334,34 +366,36 @@ def initial_method_use(): # Check correct format assert set(p_method.columns) == set(self.all_contraception_states) - assert (p_method.index == range(15, 50)).all() + assert (p_method.index == range( + self.parameters['min_age_contraception'], self.parameters['max_age_contraception'])).all() return p_method def avoid_sterilization_below30(probs): - """Prevent women below 30 years having female sterilization and adjust the probability for women 30 and over - to preserve the overall probability of initiating sterilization.""" + """Prevent women below sterilization_age_limit years having female sterilization and adjust the + probability for women at or above the limit to preserve the overall probability of initiating + sterilization.""" # Input 'probs' must include probs for all methods including 'not_using' assert set(probs.index) == set(self.all_contraception_states) - # Prevent women below 30 years having 'female_sterilization' - probs_below30 = probs.copy() - probs_below30['female_sterilization'] = 0.0 + # Prevent women below sterilization age limit from having 'female_sterilization' + probs_below_limit = probs.copy() + probs_below_limit['female_sterilization'] = 0.0 # Scale so that the probability of all outcomes sum to 1.0 - probs_below30 = probs_below30 / probs_below30.sum() - assert np.isclose(1.0, probs_below30.sum()) + probs_below_limit = probs_below_limit / probs_below_limit.sum() + assert np.isclose(1.0, probs_below_limit.sum()) # Increase prob of 'female_sterilization' in older women accordingly - probs_30plus = probs.copy() - probs_30plus['female_sterilization'] = ( + probs_limit_plus = probs.copy() + probs_limit_plus['female_sterilization'] = ( probs.loc['female_sterilization'] / self.ratio_n_females_30_49_to_15_49_in_2010 ) # Scale so that the probability of all outcomes sum to 1.0 - probs_30plus = probs_30plus / probs_30plus.sum() - assert np.isclose(1.0, probs_30plus.sum()) + probs_limit_plus = probs_limit_plus / probs_limit_plus.sum() + assert np.isclose(1.0, probs_limit_plus.sum()) - return probs_below30, probs_30plus + return probs_below_limit, probs_limit_plus def contraception_initiation(): """Generate the probability per month of a woman initiating onto each contraceptive, by the age (in whole @@ -370,9 +404,9 @@ def contraception_initiation(): # Probability of initiation by method per month (average over all ages) p_init_by_method = self.parameters['Initiation_ByMethod'].loc[0] - # Prevent women below 30 years having 'female_sterilization' while preserving the overall probability of - # 'female_sterilization' initiation - p_init_by_method_below30, p_init_by_method_30plus = avoid_sterilization_below30(p_init_by_method) + # Prevent women below sterilization_age_limit years having 'female_sterilization' while preserving + # the overall probability of 'female_sterilization' initiation + p_init_by_method_below_limit, p_init_by_method_limit_plus = avoid_sterilization_below30(p_init_by_method) # Effect of age age_effect = 1.0 + self.parameters['Initiation_ByAge'].set_index('age')['r_init1_age'].rename_axis( @@ -381,31 +415,35 @@ def contraception_initiation(): # Year effect year_effect = time_age_trend_in_initiation() - def apply_age_year_effects(probs_below30, probs_30plus): + def apply_age_year_effects(probs_below_limit, probs_limit_plus): # Assemble into age-specific data-frame: - probs_by_method_below30 = probs_below30.copy().drop('not_using') - probs_by_method_30plus = probs_30plus.copy().drop('not_using') + probs_by_method_below_limit = probs_below_limit.copy().drop('not_using') + probs_by_method_limit_plus = probs_limit_plus.copy().drop('not_using') p_init = dict() + p = self.parameters for year in year_effect.index: p_init_this_year = dict() for a in age_effect.index: - if a < 30: - p_init_this_year[a] = probs_by_method_below30 * age_effect.at[a] * year_effect.at[year, a] + if a < p['sterilization_age_limit']: + p_init_this_year[a] = (probs_by_method_below_limit * age_effect.at[a] * + year_effect.at[year, a]) else: - p_init_this_year[a] = probs_by_method_30plus * age_effect.at[a] * year_effect.at[year, a] + p_init_this_year[a] = (probs_by_method_limit_plus * age_effect.at[a] * + year_effect.at[year, a]) p_init_this_year_df = pd.DataFrame.from_dict(p_init_this_year, orient='index') # Check correct format of age/method data-frame assert set(p_init_this_year_df.columns) == set(self.all_contraception_states - {'not_using'}) - assert (p_init_this_year_df.index == range(15, 50)).all() + assert (p_init_this_year_df.index == range( + self.parameters['min_age_contraception'], p['max_age_contraception'])).all() assert (p_init_this_year_df >= 0.0).all().all() p_init[year] = p_init_this_year_df return p_init - return apply_age_year_effects(p_init_by_method_below30, p_init_by_method_30plus) + return apply_age_year_effects(p_init_by_method_below_limit, p_init_by_method_limit_plus) def contraception_switch(): """Get the probability per month of a woman switching to contraceptive method, given that she is currently @@ -419,15 +457,15 @@ def contraception_switch(): # Columns = "current method"; Row = "new method" switching_matrix = self.parameters['Prob_Switch_From_And_To'].set_index('switchfrom').transpose() - # Prevent women below 30 years having 'female_sterilization' - switching_matrix_below30 = switching_matrix.copy() - switching_matrix_below30.loc['female_sterilization', :] = 0.0 - switching_matrix_below30 = switching_matrix_below30.apply(lambda col: col / col.sum()) + # Prevent women below sterilization age limit from having 'female_sterilization' + switching_matrix_below_limit = switching_matrix.copy() + switching_matrix_below_limit.loc['female_sterilization', :] = 0.0 + switching_matrix_below_limit = switching_matrix_below_limit.apply(lambda col: col / col.sum()) - assert set(switching_matrix_below30.columns) == ( + assert set(switching_matrix_below_limit.columns) == ( self.all_contraception_states - {"not_using", "female_sterilization"}) - assert set(switching_matrix_below30.index) == (self.all_contraception_states - {"not_using"}) - assert np.isclose(1.0, switching_matrix_below30.sum(axis=0)).all() + assert set(switching_matrix_below_limit.index) == (self.all_contraception_states - {"not_using"}) + assert np.isclose(1.0, switching_matrix_below_limit.sum(axis=0)).all() # Increase prob of 'female_sterilization' in older women accordingly new_fs_probs_30plus = ( @@ -444,7 +482,7 @@ def contraception_switch(): assert set(switching_matrix_30plus.index) == (self.all_contraception_states - {"not_using"}) assert np.isclose(1.0, switching_matrix_30plus.sum(axis=0)).all() - return p_switch_from, switching_matrix_below30, switching_matrix_30plus + return p_switch_from, switching_matrix_below_limit, switching_matrix_30plus def contraception_stop(): """Get the probability per month of a woman stopping use of contraceptive method.""" @@ -477,12 +515,21 @@ def time_age_trend_in_initiation(): (multiplicative effect). Values are chosen to induce a trend in age-specific fertility consistent with the WPP estimates.""" - _years = np.arange(2010, 2101) - _ages = np.arange(15, 50) + _years = np.arange(self.parameters['min_simulation_year'], self.parameters['max_simulation_year']) + _ages = np.arange(self.parameters['min_age_contraception'], self.parameters['max_age_contraception']) - _init_over_time = np.exp(+0.05 * np.minimum(2020 - 2010, (_years - 2010))) * np.maximum(1.0, np.exp( - +0.01 * (_years - 2020))) - _init_over_time_modification_by_age = 1.0 / expand_to_age_years([1.0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], _ages) + pre_transition_years = np.minimum( + self.parameters['transition_year'] - self.parameters['reference_year'], + (_years - self.parameters['reference_year']) + ) + post_transition_years = _years - self.parameters['transition_year'] + + _init_over_time = (np.exp(self.parameters['initiation_trend_rate_pre_transition'] * + pre_transition_years) * + np.maximum(1.0, np.exp(self.parameters['initiation_trend_rate_post_transition'] * + post_transition_years))) + _init_over_time_modification_by_age = 1.0 / expand_to_age_years( + self.parameters['age_modification_factors'], _ages) _init = np.outer(_init_over_time, _init_over_time_modification_by_age) return pd.DataFrame(index=_years, columns=_ages, data=_init) @@ -492,12 +539,21 @@ def time_age_trend_in_stopping(): (multiplicative effect). Values are chosen to induce a trend in age-specific fertility consistent with the WPP estimates.""" - _years = np.arange(2010, 2101) - _ages = np.arange(15, 50) + _years = np.arange(self.parameters['min_simulation_year'], self.parameters['max_simulation_year']) + _ages = np.arange(self.parameters['min_age_contraception'], self.parameters['max_age_contraception']) - _discont_over_time = np.exp(-0.05 * np.minimum(2020 - 2010, (_years - 2010))) * np.minimum(1.0, np.exp( - -0.01 * (_years - 2020))) - _discont_over_time_modification_by_age = expand_to_age_years([1.0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], _ages) + pre_transition_years = np.minimum( + self.parameters['transition_year'] - self.parameters['reference_year'], + (_years - self.parameters['reference_year']) + ) + post_transition_years = _years - self.parameters['transition_year'] + + _discont_over_time = (np.exp(self.parameters['discontinuation_trend_rate_pre_transition'] * + pre_transition_years) * + np.minimum(1.0, np.exp(self.parameters['discontinuation_trend_rate_post_transition'] * + post_transition_years))) + _discont_over_time_modification_by_age = expand_to_age_years( + self.parameters['age_modification_factors'], _ages) _discont = np.outer(_discont_over_time, _discont_over_time_modification_by_age) return pd.DataFrame(index=_years, columns=_ages, data=_discont) @@ -517,7 +573,7 @@ def scaling_factor_on_monthly_risk_of_pregnancy(): # first scaling factor is that worked out from the calibration script scaling_factor_as_dict = dict(zip( - ['15-19', '20-24', '25-29', '30-34', '35-39', '40-44', '45-49'], + self.parameters['age_ranges'], self.parameters['scaling_factor_on_monthly_risk_of_pregnancy'] )) @@ -841,9 +897,9 @@ class ContraceptionPoll(RegularEvent, PopulationScopeEventMixin): """ def __init__(self, module, run_do_pregnancy=True, run_update_contraceptive=True): - super().__init__(module, frequency=DateOffset(months=1)) - self.age_low = 15 - self.age_high = 49 + super().__init__(module, frequency=DateOffset(months=module.parameters['polling_frequency_months'])) + self.age_low = module.parameters['min_age_contraception'] + self.age_high = module.parameters['max_age_contraception'] - 1 self.run_do_pregnancy = run_do_pregnancy # (Provided for testing only) self.run_update_contraceptive = run_update_contraceptive # (Provided for testing only)