Skip to content

Commit c01e814

Browse files
author
sakshimohan
committed
add xpert costs
- this is a temporary fix while issue #1602 gets sorted
1 parent f03c6c9 commit c01e814

File tree

1 file changed

+131
-3
lines changed

1 file changed

+131
-3
lines changed

src/scripts/comparison_of_horizontal_and_vertical_programs/manuscript_analysis/roi_analysis_horizontal_vs_vertical.py

+131-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
load_pickled_dataframes,
2626
summarize
2727
)
28+
from collections import defaultdict
2829

2930
from scripts.costing.cost_estimation import (estimate_input_cost_of_scenarios,
3031
summarize_cost_data,
@@ -208,6 +209,7 @@ def do_standard_bar_plot_with_ci(_df, set_colors=None, annotations=None,
208209

209210
return fig, ax
210211

212+
# %%
211213
# Estimate standard input costs of scenario
212214
#-----------------------------------------------------------------------------------------------------------------------
213215
input_costs = estimate_input_cost_of_scenarios(results_folder, resourcefilepath,
@@ -217,7 +219,6 @@ def do_standard_bar_plot_with_ci(_df, set_colors=None, annotations=None,
217219
# Add additional costs pertaining to simulation (Only for scenarios with Malaria scale-up)
218220
#-----------------------------------------------------------------------------------------------------------------------
219221
def estimate_malaria_scale_up_costs(_params, _relevant_period_for_costing):
220-
# Extract supply chain cost as a proportion of consumable costs to apply to malaria scale-up commodities
221222
# Load primary costing resourcefile
222223
workbook_cost = pd.read_excel((resourcefilepath / "costing/ResourceFile_Costing.xlsx"),
223224
sheet_name=None)
@@ -360,9 +361,9 @@ def get_number_of_people_covered_by_malaria_scaleup(_df, list_of_districts_cover
360361
]
361362
return malaria_scaleup_costs
362363

364+
print("Appending malaria scale-up costs")
363365
malaria_scaleup_costs = estimate_malaria_scale_up_costs(_params = params,
364366
_relevant_period_for_costing = relevant_period_for_costing)
365-
366367
def append_malaria_scale_up_costs_to_total_input_costs(_malaria_scale_up_costs, _total_input_costs, _relevant_period_for_costing):
367368
# Re-format malaria scale-up costs to append to the rest of the input_costs
368369
def melt_and_label_malaria_scaleup_cost(_df, label):
@@ -387,12 +388,139 @@ def melt_and_label_malaria_scaleup_cost(_df, label):
387388
new_df = apply_discounting_to_cost_data(new_df, _discount_rate= discount_rate, _year = _relevant_period_for_costing[0])
388389
_total_input_costs = pd.concat([_total_input_costs, new_df], ignore_index=True)
389390

391+
return _total_input_costs
392+
393+
# Update input costs to include malaria scale up costs
390394
input_costs = append_malaria_scale_up_costs_to_total_input_costs(_malaria_scale_up_costs = malaria_scaleup_costs,
391395
_total_input_costs = input_costs,
392396
_relevant_period_for_costing = relevant_period_for_costing)
393397

394-
# Extract input_costs for browsing
398+
def estimate_xpert_costs(_results_folder, _relevant_period_for_costing):
399+
# Load primary costing resourcefile
400+
workbook_cost = pd.read_excel((resourcefilepath / "costing/ResourceFile_Costing.xlsx"),
401+
sheet_name=None)
402+
# Read parameters for consumables costs
403+
# Load consumables cost data
404+
unit_price_consumable = workbook_cost["consumables"]
405+
unit_price_consumable = unit_price_consumable.rename(columns=unit_price_consumable.iloc[0])
406+
unit_price_consumable = unit_price_consumable[['Item_Code', 'Final_price_per_chosen_unit (USD, 2023)']].reset_index(
407+
drop=True).iloc[1:]
408+
unit_price_consumable = unit_price_consumable[unit_price_consumable['Item_Code'].notna()]
409+
410+
# Add cost of Xpert consumables which was missed in the current analysis
411+
def get_counts_of_items_requested(_df):
412+
counts_of_used = defaultdict(lambda: defaultdict(int))
413+
counts_of_available = defaultdict(lambda: defaultdict(int))
414+
counts_of_not_available = defaultdict(lambda: defaultdict(int))
415+
416+
for _, row in _df.iterrows():
417+
date = row['date']
418+
for item, num in row['Item_Used'].items():
419+
counts_of_used[date][item] += num
420+
for item, num in row['Item_Available'].items():
421+
counts_of_available[date][item] += num
422+
for item, num in row['Item_NotAvailable'].items():
423+
counts_of_not_available[date][item] += num
424+
425+
used_df = pd.DataFrame(counts_of_used).fillna(0).astype(int).stack().rename('Used')
426+
available_df = pd.DataFrame(counts_of_available).fillna(0).astype(int).stack().rename('Available')
427+
not_available_df = pd.DataFrame(counts_of_not_available).fillna(0).astype(int).stack().rename('Not_Available')
428+
429+
# Combine the two dataframes into one series with MultiIndex (date, item, availability_status)
430+
combined_df = pd.concat([used_df, available_df, not_available_df], axis=1).fillna(0).astype(int)
431+
432+
# Convert to a pd.Series, as expected by the custom_generate_series function
433+
return combined_df.stack()
434+
435+
cons_req = extract_results(
436+
_results_folder,
437+
module='tlo.methods.healthsystem.summary',
438+
key='Consumables',
439+
custom_generate_series=get_counts_of_items_requested,
440+
do_scaling=True)
441+
keep_xpert = cons_req.index.get_level_values(0) == '187'
442+
keep_instances_logged_as_not_available = cons_req.index.get_level_values(2) == 'Not_Available'
443+
cons_req = cons_req[keep_xpert & keep_instances_logged_as_not_available]
444+
cons_req = cons_req.reset_index()
445+
446+
# Keep only relevant draws
447+
# Filter columns based on keys from all_manuscript_scenarios
448+
col_subset = [col for col in cons_req.columns if
449+
((col[0] in all_manuscript_scenarios.keys()) | (col[0] == 'level_1'))]
450+
# Keep only the relevant columns
451+
cons_req = cons_req[col_subset]
452+
453+
def transform_cons_requested_for_costing(_df, date_column):
454+
_df['year'] = pd.to_datetime(_df[date_column]).dt.year
455+
456+
# Validate that all necessary years are in the DataFrame
457+
if not set(_relevant_period_for_costing).issubset(_df['year'].unique()):
458+
raise ValueError("Some years are not recorded in the dataset.")
459+
460+
# Filter for relevant years and return the total population as a Series
461+
return _df.loc[_df['year'].between(min(_relevant_period_for_costing), max(_relevant_period_for_costing))].drop(columns=date_column).set_index(
462+
'year')
463+
464+
xpert_cost_per_cartridge = unit_price_consumable[unit_price_consumable.Item_Code == 187][
465+
'Final_price_per_chosen_unit (USD, 2023)']
466+
xpert_availability_adjustment = 0.31
467+
468+
xpert_dispensed_cost = (transform_cons_requested_for_costing(_df=cons_req,
469+
date_column=('level_1', ''))
470+
* xpert_availability_adjustment
471+
* xpert_cost_per_cartridge.iloc[0])
472+
draws_with_positive_xpert_costs = (
473+
input_costs[(input_costs.cost_subgroup == 'Xpert') & (input_costs.cost > 0)].groupby('draw')[
474+
'cost'].sum().reset_index()['draw']
475+
.unique()).tolist()
476+
477+
def melt_and_label_xpert_cost(_df):
478+
multi_index = pd.MultiIndex.from_tuples(_df.columns)
479+
_df.columns = multi_index
480+
481+
# reshape dataframe and assign 'draw' and 'run' as the correct column headers
482+
melted_df = pd.melt(_df.reset_index(), id_vars=['year']).rename(
483+
columns={'variable_0': 'draw', 'variable_1': 'run'})
484+
# For draws where the costing is already correct, set additional costs to 0
485+
melted_df.loc[melted_df.draw.isin(draws_with_positive_xpert_costs), 'value'] = 0
486+
# Replace item_code with consumable_name_tlo
487+
melted_df['cost_category'] = 'medical consumables'
488+
melted_df['cost_subgroup'] = 'Xpert'
489+
melted_df['Facility_Level'] = 'all'
490+
melted_df = melted_df.rename(columns={'value': 'cost'})
491+
492+
# Replicate and estimate cost of consumables stocked and supply chain costs
493+
df_with_all_cost_subcategories = pd.concat([melted_df] * 3, axis=0, ignore_index=True)
494+
# Define cost subcategory values
495+
cost_categories = ['cost_of_consumables_dispensed', 'cost_of_excess_consumables_stocked', 'supply_chain']
496+
# Assign values to the new 'cost_subcategory' column
497+
df_with_all_cost_subcategories['cost_subcategory'] = np.tile(cost_categories, len(melted_df))
498+
# The excess stock ratio of Xpert as per 2018 LMIS data is 0.125833
499+
df_with_all_cost_subcategories.loc[df_with_all_cost_subcategories[
500+
'cost_subcategory'] == 'cost_of_excess_consumables_stocked', 'cost'] *= 0.125833
501+
# Supply chain costs are 0.12938884672119721 of the cost of dispensed + stocked
502+
df_with_all_cost_subcategories.loc[
503+
df_with_all_cost_subcategories['cost_subcategory'] == 'supply_chain', 'cost'] *= (
504+
0.12938884672119721 * (1 + 0.125833))
505+
return df_with_all_cost_subcategories
506+
507+
xpert_total_cost = melt_and_label_xpert_cost(xpert_dispensed_cost)
508+
xpert_total_cost.to_csv('./outputs/horizontal_v_vertical/xpert_cost.csv')
509+
return xpert_total_cost
510+
511+
print("Appending Xpert costs")
512+
xpert_total_cost = estimate_xpert_costs(_results_folder = results_folder,
513+
_relevant_period_for_costing = relevant_period_for_costing)
514+
515+
# Update input costs to include Xpert costs
516+
input_costs = pd.concat([input_costs, xpert_total_cost], ignore_index=True)
517+
input_costs = input_costs.groupby(['draw', 'run', 'year', 'cost_subcategory', 'Facility_Level',
518+
'cost_subgroup', 'cost_category'])['cost'].sum().reset_index()
519+
520+
521+
# Keep costs for relevant draws
395522
input_costs = input_costs[input_costs['draw'].isin(list(all_manuscript_scenarios.keys()))]
523+
# Extract input_costs for browsing
396524
input_costs.groupby(['draw', 'run', 'cost_category', 'cost_subcategory', 'cost_subgroup','year'])['cost'].sum().to_csv(figurespath / 'cost_detailed.csv')
397525

398526
# %%

0 commit comments

Comments
 (0)