From 8bdbd1eb91cfd0b5cec6cdc2f18c3f8ae0f3fa7e Mon Sep 17 00:00:00 2001 From: Bingling Date: Thu, 21 May 2026 12:55:02 +0100 Subject: [PATCH 01/31] Initialise the HRH scaling by year and officer type --- src/tlo/methods/healthsystem.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/tlo/methods/healthsystem.py b/src/tlo/methods/healthsystem.py index f8b0f55a03..016e13221a 100644 --- a/src/tlo/methods/healthsystem.py +++ b/src/tlo/methods/healthsystem.py @@ -2984,6 +2984,34 @@ def apply(self, population): ] +class RescalingHRCapabilities_ByYearAndOfficer(Event, PopulationScopeEventMixin): + """This event exists to scale the daily capabilities, with a factor for each Officer Type at each specified year.""" + + def __init__(self, module): + super().__init__(module) + + def apply(self, population): + # Get the set of scaling_factors that are specified by the 'HR_scaling_by_level_and_officer_type_mode' + # assumption + HR_scaling_by_year_and_officer_type_factor = self.module.parameters[ + "HR_scaling_by_year_and_officer_type_table" + ][self.module.parameters["HR_scaling_by_year_and_officer_type_mode"]].set_index("Officer_Category") + + years_for_scaling = np.array(list(HR_scaling_by_year_and_officer_type_factor.columns())) + most_recent_year_for_scaling = years_for_scaling[years_for_scaling <= self.sim.date.year].max() + + pattern = r"FacilityID_(\w+)_Officer_(\w+)" + + for clinic, clinic_cl in self.module._daily_capabilities.items(): + for officer in clinic_cl.keys(): + matches = re.match(pattern, officer) + # Extract officer type + officer_type = matches.group(2) + self.module._daily_capabilities[clinic][officer] *= HR_scaling_by_year_and_officer_type_factor.at[ + officer_type, most_recent_year_for_scaling + ] + + class RescaleHRCapabilities_ByDistrict(Event, PopulationScopeEventMixin): """This event exists to scale the daily capabilities, with a factor for each district.""" From ac6d255df13e64c2b9e11fe1a6661cdca02033a6 Mon Sep 17 00:00:00 2001 From: Bingling Date: Thu, 21 May 2026 13:51:27 +0100 Subject: [PATCH 02/31] specify parameters and resource files --- src/tlo/methods/healthsystem.py | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/tlo/methods/healthsystem.py b/src/tlo/methods/healthsystem.py index 016e13221a..03ebc28b66 100644 --- a/src/tlo/methods/healthsystem.py +++ b/src/tlo/methods/healthsystem.py @@ -308,6 +308,26 @@ class HealthSystem(Module): "(factors informed by survey data); and, `custom` (user can freely set these factors as " "parameters in the analysis).", ), + "HR_scaling_by_year_and_officer_type_table": Parameter( + Types.DICT, + "Factors by which capabilities of medical officer types will be scaled at the start of " + "the years considered. This is the imported from an Excel workbook: keys are the worksheet " + "names and values are the worksheets in the format of pd.DataFrames. Additional scenarios can " + "be added by adding worksheets to this workbook: the value of " + "`HR_scaling_by_year_and_officer_type_mode` indicates which sheet is used.", + ), + "HR_scaling_by_year_and_officer_type_mode": Parameter( + Types.STRING, + "Mode of HR scaling considered at the start of the simulation. This corresponds to the name" + "of the worksheet in `ResourceFile_HR_scaling_by_year_and_officer_type.xlsx` that should be used. " + "Options are: " + "`default` (capabilities are scaled by a factor of 1); " + "`historical_uniform` (factors across the cadres are the same, informated by the historical growth rate " + "in total HRH);" + "`historical_cadre_mix` (factors across the cadres are different, indicated by both the overall historical " + "increase rate and the gap_allocation HRH expansion scenario by She et al 2026); and," + "`custom` (user can freely set these factors as parameters in the analysis).", + ), "HR_scaling_by_district_table": Parameter( Types.DICT, "Factors by which daily capabilities in different districts will be" @@ -694,6 +714,22 @@ def read_consumables(filename): f"{self.parameters['HR_scaling_by_level_and_officer_type_mode']}" ) + self.parameters["HR_scaling_by_year_and_officer_type_table"]: Dict = read_csv_files( + path_to_resourcefiles_for_healthsystem + / "human_resources" + / "scaling_capabilities" + / "ResourceFile_HR_scaling_by_year_and_officer_type", + files=None, # all sheets read in + ) + # Ensure the mode of HR scaling to be considered in included in the tables loaded + assert ( + self.parameters["HR_scaling_by_year_and_officer_type_mode"] + in self.parameters["HR_scaling_by_year_and_officer_type_table"] + ), ( + f"Value of `HR_scaling_by_year_and_officer_type_mode` not recognised: " + f"{self.parameters['HR_scaling_by_year_and_officer_type_mode']}" + ) + self.parameters["HR_scaling_by_district_table"]: Dict = read_csv_files( path_to_resourcefiles_for_healthsystem / "human_resources" @@ -915,6 +951,9 @@ def initialise_simulation(self, sim): # whilst the actual scaling will only take effect from 2011 onwards. sim.schedule_event(DynamicRescalingHRCapabilities(self), Date(sim.date)) + # Schedule recurring event which will rescale daily capabilities annually. + sim.schedule_event(RescalingHRCapabilities_ByYearAndOfficer(self), Date(sim.date)) + # Schedule the logger to occur at the start of every year sim.schedule_event(HealthSystemLogger(self), Date(sim.date.year, 1, 1)) From 5ba29b4cbe2cd4a2cc162f62853b4d311e83db5d Mon Sep 17 00:00:00 2001 From: Bingling Date: Thu, 21 May 2026 14:17:38 +0100 Subject: [PATCH 03/31] update notes --- src/tlo/methods/healthsystem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tlo/methods/healthsystem.py b/src/tlo/methods/healthsystem.py index 03ebc28b66..0bf9829068 100644 --- a/src/tlo/methods/healthsystem.py +++ b/src/tlo/methods/healthsystem.py @@ -326,7 +326,9 @@ class HealthSystem(Module): "in total HRH);" "`historical_cadre_mix` (factors across the cadres are different, indicated by both the overall historical " "increase rate and the gap_allocation HRH expansion scenario by She et al 2026); and," - "`custom` (user can freely set these factors as parameters in the analysis).", + "`custom` (user can freely set these factors as parameters in the analysis). " + "Note that if use historical_ modes in this HRH scaling, use no_scaling mode in DynamicHRHScaling " + "to avoid duplicates.", ), "HR_scaling_by_district_table": Parameter( Types.DICT, From 7587019cc0e470dfa45ded8b29d0ce3f576ef4a3 Mon Sep 17 00:00:00 2001 From: Bingling Date: Thu, 21 May 2026 14:18:00 +0100 Subject: [PATCH 04/31] add in resource files --- .../custom.csv | 10 ++++++++++ .../default.csv | 10 ++++++++++ .../historical_cadre_mix.csv | 10 ++++++++++ .../historical_uniform.csv | 10 ++++++++++ 4 files changed, 40 insertions(+) create mode 100644 resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/custom.csv create mode 100644 resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/default.csv create mode 100644 resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/historical_cadre_mix.csv create mode 100644 resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/historical_uniform.csv diff --git a/resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/custom.csv b/resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/custom.csv new file mode 100644 index 0000000000..5edeb2def5 --- /dev/null +++ b/resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/custom.csv @@ -0,0 +1,10 @@ +Officer_Category,2010,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030 +Clinical,1,1,1,1,1,1,1,1,1,1,1,1,1 +DCSA,1,1,1,1,1,1,1,1,1,1,1,1,1 +Dental,1,1,1,1,1,1,1,1,1,1,1,1,1 +Laboratory,1,1,1,1,1,1,1,1,1,1,1,1,1 +Mental,1,1,1,1,1,1,1,1,1,1,1,1,1 +Nursing_and_Midwifery,1,1,1,1,1,1,1,1,1,1,1,1,1 +Nutrition,1,1,1,1,1,1,1,1,1,1,1,1,1 +Pharmacy,1,1,1,1,1,1,1,1,1,1,1,1,1 +Radiography,1,1,1,1,1,1,1,1,1,1,1,1,1 diff --git a/resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/default.csv b/resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/default.csv new file mode 100644 index 0000000000..5edeb2def5 --- /dev/null +++ b/resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/default.csv @@ -0,0 +1,10 @@ +Officer_Category,2010,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030 +Clinical,1,1,1,1,1,1,1,1,1,1,1,1,1 +DCSA,1,1,1,1,1,1,1,1,1,1,1,1,1 +Dental,1,1,1,1,1,1,1,1,1,1,1,1,1 +Laboratory,1,1,1,1,1,1,1,1,1,1,1,1,1 +Mental,1,1,1,1,1,1,1,1,1,1,1,1,1 +Nursing_and_Midwifery,1,1,1,1,1,1,1,1,1,1,1,1,1 +Nutrition,1,1,1,1,1,1,1,1,1,1,1,1,1 +Pharmacy,1,1,1,1,1,1,1,1,1,1,1,1,1 +Radiography,1,1,1,1,1,1,1,1,1,1,1,1,1 diff --git a/resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/historical_cadre_mix.csv b/resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/historical_cadre_mix.csv new file mode 100644 index 0000000000..10fbad62d1 --- /dev/null +++ b/resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/historical_cadre_mix.csv @@ -0,0 +1,10 @@ +Officer_Category,2010,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030 +Clinical,1,1,1.068268523,1.182359384,1.168541512,1.156685635,1.146388516,1,1,1,1,1,1 +DCSA,1,1,1,1,1,1,1,1,1,1,1,1,1 +Dental,1,1,1.019505292,1.052102681,1.048154718,1.044767324,1.04182529,1,1,1,1,1,1 +Laboratory,1,1,1.019505292,1.052102681,1.048154718,1.044767324,1.04182529,1,1,1,1,1,1 +Mental,1,1,1.019505292,1.052102681,1.048154718,1.044767324,1.04182529,1,1,1,1,1,1 +Nursing_and_Midwifery,1,1,1.039010585,1.104205363,1.096309435,1.089534649,1.08365058,1,1,1,1,1,1 +Nutrition,1,1,1.019505292,1.052102681,1.048154718,1.044767324,1.04182529,1,1,1,1,1,1 +Pharmacy,1,1,1.136537047,1.364718769,1.337083024,1.31337127,1.292777032,1,1,1,1,1,1 +Radiography,1,1,1.019505292,1.052102681,1.048154718,1.044767324,1.04182529,1,1,1,1,1,1 diff --git a/resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/historical_uniform.csv b/resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/historical_uniform.csv new file mode 100644 index 0000000000..f312cdfc80 --- /dev/null +++ b/resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/historical_uniform.csv @@ -0,0 +1,10 @@ +Officer_Category,2010,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030 +Clinical,1,1,1.027745477,1.076495291,1.076495291,1.076495291,1.076495291,1,1,1,1,1,1 +DCSA,1,1,1.027745477,1.076495291,1.076495291,1.076495291,1.076495291,1,1,1,1,1,1 +Dental,1,1,1.027745477,1.076495291,1.076495291,1.076495291,1.076495291,1,1,1,1,1,1 +Laboratory,1,1,1.027745477,1.076495291,1.076495291,1.076495291,1.076495291,1,1,1,1,1,1 +Mental,1,1,1.027745477,1.076495291,1.076495291,1.076495291,1.076495291,1,1,1,1,1,1 +Nursing_and_Midwifery,1,1,1.027745477,1.076495291,1.076495291,1.076495291,1.076495291,1,1,1,1,1,1 +Nutrition,1,1,1.027745477,1.076495291,1.076495291,1.076495291,1.076495291,1,1,1,1,1,1 +Pharmacy,1,1,1.027745477,1.076495291,1.076495291,1.076495291,1.076495291,1,1,1,1,1,1 +Radiography,1,1,1.027745477,1.076495291,1.076495291,1.076495291,1.076495291,1,1,1,1,1,1 From 53ff7fa02352959b61908d759cf25b0c480c4f45 Mon Sep 17 00:00:00 2001 From: Bingling Date: Thu, 21 May 2026 15:27:38 +0100 Subject: [PATCH 05/31] introduce absorption rate and rename parameters --- .../{default.csv => no_historical_growth.csv} | 0 src/tlo/methods/healthsystem.py | 18 +++++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) rename resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/{default.csv => no_historical_growth.csv} (100%) diff --git a/resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/default.csv b/resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/no_historical_growth.csv similarity index 100% rename from resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/default.csv rename to resources/healthsystem/human_resources/scaling_capabilities/ResourceFile_HR_scaling_by_year_and_officer_type/no_historical_growth.csv diff --git a/src/tlo/methods/healthsystem.py b/src/tlo/methods/healthsystem.py index 0bf9829068..f60fd750e8 100644 --- a/src/tlo/methods/healthsystem.py +++ b/src/tlo/methods/healthsystem.py @@ -321,7 +321,7 @@ class HealthSystem(Module): "Mode of HR scaling considered at the start of the simulation. This corresponds to the name" "of the worksheet in `ResourceFile_HR_scaling_by_year_and_officer_type.xlsx` that should be used. " "Options are: " - "`default` (capabilities are scaled by a factor of 1); " + "`no_historical_growth` (capabilities are scaled by a factor of 1); " "`historical_uniform` (factors across the cadres are the same, informated by the historical growth rate " "in total HRH);" "`historical_cadre_mix` (factors across the cadres are different, indicated by both the overall historical " @@ -330,6 +330,11 @@ class HealthSystem(Module): "Note that if use historical_ modes in this HRH scaling, use no_scaling mode in DynamicHRHScaling " "to avoid duplicates.", ), + "HR_expansion_absorption_rate": Parameter( + Types.FLOAT, + "Rate at which HRH expansion is absorbed into the system, expressed as a fraction of the total HRH " + "expansion per year. A value of 1 meaning 100% of the expansion is absorbed each year.", + ), "HR_scaling_by_district_table": Parameter( Types.DICT, "Factors by which daily capabilities in different districts will be" @@ -732,6 +737,8 @@ def read_consumables(filename): f"{self.parameters['HR_scaling_by_year_and_officer_type_mode']}" ) + self.parameters["HR_expansion_absorption_rate"] = 1 + self.parameters["HR_scaling_by_district_table"]: Dict = read_csv_files( path_to_resourcefiles_for_healthsystem / "human_resources" @@ -954,7 +961,7 @@ def initialise_simulation(self, sim): sim.schedule_event(DynamicRescalingHRCapabilities(self), Date(sim.date)) # Schedule recurring event which will rescale daily capabilities annually. - sim.schedule_event(RescalingHRCapabilities_ByYearAndOfficer(self), Date(sim.date)) + sim.schedule_event(RescalingHRCapabilities_ByYearAndOfficerAndAbsorption(self), Date(sim.date)) # Schedule the logger to occur at the start of every year sim.schedule_event(HealthSystemLogger(self), Date(sim.date.year, 1, 1)) @@ -3025,7 +3032,7 @@ def apply(self, population): ] -class RescalingHRCapabilities_ByYearAndOfficer(Event, PopulationScopeEventMixin): +class RescalingHRCapabilities_ByYearAndOfficerAndAbsorption(Event, PopulationScopeEventMixin): """This event exists to scale the daily capabilities, with a factor for each Officer Type at each specified year.""" def __init__(self, module): @@ -3048,9 +3055,10 @@ def apply(self, population): matches = re.match(pattern, officer) # Extract officer type officer_type = matches.group(2) - self.module._daily_capabilities[clinic][officer] *= HR_scaling_by_year_and_officer_type_factor.at[ + # Update capabilities by scaling factor and absorption rate + self.module._daily_capabilities[clinic][officer] *= (HR_scaling_by_year_and_officer_type_factor.at[ officer_type, most_recent_year_for_scaling - ] + ] * self.module.parameters["HR_absorption_rate"]) class RescaleHRCapabilities_ByDistrict(Event, PopulationScopeEventMixin): From 54a80103c6d43f0ed41d7d54b91d661081a31b1e Mon Sep 17 00:00:00 2001 From: Bingling Date: Thu, 21 May 2026 15:28:14 +0100 Subject: [PATCH 06/31] create the scenario file --- ...nario_historical_changes_in_hr_extended.py | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py diff --git a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py new file mode 100644 index 0000000000..138416efa4 --- /dev/null +++ b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py @@ -0,0 +1,184 @@ +"""This Scenario file run the model under different assumptions for the historical changes in Human Resources for Health + +Run on the batch system using: +``` +tlo batch-submit src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py +``` + +""" + +from pathlib import Path +from typing import Dict + +from tlo import Date, logging +from tlo.analysis.utils import get_parameters_for_status_quo, mix_scenarios +from tlo.methods.fullmodel import fullmodel +from tlo.methods.scenario_switcher import ImprovedHealthSystemAndCareSeekingScenarioSwitcher +from tlo.scenario import BaseScenario + + +class HistoricalChangesInHRH(BaseScenario): + def __init__(self): + super().__init__() + self.seed = 0 + self.start_date = Date(2010, 1, 1) + self.end_date = Date(2031, 1, 1) # <-- End at the end of year 2030 + self.pop_size = 100_000 + self._scenarios = self._get_scenarios() + self.number_of_draws = len(self._scenarios) + self.runs_per_draw = 5 + + def log_configuration(self): + return { + 'filename': 'historical_changes_in_hr_extended', + 'directory': Path('./outputs'), + 'custom_levels': { + '*': logging.WARNING, + 'tlo.methods.demography': logging.INFO, + 'tlo.methods.demography.detail': logging.WARNING, + 'tlo.methods.healthburden': logging.INFO, + 'tlo.methods.healthsystem': logging.WARNING, + 'tlo.methods.healthsystem.summary': logging.INFO, + } + } + + def modules(self): + return ( + fullmodel() + [ImprovedHealthSystemAndCareSeekingScenarioSwitcher()] + ) + + def draw_parameters(self, draw_number, rng): + if draw_number < self.number_of_draws: + return list(self._scenarios.values())[draw_number] + + def draw_name(self, draw_number) -> str: + """Store scenario name. + (This name can be retrieved by the plotting scripts to make the graphs be labelled nicely). + """ + if draw_number < self.number_of_draws: + return list(self._scenarios.keys())[draw_number] + + def _get_scenarios(self) -> Dict[str, Dict]: + """Return the Dict with values for the parameters that are changed, keyed by a name for the scenario.""" + + return { + "Baseline (no historical growth)": + self._common_baseline(), + + "Historical growth (uniform)": + self._hrh_growth_baseline(), + + "Historical growth (cadre-mix)": + mix_scenarios( + self._common_baseline(), + { + "HealthSystem": { + "HR_scaling_by_year_and_officer_type_mode": "historical_cadre_mix", + } + } + ), + + "Historical growth (uniform) + LCOA": + mix_scenarios( + self._hrh_growth_baseline(), + { + "HealthSystem": { + "policy_name": "LCOA_EHP", + } + } + ), + + "Historical growth (uniform) + Consumables (middle)": + mix_scenarios( + self._hrh_growth_baseline(), + { + "HealthSystem": { + "cons_availability_postSwitch": "scenario6", + } + } + ), + + "Historical growth (uniform) + Consumables (perfect)": + mix_scenarios( + self._hrh_growth_baseline(), + { + "HealthSystem": { + "cons_availability_postSwitch": "all", + } + } + ), + + "Historical growth (uniform) + Absorption (middle)": + mix_scenarios( + self._hrh_growth_baseline(), + { + "HealthSystem": { + "HR_expansion_absorption_rate": 0.5, + } + } + ), + + "Historical growth (uniform) + HealthSystemPerformance(max)": + mix_scenarios( + self._hrh_growth_baseline(), + { + 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { + 'max_healthsystem_function': [False, True], + 'year_of_switch': 2020, + } + } + ), + + } + + def _hrh_growth_baseline(self) -> Dict: + return mix_scenarios( + self._common_baseline(), + { + "HealthSystem": { + "HR_scaling_by_year_and_officer_type_mode": "historical_uniform", + } + }, + ) + + def _common_baseline(self) -> Dict: + return mix_scenarios( + get_parameters_for_status_quo(), + { + "HealthSystem": { + "mode_appt_constraints": 1, # <-- Mode 1 prior to change to preserve calibration + "mode_appt_constraints_postSwitch": 2, # <-- Mode 2 post-change to show effects of HRH + "scale_to_effective_capabilities": True, # <-- Transition into Mode2 with the effective capabilities in HRH 'revealed' in Mode 1 + "year_mode_switch": 2020, # <-- transition happens at start of 2020 when HRH starts to grow + + # Normalize the behaviour of Mode 2 + "policy_name": "Naive", # -- *For the alternative scenario of efficient implementation of EHP, otherwise use 'naive'* -- + "tclose_overwrite": 1, + "tclose_days_offset_overwrite": 7, + + # Clarify the consumable availability + "cons_availability": "default", + "cons_availability_postSwitch": "default", + "year_cons_availability_switch": 2020, + + # Clarify the historical HRH growth mode between 2020-2024 + "yearly_HR_scaling_mode": 'no_scaling', + "HR_scaling_by_year_and_officer_type_mode": 'no_historical_growth', + + # Clarify the HRH expansion absorption rate + "HR_absorption_rate": 1, + + }, + # -- *For the alternative scenario of increased demand and improved clinician performance* -- + 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { + 'max_healthcare_seeking': [False, False], # <-- switch from False to True mid-way + 'max_healthsystem_function': [False, False], + 'year_of_switch': 2020, + } + }, + ) + + +if __name__ == '__main__': + from tlo.cli import scenario_run + scenario_run([__file__]) From 51382c12bf7527a8f7251cbc1bc7c839acd22900 Mon Sep 17 00:00:00 2001 From: Bingling Date: Thu, 21 May 2026 16:22:28 +0100 Subject: [PATCH 07/31] add the new parameters in the healthsystem resource file --- resources/healthsystem/ResourceFile_HealthSystem_parameters.csv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/healthsystem/ResourceFile_HealthSystem_parameters.csv b/resources/healthsystem/ResourceFile_HealthSystem_parameters.csv index c6bd6414e7..9822923092 100644 --- a/resources/healthsystem/ResourceFile_HealthSystem_parameters.csv +++ b/resources/healthsystem/ResourceFile_HealthSystem_parameters.csv @@ -14,6 +14,8 @@ equip_availability_postSwitch,default year_equip_availability_switch,2100 tclose_overwrite,0 tclose_days_offset_overwrite,7 +HR_scaling_by_year_and_officer_type_mode,no_historical_growth +HR_expansion_absorption_rate,1.0 HR_scaling_by_level_and_officer_type_mode,default year_HR_scaling_by_level_and_officer_type,2100 HR_scaling_by_district_mode,default From 3bcc5abc2b617f71fd0133d5db0a5f3492e9a982 Mon Sep 17 00:00:00 2001 From: Bingling Date: Thu, 21 May 2026 16:22:57 +0100 Subject: [PATCH 08/31] fix error --- src/tlo/methods/healthsystem.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/tlo/methods/healthsystem.py b/src/tlo/methods/healthsystem.py index f60fd750e8..89419f2b26 100644 --- a/src/tlo/methods/healthsystem.py +++ b/src/tlo/methods/healthsystem.py @@ -331,7 +331,7 @@ class HealthSystem(Module): "to avoid duplicates.", ), "HR_expansion_absorption_rate": Parameter( - Types.FLOAT, + Types.REAL, "Rate at which HRH expansion is absorbed into the system, expressed as a fraction of the total HRH " "expansion per year. A value of 1 meaning 100% of the expansion is absorbed each year.", ), @@ -737,8 +737,6 @@ def read_consumables(filename): f"{self.parameters['HR_scaling_by_year_and_officer_type_mode']}" ) - self.parameters["HR_expansion_absorption_rate"] = 1 - self.parameters["HR_scaling_by_district_table"]: Dict = read_csv_files( path_to_resourcefiles_for_healthsystem / "human_resources" From 07683b7b5555795af042985306ccd399c338120a Mon Sep 17 00:00:00 2001 From: Bingling Date: Thu, 21 May 2026 16:45:51 +0100 Subject: [PATCH 09/31] fix year type error --- src/tlo/methods/healthsystem.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/tlo/methods/healthsystem.py b/src/tlo/methods/healthsystem.py index 89419f2b26..528ed0b4f2 100644 --- a/src/tlo/methods/healthsystem.py +++ b/src/tlo/methods/healthsystem.py @@ -3043,7 +3043,10 @@ def apply(self, population): "HR_scaling_by_year_and_officer_type_table" ][self.module.parameters["HR_scaling_by_year_and_officer_type_mode"]].set_index("Officer_Category") - years_for_scaling = np.array(list(HR_scaling_by_year_and_officer_type_factor.columns())) + HR_scaling_by_year_and_officer_type_factor.columns = ( + HR_scaling_by_year_and_officer_type_factor.columns.astype(int) + ) + years_for_scaling = np.array(HR_scaling_by_year_and_officer_type_factor.columns) most_recent_year_for_scaling = years_for_scaling[years_for_scaling <= self.sim.date.year].max() pattern = r"FacilityID_(\w+)_Officer_(\w+)" @@ -3056,7 +3059,7 @@ def apply(self, population): # Update capabilities by scaling factor and absorption rate self.module._daily_capabilities[clinic][officer] *= (HR_scaling_by_year_and_officer_type_factor.at[ officer_type, most_recent_year_for_scaling - ] * self.module.parameters["HR_absorption_rate"]) + ] * self.module.parameters["HR_expansion_absorption_rate"]) class RescaleHRCapabilities_ByDistrict(Event, PopulationScopeEventMixin): From f47f4f843b55098f5d1cbc1b9e293348a7e0e52c Mon Sep 17 00:00:00 2001 From: Bingling Date: Fri, 22 May 2026 07:16:48 +0100 Subject: [PATCH 10/31] correct absorption adjusted scaling factor --- src/tlo/methods/healthsystem.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tlo/methods/healthsystem.py b/src/tlo/methods/healthsystem.py index 528ed0b4f2..95cda68b0c 100644 --- a/src/tlo/methods/healthsystem.py +++ b/src/tlo/methods/healthsystem.py @@ -3057,9 +3057,11 @@ def apply(self, population): # Extract officer type officer_type = matches.group(2) # Update capabilities by scaling factor and absorption rate - self.module._daily_capabilities[clinic][officer] *= (HR_scaling_by_year_and_officer_type_factor.at[ + sf = HR_scaling_by_year_and_officer_type_factor.at[ officer_type, most_recent_year_for_scaling - ] * self.module.parameters["HR_expansion_absorption_rate"]) + ] + sf_ar_adjusted = 1 + (sf-1) * self.module.parameters["HR_expansion_absorption_rate"] + self.module._daily_capabilities[clinic][officer] *= sf_ar_adjusted class RescaleHRCapabilities_ByDistrict(Event, PopulationScopeEventMixin): From 2e14adec13e6c61dbaa340da557db4370b73af2f Mon Sep 17 00:00:00 2001 From: Bingling Date: Fri, 22 May 2026 07:34:14 +0100 Subject: [PATCH 11/31] correct absorption adjusted scaling factor --- src/tlo/methods/healthsystem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tlo/methods/healthsystem.py b/src/tlo/methods/healthsystem.py index 95cda68b0c..099c8b46de 100644 --- a/src/tlo/methods/healthsystem.py +++ b/src/tlo/methods/healthsystem.py @@ -3060,7 +3060,8 @@ def apply(self, population): sf = HR_scaling_by_year_and_officer_type_factor.at[ officer_type, most_recent_year_for_scaling ] - sf_ar_adjusted = 1 + (sf-1) * self.module.parameters["HR_expansion_absorption_rate"] + ar = self.module.parameters["HR_expansion_absorption_rate"] if sf >= 1 else 1 + sf_ar_adjusted = 1 + (sf-1) * ar self.module._daily_capabilities[clinic][officer] *= sf_ar_adjusted From 6453694cc9e7d222494405758ff6203f70042fb7 Mon Sep 17 00:00:00 2001 From: Bingling Date: Fri, 22 May 2026 11:19:29 +0100 Subject: [PATCH 12/31] initiate analysis file to extract data for Izzy --- ...lysis_historical_changes_in_hr_extended.py | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py diff --git a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py new file mode 100644 index 0000000000..7ffa715bb2 --- /dev/null +++ b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py @@ -0,0 +1,184 @@ +""" +Extract results of HRH staff counts, DALYs and Deaths across multiple historical HRH growth scenarios. +""" + +import argparse +import textwrap +from pathlib import Path +from typing import Tuple + +import numpy as np +import pandas as pd +from matplotlib import pyplot as plt + +from scripts.impact_of_historical_changes_in_hr.scenario_historical_changes_in_hr import ( + HistoricalChangesInHRH, +) +from tlo import Date +from tlo.analysis.utils import extract_results, make_age_grp_lookup, summarize + + +def apply(results_folder: Path, output_folder: Path, resourcefilepath: Path = None, the_target_period: Tuple[Date, Date] = None): + + TARGET_PERIOD = the_target_period + hrh_check_period = (Date(2020, 1, 1), Date(2030, 12, 31)) + + def target_period() -> str: + """Returns the target period as a string of the form YYYY-YYYY""" + return "-".join(str(t.year) for t in TARGET_PERIOD) + + def get_parameter_names_from_scenario_file() -> Tuple[str]: + """Get the tuple of names of the scenarios from `Scenario` class used to create the results.""" + e = HistoricalChangesInHRH() + return tuple(e._scenarios.keys()) + + def get_num_deaths(_df): + """Return total number of Deaths (total within the TARGET_PERIOD)""" + return pd.Series(data=len(_df.loc[pd.to_datetime(_df.date).between(*TARGET_PERIOD)])) + + def get_num_dalys(_df): + """Return total number of DALYS (Stacked) by label (total within the TARGET_PERIOD). + Throw error if not a record for every year in the TARGET PERIOD (to guard against inadvertently using + results from runs that crashed mid-way through the simulation. + """ + years_needed = [i.year for i in TARGET_PERIOD] + assert set(_df.year.unique()).issuperset(years_needed), "Some years are not recorded." + return pd.Series( + data=_df + .loc[_df.year.between(*years_needed)] + .drop(columns=['date', 'sex', 'age_range', 'year']) + .sum().sum() + ) + + def set_param_names_as_column_index_level_0(_df): + """Set the columns index (level 0) as the param_names.""" + ordered_param_names_no_prefix = {i: x for i, x in enumerate(param_names)} + names_of_cols_level0 = [ordered_param_names_no_prefix.get(col) for col in _df.columns.levels[0]] + assert len(names_of_cols_level0) == len(_df.columns.levels[0]) + _df.columns = _df.columns.set_levels(names_of_cols_level0, level=0) + return _df + + def get_total_num_dalys_by_label_htm(_df): + """Return the total number of DALYS in the TARGET_PERIOD by wealth and cause label.""" + y = _df \ + .loc[_df['year'].between(*[d.year for d in TARGET_PERIOD])] \ + .drop(columns=['date', 'year', 'sex', 'age_range']) \ + .sum(axis=0) + + # define course cause mapper for HIV, TB, MALARIA and OTHER + causes = { + 'AIDS': 'HIV/AIDS', + 'TB (non-AIDS)': 'TB', + 'Malaria': 'Malaria', + 'Lower respiratory infections': 'Lower respiratory infections', + 'Neonatal Disorders': 'Neonatal Disorders', + 'Maternal Disorders': 'Maternal Disorders', + '': 'Other', # defined in order to use this dict to determine ordering of the causes in output + } + causes_relabels = y.index.map(causes).fillna('Other') + + return y.groupby(by=causes_relabels).sum()[list(causes.values())] + + def get_total_num_dalys_by_label_all_causes(_df): + """Return the total number of DALYS in the TARGET_PERIOD cause label.""" + return _df \ + .loc[_df['year'].between(*[d.year for d in TARGET_PERIOD])] \ + .drop(columns=['date', 'year', 'age_range', 'sex']) \ + .sum(axis=0) + + # todo: to get HRH counts by cadre group and year + def get_staff_counts(_df): + _df = _df.loc[pd.to_datetime(_df['date']).between(*TARGET_PERIOD), :] + _df_staff = ( + pd.Series(_df.GenericClinic[0], name="staff_count") + .rename_axis("facility_officer") + .reset_index() + ) + + _df_staff[["facility_id", "officer_type"]] = _df_staff["facility_officer"].str.extract( + r"FacilityID_(\d+)_Officer_(.*)" + ) + + _df_staff["facility_id"] = _df_staff["facility_id"].astype(int) + + _df_staff = _df_staff[["facility_id", "officer_type", "staff_count"]] + + _df_staff = _df_staff.loc[_df_staff.officer_type != 'DCSA'] + + _df_staff = pd.Series(_df_staff.staff_count.sum()) + + _df_staff.index = [pd.to_datetime(_df["date"].iloc[0])] + _df_staff.name = 'yearly_staff_count' + + return _df_staff + + # %% Define parameter names + param_names = get_parameter_names_from_scenario_file() + + # HRH staff counts + hcw_count = extract_results( + results_folder, + module="tlo.methods.healthsystem.summary", + key="number_of_hcw_staff", + custom_generate_series=get_staff_counts, + do_scaling=False + ) + + # Absolute Number of Deaths and DALYs + num_deaths = extract_results( + results_folder, + module='tlo.methods.demography', + key='death', + custom_generate_series=get_num_deaths, + do_scaling=True + ).pipe(set_param_names_as_column_index_level_0) + + num_dalys = extract_results( + results_folder, + module='tlo.methods.healthburden', + key='dalys_stacked', + custom_generate_series=get_num_dalys, + do_scaling=True + ).pipe(set_param_names_as_column_index_level_0) + + # %% Total numbers of deaths / DALYS + num_dalys_summarized = summarize(num_dalys).loc[0].unstack().reindex(param_names) + num_deaths_summarized = summarize(num_deaths).loc[0].unstack().reindex(param_names) + + # Results by disease (HTM/OTHER and split by age/sex) + total_num_dalys_by_label_results = extract_results( + results_folder, + module="tlo.methods.healthburden", + key="dalys_stacked_by_age_and_time", + custom_generate_series=get_total_num_dalys_by_label_htm, + do_scaling=True, + ).pipe(set_param_names_as_column_index_level_0) + + total_num_dalys_by_label_results_all_causes = extract_results( + results_folder, + module="tlo.methods.healthburden", + key="dalys_stacked_by_age_and_time", + custom_generate_series=get_total_num_dalys_by_label_all_causes, + do_scaling=True, + ).pipe(set_param_names_as_column_index_level_0) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("results_folder", type=Path) # outputs/horizontal_and_vertical_programs-2024-05-16 + args = parser.parse_args() + + # Produce results for short-term analysis - 2020 - 2024 (incl.) + apply( + results_folder=args.results_folder, + output_folder=args.results_folder, + resourcefilepath=Path('./resources'), + the_target_period=(Date(2020, 1, 1), Date(2024, 12, 31)) + ) + # Produce results for only later period 2025-2030 (incl.) + apply( + results_folder=args.results_folder, + output_folder=args.results_folder, + resourcefilepath=Path('./resources'), + the_target_period=(Date(2025, 1, 1), Date(2030, 12, 31)) + ) From b9fc2ea47c32c7e677637058249af0957283c5c3 Mon Sep 17 00:00:00 2001 From: Bingling Date: Fri, 22 May 2026 11:20:44 +0100 Subject: [PATCH 13/31] correct scenario file and set for small local run to check HRH counts across scenarios --- ...cenario_historical_changes_in_hr_extended.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py index 138416efa4..4e1ceb72b5 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py @@ -1,10 +1,15 @@ -"""This Scenario file run the model under different assumptions for the historical changes in Human Resources for Health +"""This Scenario file runs the model under different assumptions for the historical changes in Human Resources for Health Run on the batch system using: ``` tlo batch-submit src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py ``` +Or locally using: +``` +tlo run src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py +``` + """ from pathlib import Path @@ -23,10 +28,10 @@ def __init__(self): self.seed = 0 self.start_date = Date(2010, 1, 1) self.end_date = Date(2031, 1, 1) # <-- End at the end of year 2030 - self.pop_size = 100_000 + self.pop_size = 50 self._scenarios = self._get_scenarios() self.number_of_draws = len(self._scenarios) - self.runs_per_draw = 5 + self.runs_per_draw = 1 def log_configuration(self): return { @@ -36,8 +41,8 @@ def log_configuration(self): '*': logging.WARNING, 'tlo.methods.demography': logging.INFO, 'tlo.methods.demography.detail': logging.WARNING, - 'tlo.methods.healthburden': logging.INFO, - 'tlo.methods.healthsystem': logging.WARNING, + # 'tlo.methods.healthburden': logging.INFO, + # 'tlo.methods.healthsystem': logging.WARNING, 'tlo.methods.healthsystem.summary': logging.INFO, } } @@ -166,7 +171,7 @@ def _common_baseline(self) -> Dict: "HR_scaling_by_year_and_officer_type_mode": 'no_historical_growth', # Clarify the HRH expansion absorption rate - "HR_absorption_rate": 1, + "HR_expansion_absorption_rate": 1.0, }, # -- *For the alternative scenario of increased demand and improved clinician performance* -- From 47b8b37fb085d881353ff6e58fe5b28072162a63 Mon Sep 17 00:00:00 2001 From: Bingling Date: Mon, 25 May 2026 23:46:50 +0100 Subject: [PATCH 14/31] fix the HRH growth class --- src/tlo/methods/healthsystem.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/tlo/methods/healthsystem.py b/src/tlo/methods/healthsystem.py index 099c8b46de..ea7aa280ba 100644 --- a/src/tlo/methods/healthsystem.py +++ b/src/tlo/methods/healthsystem.py @@ -3030,24 +3030,28 @@ def apply(self, population): ] -class RescalingHRCapabilities_ByYearAndOfficerAndAbsorption(Event, PopulationScopeEventMixin): +class RescalingHRCapabilities_ByYearAndOfficerAndAbsorption(RegularEvent, PopulationScopeEventMixin): """This event exists to scale the daily capabilities, with a factor for each Officer Type at each specified year.""" def __init__(self, module): - super().__init__(module) + # set the frequency + super().__init__(module, frequency=DateOffset(years=1)) - def apply(self, population): # Get the set of scaling_factors that are specified by the 'HR_scaling_by_level_and_officer_type_mode' # assumption - HR_scaling_by_year_and_officer_type_factor = self.module.parameters[ + self.HR_scaling_by_year_and_officer_type_factor = self.module.parameters[ "HR_scaling_by_year_and_officer_type_table" ][self.module.parameters["HR_scaling_by_year_and_officer_type_mode"]].set_index("Officer_Category") - HR_scaling_by_year_and_officer_type_factor.columns = ( - HR_scaling_by_year_and_officer_type_factor.columns.astype(int) + self.HR_scaling_by_year_and_officer_type_factor.columns = ( + self.HR_scaling_by_year_and_officer_type_factor.columns.astype(int) ) - years_for_scaling = np.array(HR_scaling_by_year_and_officer_type_factor.columns) - most_recent_year_for_scaling = years_for_scaling[years_for_scaling <= self.sim.date.year].max() + + self.years_for_scaling = np.array(self.HR_scaling_by_year_and_officer_type_factor.columns) + + def apply(self, population): + + most_recent_year_for_scaling = self.years_for_scaling[self.years_for_scaling <= self.sim.date.year].max() pattern = r"FacilityID_(\w+)_Officer_(\w+)" @@ -3057,7 +3061,7 @@ def apply(self, population): # Extract officer type officer_type = matches.group(2) # Update capabilities by scaling factor and absorption rate - sf = HR_scaling_by_year_and_officer_type_factor.at[ + sf = self.HR_scaling_by_year_and_officer_type_factor.at[ officer_type, most_recent_year_for_scaling ] ar = self.module.parameters["HR_expansion_absorption_rate"] if sf >= 1 else 1 From cc6c86c08b3f6d7eefe8bacf47a8cb9a5f4a2bfc Mon Sep 17 00:00:00 2001 From: Bingling Date: Mon, 25 May 2026 23:48:43 +0100 Subject: [PATCH 15/31] update analysis of HRH counts --- ...lysis_historical_changes_in_hr_extended.py | 103 ++++++++++++++---- 1 file changed, 80 insertions(+), 23 deletions(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py index 7ffa715bb2..bbe0a5e63a 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py @@ -11,7 +11,7 @@ import pandas as pd from matplotlib import pyplot as plt -from scripts.impact_of_historical_changes_in_hr.scenario_historical_changes_in_hr import ( +from scripts.impact_of_historical_changes_in_hr.scenario_historical_changes_in_hr_extended import ( HistoricalChangesInHRH, ) from tlo import Date @@ -21,7 +21,7 @@ def apply(results_folder: Path, output_folder: Path, resourcefilepath: Path = None, the_target_period: Tuple[Date, Date] = None): TARGET_PERIOD = the_target_period - hrh_check_period = (Date(2020, 1, 1), Date(2030, 12, 31)) + hrh_check_period = (Date(2019, 1, 1), Date(2030, 12, 31)) def target_period() -> str: """Returns the target period as a string of the form YYYY-YYYY""" @@ -86,44 +86,101 @@ def get_total_num_dalys_by_label_all_causes(_df): .drop(columns=['date', 'year', 'age_range', 'sex']) \ .sum(axis=0) - # todo: to get HRH counts by cadre group and year def get_staff_counts(_df): - _df = _df.loc[pd.to_datetime(_df['date']).between(*TARGET_PERIOD), :] - _df_staff = ( - pd.Series(_df.GenericClinic[0], name="staff_count") - .rename_axis("facility_officer") - .reset_index() + _df['year'] = _df['date'].dt.year + _df = _df.loc[pd.to_datetime(_df['date']).between(*hrh_check_period), ['year', 'GenericClinic'] + ].set_index('year').rename(columns={'GenericClinic': 'facility_officer'}) + _df_staff = _df['facility_officer'].apply(pd.Series).stack().reset_index() + _df_staff.columns = ['year', 'facility_officer', 'staff_count'] + _df_staff[['facility_id', 'officer_type']] = _df_staff['facility_officer'].str.extract( + r'FacilityID_(\d+)_Officer_(.*)' ) + _df_staff['facility_id'] = _df_staff['facility_id'].astype(int) - _df_staff[["facility_id", "officer_type"]] = _df_staff["facility_officer"].str.extract( - r"FacilityID_(\d+)_Officer_(.*)" - ) - - _df_staff["facility_id"] = _df_staff["facility_id"].astype(int) - - _df_staff = _df_staff[["facility_id", "officer_type", "staff_count"]] - - _df_staff = _df_staff.loc[_df_staff.officer_type != 'DCSA'] - - _df_staff = pd.Series(_df_staff.staff_count.sum()) - - _df_staff.index = [pd.to_datetime(_df["date"].iloc[0])] - _df_staff.name = 'yearly_staff_count' + main_cadres = ['Clinical', 'Nursing_and_Midwifery', 'Pharmacy', 'DCSA'] + _df_staff.loc[ + ~_df_staff["officer_type"].isin(main_cadres), + "officer_type" + ] = "Other" + _df_staff = _df_staff.groupby(['year', 'officer_type'])['staff_count'].sum() return _df_staff # %% Define parameter names param_names = get_parameter_names_from_scenario_file() # HRH staff counts - hcw_count = extract_results( + hcw_count = (extract_results( results_folder, module="tlo.methods.healthsystem.summary", key="number_of_hcw_staff", custom_generate_series=get_staff_counts, do_scaling=False + )).pipe(set_param_names_as_column_index_level_0) + hcw_count.columns = hcw_count.columns.get_level_values(0) + + hcw_count = ( + hcw_count.stack() + .reset_index(name='value') + .rename(columns={'draw': 'scenario'}) ) + hcw_count = hcw_count.sort_values(['officer_type', 'scenario', 'year']) + + hcw_count['scale_factor'] = ( + hcw_count['value'] / + hcw_count.groupby(['officer_type', 'scenario'])['value'].shift(1) + ) + + # marker for each officer type + markers = { + 'Clinical': 'o', + 'Nursing_and_Midwifery': 's', + 'Pharmacy': '^', + 'DCSA': 'D', + 'Other': 'X', + } + + # color for each scenario + cmap = plt.cm.get_cmap('tab10', len(param_names)) + colors = { + scenario: cmap(i) + for i, scenario in enumerate(param_names) + } + + fig, ax = plt.subplots() + + for officer in hcw_count['officer_type'].unique(): + for scenario in hcw_count['scenario'].unique(): + subset = hcw_count[ + (hcw_count['officer_type'] == officer) & + (hcw_count['scenario'] == scenario) + ] + + ax.plot( + subset['year'], + subset['value'], + linestyle="--", + marker=markers[officer], + color=colors[scenario], + label=f'{officer} - {scenario}', + ) + + ax.set_xlabel('Year') + ax.set_ylabel('Number of staff') + ax.set_xticks(sorted(hcw_count['year'].unique())) + + ax.legend( + loc='center left', + bbox_to_anchor=(1.02, 0.5), + fontsize=8 + ) + + plt.tight_layout() + + plt.show() + + # Absolute Number of Deaths and DALYs num_deaths = extract_results( results_folder, From c52ca6a8fd4b1564917d2b49b69a580313de3dc7 Mon Sep 17 00:00:00 2001 From: Bingling Date: Mon, 25 May 2026 23:52:21 +0100 Subject: [PATCH 16/31] update scenario file for small local run --- .../scenario_historical_changes_in_hr_extended.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py index 4e1ceb72b5..dab1a5f073 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py @@ -7,7 +7,9 @@ Or locally using: ``` -tlo run src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py + +tlo scenario-run src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py + ``` """ @@ -27,8 +29,8 @@ def __init__(self): super().__init__() self.seed = 0 self.start_date = Date(2010, 1, 1) - self.end_date = Date(2031, 1, 1) # <-- End at the end of year 2030 - self.pop_size = 50 + self.end_date = Date(2026, 1, 2) # <-- End at the end of year 2030 + self.pop_size = 30 self._scenarios = self._get_scenarios() self.number_of_draws = len(self._scenarios) self.runs_per_draw = 1 @@ -129,7 +131,6 @@ def _get_scenarios(self) -> Dict[str, Dict]: { 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { 'max_healthsystem_function': [False, True], - 'year_of_switch': 2020, } } ), From 014fb6f18f4ffd254113006818216fc14e65e989 Mon Sep 17 00:00:00 2001 From: Bingling Date: Tue, 26 May 2026 11:50:02 +0100 Subject: [PATCH 17/31] update analysis script to extract dalys and deaths by year. cause, draw, run. --- ...lysis_historical_changes_in_hr_extended.py | 120 ++++++------------ 1 file changed, 37 insertions(+), 83 deletions(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py index bbe0a5e63a..0d8dbd6920 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py @@ -21,34 +21,22 @@ def apply(results_folder: Path, output_folder: Path, resourcefilepath: Path = None, the_target_period: Tuple[Date, Date] = None): TARGET_PERIOD = the_target_period - hrh_check_period = (Date(2019, 1, 1), Date(2030, 12, 31)) - - def target_period() -> str: - """Returns the target period as a string of the form YYYY-YYYY""" - return "-".join(str(t.year) for t in TARGET_PERIOD) + hrh_check_period = (Date(2018, 1, 1), Date(2026, 1, 1)) def get_parameter_names_from_scenario_file() -> Tuple[str]: """Get the tuple of names of the scenarios from `Scenario` class used to create the results.""" e = HistoricalChangesInHRH() return tuple(e._scenarios.keys()) - def get_num_deaths(_df): + def get_num_deaths_by_year_cause(_df): """Return total number of Deaths (total within the TARGET_PERIOD)""" - return pd.Series(data=len(_df.loc[pd.to_datetime(_df.date).between(*TARGET_PERIOD)])) - - def get_num_dalys(_df): - """Return total number of DALYS (Stacked) by label (total within the TARGET_PERIOD). - Throw error if not a record for every year in the TARGET PERIOD (to guard against inadvertently using - results from runs that crashed mid-way through the simulation. - """ - years_needed = [i.year for i in TARGET_PERIOD] - assert set(_df.year.unique()).issuperset(years_needed), "Some years are not recorded." - return pd.Series( - data=_df - .loc[_df.year.between(*years_needed)] - .drop(columns=['date', 'sex', 'age_range', 'year']) - .sum().sum() - ) + _df = _df.loc[pd.to_datetime(_df.date).between(*TARGET_PERIOD)] + _df['year'] = _df['date'].dt.year + _df = _df.groupby(['year', 'cause']) \ + .agg({'person_id': 'count'}) \ + .rename(columns={'person_id': 'num_deaths'})['num_deaths'] + + return _df def set_param_names_as_column_index_level_0(_df): """Set the columns index (level 0) as the param_names.""" @@ -58,33 +46,18 @@ def set_param_names_as_column_index_level_0(_df): _df.columns = _df.columns.set_levels(names_of_cols_level0, level=0) return _df - def get_total_num_dalys_by_label_htm(_df): - """Return the total number of DALYS in the TARGET_PERIOD by wealth and cause label.""" - y = _df \ - .loc[_df['year'].between(*[d.year for d in TARGET_PERIOD])] \ - .drop(columns=['date', 'year', 'sex', 'age_range']) \ - .sum(axis=0) - - # define course cause mapper for HIV, TB, MALARIA and OTHER - causes = { - 'AIDS': 'HIV/AIDS', - 'TB (non-AIDS)': 'TB', - 'Malaria': 'Malaria', - 'Lower respiratory infections': 'Lower respiratory infections', - 'Neonatal Disorders': 'Neonatal Disorders', - 'Maternal Disorders': 'Maternal Disorders', - '': 'Other', # defined in order to use this dict to determine ordering of the causes in output - } - causes_relabels = y.index.map(causes).fillna('Other') - - return y.groupby(by=causes_relabels).sum()[list(causes.values())] - def get_total_num_dalys_by_label_all_causes(_df): """Return the total number of DALYS in the TARGET_PERIOD cause label.""" - return _df \ + _df = _df \ .loc[_df['year'].between(*[d.year for d in TARGET_PERIOD])] \ - .drop(columns=['date', 'year', 'age_range', 'sex']) \ - .sum(axis=0) + .drop(columns=['date', 'age_range', 'sex']) \ + .groupby('year') \ + .sum() \ + .reset_index() \ + .melt(id_vars='year', var_name='cause', value_name='dalys') \ + .set_index(['year', 'cause'])['dalys'] + + return _df def get_staff_counts(_df): _df['year'] = _df['date'].dt.year @@ -135,9 +108,9 @@ def get_staff_counts(_df): # marker for each officer type markers = { 'Clinical': 'o', - 'Nursing_and_Midwifery': 's', + 'Nursing_and_Midwifery': '*', 'Pharmacy': '^', - 'DCSA': 'D', + 'DCSA': 'd', 'Other': 'X', } @@ -148,7 +121,7 @@ def get_staff_counts(_df): for i, scenario in enumerate(param_names) } - fig, ax = plt.subplots() + fig, ax = plt.subplots(figsize=(12, 5)) for officer in hcw_count['officer_type'].unique(): for scenario in hcw_count['scenario'].unique(): @@ -180,44 +153,25 @@ def get_staff_counts(_df): plt.show() - # Absolute Number of Deaths and DALYs - num_deaths = extract_results( + num_deaths_by_year_cause = extract_results( results_folder, module='tlo.methods.demography', key='death', - custom_generate_series=get_num_deaths, + custom_generate_series=get_num_deaths_by_year_cause, do_scaling=True - ).pipe(set_param_names_as_column_index_level_0) + ).pipe(set_param_names_as_column_index_level_0).stack(['draw', 'run']).reset_index(name='num_deaths') - num_dalys = extract_results( - results_folder, - module='tlo.methods.healthburden', - key='dalys_stacked', - custom_generate_series=get_num_dalys, - do_scaling=True - ).pipe(set_param_names_as_column_index_level_0) - - # %% Total numbers of deaths / DALYS - num_dalys_summarized = summarize(num_dalys).loc[0].unstack().reindex(param_names) - num_deaths_summarized = summarize(num_deaths).loc[0].unstack().reindex(param_names) - - # Results by disease (HTM/OTHER and split by age/sex) - total_num_dalys_by_label_results = extract_results( - results_folder, - module="tlo.methods.healthburden", - key="dalys_stacked_by_age_and_time", - custom_generate_series=get_total_num_dalys_by_label_htm, - do_scaling=True, - ).pipe(set_param_names_as_column_index_level_0) - - total_num_dalys_by_label_results_all_causes = extract_results( + num_dalys_by_year_cause = extract_results( results_folder, module="tlo.methods.healthburden", key="dalys_stacked_by_age_and_time", custom_generate_series=get_total_num_dalys_by_label_all_causes, do_scaling=True, - ).pipe(set_param_names_as_column_index_level_0) + ).pipe(set_param_names_as_column_index_level_0).stack(['draw', 'run']).reset_index(name='num_dalys') + + num_dalys_by_year_cause.to_csv(output_folder / 'num_dalys_by_year_cause (for Izzy).csv', index=False) + num_deaths_by_year_cause.to_csv(output_folder / 'num_deaths_by_year_cause (for Izzy).csv', index=False) if __name__ == "__main__": @@ -225,17 +179,17 @@ def get_staff_counts(_df): parser.add_argument("results_folder", type=Path) # outputs/horizontal_and_vertical_programs-2024-05-16 args = parser.parse_args() - # Produce results for short-term analysis - 2020 - 2024 (incl.) - apply( - results_folder=args.results_folder, - output_folder=args.results_folder, - resourcefilepath=Path('./resources'), - the_target_period=(Date(2020, 1, 1), Date(2024, 12, 31)) - ) + # # Produce results for short-term analysis - 2020 - 2024 (incl.) + # apply( + # results_folder=args.results_folder, + # output_folder=args.results_folder, + # resourcefilepath=Path('./resources'), + # the_target_period=(Date(2020, 1, 1), Date(2024, 12, 31)) + # ) # Produce results for only later period 2025-2030 (incl.) apply( results_folder=args.results_folder, output_folder=args.results_folder, resourcefilepath=Path('./resources'), - the_target_period=(Date(2025, 1, 1), Date(2030, 12, 31)) + the_target_period=(Date(2020, 1, 1), Date(2030, 12, 31)) ) From a2c61406c60a17f7d617caad28994c1ca320f7f4 Mon Sep 17 00:00:00 2001 From: Bingling Date: Tue, 26 May 2026 12:07:20 +0100 Subject: [PATCH 18/31] prepare scenario file for full run --- .../scenario_historical_changes_in_hr_extended.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py index dab1a5f073..68fc2a8545 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py @@ -29,11 +29,11 @@ def __init__(self): super().__init__() self.seed = 0 self.start_date = Date(2010, 1, 1) - self.end_date = Date(2026, 1, 2) # <-- End at the end of year 2030 - self.pop_size = 30 + self.end_date = Date(2031, 1, 1) # <-- End at the end of year 2030 + self.pop_size = 100_000 # <-- Local small run: 30 self._scenarios = self._get_scenarios() self.number_of_draws = len(self._scenarios) - self.runs_per_draw = 1 + self.runs_per_draw = 5 def log_configuration(self): return { @@ -43,8 +43,8 @@ def log_configuration(self): '*': logging.WARNING, 'tlo.methods.demography': logging.INFO, 'tlo.methods.demography.detail': logging.WARNING, - # 'tlo.methods.healthburden': logging.INFO, - # 'tlo.methods.healthsystem': logging.WARNING, + 'tlo.methods.healthburden': logging.INFO, + 'tlo.methods.healthsystem': logging.WARNING, 'tlo.methods.healthsystem.summary': logging.INFO, } } From 8f78c81ba6463d2d52d31814168a803f27912f63 Mon Sep 17 00:00:00 2001 From: Bingling Date: Thu, 28 May 2026 10:36:20 +0100 Subject: [PATCH 19/31] submit full runs, using the same pop size with the previous study and 5 runs per draw --- .../scenario_historical_changes_in_hr_extended.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py index 68fc2a8545..91528ca4cf 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py @@ -30,7 +30,7 @@ def __init__(self): self.seed = 0 self.start_date = Date(2010, 1, 1) self.end_date = Date(2031, 1, 1) # <-- End at the end of year 2030 - self.pop_size = 100_000 # <-- Local small run: 30 + self.pop_size = 20_000 # <-- Local small run: 30; previous study run: 20_000 self._scenarios = self._get_scenarios() self.number_of_draws = len(self._scenarios) self.runs_per_draw = 5 From 6a7bf5b24d5a1ce1d486d1e4984715ca7ced16b3 Mon Sep 17 00:00:00 2001 From: Bingling Date: Tue, 2 Jun 2026 10:54:44 +0100 Subject: [PATCH 20/31] plots to check results --- ...lysis_historical_changes_in_hr_extended.py | 291 +++++++++++++++++- 1 file changed, 287 insertions(+), 4 deletions(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py index 0d8dbd6920..c917e4d783 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py @@ -79,10 +79,116 @@ def get_staff_counts(_df): _df_staff = _df_staff.groupby(['year', 'officer_type'])['staff_count'].sum() return _df_staff + make_graph_file_name = lambda stub: output_folder / f"{stub.replace('*', '_star_')}.png" # noqa: E731 + + _, age_grp_lookup = make_age_grp_lookup() + + def target_period() -> str: + """Returns the target period as a string of the form YYYY-YYYY""" + return "-".join(str(t.year) for t in TARGET_PERIOD) + + def get_num_deaths(_df): + """Return total number of Deaths (total within the TARGET_PERIOD)""" + return pd.Series(data=len(_df.loc[pd.to_datetime(_df.date).between(*TARGET_PERIOD)])) + + def get_num_dalys(_df): + """Return total number of DALYS (Stacked) by label (total within the TARGET_PERIOD). + Throw error if not a record for every year in the TARGET PERIOD (to guard against inadvertently using + results from runs that crashed mid-way through the simulation. + """ + years_needed = [i.year for i in TARGET_PERIOD] + assert set(_df.year.unique()).issuperset(years_needed), "Some years are not recorded." + return pd.Series( + data=_df + .loc[_df.year.between(*years_needed)] + .drop(columns=['date', 'sex', 'age_range', 'year']) + .sum().sum() + ) + + def find_difference_relative_to_comparison_series( + _ser: pd.Series, + comparison: str, + scaled: bool = False, + drop_comparison: bool = True, + ): + """Find the difference in the values in a pd.Series with a multi-index, between the draws (level 0) + within the runs (level 1), relative to where draw = `comparison`. + The comparison is `X - COMPARISON`.""" + return _ser \ + .unstack(level=0) \ + .apply(lambda x: (x - x[comparison]) / (x[comparison] if scaled else 1.0), axis=1) \ + .drop(columns=([comparison] if drop_comparison else [])) \ + .stack() + + def find_difference_relative_to_comparison_series_dataframe(_df: pd.DataFrame, **kwargs): + """Apply `find_difference_relative_to_comparison_series` to each row in a dataframe""" + return pd.concat({ + _idx: find_difference_relative_to_comparison_series(row, **kwargs) + for _idx, row in _df.iterrows() + }, axis=1).T + + def do_bar_plot_with_ci(_df, annotations=None, xticklabels_horizontal_and_wrapped=False, put_labels_in_legend=True): + """Make a vertical bar plot for each row of _df, using the columns to identify the height of the bar and the + extent of the error bar.""" + + substitute_labels = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + + yerr = np.array([ + (_df['mean'] - _df['lower']).values, + (_df['upper'] - _df['mean']).values, + ]) + + xticks = {(i + 0.5): k for i, k in enumerate(_df.index)} + + # Define colormap (used only with option `put_labels_in_legend=True`) + cmap = plt.get_cmap("tab20") + rescale = lambda y: (y - np.min(y)) / (np.max(y) - np.min(y)) # noqa: E731 + colors = list(map(cmap, rescale(np.array(list(xticks.keys()))))) if put_labels_in_legend else None + + fig, ax = plt.subplots(figsize=(10, 5)) + ax.bar( + xticks.keys(), + _df['mean'].values, + yerr=yerr, + alpha=0.8, + ecolor='black', + color=colors, + capsize=10, + label=xticks.values(), + zorder=100, + ) + if annotations: + for xpos, ypos, text in zip(xticks.keys(), _df['upper'].values, annotations): + ax.text(xpos, ypos*1.15, text, horizontalalignment='center', rotation='vertical', fontsize='x-small') + ax.set_xticks(list(xticks.keys())) + + if put_labels_in_legend: + # Update xticks label with substitute labels + # Insert legend with updated labels that shows correspondence between substitute label and original label + xtick_values = [letter for letter, label in zip(substitute_labels, xticks.values())] + xtick_legend = [f'{letter}: {label}' for letter, label in zip(substitute_labels, xticks.values())] + h, _ = ax.get_legend_handles_labels() + ax.legend(h, xtick_legend, loc='center left', fontsize='small', bbox_to_anchor=(1, 0.5)) + ax.set_xticklabels(list(xtick_values)) + else: + if not xticklabels_horizontal_and_wrapped: + # xticklabels will be vertical and not wrapped + ax.set_xticklabels(list(xticks.values()), rotation=90) + else: + wrapped_labs = ["\n".join(textwrap.wrap(_lab, 20)) for _lab in xticks.values()] + ax.set_xticklabels(wrapped_labs) + + ax.grid(axis="y") + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + fig.tight_layout() + + return fig, ax + # %% Define parameter names param_names = get_parameter_names_from_scenario_file() - # HRH staff counts + # Check HRH staff counts hcw_count = (extract_results( results_folder, module="tlo.methods.healthsystem.summary", @@ -90,6 +196,7 @@ def get_staff_counts(_df): custom_generate_series=get_staff_counts, do_scaling=False )).pipe(set_param_names_as_column_index_level_0) + hcw_count = hcw_count.loc[:, hcw_count.columns.get_level_values("run") == 0] hcw_count.columns = hcw_count.columns.get_level_values(0) hcw_count = ( @@ -105,6 +212,7 @@ def get_staff_counts(_df): hcw_count.groupby(['officer_type', 'scenario'])['value'].shift(1) ) + from matplotlib.lines import Line2D # marker for each officer type markers = { 'Clinical': 'o', @@ -113,6 +221,13 @@ def get_staff_counts(_df): 'DCSA': 'd', 'Other': 'X', } + marker_sizes = { + 'Clinical': 6, + 'Nursing_and_Midwifery': 8, + 'Pharmacy': 6, + 'DCSA': 6, + 'Other': 6, + } # color for each scenario cmap = plt.cm.get_cmap('tab10', len(param_names)) @@ -121,6 +236,7 @@ def get_staff_counts(_df): for i, scenario in enumerate(param_names) } + name_of_plot = "Number of healthcare workers" fig, ax = plt.subplots(figsize=(12, 5)) for officer in hcw_count['officer_type'].unique(): @@ -135,6 +251,7 @@ def get_staff_counts(_df): subset['value'], linestyle="--", marker=markers[officer], + markersize=marker_sizes[officer], color=colors[scenario], label=f'{officer} - {scenario}', ) @@ -143,17 +260,183 @@ def get_staff_counts(_df): ax.set_ylabel('Number of staff') ax.set_xticks(sorted(hcw_count['year'].unique())) + officer_handles = [ + Line2D( + [0], + [0], + marker=markers[officer], + color='black', + linestyle='None', + markersize=8, + label=officer + ) + for officer in markers.keys() + ] + + legend_officer = ax.legend( + handles=officer_handles, + title='Officer type', + loc='center left', + bbox_to_anchor=(1.02, 0.65), + fontsize=8, + title_fontsize=9 + ) + ax.add_artist(legend_officer) + + scenario_handles = [ + Line2D( + [0], + [0], + color=colors[scenario], + linestyle='--', + linewidth=2, + label=scenario + ) + for scenario in colors.keys() + ] + ax.legend( + handles=scenario_handles, + title='Scenario', loc='center left', - bbox_to_anchor=(1.02, 0.5), - fontsize=8 + bbox_to_anchor=(1.02, 0.25), + fontsize=8, + title_fontsize=9 ) plt.tight_layout() - + fig.savefig(make_graph_file_name(name_of_plot.replace(' ', '_').replace(',', ''))) plt.show() + plt.close(fig) + + # Check total DALYs + # %% Define parameter names + counterfactual_scenario = 'Baseline (no historical growth)' + actual_scenario = 'Historical growth (uniform)' # Absolute Number of Deaths and DALYs + num_deaths = extract_results( + results_folder, + module='tlo.methods.demography', + key='death', + custom_generate_series=get_num_deaths, + do_scaling=True + ).pipe(set_param_names_as_column_index_level_0) + + num_dalys = extract_results( + results_folder, + module='tlo.methods.healthburden', + key='dalys_stacked', + custom_generate_series=get_num_dalys, + do_scaling=True + ).pipe(set_param_names_as_column_index_level_0) + + # %% Charts of total numbers of deaths / DALYS + num_dalys_summarized = summarize(num_dalys).loc[0].unstack().reindex(param_names) + num_deaths_summarized = summarize(num_deaths).loc[0].unstack().reindex(param_names) + + name_of_plot = f'Deaths, {target_period()}' + fig, ax = do_bar_plot_with_ci(num_deaths_summarized / 1e6, xticklabels_horizontal_and_wrapped=True, + put_labels_in_legend=True) + ax.set_title(name_of_plot) + ax.set_ylabel('(Millions)') + fig.tight_layout() + ax.axhline(num_deaths_summarized.loc[counterfactual_scenario, 'mean'] / 1e6, color='black', alpha=0.5) + fig.savefig(make_graph_file_name(name_of_plot.replace(' ', '_').replace(',', ''))) + fig.show() + plt.close(fig) + + name_of_plot = f'DALYs, {target_period()}' + fig, ax = do_bar_plot_with_ci(num_dalys_summarized / 1e6, xticklabels_horizontal_and_wrapped=True, + put_labels_in_legend=True) + ax.set_title(name_of_plot) + ax.set_ylabel('(Millions)') + ax.axhline(num_dalys_summarized.loc[counterfactual_scenario, 'mean'] / 1e6, color='black', alpha=0.5) + fig.tight_layout() + fig.savefig(make_graph_file_name(name_of_plot.replace(' ', '_').replace(',', ''))) + fig.show() + plt.close(fig) + + # %% Deaths and DALYS averted relative to Actual + num_deaths_averted = summarize( + -1.0 * + pd.DataFrame( + find_difference_relative_to_comparison_series( + num_deaths.loc[0], + comparison=actual_scenario) + ).T + ).iloc[0].unstack().reindex(param_names).drop([actual_scenario]) + + pc_deaths_averted = 100.0 * summarize( + -1.0 * + pd.DataFrame( + find_difference_relative_to_comparison_series( + num_deaths.loc[0], + comparison=actual_scenario, + scaled=True) + ).T + ).iloc[0].unstack().reindex(param_names).drop([actual_scenario]) + + num_dalys_averted = summarize( + -1.0 * + pd.DataFrame( + find_difference_relative_to_comparison_series( + num_dalys.loc[0], + comparison=actual_scenario) + ).T + ).iloc[0].unstack().reindex(param_names).drop([actual_scenario]) + + pc_dalys_averted = 100.0 * summarize( + -1.0 * + pd.DataFrame( + find_difference_relative_to_comparison_series( + num_dalys.loc[0], + comparison=actual_scenario, + scaled=True) + ).T + ).iloc[0].unstack().reindex(param_names).drop([actual_scenario]) + + # DEATHS + name_of_plot = f'Deaths Averted vs Actual, {target_period()}' + fig, ax = do_bar_plot_with_ci( + pc_deaths_averted, # num_deaths_averted + annotations=None, + put_labels_in_legend=True, + xticklabels_horizontal_and_wrapped=True, + ) + # annotation = ( + # f"{int(round(num_deaths_averted.loc[actual_scenario, 'mean'], -3))} ({int(round(num_deaths_averted.loc[actual_scenario, 'lower'], -3))} - {int(round(num_deaths_averted.loc[actual_scenario, 'upper'], -3))})\n" + # f"{round(pc_deaths_averted.loc[actual_scenario, 'mean'])} ({round(pc_deaths_averted.loc[actual_scenario, 'lower'], 1)} - {round(pc_deaths_averted.loc[actual_scenario, 'upper'], 1)})% of that in Counterfactual" + # ) + # ax.set_title(f"{name_of_plot}\n{annotation}") + ax.set_ylabel('Deaths Averted vs Historical growth (uniform)') + # fig.set_figwidth(5) + fig.tight_layout() + fig.savefig(make_graph_file_name(name_of_plot.replace(' ', '_').replace(',', ''))) + fig.show() + plt.close(fig) + + # DALYS + name_of_plot = f'DALYs Averted vs Actual, {target_period()}' + fig, ax = do_bar_plot_with_ci( + pc_dalys_averted, # (num_dalys_averted / 1e6), + annotations=None, + put_labels_in_legend=True, + xticklabels_horizontal_and_wrapped=True, + ) + # annotation = ( + # f"{int(round(num_dalys_averted.loc[actual_scenario, 'mean'], -4))} ({int(round(num_dalys_averted.loc[actual_scenario, 'lower'], -4))} - {int(round(num_dalys_averted.loc[actual_scenario, 'upper'], -4))})\n" + # f"{round(pc_dalys_averted.loc[actual_scenario, 'mean'])} ({round(pc_dalys_averted.loc[actual_scenario, 'lower'], 1)} - {round(pc_dalys_averted.loc[actual_scenario, 'upper'], 1)})% of that in Counterfactual" + # ) + # ax.set_title(f"{name_of_plot}\n{annotation}") + ax.set_ylabel('DALYS Averted vs Historical growth (uniform)') + # fig.set_figwidth(5) + fig.tight_layout() + fig.savefig(make_graph_file_name(name_of_plot.replace(' ', '_').replace(',', ''))) + fig.show() + plt.close(fig) + + # Prepare Absolute Number of Deaths and DALYs for Izzy num_deaths_by_year_cause = extract_results( results_folder, module='tlo.methods.demography', From 5dab9e6e534c89b33e06caa16457264003b3020c Mon Sep 17 00:00:00 2001 From: Bingling Date: Tue, 2 Jun 2026 11:09:39 +0100 Subject: [PATCH 21/31] update scenarios: add "Best settings assembled" --- ...nario_historical_changes_in_hr_extended.py | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py index 91528ca4cf..9dbac709e6 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py @@ -69,7 +69,7 @@ def _get_scenarios(self) -> Dict[str, Dict]: """Return the Dict with values for the parameters that are changed, keyed by a name for the scenario.""" return { - "Baseline (no historical growth)": + "Counterfactual (no historical growth)": self._common_baseline(), "Historical growth (uniform)": @@ -125,15 +125,27 @@ def _get_scenarios(self) -> Dict[str, Dict]: } ), - "Historical growth (uniform) + HealthSystemPerformance(max)": - mix_scenarios( - self._hrh_growth_baseline(), - { - 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { - 'max_healthsystem_function': [False, True], + "Best settings assembled": + mix_scenarios( + self._common_baseline(), + { + "HealthSystem": { + "HR_scaling_by_year_and_officer_type_mode": "historical_cadre_mix", + "policy_name": "LCOA_EHP", + "cons_availability_postSwitch": "all", + } } - } - ), + ), + + # "Historical growth (uniform) + HealthSystemPerformance(max)": + # mix_scenarios( + # self._hrh_growth_baseline(), + # { + # 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { + # 'max_healthsystem_function': [False, True], + # } + # } + # ), } From bea97c7fe1b04571dfbee1b45c16b8517bd7bcc9 Mon Sep 17 00:00:00 2001 From: Bingling Date: Tue, 2 Jun 2026 11:16:25 +0100 Subject: [PATCH 22/31] refactor plot texts --- .../analysis_historical_changes_in_hr_extended.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py index c917e4d783..a86e109865 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py @@ -311,7 +311,7 @@ def do_bar_plot_with_ci(_df, annotations=None, xticklabels_horizontal_and_wrappe # Check total DALYs # %% Define parameter names - counterfactual_scenario = 'Baseline (no historical growth)' + counterfactual_scenario = 'Counterfactual (no historical growth)' actual_scenario = 'Historical growth (uniform)' # Absolute Number of Deaths and DALYs @@ -397,7 +397,7 @@ def do_bar_plot_with_ci(_df, annotations=None, xticklabels_horizontal_and_wrappe ).iloc[0].unstack().reindex(param_names).drop([actual_scenario]) # DEATHS - name_of_plot = f'Deaths Averted vs Actual, {target_period()}' + name_of_plot = f'Deaths Averted vs Historical growth (uniform), {target_period()}' fig, ax = do_bar_plot_with_ci( pc_deaths_averted, # num_deaths_averted annotations=None, @@ -408,7 +408,7 @@ def do_bar_plot_with_ci(_df, annotations=None, xticklabels_horizontal_and_wrappe # f"{int(round(num_deaths_averted.loc[actual_scenario, 'mean'], -3))} ({int(round(num_deaths_averted.loc[actual_scenario, 'lower'], -3))} - {int(round(num_deaths_averted.loc[actual_scenario, 'upper'], -3))})\n" # f"{round(pc_deaths_averted.loc[actual_scenario, 'mean'])} ({round(pc_deaths_averted.loc[actual_scenario, 'lower'], 1)} - {round(pc_deaths_averted.loc[actual_scenario, 'upper'], 1)})% of that in Counterfactual" # ) - # ax.set_title(f"{name_of_plot}\n{annotation}") + ax.set_title(f"{name_of_plot}") ax.set_ylabel('Deaths Averted vs Historical growth (uniform)') # fig.set_figwidth(5) fig.tight_layout() @@ -417,7 +417,7 @@ def do_bar_plot_with_ci(_df, annotations=None, xticklabels_horizontal_and_wrappe plt.close(fig) # DALYS - name_of_plot = f'DALYs Averted vs Actual, {target_period()}' + name_of_plot = f'DALYs Averted vs Historical growth (uniform), {target_period()}' fig, ax = do_bar_plot_with_ci( pc_dalys_averted, # (num_dalys_averted / 1e6), annotations=None, @@ -428,7 +428,7 @@ def do_bar_plot_with_ci(_df, annotations=None, xticklabels_horizontal_and_wrappe # f"{int(round(num_dalys_averted.loc[actual_scenario, 'mean'], -4))} ({int(round(num_dalys_averted.loc[actual_scenario, 'lower'], -4))} - {int(round(num_dalys_averted.loc[actual_scenario, 'upper'], -4))})\n" # f"{round(pc_dalys_averted.loc[actual_scenario, 'mean'])} ({round(pc_dalys_averted.loc[actual_scenario, 'lower'], 1)} - {round(pc_dalys_averted.loc[actual_scenario, 'upper'], 1)})% of that in Counterfactual" # ) - # ax.set_title(f"{name_of_plot}\n{annotation}") + ax.set_title(f"{name_of_plot}") ax.set_ylabel('DALYS Averted vs Historical growth (uniform)') # fig.set_figwidth(5) fig.tight_layout() From 0722f0ba17fb0e51e79530a12d31800942799cec Mon Sep 17 00:00:00 2001 From: Bingling Date: Tue, 2 Jun 2026 11:20:43 +0100 Subject: [PATCH 23/31] refactor --- ...nario_historical_changes_in_hr_extended.py | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py index 9dbac709e6..44c788f74b 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py @@ -125,27 +125,27 @@ def _get_scenarios(self) -> Dict[str, Dict]: } ), - "Best settings assembled": - mix_scenarios( - self._common_baseline(), - { - "HealthSystem": { - "HR_scaling_by_year_and_officer_type_mode": "historical_cadre_mix", - "policy_name": "LCOA_EHP", - "cons_availability_postSwitch": "all", - } - } - ), - - # "Historical growth (uniform) + HealthSystemPerformance(max)": - # mix_scenarios( - # self._hrh_growth_baseline(), - # { - # 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { - # 'max_healthsystem_function': [False, True], + # "Best settings assembled": + # mix_scenarios( + # self._common_baseline(), + # { + # "HealthSystem": { + # "HR_scaling_by_year_and_officer_type_mode": "historical_cadre_mix", + # "policy_name": "LCOA_EHP", + # "cons_availability_postSwitch": "all", + # } # } - # } - # ), + # ), + + "Historical growth (uniform) + HealthSystemPerformance(max)": + mix_scenarios( + self._hrh_growth_baseline(), + { + 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { + 'max_healthsystem_function': [False, True], + } + } + ), } From 10fbee7aed2b1b49ada9817d5280c3606499b271 Mon Sep 17 00:00:00 2001 From: Bingling Date: Wed, 3 Jun 2026 14:09:13 +0100 Subject: [PATCH 24/31] add in more scenarios and keep the order of existing scenarios to make use of already output results --- ...nario_historical_changes_in_hr_extended.py | 95 +++++++++++++++---- 1 file changed, 76 insertions(+), 19 deletions(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py index 44c788f74b..54d8710ad9 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py @@ -69,10 +69,10 @@ def _get_scenarios(self) -> Dict[str, Dict]: """Return the Dict with values for the parameters that are changed, keyed by a name for the scenario.""" return { - "Counterfactual (no historical growth)": + "Main Counterfactual": self._common_baseline(), - "Historical growth (uniform)": + "Main Actual": self._hrh_growth_baseline(), "Historical growth (cadre-mix)": @@ -85,7 +85,7 @@ def _get_scenarios(self) -> Dict[str, Dict]: } ), - "Historical growth (uniform) + LCOA": + "Historical growth + LCOA policy": mix_scenarios( self._hrh_growth_baseline(), { @@ -95,7 +95,7 @@ def _get_scenarios(self) -> Dict[str, Dict]: } ), - "Historical growth (uniform) + Consumables (middle)": + "Historical growth + Consumables (better)": mix_scenarios( self._hrh_growth_baseline(), { @@ -105,7 +105,7 @@ def _get_scenarios(self) -> Dict[str, Dict]: } ), - "Historical growth (uniform) + Consumables (perfect)": + "Historical growth + Consumables (perfect)": mix_scenarios( self._hrh_growth_baseline(), { @@ -115,7 +115,7 @@ def _get_scenarios(self) -> Dict[str, Dict]: } ), - "Historical growth (uniform) + Absorption (middle)": + "Historical growth + Low absorption rate": mix_scenarios( self._hrh_growth_baseline(), { @@ -125,19 +125,7 @@ def _get_scenarios(self) -> Dict[str, Dict]: } ), - # "Best settings assembled": - # mix_scenarios( - # self._common_baseline(), - # { - # "HealthSystem": { - # "HR_scaling_by_year_and_officer_type_mode": "historical_cadre_mix", - # "policy_name": "LCOA_EHP", - # "cons_availability_postSwitch": "all", - # } - # } - # ), - - "Historical growth (uniform) + HealthSystemPerformance(max)": + "Historical growth + Max HS performance": mix_scenarios( self._hrh_growth_baseline(), { @@ -147,6 +135,75 @@ def _get_scenarios(self) -> Dict[str, Dict]: } ), + "No growth + LCOA policy": + mix_scenarios( + self._common_baseline(), + { + "HealthSystem": { + "policy_name": "LCOA_EHP", + } + } + ), + + "No growth + Consumables (better)": + mix_scenarios( + self._common_baseline(), + { + "HealthSystem": { + "cons_availability_postSwitch": "scenario6", + } + } + ), + + "No growth + Consumables (perfect)": + mix_scenarios( + self._common_baseline(), + { + "HealthSystem": { + "cons_availability_postSwitch": "all", + } + } + ), + + "No growth + Max HS performance": + mix_scenarios( + self._common_baseline(), + { + 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { + 'max_healthsystem_function': [False, True], + } + } + ), + + "No growth + Upper bound settings": + mix_scenarios( + self._common_baseline(), + { + 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { + 'max_healthsystem_function': [False, True], + }, + "HealthSystem": { + "policy_name": "LCOA_EHP", + "cons_availability_postSwitch": "all", + } + } + ), + + "Historical growth + Upper bound settings": + mix_scenarios( + self._common_baseline(), + { + 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { + 'max_healthsystem_function': [False, True], + }, + "HealthSystem": { + "policy_name": "LCOA_EHP", + "cons_availability_postSwitch": "all", + "HR_scaling_by_year_and_officer_type_mode": "historical_cadre_mix", + } + } + ), + } def _hrh_growth_baseline(self) -> Dict: From 54da78ff865b7aa6dd9a0eab65fcd76ea1e6460a Mon Sep 17 00:00:00 2001 From: Bingling Date: Wed, 3 Jun 2026 14:17:15 +0100 Subject: [PATCH 25/31] correct hs setting in the upper bound scenarios --- .../scenario_historical_changes_in_hr_extended.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py index 54d8710ad9..1a32015609 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py @@ -180,7 +180,7 @@ def _get_scenarios(self) -> Dict[str, Dict]: self._common_baseline(), { 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { - 'max_healthsystem_function': [False, True], + 'max_healthsystem_function': [False, False], }, "HealthSystem": { "policy_name": "LCOA_EHP", @@ -194,7 +194,7 @@ def _get_scenarios(self) -> Dict[str, Dict]: self._common_baseline(), { 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { - 'max_healthsystem_function': [False, True], + 'max_healthsystem_function': [False, False], }, "HealthSystem": { "policy_name": "LCOA_EHP", From 7c27d89cc966292c8442f4bd94fc7c72b46e71ac Mon Sep 17 00:00:00 2001 From: Bingling Date: Wed, 3 Jun 2026 14:23:36 +0100 Subject: [PATCH 26/31] update the scenario names --- .../analysis_historical_changes_in_hr_extended.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py index a86e109865..9a8fd6d3d6 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py @@ -311,8 +311,8 @@ def do_bar_plot_with_ci(_df, annotations=None, xticklabels_horizontal_and_wrappe # Check total DALYs # %% Define parameter names - counterfactual_scenario = 'Counterfactual (no historical growth)' - actual_scenario = 'Historical growth (uniform)' + counterfactual_scenario = 'Main Counterfactual' + actual_scenario = 'Main Actual' # Absolute Number of Deaths and DALYs num_deaths = extract_results( From 6c4480e3d92e29515fca176521a9675ce9b82381 Mon Sep 17 00:00:00 2001 From: Bingling Date: Wed, 3 Jun 2026 15:05:30 +0100 Subject: [PATCH 27/31] extract number of treatments, upon analysis needs --- ...lysis_historical_changes_in_hr_extended.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py index 9a8fd6d3d6..6c15b25d72 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py @@ -127,6 +127,18 @@ def find_difference_relative_to_comparison_series_dataframe(_df: pd.DataFrame, * for _idx, row in _df.iterrows() }, axis=1).T + def get_num_treatments_by_year_treatment(_df): + """Return the number of treatments by short treatment id and year (within the TARGET_PERIOD)""" + _df['year'] = _df['date'].dt.year + _df = _df.loc[pd.to_datetime(_df.date).between(*TARGET_PERIOD), ['year', 'TREATMENT_ID']].set_index('year') + _df = _df['TREATMENT_ID'].apply(pd.Series) + _df.columns = _df.columns.map(lambda x: x.split('_')[0] + "*") + _df = _df.T.groupby(level=0).sum().T + _df = _df.stack() + _df.index = _df.index.set_names(["year", "treatment_type"]) + _df.name = "count" + return _df + def do_bar_plot_with_ci(_df, annotations=None, xticklabels_horizontal_and_wrapped=False, put_labels_in_legend=True): """Make a vertical bar plot for each row of _df, using the columns to identify the height of the bar and the extent of the error bar.""" @@ -453,8 +465,20 @@ def do_bar_plot_with_ci(_df, annotations=None, xticklabels_horizontal_and_wrappe do_scaling=True, ).pipe(set_param_names_as_column_index_level_0).stack(['draw', 'run']).reset_index(name='num_dalys') + # And absolute Number of treatments upon analysis needs + num_treatments_by_year_treatment = extract_results( + results_folder, + module='tlo.methods.healthsystem.summary', + key='HSI_Event_non_blank_appt_footprint', + custom_generate_series=get_num_treatments_by_year_treatment, + do_scaling=True + ).pipe(set_param_names_as_column_index_level_0).stack(['draw', 'run']).reset_index(name='num_treatments') + num_dalys_by_year_cause.to_csv(output_folder / 'num_dalys_by_year_cause (for Izzy).csv', index=False) num_deaths_by_year_cause.to_csv(output_folder / 'num_deaths_by_year_cause (for Izzy).csv', index=False) + num_treatments_by_year_treatment.to_csv( + output_folder / 'num_treatments_by_year_treatment (for Izzy).csv', index=False + ) if __name__ == "__main__": From 8eba439abec0c473bdb1fb140b9c317a32c7ff31 Mon Sep 17 00:00:00 2001 From: Bingling Date: Thu, 4 Jun 2026 11:08:13 +0100 Subject: [PATCH 28/31] submit additional scenarios --- ...nario_historical_changes_in_hr_extended.py | 134 +++++++++--------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py index 1a32015609..2ba3f9bb0c 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py @@ -30,10 +30,10 @@ def __init__(self): self.seed = 0 self.start_date = Date(2010, 1, 1) self.end_date = Date(2031, 1, 1) # <-- End at the end of year 2030 - self.pop_size = 20_000 # <-- Local small run: 30; previous study run: 20_000 + self.pop_size = 20_000 # <-- Local small run: 30; previous study run: 20_000; for publication: 100_000 self._scenarios = self._get_scenarios() self.number_of_draws = len(self._scenarios) - self.runs_per_draw = 5 + self.runs_per_draw = 5 # for publication: 10 def log_configuration(self): return { @@ -69,71 +69,71 @@ def _get_scenarios(self) -> Dict[str, Dict]: """Return the Dict with values for the parameters that are changed, keyed by a name for the scenario.""" return { - "Main Counterfactual": - self._common_baseline(), - - "Main Actual": - self._hrh_growth_baseline(), - - "Historical growth (cadre-mix)": - mix_scenarios( - self._common_baseline(), - { - "HealthSystem": { - "HR_scaling_by_year_and_officer_type_mode": "historical_cadre_mix", - } - } - ), - - "Historical growth + LCOA policy": - mix_scenarios( - self._hrh_growth_baseline(), - { - "HealthSystem": { - "policy_name": "LCOA_EHP", - } - } - ), - - "Historical growth + Consumables (better)": - mix_scenarios( - self._hrh_growth_baseline(), - { - "HealthSystem": { - "cons_availability_postSwitch": "scenario6", - } - } - ), - - "Historical growth + Consumables (perfect)": - mix_scenarios( - self._hrh_growth_baseline(), - { - "HealthSystem": { - "cons_availability_postSwitch": "all", - } - } - ), - - "Historical growth + Low absorption rate": - mix_scenarios( - self._hrh_growth_baseline(), - { - "HealthSystem": { - "HR_expansion_absorption_rate": 0.5, - } - } - ), - - "Historical growth + Max HS performance": - mix_scenarios( - self._hrh_growth_baseline(), - { - 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { - 'max_healthsystem_function': [False, True], - } - } - ), + # "Main Counterfactual": + # self._common_baseline(), + # + # "Main Actual": + # self._hrh_growth_baseline(), + # + # "Historical growth (cadre-mix)": + # mix_scenarios( + # self._common_baseline(), + # { + # "HealthSystem": { + # "HR_scaling_by_year_and_officer_type_mode": "historical_cadre_mix", + # } + # } + # ), + # + # "Historical growth + LCOA policy": + # mix_scenarios( + # self._hrh_growth_baseline(), + # { + # "HealthSystem": { + # "policy_name": "LCOA_EHP", + # } + # } + # ), + # + # "Historical growth + Consumables (better)": + # mix_scenarios( + # self._hrh_growth_baseline(), + # { + # "HealthSystem": { + # "cons_availability_postSwitch": "scenario6", + # } + # } + # ), + # + # "Historical growth + Consumables (perfect)": + # mix_scenarios( + # self._hrh_growth_baseline(), + # { + # "HealthSystem": { + # "cons_availability_postSwitch": "all", + # } + # } + # ), + # + # "Historical growth + Low absorption rate": + # mix_scenarios( + # self._hrh_growth_baseline(), + # { + # "HealthSystem": { + # "HR_expansion_absorption_rate": 0.5, + # } + # } + # ), + # + # "Historical growth + Max HS performance": + # mix_scenarios( + # self._hrh_growth_baseline(), + # { + # 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { + # 'max_healthsystem_function': [False, True], + # } + # } + # ), "No growth + LCOA policy": mix_scenarios( From bfea07133f85582b22f0e35d6474d8f4cd93c19d Mon Sep 17 00:00:00 2001 From: Bingling Date: Sat, 6 Jun 2026 10:16:05 +0100 Subject: [PATCH 29/31] plot all scenarios --- ...nario_historical_changes_in_hr_extended.py | 130 +++++++++--------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py index 2ba3f9bb0c..a7be1d0e65 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py @@ -69,71 +69,71 @@ def _get_scenarios(self) -> Dict[str, Dict]: """Return the Dict with values for the parameters that are changed, keyed by a name for the scenario.""" return { - # "Main Counterfactual": - # self._common_baseline(), - # - # "Main Actual": - # self._hrh_growth_baseline(), - # - # "Historical growth (cadre-mix)": - # mix_scenarios( - # self._common_baseline(), - # { - # "HealthSystem": { - # "HR_scaling_by_year_and_officer_type_mode": "historical_cadre_mix", - # } - # } - # ), - # - # "Historical growth + LCOA policy": - # mix_scenarios( - # self._hrh_growth_baseline(), - # { - # "HealthSystem": { - # "policy_name": "LCOA_EHP", - # } - # } - # ), - # - # "Historical growth + Consumables (better)": - # mix_scenarios( - # self._hrh_growth_baseline(), - # { - # "HealthSystem": { - # "cons_availability_postSwitch": "scenario6", - # } - # } - # ), - # - # "Historical growth + Consumables (perfect)": - # mix_scenarios( - # self._hrh_growth_baseline(), - # { - # "HealthSystem": { - # "cons_availability_postSwitch": "all", - # } - # } - # ), - # - # "Historical growth + Low absorption rate": - # mix_scenarios( - # self._hrh_growth_baseline(), - # { - # "HealthSystem": { - # "HR_expansion_absorption_rate": 0.5, - # } - # } - # ), - # - # "Historical growth + Max HS performance": - # mix_scenarios( - # self._hrh_growth_baseline(), - # { - # 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { - # 'max_healthsystem_function': [False, True], - # } - # } - # ), + "Main Counterfactual": + self._common_baseline(), + + "Main Actual": + self._hrh_growth_baseline(), + + "Historical growth (cadre-mix)": + mix_scenarios( + self._common_baseline(), + { + "HealthSystem": { + "HR_scaling_by_year_and_officer_type_mode": "historical_cadre_mix", + } + } + ), + + "Historical growth + LCOA policy": + mix_scenarios( + self._hrh_growth_baseline(), + { + "HealthSystem": { + "policy_name": "LCOA_EHP", + } + } + ), + + "Historical growth + Consumables (better)": + mix_scenarios( + self._hrh_growth_baseline(), + { + "HealthSystem": { + "cons_availability_postSwitch": "scenario6", + } + } + ), + + "Historical growth + Consumables (perfect)": + mix_scenarios( + self._hrh_growth_baseline(), + { + "HealthSystem": { + "cons_availability_postSwitch": "all", + } + } + ), + + "Historical growth + Low absorption rate": + mix_scenarios( + self._hrh_growth_baseline(), + { + "HealthSystem": { + "HR_expansion_absorption_rate": 0.5, + } + } + ), + + "Historical growth + Max HS performance": + mix_scenarios( + self._hrh_growth_baseline(), + { + 'ImprovedHealthSystemAndCareSeekingScenarioSwitcher': { + 'max_healthsystem_function': [False, True], + } + } + ), "No growth + LCOA policy": mix_scenarios( From 70bfee79929dde5396a3e2602589ed58c525f28f Mon Sep 17 00:00:00 2001 From: Bingling Date: Mon, 8 Jun 2026 15:23:54 +0100 Subject: [PATCH 30/31] update scenario names --- .../analysis_historical_changes_in_hr_extended.py | 4 ++-- .../scenario_historical_changes_in_hr_extended.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py index 6c15b25d72..a4a78eb7c6 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py @@ -242,7 +242,7 @@ def do_bar_plot_with_ci(_df, annotations=None, xticklabels_horizontal_and_wrappe } # color for each scenario - cmap = plt.cm.get_cmap('tab10', len(param_names)) + cmap = plt.cm.get_cmap('tab20', len(param_names)) colors = { scenario: cmap(i) for i, scenario in enumerate(param_names) @@ -323,7 +323,7 @@ def do_bar_plot_with_ci(_df, annotations=None, xticklabels_horizontal_and_wrappe # Check total DALYs # %% Define parameter names - counterfactual_scenario = 'Main Counterfactual' + counterfactual_scenario = 'Main Counterfactual/No growth + Lower bound settings' actual_scenario = 'Main Actual' # Absolute Number of Deaths and DALYs diff --git a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py index a7be1d0e65..ccfc79aa78 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/scenario_historical_changes_in_hr_extended.py @@ -69,7 +69,7 @@ def _get_scenarios(self) -> Dict[str, Dict]: """Return the Dict with values for the parameters that are changed, keyed by a name for the scenario.""" return { - "Main Counterfactual": + "Main Counterfactual/No growth + Lower bound settings": self._common_baseline(), "Main Actual": @@ -115,7 +115,7 @@ def _get_scenarios(self) -> Dict[str, Dict]: } ), - "Historical growth + Low absorption rate": + "Historical growth + Low absorption rate/Historical growth + Lower bound settings": mix_scenarios( self._hrh_growth_baseline(), { From 200df4717bc624b0618f6473f62d11a92eca540c Mon Sep 17 00:00:00 2001 From: Bingling Date: Mon, 8 Jun 2026 15:27:47 +0100 Subject: [PATCH 31/31] extract HRH count data for Izzy --- .../analysis_historical_changes_in_hr_extended.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py index a4a78eb7c6..1a333af5eb 100644 --- a/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py +++ b/src/scripts/impact_of_historical_changes_in_hr/analysis_historical_changes_in_hr_extended.py @@ -479,6 +479,7 @@ def do_bar_plot_with_ci(_df, annotations=None, xticklabels_horizontal_and_wrappe num_treatments_by_year_treatment.to_csv( output_folder / 'num_treatments_by_year_treatment (for Izzy).csv', index=False ) + hcw_count.to_csv(output_folder / 'hcw_count (for Izzy).csv', index=False) if __name__ == "__main__":