Skip to content

Commit e805c62

Browse files
Increase range of options for yearly rescaling of HR capabilities (#1297)
* Increase range of options for yearly rescaling of HR capabilities * Style fix * Fix cond statement * refactor to put two resource files for scaling into same folder * elaborate comment * move checks to point of reading-in data * update comments concerning the first scheduled occurennce of DynamicRescalinHRCapabilities * guarantee correct read-in of types * tidy formatting of xlsx * engross comment * let the default be called something more informative ('no_scaling') and provide 'scaling_by_population_growth' to give easy access to that configuration * check that there is an entry that is early enough for the first call in the simulation * add test for using sequence of factors for dynamic scaling * [OPTIONAL - DISCUSS WITH MM] possible refactor for readability and speed (avoid access DataFrame using .loc and .iloc repeatedely) * isort linting * ruff linting --------- Co-authored-by: Tim Hallett <[email protected]>
1 parent 2f16e28 commit e805c62

File tree

5 files changed

+158
-43
lines changed

5 files changed

+158
-43
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:e36cbd225191f893f2de5f0f34adc251046af5ec58daaf7c86b09a6c83c1e31d
3-
size 379
2+
oid sha256:0efd27feb7dbb09331cb29ebeb10d8428981c7246c4fdee5ebacf510b9514795
3+
size 372
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:75ac6ae6b873d2b6e95090da64da98c5646e21a6c485ee6a10d5b8bc0e352950
3+
size 10780

src/tlo/methods/healthsystem.py

+67-24
Original file line numberDiff line numberDiff line change
@@ -554,14 +554,24 @@ class HealthSystem(Module):
554554
"and custom (user can freely set these factors as parameters in the analysis).",
555555
),
556556

