Skip to content

Commit 5c2fce3

Browse files
committed
Merge branch 'master' into hallett/wasting_module
2 parents 83d3ec4 + b678823 commit 5c2fce3

File tree

8 files changed

+182
-4
lines changed

8 files changed

+182
-4
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:b06f61949b83b93f08f187af4cb9c7515b21ab8c28613e1df7ea04d0150a12bb
3-
size 304
2+
oid sha256:187302cf1744ee6a9538665c5dff5650f58e4d00c8d74844ff8b569b6b38f9d1
3+
size 335
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:92d2a71c58a8232d9c1b50da58c63db18f1e1cf47d8a02adb7c0467afd40fb7a
3+
size 8903
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:e9415f5249a5c4ddd2b5ccc3fbf1a64b33132b4b9e379ad079a9539cb109b24e
3+
size 8515
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import pandas as pd
2+
3+
dict = { "1a" : "L1a_Av_Mins_Per_Day", "1b":"L1b_Av_Mins_Per_Day", "2":"L2_Av_Mins_Per_Day", "0":"L0_Av_Mins_Per_Day", "3": "L3_Av_Mins_Per_Day", "4": "L4_Av_Mins_Per_Day", "5": "L5_Av_Mins_Per_Day"}
4+
5+
# Specify the file paths
6+
file_path1 = "resources/healthsystem/human_resources/actual/ResourceFile_Daily_Capabilities.csv"
7+
file_path2 = "resources/healthsystem/human_resources/definitions/ResourceFile_Officer_Types_Table.csv"
8+
file_path3 = "resources/healthsystem/absenteeism/HHFA_amended_ResourceFile_patient_facing_time.xlsx"
9+
10+
# Load Excel files into DataFrames
11+
daily_capabilities = pd.read_csv(file_path1)
12+
officer_types = pd.read_csv(file_path2)
13+
survey_daily_capabilities = pd.read_excel(file_path3, sheet_name="Scenario 2")
14+
15+
# Clean survey_daily_capabilities by replacing officer codes with category, and calculating mean within category
16+
merged_df = pd.merge(survey_daily_capabilities, officer_types, on="Officer_Type_Code", how="left")
17+
survey_daily_capabilities["Officer_Category"] = merged_df["Officer_Category"]
18+
del survey_daily_capabilities["Officer_Type_Code"]
19+
del survey_daily_capabilities["Total_Av_Working_Days"]
20+
survey_daily_capabilities = survey_daily_capabilities.groupby("Officer_Category").mean().reset_index()
21+
22+
# Obtain average mins per day
23+
daily_capabilities["Av_mins_per_day"] = (daily_capabilities["Total_Mins_Per_Day"]/daily_capabilities["Staff_Count"]).fillna(0)
24+
25+
# Obtain officers types
26+
officers = daily_capabilities["Officer_Category"].drop_duplicates()
27+
28+
# Obtain mean daily capabilities for given facility level and officer category across all facilities
29+
summarise_daily_capabilities = pd.DataFrame(columns=survey_daily_capabilities.columns)
30+
summarise_daily_capabilities["Officer_Category"] = survey_daily_capabilities["Officer_Category"]
31+
32+
for level in ["0", "1a", "1b", "2"]:
33+
dc_at_level = daily_capabilities[daily_capabilities["Facility_Level"]==level]
34+
for officer in officers:
35+
dc_at_level_officer = dc_at_level[dc_at_level["Officer_Category"]==officer]
36+
mean_val = dc_at_level_officer["Av_mins_per_day"].mean()
37+
summarise_daily_capabilities.loc[summarise_daily_capabilities["Officer_Category"] == officer, dict[level]] = mean_val
38+
39+
survey_daily_capabilities = survey_daily_capabilities.set_index("Officer_Category")
40+
summarise_daily_capabilities = summarise_daily_capabilities.set_index("Officer_Category")
41+
42+
# If not data is available, assume scaling factor of 1
43+
absenteeism_factor = (survey_daily_capabilities/summarise_daily_capabilities).fillna(1.)
44+
45+
# Output absenteeism file
46+
absenteeism_factor.to_excel("absenteeism_factor.xlsx")
47+
48+
print(absenteeism_factor)

src/tlo/analysis/utils.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1217,7 +1217,8 @@ def construct_multiindex_if_implied(df):
12171217

12181218
def mix_scenarios(*dicts) -> Dict:
12191219
"""Helper function to combine a Dicts that show which parameters should be over-written.
1220-
* Warnings are generated if a parameter appears in more than one Dict with a different value;
1220+
* If a parameter appears in more than one Dict, the value in the last-added dict is taken, and a UserWarning
1221+
is raised;
12211222
* Items under the same top-level key (i.e., for the Module) are merged rather than being over-written."""
12221223

