Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/tlo/methods/healthburden.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
get_gbd_causes_not_represented_in_disease_modules,
)
from tlo.methods.demography import age_at_date
from tlo.notify import notifier

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
Expand Down Expand Up @@ -614,6 +615,20 @@ def apply(self, population):
# Multiply 1/12 as these weights are for one month only
disease_specific_daly_values_this_month = disease_specific_daly_values_this_month * (1 / 12)

if notifier.has_listeners("healthburden.monthly_daly_report"):
# Do not dispatch individuals or causes that have zero dalys reported this month
monthly_dalys = disease_specific_daly_values_this_month.copy()
Comment thread
marghe-molaro marked this conversation as resolved.
Outdated
monthly_dalys_nonzero = (
monthly_dalys.loc[(monthly_dalys != 0).any(axis=1), (monthly_dalys != 0).any(axis=0)]
.to_dict(orient="index"))

# Store data as dictionary
data = {
person: {col: val for col, val in cols.items() if val != 0}
for person, cols in monthly_dalys_nonzero.items()
}
notifier.dispatch("healthburden.monthly_daly_report", data=data)
Comment on lines +625 to +630
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering why you pack it into a dictionary here (two nested for-loops), and then unpack again (two more nested for-loops) when it goes to the function in the notifier? Could the pd.Dataframe be passed here, and then packed-into the eav format once?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes that's a good point; my naive guess was that the cost of reformatting non-zero entries into a dictionary is less than dispatching the entire dataframe which may contain a lot of zero entries*, but this may be incorrect. @tamuri happy for you to advise.

*This also a naive guess, based on the assumption that most individuals only have one or two conditions reported every months (meaning that all other columns would be zero for them), and that a single condition would typically affect <<20% (?) of the population.

Copy link
Copy Markdown
Collaborator

@tamuri tamuri Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tim is right - you don't need any filtering here. You're passing a reference, not a copy of the dataframe. You do need to ensure there is no editing of the dataframe in the receiver because it is used after the notification.

It's also how notifications are usually setup. You don't know how receivers need the information so send everything and then the receiver can do its own filtering (or not, as needed).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An additional benefit is that you can remove the has_listeners check

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use dash instead of underscores here, to match other listeners.

Suggested change
notifier.dispatch("healthburden.monthly_daly_report", data=data)
notifier.dispatch("healthburden.monthly-daly-report", data=data)


# 4) Summarise the results for this month wrt sex/age/wealth
# - merge in age/wealth/sex information
disease_specific_daly_values_this_month = disease_specific_daly_values_this_month.merge(
Expand Down
23 changes: 23 additions & 0 deletions src/tlo/methods/individual_history_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def initialise_simulation(self, sim):
notifier.add_listener("hsi_event.pre-run", self.on_event_pre_run)
notifier.add_listener("hsi_event.post-run", self.on_event_post_run)
notifier.add_listener("consumables.post-request_consumables", self.on_consumable_request)
notifier.add_listener("healthburden.monthly_daly_report", self.on_monthly_daly_report)

def read_parameters(self, resourcefilepath: Optional[Path] = None):
self.load_parameters_from_dataframe(
Expand Down Expand Up @@ -371,6 +372,28 @@ def on_event_post_run(self, data):
self.consumable_access = {}
self.cons_call_number_within_event = 0

def on_monthly_daly_report(self,data):
"""Upon receiving monthly daly report, convert it to custom EAV format and log"""
rows = []

# This is not a real event, so create custom name and create custom tag associated with it
event_name = "monthly_daly_report"
event_tag = -1

for person, dalys in data.items():
for cause, value in dalys.items():
rows.append({
"entity": person,
"event_name": event_name,
"event_tag": event_tag,
"attribute": cause,
"value": value
})

eav = pd.DataFrame(rows)
self.log_eav_dataframe_to_individual_histories(eav)
return

def mni_values_differ(self, v1, v2):

if isinstance(v1, list) and isinstance(v2, list):
Expand Down
10 changes: 10 additions & 0 deletions tests/test_individual_history_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
contraception,
demography,
enhanced_lifestyle,
healthburden,
healthseekingbehaviour,
healthsystem,
hiv,
Expand Down Expand Up @@ -56,6 +57,7 @@ def test_individual_history_tracker(tmpdir, seed):
sim.register(demography.Demography(),
enhanced_lifestyle.Lifestyle(),
healthsystem.HealthSystem(),
healthburden.HealthBurden(),
individual_history_tracker.IndividualHistoryTracker(),
symptommanager.SymptomManager(),
healthseekingbehaviour.HealthSeekingBehaviour(),
Expand All @@ -79,6 +81,14 @@ def test_individual_history_tracker(tmpdir, seed):
output_chains = parse_log_file(sim.log_filepath, level=logging.INFO)
individual_histories = reconstruct_individual_histories(
output_chains['tlo.methods.individual_history_tracker']['individual_histories'])

# Check that monthly daly reporting is included
assert (individual_histories['event_name'] == 'monthly_daly_report').sum() > 0

# Cannot estimate how many monthly reports should be expected, since monthly report is only logged if individual
# experienced dalys that month, so check that at or below this max
max_monthly_reports = ((end_date.year - start_date.year) * 12 + (end_date.month - start_date.month))*popsize
assert (individual_histories['event_name'] == 'monthly_daly_report').sum() <= max_monthly_reports

# Check that we have a "StartOfSimulation" event for every individual in the initial population,
#   and that this was logged at the start date
Expand Down
Loading