557-
'dynamic_HR_scaling_factor': Parameter(
558-
Types.REAL, "Factor by which HR capabilities are scaled at regular intervals of 1 year (in addition to the"
559-
" [optional] scaling with population size (controlled by `scale_HR_by_popsize`"
557+
'yearly_HR_scaling': Parameter(
558+
Types.DICT, "Factors by which HR capabilities are scaled. "
559+
"Each sheet specifies a 'mode' for dynamic HR scaling. The mode to use is determined by the "
560+
"parameter `yearly_HR_scaling_mode`. Each sheet must have the same format, including the same "
561+
"column headers. On each sheet, the first row (for `2010`, when the simulation starts) "
562+
"specifies the initial configuration: `dynamic_HR_scaling_factor` (float) is the factor by "
563+
"which all human resoucres capabilities and multiplied; `scale_HR_by_popsize` (bool) specifies "
564+
"whether the capabilities should (also) grow by the factor by which the population has grown in"
565+
" the last year. Each subsequent row specifies a year where there should be a CHANGE in the "
566+
"configuration. If there are no further rows, then there is no change. But, for example, an"
567+
" additional row of the form ```2015, 1.05, TRUE``` would mean that on 1st January of 2015, "
568+
"2016, 2017, ....(and the rest of the simulation), the capabilities would increase by the "
569+
"product of 1.05 and by the ratio of the population size to that in the year previous."
560570
),
561571

562-
'scale_HR_by_popsize': Parameter(
563-
Types.BOOL, "Decide whether to scale HR capabilities by population size every year. Can be used as well as"
564-
" the dynamic_HR_scaling_factor"
572+
'yearly_HR_scaling_mode': Parameter(
573+
Types.STRING, "Specifies which of the policies in yearly_HR_scaling should be adopted. This corresponds to"
574+
"a worksheet of the file `ResourceFile_dynamic_HR_scaling.xlsx`."
565575
),
566576

567577
'tclose_overwrite': Parameter(
@@ -685,6 +695,7 @@ def __init__(
685695
'LCOA_EHP']
686696
self.arg_policy_name = policy_name
687697

698+
688699
self.tclose_overwrite = None
689700
self.tclose_days_offset_overwrite = None
690701

@@ -820,19 +831,36 @@ def read_parameters(self, data_folder):
820831
self.parameters['const_HR_scaling_table']: Dict = pd.read_excel(
821832
path_to_resourcefiles_for_healthsystem /
822833
"human_resources" /
823-
"const_HR_scaling" /
834+
"scaling_capabilities" /
824835
"ResourceFile_const_HR_scaling.xlsx",
825836
sheet_name=None # all sheets read in
826837
)
838+
# Ensure the mode of HR scaling to be considered in included in the tables loaded
839+
assert self.parameters['const_HR_scaling_mode'] in self.parameters['const_HR_scaling_table'], \
840+
f"Value of `const_HR_scaling_mode` not recognised: {self.parameters['const_HR_scaling_mode']}"
841+
842+
self.parameters['yearly_HR_scaling']: Dict = pd.read_excel(
843+
path_to_resourcefiles_for_healthsystem /
844+
"human_resources" /
845+
"scaling_capabilities" /
846+
"ResourceFile_dynamic_HR_scaling.xlsx",
847+
sheet_name=None, # all sheets read in
848+
dtype={
849+
'year': int,
850+
'dynamic_HR_scaling_factor': float,
851+
'scale_HR_by_popsize': bool
852+
} # Ensure that these column are read as the right type
853+
)
854+
# Ensure the mode of yearly HR scaling to be considered in included in the tables loaded
855+
assert self.parameters['yearly_HR_scaling_mode'] in self.parameters['yearly_HR_scaling'], \
856+
f"Value of `yearly_HR_scaling` not recognised: {self.parameters['yearly_HR_scaling_mode']}"
857+
# Ensure that a value for the year at the start of the simulation is provided.
858+
assert all(2010 in sheet['year'].values for sheet in self.parameters['yearly_HR_scaling'].values())
827859

828860

829861
def pre_initialise_population(self):
830862
"""Generate the accessory classes used by the HealthSystem and pass to them the data that has been read."""
831863

832-
# Ensure the mode of HR scaling to be considered in included in the tables loaded
833-
assert self.parameters['const_HR_scaling_mode'] in self.parameters['const_HR_scaling_table'], \
834-
f"Value of `const_HR_scaling_mode` not recognised: {self.parameters['const_HR_scaling_mode']}"
835-
836864
# Create dedicated RNGs for separate functions done by the HealthSystem module
837865
self.rng_for_hsi_queue = np.random.RandomState(self.rng.randint(2 ** 31 - 1))
838866
self.rng_for_dx = np.random.RandomState(self.rng.randint(2 ** 31 - 1))
@@ -914,10 +942,10 @@ def initialise_simulation(self, sim):
914942
sim.schedule_event(HealthSystemChangeMode(self),
915943
Date(self.parameters["year_mode_switch"], 1, 1))
916944

917-
# Schedule recurring event which will rescale daily capabilities at regular intervals.
918-
# The first event scheduled will only be used to update self.last_year_pop_size parameter,
919-
# actual scaling will only take effect from 2011 onwards
920-
sim.schedule_event(DynamicRescalingHRCapabilities(self), Date(sim.date) + pd.DateOffset(years=1))
945+
# Schedule recurring event which will rescale daily capabilities (at yearly intervals).
946+
# The first event scheduled for the start of the simulation is only used to update self.last_year_pop_size,
947+
# whilst the actual scaling will only take effect from 2011 onwards.
948+
sim.schedule_event(DynamicRescalingHRCapabilities(self), Date(sim.date))
921949

922950
def on_birth(self, mother_id, child_id):
923951
self.bed_days.on_birth(self.sim.population.props, mother_id, child_id)
@@ -2861,27 +2889,42 @@ class DynamicRescalingHRCapabilities(RegularEvent, PopulationScopeEventMixin):
28612889
""" This event exists to scale the daily capabilities assumed at fixed time intervals"""
28622890
def __init__(self, module):
28632891
super().__init__(module, frequency=DateOffset(years=1))
2864-
self.last_year_pop_size = self.current_pop_size # store population size at initiation (when this class is
2865-
# created)
2892+
self.last_year_pop_size = self.current_pop_size # will store population size at initiation (when this class is
2893+
# created, at the start of the simulation)
2894+
2895+
# Store the sequence of updates as a dict of the form
2896+
# {<year_of_change>: {`dynamic_HR_scaling_factor`: float, `scale_HR_by_popsize`: bool}}
2897+
self.scaling_values = self.module.parameters['yearly_HR_scaling'][
2898+
self.module.parameters['yearly_HR_scaling_mode']].set_index("year").to_dict("index")
28662899

28672900
@property
28682901
def current_pop_size(self) -> float:
28692902
"""Returns current population size"""
28702903
df = self.sim.population.props
28712904
return df.is_alive.sum()
28722905

2873-
def apply(self, population):
2906+
def _get_most_recent_year_specified_for_a_change_in_configuration(self) -> int:
2907+
"""Get the most recent year (in the past), for which there is an entry in `parameters['yearly_HR_scaling']`."""
2908+
years = np.array(list(self.scaling_values.keys()))
2909+
return years[years <= self.sim.date.year].max()
28742910

2911+
def apply(self, population):
2912+
"""Do the scaling on the capabilities based on instruction that is in force at this time."""
2913+
# Get current population size
28752914
this_year_pop_size = self.current_pop_size
28762915

2877-
# Rescale by fixed amount
2878-
self.module._daily_capabilities *= self.module.parameters['dynamic_HR_scaling_factor']
2916+
# Get the configuration to apply now (the latest entry in the `parameters['yearly_HR_scaling']`)
2917+
config = self.scaling_values.get(self._get_most_recent_year_specified_for_a_change_in_configuration())
2918+
2919+
# ... Do the rescaling specified for this year by the specified factor
2920+
self.module._daily_capabilities *= config['dynamic_HR_scaling_factor']
28792921

2880-
# Rescale daily capabilities by population size, if this option is included
2881-
if self.module.parameters['scale_HR_by_popsize']:
2882-
self.module._daily_capabilities *= this_year_pop_size/self.last_year_pop_size
2922+
# ... If requested, also do the scaling for the population growth that has occurred since the last year
2923+
if config['scale_HR_by_popsize']:
2924+
self.module._daily_capabilities *= this_year_pop_size / self.last_year_pop_size
28832925

2884-
self.last_year_pop_size = this_year_pop_size # Save for next year
2926+
# Save current population size as that for 'last year'.
2927+
self.last_year_pop_size = this_year_pop_size
28852928

28862929

28872930
class HealthSystemChangeMode(RegularEvent, PopulationScopeEventMixin):

tests/test_healthsystem.py

+86-17
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import heapq as hp
22
import os
33
from pathlib import Path
4-
from typing import Set, Tuple
4+
from typing import Dict, Set, Tuple
55

66
import numpy as np
77
import pandas as pd
@@ -2244,8 +2244,8 @@ def get_hsi_log(service_availability, randomise_hsi_queue) -> pd.DataFrame:
22442244

22452245

22462246
def test_const_HR_scaling_assumption(seed, tmpdir):
2247-
"""Check that we can use the parameter `const_HR_scaling_mode` to manipulate the minutes of time available for healthcare
2248-
workers."""
2247+
"""Check that we can use the parameter `const_HR_scaling_mode` to manipulate the minutes of time available for
2248+
healthcare workers."""
22492249

22502250
def get_capabilities_today(const_HR_scaling_mode: str) -> pd.Series:
22512251
sim = Simulation(start_date=start_date, seed=seed)
@@ -2292,25 +2292,28 @@ def get_initial_capabilities() -> pd.Series:
22922292

22932293
return sim.modules['HealthSystem'].capabilities_today
22942294

2295-
def get_capabilities_after_two_years(dynamic_HR_scaling_factor: float, scale_HR_by_pop_size: bool) -> tuple:
2295+
def get_capabilities_after_two_updates(dynamic_HR_scaling_factor: float, scale_HR_by_pop_size: bool) -> tuple:
22962296
sim = Simulation(start_date=start_date, seed=seed)
22972297
sim.register(
22982298
demography.Demography(resourcefilepath=resourcefilepath),
22992299
healthsystem.HealthSystem(resourcefilepath=resourcefilepath),
23002300
simplified_births.SimplifiedBirths(resourcefilepath=resourcefilepath),
23012301

23022302
)
2303-
sim.modules['HealthSystem'].parameters['dynamic_HR_scaling_factor'] = dynamic_HR_scaling_factor
2304-
sim.modules['HealthSystem'].parameters['scale_HR_by_popsize'] = scale_HR_by_pop_size
2305-
sim.make_initial_population(n=100)
2306-
2307-
# Ensure simulation lasts long enough so that current capabilities reflect that used in the third year of
2308-
# simulation (i.e. after two annual updates)
2309-
sim.simulate(end_date=start_date + pd.DateOffset(years=2, days=1))
2303+
params = sim.modules['HealthSystem'].parameters
2304+
df = params['yearly_HR_scaling'][params['yearly_HR_scaling_mode']]
2305+
df.loc[df['year'] == 2010, 'dynamic_HR_scaling_factor'] = dynamic_HR_scaling_factor
2306+
df.loc[df['year'] == 2010, 'scale_HR_by_popsize'] = scale_HR_by_pop_size
2307+
popsize = 100
2308+
sim.make_initial_population(n=popsize)
23102309

2311-
popsize = sim.modules['Demography'].popsize_by_year
2310+
# Ensure simulation lasts long enough so that current capabilities reflect that used after two updates
2311+
# (updates occur on 1st Jan, starting in 2010, so simulation should stop on 2nd Jan 2011).
2312+
sim.simulate(end_date=Date(2011, 1, 2))
23122313

2313-
final_popsize_increase = popsize[2012]/popsize[2010]
2314+
popsize_start = popsize
2315+
popsize_curr = sim.population.props['is_alive'].sum()
2316+
final_popsize_increase = popsize_curr / popsize_start
23142317

23152318
return sim.modules['HealthSystem'].capabilities_today, final_popsize_increase
23162319

@@ -2321,17 +2324,17 @@ def get_capabilities_after_two_years(dynamic_HR_scaling_factor: float, scale_HR_
23212324
initial_caps = initial_caps[initial_caps != 0]
23222325

23232326
# Check that dynamic expansion over two years leads to expansion = dynamic_HR_scaling_factor^2
2324-
caps, final_popsize_increase = get_capabilities_after_two_years(
2327+
caps, final_popsize_increase = get_capabilities_after_two_updates(
23252328
dynamic_HR_scaling_factor=dynamic_HR_scaling_factor,
23262329
scale_HR_by_pop_size=False
23272330
)
23282331
caps = caps[caps != 0]
23292332
ratio_in_sim = caps/initial_caps
2330-
expected_value = dynamic_HR_scaling_factor*dynamic_HR_scaling_factor
2333+
expected_value = dynamic_HR_scaling_factor * dynamic_HR_scaling_factor
23312334
assert np.allclose(ratio_in_sim, expected_value)
23322335

23332336
# Check that expansion over two years with scaling prop to pop expansion works as expected
2334-
caps, final_popsize_increase = get_capabilities_after_two_years(
2337+
caps, final_popsize_increase = get_capabilities_after_two_updates(
23352338
dynamic_HR_scaling_factor=1.0,
23362339
scale_HR_by_pop_size=True
23372340
)
@@ -2341,11 +2344,77 @@ def get_capabilities_after_two_years(dynamic_HR_scaling_factor: float, scale_HR_
23412344
assert np.allclose(ratio_in_sim, expected_value)
23422345

23432346
# Check that expansion over two years with both fixed scaling and pop expansion scaling works as expected
2344-
caps, final_popsize_increase = get_capabilities_after_two_years(
2347+
caps, final_popsize_increase = get_capabilities_after_two_updates(
23452348
dynamic_HR_scaling_factor=dynamic_HR_scaling_factor,
23462349
scale_HR_by_pop_size=True
23472350
)
23482351
caps = caps[caps != 0]
23492352
ratio_in_sim = caps/initial_caps
23502353
expected_value = final_popsize_increase*dynamic_HR_scaling_factor*dynamic_HR_scaling_factor
23512354
assert np.allclose(ratio_in_sim, expected_value)
2355+
2356+
2357+
def test_dynamic_HR_scaling_multiple_changes(seed, tmpdir):
2358+
"""Check that we can scale the minutes of time available for healthcare workers with a sequence of factors that
2359+
apply in different years."""
2360+
2361+
def get_initial_capabilities() -> pd.Series:
2362+
sim = Simulation(start_date=start_date, seed=seed)
2363+
sim.register(
2364+
demography.Demography(resourcefilepath=resourcefilepath),
2365+
healthsystem.HealthSystem(resourcefilepath=resourcefilepath)
2366+
)
2367+
sim.make_initial_population(n=100)
2368+
sim.simulate(end_date=start_date + pd.DateOffset(days=0))
2369+
2370+
return sim.modules['HealthSystem'].capabilities_today
2371+
2372+
def run_sim(dynamic_HR_scaling_factor: Dict[int, float]) -> tuple:
2373+
"""Run simulation for 10 years, with a sequence of factors that apply, specified in a dict of the form
2374+
{year: factor_to_apply_this_year_and_subsequent_years_until_next_instruction} (i.e. how the ResourceFile should
2375+
be structured.)
2376+
Returns capabilities at the end of the 10-year simulation"""
2377+
2378+
sim = Simulation(start_date=start_date, seed=seed)
2379+
sim.register(
2380+
demography.Demography(resourcefilepath=resourcefilepath),
2381+
healthsystem.HealthSystem(resourcefilepath=resourcefilepath),
2382+
simplified_births.SimplifiedBirths(resourcefilepath=resourcefilepath),
2383+
2384+
)
2385+
params = sim.modules['HealthSystem'].parameters
2386+
params['yearly_HR_scaling'][params['yearly_HR_scaling_mode']] = pd.DataFrame({
2387+
'year': dynamic_HR_scaling_factor.keys(),
2388+
'dynamic_HR_scaling_factor': dynamic_HR_scaling_factor.values(),
2389+
'scale_HR_by_popsize': False,
2390+
})
2391+
2392+
popsize = 100
2393+
sim.make_initial_population(n=popsize)
2394+
2395+
# Ensure simulation lasts long enough so that current capabilities reflect that used after two updates
2396+
# (updates occur on 1st Jan, starting in 2010, so simulation should stop on 2nd Jan 2011).
2397+
sim.simulate(end_date=sim.date + pd.DateOffset(years=10, days=1))
2398+
2399+
return sim.modules['HealthSystem'].capabilities_today
2400+
2401+
dynamic_HR_scaling_factor = {
2402+
2010: 1.0,
2403+
2011: 2.0,
2404+
# (2012 and 2013) are skipped: implies that the value for 2010 should apply in these years
2405+
2014: 0.2,
2406+
2015: 1.0,
2407+
# (2016, ..., 2020 are skipped: implies that the value for 2015 should apply in the years)
2408+
}
2409+
expected_overall_scaling = 2.0 * 2.0 * 2.0 * 0.2
2410+
2411+
# Get initial capabilities and remove all officers with no minutes available
2412+
initial_caps = get_initial_capabilities()
2413+
initial_caps = initial_caps[initial_caps != 0]
2414+
2415+
# Check that dynamic expansion over two years leads to expansion = dynamic_HR_scaling_factor^2
2416+
caps = run_sim(dynamic_HR_scaling_factor=dynamic_HR_scaling_factor)
2417+
caps = caps[caps != 0]
2418+
ratio_in_sim = caps / initial_caps
2419+
2420+
assert np.allclose(ratio_in_sim, expected_overall_scaling)

0 commit comments

Comments
 (0)