12231224
d = defaultdict(lambda: defaultdict(dict))

src/tlo/methods/healthsystem.py

+56-1
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,19 @@ class HealthSystem(Module):
541541
" the priority, and on which categories of individuals classify for fast-tracking "
542542
" for specific treatments"),
543543

544+
'const_HR_scaling_table': Parameter(
545+
Types.DICT, "Factors by which capabilities of medical officer categories at different levels will be"
546+
"scaled at the start of the simulation to simulate a number of effects (e.g. absenteeism,"
547+
"boosting of specific medical cadres, etc). This is the imported from an"
548+
"Excel workbook: keys are the worksheet names and values are the worksheets in "
549+
"the format of pd.DataFrames."),
550+
551+
'const_HR_scaling_mode': Parameter(
552+
Types.STRING, "Mode of HR scaling considered at the start of the simulation. Options are default"
553+
" (capabilities are scaled by a constaint factor of 1), data (factors informed by survey data),"
554+
"and custom (user can freely set these factors as parameters in the analysis).",
555+
),
556+
544557
'tclose_overwrite': Parameter(
545558
Types.INT, "Decide whether to overwrite tclose variables assigned by disease modules"),
546559

@@ -794,8 +807,22 @@ def read_parameters(self, data_folder):
794807
'ResourceFile_PriorityRanking_ALLPOLICIES.xlsx',
795808
sheet_name=None)
796809

810+
self.parameters['const_HR_scaling_table']: Dict = pd.read_excel(
811+
path_to_resourcefiles_for_healthsystem /
812+
"human_resources" /
813+
"const_HR_scaling" /
814+
"ResourceFile_const_HR_scaling.xlsx",
815+
sheet_name=None # all sheets read in
816+
)
817+
818+
797819
def pre_initialise_population(self):
798820
"""Generate the accessory classes used by the HealthSystem and pass to them the data that has been read."""
821+
822+
# Ensure the mode of HR scaling to be considered in included in the tables loaded
823+
assert self.parameters['const_HR_scaling_mode'] in self.parameters['const_HR_scaling_table'], \
824+
f"Value of `const_HR_scaling_mode` not recognised: {self.parameters['const_HR_scaling_mode']}"
825+
799826
# Create dedicated RNGs for separate functions done by the HealthSystem module
800827
self.rng_for_hsi_queue = np.random.RandomState(self.rng.randint(2 ** 31 - 1))
801828
self.rng_for_dx = np.random.RandomState(self.rng.randint(2 ** 31 - 1))
@@ -834,6 +861,7 @@ def pre_initialise_population(self):
834861
# Set up framework for considering a priority policy
835862
self.setup_priority_policy()
836863

864+
837865
def initialise_population(self, population):
838866
self.bed_days.initialise_population(population.props)
839867

@@ -932,6 +960,8 @@ def setup_priority_policy(self):
932960
def process_human_resources_files(self, use_funded_or_actual_staffing: str):
933961
"""Create the data-structures needed from the information read into the parameters."""
934962

963+
964+
935965
# * Define Facility Levels
936966
self._facility_levels = set(self.parameters['Master_Facilities_List']['Facility_Level']) - {'5'}
937967
assert self._facility_levels == {'0', '1a', '1b', '2', '3', '4'} # todo soft code this?
@@ -1031,6 +1061,28 @@ def process_human_resources_files(self, use_funded_or_actual_staffing: str):
10311061
# never available.)
10321062
self._officers_with_availability = set(self._daily_capabilities.index[self._daily_capabilities > 0])
10331063

