25
25
load_pickled_dataframes ,
26
26
summarize
27
27
)
28
+ from collections import defaultdict
28
29
29
30
from scripts .costing .cost_estimation import (estimate_input_cost_of_scenarios ,
30
31
summarize_cost_data ,
@@ -208,6 +209,7 @@ def do_standard_bar_plot_with_ci(_df, set_colors=None, annotations=None,
208
209
209
210
return fig , ax
210
211
212
+ # %%
211
213
# Estimate standard input costs of scenario
212
214
#-----------------------------------------------------------------------------------------------------------------------
213
215
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,
217
219
# Add additional costs pertaining to simulation (Only for scenarios with Malaria scale-up)
218
220
#-----------------------------------------------------------------------------------------------------------------------
219
221
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
221
222
# Load primary costing resourcefile
222
223
workbook_cost = pd .read_excel ((resourcefilepath / "costing/ResourceFile_Costing.xlsx" ),
223
224
sheet_name = None )
@@ -360,9 +361,9 @@ def get_number_of_people_covered_by_malaria_scaleup(_df, list_of_districts_cover
360
361
]
361
362
return malaria_scaleup_costs
362
363
364
+ print ("Appending malaria scale-up costs" )
363
365
malaria_scaleup_costs = estimate_malaria_scale_up_costs (_params = params ,
364
366
_relevant_period_for_costing = relevant_period_for_costing )
365
-
366
367
def append_malaria_scale_up_costs_to_total_input_costs (_malaria_scale_up_costs , _total_input_costs , _relevant_period_for_costing ):
367
368
# Re-format malaria scale-up costs to append to the rest of the input_costs
368
369
def melt_and_label_malaria_scaleup_cost (_df , label ):
@@ -387,12 +388,139 @@ def melt_and_label_malaria_scaleup_cost(_df, label):
387
388
new_df = apply_discounting_to_cost_data (new_df , _discount_rate = discount_rate , _year = _relevant_period_for_costing [0 ])
388
389
_total_input_costs = pd .concat ([_total_input_costs , new_df ], ignore_index = True )
389
390
391
+ return _total_input_costs
392
+
393
+ # Update input costs to include malaria scale up costs
390
394
input_costs = append_malaria_scale_up_costs_to_total_input_costs (_malaria_scale_up_costs = malaria_scaleup_costs ,
391
395
_total_input_costs = input_costs ,
392
396
_relevant_period_for_costing = relevant_period_for_costing )
393
397
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
395
522
input_costs = input_costs [input_costs ['draw' ].isin (list (all_manuscript_scenarios .keys ()))]
523
+ # Extract input_costs for browsing
396
524
input_costs .groupby (['draw' , 'run' , 'cost_category' , 'cost_subcategory' , 'cost_subgroup' ,'year' ])['cost' ].sum ().to_csv (figurespath / 'cost_detailed.csv' )
397
525
398
526
# %%
0 commit comments