diff --git a/resources/ResourceFile_Measles/beta.csv b/resources/ResourceFile_Measles/beta.csv deleted file mode 100644 index a7ba7e5efa..0000000000 --- a/resources/ResourceFile_Measles/beta.csv +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:427be2f0f44c3da740657ee64a0a10f4bccca798ca655712cc14f7eba08575b0 -size 125 diff --git a/resources/ResourceFile_Measles/cfr.csv b/resources/ResourceFile_Measles/cfr.csv index 0d84e74583..c984d44bba 100644 --- a/resources/ResourceFile_Measles/cfr.csv +++ b/resources/ResourceFile_Measles/cfr.csv @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3af172fa61482e208e10a8c247ce5ec79faf60e1f4fb83af77bfd8347fc1dc27 -size 643 +oid sha256:ef6fc870cc755eed89574f8d55c29636aa0f9bbfa8d38046ff9f463ae09f5b3f +size 1073 diff --git a/resources/ResourceFile_Measles/parameters.csv b/resources/ResourceFile_Measles/parameters.csv index f1962afba7..d8df3e049f 100644 --- a/resources/ResourceFile_Measles/parameters.csv +++ b/resources/ResourceFile_Measles/parameters.csv @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:203376e1d0b74fe76c66ac2ff66a4db4efb24d46635db05afcb04a2fdd141205 -size 249 +oid sha256:74f90b3dfd2ac370aa8f379a7ac83785e7db018bd504ff21fea627970153b6a7 +size 2542 diff --git a/resources/ResourceFile_Measles/symptoms.csv b/resources/ResourceFile_Measles/symptoms.csv index c5209ee073..cef6c0a879 100644 --- a/resources/ResourceFile_Measles/symptoms.csv +++ b/resources/ResourceFile_Measles/symptoms.csv @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d967b5051c192d15eb202efad5cd6897f980b440d174f4e6b4a21d4a78e28361 -size 4466 +oid sha256:8369f4f264b5498c6fd23fe23a9ce92716a5b1b5402f456ef96b80676b8ec088 +size 9322 diff --git a/src/tlo/core.py b/src/tlo/core.py index d88ab7e0e6..eaa96e8d31 100644 --- a/src/tlo/core.py +++ b/src/tlo/core.py @@ -7,8 +7,9 @@ from __future__ import annotations import json +import warnings from enum import Enum, auto -from typing import TYPE_CHECKING, Any, Dict, FrozenSet, List +from typing import TYPE_CHECKING, Any, Dict, FrozenSet, List, Literal import numpy as np import pandas as pd @@ -399,6 +400,22 @@ def load_parameters_from_dataframe(self, resource: pd.DataFrame) -> None: prior_max=prior_max ) + def declare_parameter_metadata(self, + parameter_name: str, + param_label: Optional[Literal["local", "universal", "undetermined"]] = None, + prior_min: Optional = None, + prior_max: Optional = None) -> None: + """Declare metadata for a parameter""" + if parameter_name not in self.PARAMETERS: + raise ValueError(f"Parameter {parameter_name} not declared in PARAMETERS dictionary") + + if self.PARAMETERS[parameter_name].metadata != {}: + warnings.warn(f"Parameter {parameter_name} already has metadata declared") + + self.PARAMETERS[parameter_name].metadata.update( + param_label=param_label, prior_min=prior_min, prior_max=prior_max) + + def read_parameters(self, data_folder: str | Path) -> None: """Read parameter values from file, if required. diff --git a/src/tlo/methods/measles.py b/src/tlo/methods/measles.py index 075cc1c67c..d72793e53b 100644 --- a/src/tlo/methods/measles.py +++ b/src/tlo/methods/measles.py @@ -64,14 +64,51 @@ class Measles(Module, GenericFirstAppointmentsMixin): Types.REAL, "Efficacy of first measles vaccine dose against measles infection"), "vaccine_efficacy_2": Parameter( Types.REAL, "Efficacy of second measles vaccine dose against measles infection"), - "prob_severe": Parameter( - Types.REAL, "Probability of severe measles infection, requiring hospitalisation"), - "risk_death_on_treatment": Parameter( - Types.REAL, "Risk of scheduled death occurring if on treatment for measles complications"), "symptom_prob": Parameter( Types.DATA_FRAME, "Probability of each symptom with measles infection"), + "odds_ratio_health_seeking_in_children_rash": Parameter( + Types.REAL, "Odds ratio seeking care in children rash"), + "odds_ratio_health_seeking_in_adults_rash": Parameter( + Types.REAL, "Odds ratio seeking care in adults rash"), + "odds_ratio_health_seeking_in_children_otitis_media": Parameter( + Types.REAL, "Odds ratio seeking care in children otitis media"), + "odds_ratio_health_seeking_in_adults_otitis_media": Parameter( + Types.REAL, "Odds ratio seeking care in adults otitis media"), + "reduction_in_death_risk_measle_treated": Parameter( + Types.REAL, "Reduction in death risk when measles are treated" + ), + "maternal_immunity_age_threshold": Parameter( + Types.REAL, "Age threshold below which children are protected by maternal immunity" + ), + "age_min_symptoms_constant_rate": Parameter( + Types.INT, "Maximum age for measles symptom probability lookup" + ), + "death_timing_min_days": Parameter( + Types.INT, "Minimum days from symptom onset to death" + ), + "death_timing_max_days": Parameter( + Types.INT, "Maximum days from symptom onset to death" + ), + "natural_resolution_min_days": Parameter( + Types.INT, "Minimum days from symptom onset to natural resolution" + ), + "natural_resolution_max_days": Parameter( + Types.INT, "Maximum days from symptom onset to natural resolution" + ), + "symptom_onset_min_days": Parameter( + Types.INT, "Minimum days from infection to symptom onset (incubation period)" + ), + "symptom_onset_max_days": Parameter( + Types.INT, "Maximum days from infection to symptom onset (incubation period)" + ), + "sympt_resolution_after_treatment": Parameter( + Types.INT, "Days until symptom resolution after treatment" + ), + "main_polling_event_frequency_months": Parameter( + Types.INT, "Measles main polling event frequency months" + ), "case_fatality_rate": Parameter( - Types.DICT, "Probability that case of measles will result in death if not treated") + Types.DICT, "Case fatality rate for measles infection"), } PROPERTIES = { @@ -105,9 +142,14 @@ def read_parameters(self, resourcefilepath: Optional[Path] = None): self.load_parameters_from_dataframe(workbook["parameters"]) self.parameters["symptom_prob"] = workbook["symptoms"] + self.declare_parameter_metadata(parameter_name="symptom_prob", param_label="local") + self.parameters["case_fatality_rate"] = workbook["cfr"].set_index('age')["probability"].to_dict() + self.declare_parameter_metadata(parameter_name="case_fatality_rate", param_label="universal") # moderate symptoms all mapped to moderate_measles, pneumonia/encephalitis mapped to severe_measles + + p = self.parameters if "HealthBurden" in self.sim.modules.keys(): self.parameters["daly_wts"] = { "rash": self.sim.modules["HealthBurden"].get_daly_weight(sequlae_code=205), @@ -122,14 +164,14 @@ def read_parameters(self, resourcefilepath: Optional[Path] = None): # Declare symptoms that this module will cause and which are not included in the generic symptoms: self.sim.modules['SymptomManager'].register_symptom( Symptom(name='rash', - odds_ratio_health_seeking_in_children=2.5, - odds_ratio_health_seeking_in_adults=2.5) # non-emergencies + odds_ratio_health_seeking_in_children=p['odds_ratio_health_seeking_in_children_rash'], + odds_ratio_health_seeking_in_adults=p['odds_ratio_health_seeking_in_adults_rash']) ) self.sim.modules['SymptomManager'].register_symptom( Symptom(name='otitis_media', - odds_ratio_health_seeking_in_children=2.5, - odds_ratio_health_seeking_in_adults=2.5) # non-emergencies + odds_ratio_health_seeking_in_children=p['odds_ratio_health_seeking_in_children_otitis_media'], + odds_ratio_health_seeking_in_adults=p['odds_ratio_health_seeking_in_adults_otitis_media']) ) self.sim.modules['SymptomManager'].register_symptom(Symptom.emergency('encephalitis')) @@ -149,6 +191,7 @@ def initialise_population(self, population): def initialise_simulation(self, sim): """Schedule measles event to start straight away. Each month it will assign new infections""" + sim.schedule_event(MeaslesEvent(self), sim.date) sim.schedule_event(MeaslesLoggingEvent(self), sim.date) sim.schedule_event(MeaslesLoggingFortnightEvent(self), sim.date) @@ -165,6 +208,7 @@ def initialise_simulation(self, sim): "oxygen cylinders") } + def on_birth(self, mother_id, child_id): """Initialise our properties for a newborn individual assume all newborns are uninfected @@ -196,12 +240,13 @@ def process_parameters(self): """Process the parameters (following being read-in) prior to the simulation starting. Make `self.symptom_probs` to be a dictionary keyed by age, with values of dictionaries keyed by symptoms and the probability of symptom onset.""" + p = self.parameters probs = self.parameters["symptom_prob"].set_index(["age", "symptom"])["probability"] self.symptom_probs = {level: probs.loc[(level, slice(None))].to_dict() for level in probs.index.levels[0]} # Check that a sensible value for a probability of symptom onset is declared for each symptom and for each age - # up to and including age 30 - for _age in range(30 + 1): + # up to and including age for which symptom probabilities stabilizes (age >= 'age_min_symptoms_constant_rate') + for _age in range(p["age_min_symptoms_constant_rate"] + 1): assert set(self.symptoms) == set(self.symptom_probs.get(_age).keys()) assert all([0.0 <= x <= 1.0 for x in self.symptom_probs.get(_age).values()]) @@ -225,7 +270,8 @@ class MeaslesEvent(RegularEvent, PopulationScopeEventMixin): """ def __init__(self, module): - super().__init__(module, frequency=DateOffset(months=1)) + p = module.parameters + super().__init__(module, frequency=DateOffset(months=p['main_polling_event_frequency_months'])) assert isinstance(module, Measles) def apply(self, population): @@ -248,8 +294,8 @@ def apply(self, population): protected_by_vaccine.loc[(df.va_measles == 1)] *= (1 - p["vaccine_efficacy_1"]) # partially susceptible protected_by_vaccine.loc[(df.va_measles > 1)] *= (1 - p["vaccine_efficacy_2"]) # partially susceptible - # Find persons to be newly infected (no risk to children under 6 months as protected by maternal immunity) - new_inf = df.index[~df.me_has_measles & (df.age_exact_years >= 0.5) & + # Find persons to be newly infected (no risk to children under maternal immunity threshold ) + new_inf = df.index[~df.me_has_measles & (df.age_exact_years >= p["maternal_immunity_age_threshold"]) & (rng.random_sample(size=len(df)) < (trans_prob * protected_by_vaccine))] logger.debug(key="MeaslesEvent", @@ -261,7 +307,8 @@ def apply(self, population): for person_index in new_inf: self.sim.schedule_event( MeaslesOnsetEvent(self.module, person_index), - random_date(start=self.sim.date, end=self.sim.date + pd.DateOffset(months=1), rng=rng) + random_date(start=self.sim.date, + end=self.sim.date + pd.DateOffset(months=p['main_polling_event_frequency_months']), rng=rng) ) @@ -274,11 +321,12 @@ def apply(self, person_id): df = self.sim.population.props # shortcut to the dataframe rng = self.module.rng + p = self.module.parameters if not df.at[person_id, "is_alive"]: return - ref_age = df.at[person_id, "age_years"].clip(max=30) # (For purpose of look-up age limit is 30 years) + ref_age = df.at[person_id, "age_years"].clip(max=p["age_min_symptoms_constant_rate"]) # Determine if the person has "untreated HIV", which is defined as a person in any stage of HIV but not on # successful treatment currently. @@ -303,11 +351,14 @@ def apply(self, person_id): # schedule the death self.sim.schedule_event( - death_event, symp_onset + DateOffset(days=rng.randint(3, 7))) + death_event, symp_onset + + DateOffset(days=rng.randint(p["death_timing_min_days"], p["death_timing_max_days"]))) else: # schedule symptom resolution without treatment - this only occurs if death doesn't happen first - symp_resolve = symp_onset + DateOffset(days=rng.randint(7, 14)) + symp_resolve = (symp_onset + + DateOffset(days=rng.randint(p["natural_resolution_min_days"], + p["natural_resolution_max_days"]))) self.sim.schedule_event(MeaslesSymptomResolveEvent(self.module, person_id), symp_resolve) def assign_symptoms(self, _age): @@ -315,9 +366,11 @@ def assign_symptoms(self, _age): (Parameter values specify that everybody gets rash, fever and eye complain.)""" rng = self.module.rng + p = self.module.parameters person_id = self.target symptom_probs_for_this_person = self.module.symptom_probs.get(_age) - date_of_symp_onset = self.sim.date + DateOffset(days=rng.randint(7, 21)) + date_of_symp_onset = self.sim.date + DateOffset(days=rng.randint(p["symptom_onset_min_days"], + p["symptom_onset_max_days"])) symptoms_to_onset = [ _symp for (_symp, _prob), _rand in zip( @@ -379,6 +432,7 @@ def __init__(self, module, person_id): def apply(self, person_id): df = self.sim.population.props + p = self.module.parameters if not df.at[person_id, "is_alive"]: return @@ -389,10 +443,8 @@ def apply(self, person_id): if df.at[person_id, "me_on_treatment"]: - reduction_in_death_risk = 0.6 - # Certain death (100%) is reduced by specified amount - p_death_with_treatment = 1. - reduction_in_death_risk + p_death_with_treatment = 1. - p['reduction_in_death_risk_measle_treated'] # If below that probability, death goes ahead if self.module.rng.random_sample() < p_death_with_treatment: @@ -438,6 +490,7 @@ def apply(self, person_id, squeeze_factor): df = self.sim.population.props symptoms = self.sim.modules["SymptomManager"].has_what(person_id=person_id) + p = self.module.parameters # for non-complicated measles item_codes = [self.module.consumables['vit_A']] @@ -463,7 +516,7 @@ def apply(self, person_id, squeeze_factor): # schedule symptom resolution following treatment self.sim.schedule_event(MeaslesSymptomResolveEvent(self.module, person_id), - self.sim.date + DateOffset(days=7)) + self.sim.date + DateOffset(days=p["sympt_resolution_after_treatment"])) def did_not_run(self): logger.debug(key="HSI_Measles_Treatment",