1064+
def adjust_for_const_HR_scaling(self, df: pd.DataFrame) -> pd.DataFrame:
1065+
"""Adjust the Daily_Capabilities pd.DataFrame to account for assumptions about scaling of HR resources"""
1066+
1067+
# Get the set of scaling_factors that are specified by the 'const_HR_scaling_mode' assumption
1068+
const_HR_scaling_factor = self.parameters['const_HR_scaling_table'][self.parameters['const_HR_scaling_mode']]
1069+
const_HR_scaling_factor = const_HR_scaling_factor.set_index('Officer_Category')
1070+
1071+
level_conversion = {"1a": "L1a_Av_Mins_Per_Day", "1b": "L1b_Av_Mins_Per_Day",
1072+
"2": "L2_Av_Mins_Per_Day", "0": "L0_Av_Mins_Per_Day", "3": "L3_Av_Mins_Per_Day",
1073+
"4": "L4_Av_Mins_Per_Day", "5": "L5_Av_Mins_Per_Day"}
1074+
1075+
scaler = df[['Officer_Category', 'Facility_Level']].apply(
1076+
lambda row: const_HR_scaling_factor.loc[row['Officer_Category'], level_conversion[row['Facility_Level']]],
1077+
axis=1
1078+
)
1079+
1080+
# Apply scaling to 'Total_Mins_Per_Day'
1081+
df['Total_Mins_Per_Day'] *= scaler
1082+
1083+
return df
1084+
1085+
10341086
def format_daily_capabilities(self, use_funded_or_actual_staffing: str) -> pd.Series:
10351087
"""
10361088
This will updates the dataframe for the self.parameters['Daily_Capabilities'] so as to include
@@ -1044,7 +1096,10 @@ def format_daily_capabilities(self, use_funded_or_actual_staffing: str) -> pd.Se
10441096

10451097
# Get the capabilities data imported (according to the specified underlying assumptions).
10461098
capabilities = pool_capabilities_at_levels_1b_and_2(
1047-
self.parameters[f'Daily_Capabilities_{use_funded_or_actual_staffing}'])
1099+
self.adjust_for_const_HR_scaling(
1100+
self.parameters[f'Daily_Capabilities_{use_funded_or_actual_staffing}']
1101+
)
1102+
)
10481103
capabilities = capabilities.rename(columns={'Officer_Category': 'Officer_Type_Code'}) # neaten
10491104

10501105
# Create dataframe containing background information about facility and officer types

tests/test_analysis.py

+31
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,37 @@ def test_mix_scenarios():
388388
assert 1 == len(record)
389389
assert record.list[0].message.args[0] == 'Parameter is being updated more than once: module=Mod1, parameter=param_b'
390390

391+
# Test the behaviour of the `mix_scenarios` taking the value in the right-most dict.
392+
assert mix_scenarios(
393+
{'Mod1': {
394+
'param_a': 'value_in_dict1',
395+
'param_b': 'value_in_dict1',
396+
'param_c': 'value_in_dict1',
397+
}},
398+
{'Mod1': {
399+
'param_a': 'value_in_dict2',
400+
'param_b': 'value_in_dict2',
401+
'param_c': 'value_in_dict2',
402+
}},
403+
{'Mod1': {
404+
'param_a': 'value_in_dict3',
405+
'param_b': 'value_in_dict_right_most',
406+
'param_c': 'value_in_dict3',
407+
}},
408+
{'Mod1': {
409+
'param_a': 'value_in_dict_right_most',
410+
'param_c': 'value_in_dict4',
411+
}},
412+
{"Mod1": {
413+
"param_c": "value_in_dict_right_most",
414+
}},
415+
) == {
416+
'Mod1': {'param_a': 'value_in_dict_right_most',
417+
'param_b': 'value_in_dict_right_most',
418+
'param_c': 'value_in_dict_right_most',
419+
}
420+
}
421+
391422

392423
def test_scenario_switcher(seed):
393424
"""Check the `ScenarioSwitcher` module can update parameter values in a manner similar to them being changed

tests/test_healthsystem.py

+37
Original file line numberDiff line numberDiff line change
@@ -2240,3 +2240,40 @@ def get_hsi_log(service_availability, randomise_hsi_queue) -> pd.DataFrame:
22402240

22412241
# Check that HSI event logs are identical
22422242
pd.testing.assert_frame_equal(run_with_asterisk, run_with_list)
2243+
2244+
2245+
def test_const_HR_scaling_assumption(seed, tmpdir):
2246+
"""Check that we can use the parameter `const_HR_scaling_mode` to manipulate the minutes of time available for healthcare
2247+
workers."""
2248+
2249+
def get_capabilities_today(const_HR_scaling_mode: str) -> pd.Series:
2250+
sim = Simulation(start_date=start_date, seed=seed)
2251+
sim.register(
2252+
demography.Demography(resourcefilepath=resourcefilepath),
2253+
healthsystem.HealthSystem(resourcefilepath=resourcefilepath)
2254+
)
2255+
sim.modules['HealthSystem'].parameters['const_HR_scaling_mode'] = const_HR_scaling_mode
2256+
sim.make_initial_population(n=100)
2257+
sim.simulate(end_date=start_date + pd.DateOffset(days=0))
2258+
2259+
return sim.modules['HealthSystem'].capabilities_today
2260+
2261+
caps = {
2262+
_const_HR_scaling_mode: get_capabilities_today(_const_HR_scaling_mode)
2263+
for _const_HR_scaling_mode in ('default', 'data', 'custom')
2264+
}
2265+
2266+
# Check that the custom assumption (multiplying all capabilities by 0.5) gives expected result
2267+
assert np.allclose(
2268+
caps['custom'].values,
2269+
caps['default'].values * 0.5
2270+
)
2271+
2272+
# Check that the "data" assumptions leads to changes in the capabilities (of any direction)
2273+
assert not np.allclose(
2274+
caps['data'].values,
2275+
caps['default'].values
2276+
)
2277+
2278+
2279+

0 commit comments

Comments
 (0)