-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathcardio_metabolic_disorders.py
1815 lines (1580 loc) · 90.6 KB
/
cardio_metabolic_disorders.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
The joint Cardio-Metabolic Disorders model determines onset, outcome and treatment of:
* Diabetes
* Hypertension
* Chronic Kidney Disease
* Chronic Ischemic Heart Disease
* Stroke
* Heart Attack
And:
* Chronic Lower Back Pain
"""
from __future__ import annotations
import math
from itertools import combinations
from pathlib import Path
from typing import TYPE_CHECKING, List
import numpy as np
import pandas as pd
from tlo import DAYS_IN_YEAR, DateOffset, Module, Parameter, Property, Types, logging
from tlo.core import IndividualPropertyUpdates
from tlo.events import Event, IndividualScopeEventMixin, PopulationScopeEventMixin, RegularEvent
from tlo.lm import LinearModel, LinearModelType, Predictor
from tlo.methods import Metadata
from tlo.methods import demography as de
from tlo.methods.causes import Cause
from tlo.methods.dxmanager import DxTest
from tlo.methods.hsi_event import HSI_Event
from tlo.methods.symptommanager import Symptom
from tlo.util import random_date
if TYPE_CHECKING:
from tlo.population import PatientDetails
# ---------------------------------------------------------------------------------------------------------
# MODULE DEFINITIONS
# ---------------------------------------------------------------------------------------------------------
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class CardioMetabolicDisorders(Module):
"""
CardioMetabolicDisorders module covers a subset of cardio-metabolic conditions and events. Conditions are binary
and individuals experience a risk of acquiring or losing a condition based on annual probability and
demographic/lifestyle risk factors.
"""
# Save a master list of the events that are covered in this module
conditions = ['diabetes',
'hypertension',
'chronic_kidney_disease',
'chronic_lower_back_pain',
'chronic_ischemic_hd']
# Save a master list of the events that are covered in this module
events = ['ever_stroke',
'ever_heart_attack']
INIT_DEPENDENCIES = {'Demography', 'Lifestyle', 'HealthSystem', 'SymptomManager'}
OPTIONAL_INIT_DEPENDENCIES = {'HealthBurden', 'Hiv'}
ADDITIONAL_DEPENDENCIES = {'Depression'}
# Declare Metadata
METADATA = {
Metadata.DISEASE_MODULE,
Metadata.USES_SYMPTOMMANAGER,
Metadata.USES_HEALTHSYSTEM,
Metadata.USES_HEALTHBURDEN
}
# Declare Causes of Death
CAUSES_OF_DEATH = {
'diabetes': Cause(
gbd_causes='Diabetes mellitus', label='Diabetes'),
'chronic_ischemic_hd': Cause(
gbd_causes={'Ischemic heart disease', 'Hypertensive heart disease'}, label='Heart Disease'),
'ever_heart_attack': Cause(
gbd_causes={'Ischemic heart disease', 'Hypertensive heart disease'}, label='Heart Disease'),
'ever_stroke': Cause(
gbd_causes='Stroke', label='Stroke'),
'chronic_kidney_disease': Cause(
gbd_causes='Chronic kidney disease', label='Kidney Disease')
}
# Declare Causes of Disability
CAUSES_OF_DISABILITY = {
'diabetes': Cause(
gbd_causes='Diabetes mellitus', label='Diabetes'),
'chronic_ischemic_hd': Cause(
gbd_causes={'Ischemic heart disease', 'Hypertensive heart disease'}, label='Heart Disease'),
'heart_attack': Cause(
gbd_causes={'Ischemic heart disease', 'Hypertensive heart disease'}, label='Heart Disease'),
'stroke': Cause(
gbd_causes='Stroke', label='Stroke'),
'chronic_kidney_disease': Cause(
gbd_causes='Chronic kidney disease', label='Kidney Disease'),
'lower_back_pain': Cause(
gbd_causes={'Low back pain'}, label='Lower Back Pain'
)
}
# Create separate dicts for params for conditions and events which are read in via excel documents in resources/cmd
onset_conditions_param_dicts = {
f"{p}_onset": Parameter(Types.DICT, f"all the parameters that specify the linear models for onset of {p}")
for p in conditions
}
removal_conditions_param_dicts = {
f"{p}_removal": Parameter(Types.DICT, f"all the parameters that specify the linear models for removal of {p}")
for p in conditions
}
hsi_conditions_param_dicts = {
f"{p}_hsi": Parameter(Types.DICT, f"all the parameters that specify diagnostic tests and treatments for {p}")
for p in conditions
}
onset_events_param_dicts = {
f"{p}_onset": Parameter(Types.DICT, f"all the parameters that specify the linear models for onset of {p}")
for p in events
}
hsi_events_param_dicts = {
f"{p}_hsi": Parameter(Types.DICT, f"all the parameters that specify diagnostic tests and treatments for {p}")
for p in events
}
death_conditions_param_dicts = {
f"{p}_death": Parameter(Types.DICT, f"all the parameters that specify the linear models for death from {p}")
for p in conditions
}
death_events_param_dicts = {
f"{p}_death": Parameter(Types.DICT, f"all the parameters that specify the linear models for death from {p}")
for p in events
}
initial_prev_param_dicts = {
f"{p}_initial_prev": Parameter(Types.DICT, 'initial prevalence of condition') for p in conditions
}
other_params_dict = {
'interval_between_polls': Parameter(Types.INT, 'months between the main polling event'),
'pr_bmi_reduction': Parameter(Types.INT, 'probability of an individual having a reduction in BMI following '
'weight loss treatment')
}
PARAMETERS = {
**onset_conditions_param_dicts, **removal_conditions_param_dicts, **hsi_conditions_param_dicts,
**onset_events_param_dicts, **death_conditions_param_dicts, **death_events_param_dicts,
**hsi_events_param_dicts, **initial_prev_param_dicts, **other_params_dict,
'prob_care_provided_given_seek_emergency_care': Parameter(
Types.REAL, "The probability that correct care is fully provided to persons that have sought emergency care"
" for a Cardio-metabolic disorder.")
}
# Convert conditions and events to dicts and merge together into PROPERTIES
condition_list = {
f"nc_{p}": Property(Types.BOOL, f"Whether or not someone has {p}") for p in conditions
}
condition_diagnosis_list = {
f"nc_{p}_ever_diagnosed": Property(Types.BOOL, f"Whether or not someone has ever been diagnosed with {p}") for p
in conditions
}
condition_date_diagnosis_list = {
f"nc_{p}_date_diagnosis": Property(Types.DATE, f"When someone has been diagnosed with {p}") for p
in conditions
}
condition_date_of_last_test_list = {
f"nc_{p}_date_last_test": Property(Types.DATE, f"When someone has last been tested for {p}") for p
in conditions
}
condition_medication_list = {
f"nc_{p}_on_medication": Property(Types.BOOL, f"Whether or not someone is on medication for {p}") for p
in conditions
}
condition_medication_death_list = {
f"nc_{p}_medication_prevents_death": Property(Types.BOOL, f"Whether or not medication (if provided) will "
f"prevent death from {p}") for p in conditions
}
event_list = {
f"nc_{p}": Property(Types.BOOL, f"Whether or not someone has had a {p}") for p in events}
event_date_last_list = {
f"nc_{p}_date_last_event": Property(Types.DATE, f"Date of last {p}") for p in events
}
event_diagnosis_list = {
f"nc_{p}_ever_diagnosed": Property(Types.BOOL, f"Whether or not someone has ever been diagnosed with {p}") for p
in events
}
event_date_diagnosis_list = {
f"nc_{p}_date_diagnosis": Property(Types.DATE, f"When someone has last been diagnosed with {p}") for p
in events}
event_medication_list = {
f"nc_{p}_on_medication": Property(Types.BOOL, f"Whether or not someone has ever been diagnosed with {p}") for p
in events
}
event_scheduled_date_death_list = {
f"nc_{p}_scheduled_date_death": Property(Types.DATE, f"Scheduled date of death from {p}") for p
in events
}
event_medication_death_list = {
f"nc_{p}_medication_prevents_death": Property(Types.BOOL, f"Whether or not medication will prevent death from "
f"{p}") for p in events
}
PROPERTIES = {**condition_list, **event_list, **condition_diagnosis_list, **condition_date_diagnosis_list,
**condition_date_of_last_test_list, **condition_medication_list, **condition_medication_death_list,
**event_date_last_list, **event_diagnosis_list, **event_date_diagnosis_list, **event_medication_list,
**event_medication_death_list, **event_scheduled_date_death_list,
'nc_ever_weight_loss_treatment': Property(Types.BOOL,
'whether or not the person has ever had weight loss '
'treatment'),
'nc_weight_loss_worked': Property(Types.BOOL,
'whether or not weight loss treatment worked'),
'nc_risk_score': Property(Types.INT, 'score to represent number of risk conditions the person has')
}
def __init__(self, name=None, resourcefilepath=None, do_log_df: bool = False, do_condition_combos: bool = False):
super().__init__(name)
self.resourcefilepath = resourcefilepath
self.conditions = CardioMetabolicDisorders.conditions
self.events = CardioMetabolicDisorders.events
# Create list that includes the nc_ prefix for conditions in this module
self.condition_list = ['nc_' + cond for cond in CardioMetabolicDisorders.conditions]
# Store the symptoms that this module will use (for conditions only):
self.symptoms = {f"{s}_symptoms" for s in self.conditions if s != "hypertension"}
# Dict to hold the probability of onset of different types of symptom given a condition
self.prob_symptoms = dict()
# Retrieve age range categories from Demography module
self.age_cats = None
# Dict to hold the DALY weights
self.daly_wts = dict()
# Retrieve age range categories from Demography module
self.age_cats = None
# Store bools for whether or not to log the df or log combinations of co-morbidities
self.do_log_df = do_log_df
self.do_condition_combos = do_condition_combos
# Dict to hold trackers for counting events
self.trackers = None
# Dicts of linear models
self.lms_onset = dict()
self.lms_removal = dict()
self.lms_death = dict()
self.lms_symptoms = dict()
self.lms_testing = dict()
# Build the LinearModel for occurrence of events
self.lms_event_onset = dict()
self.lms_event_death = dict()
self.lms_event_symptoms = dict()
def read_parameters(self, data_folder):
"""Read parameter values from files for condition onset, removal, deaths, and initial prevalence.
ResourceFile_cmd_condition_onset.xlsx = parameters for onset of conditions
ResourceFile_cmd_condition_removal.xlsx = parameters for removal of conditions
ResourceFile_cmd_condition_death.xlsx = parameters for death rate from conditions
ResourceFile_cmd_condition_prevalence.xlsx = initial and target prevalence for conditions
ResourceFile_cmd_condition_symptoms.xlsx = symptoms for conditions
ResourceFile_cmd_condition_hsi.xlsx = HSI parameters for conditions
ResourceFile_cmd_condition_testing.xlsx = community testing parameters for conditions (currently only
hypertension)
ResourceFile_cmd_events.xlsx = parameters for occurrence of events
ResourceFile_cmd_events_death.xlsx = parameters for death rate from events
ResourceFile_cmd_events_symptoms.xlsx = symptoms for events
ResourceFile_cmd_events_hsi.xlsx = HSI parameters for events
"""
cmd_path = Path(self.resourcefilepath) / "cmd"
cond_onset = pd.read_excel(cmd_path / "ResourceFile_cmd_condition_onset.xlsx", sheet_name=None)
cond_removal = pd.read_excel(cmd_path / "ResourceFile_cmd_condition_removal.xlsx", sheet_name=None)
cond_death = pd.read_excel(cmd_path / "ResourceFile_cmd_condition_death.xlsx", sheet_name=None)
cond_prevalence = pd.read_excel(cmd_path / "ResourceFile_cmd_condition_prevalence.xlsx", sheet_name=None)
cond_symptoms = pd.read_excel(cmd_path / "ResourceFile_cmd_condition_symptoms.xlsx", sheet_name=None)
cond_hsi = pd.read_excel(cmd_path / "ResourceFile_cmd_condition_hsi.xlsx", sheet_name=None)
cond_testing = pd.read_excel(cmd_path / "ResourceFile_cmd_condition_testing.xlsx", sheet_name=None)
events_onset = pd.read_excel(cmd_path / "ResourceFile_cmd_events.xlsx", sheet_name=None)
events_death = pd.read_excel(cmd_path / "ResourceFile_cmd_events_death.xlsx", sheet_name=None)
events_symptoms = pd.read_excel(cmd_path / "ResourceFile_cmd_events_symptoms.xlsx", sheet_name=None)
events_hsi = pd.read_excel(cmd_path / "ResourceFile_cmd_events_hsi.xlsx", sheet_name=None)
self.load_parameters_from_dataframe(pd.read_csv(cmd_path / "ResourceFile_cmd_parameters.csv"))
def get_values(params, value):
"""replaces nans in the 'value' key with specified value"""
params['value'] = params['value'].replace(np.nan, value)
params['value'] = params['value'].astype(float)
return params.set_index('parameter_name')['value']
p = self.parameters
for condition in self.conditions:
p[f'{condition}_onset'] = get_values(cond_onset[condition], 1)
p[f'{condition}_removal'] = get_values(cond_removal[condition], 1)
p[f'{condition}_death'] = get_values(cond_death[condition], 1)
p[f'{condition}_initial_prev'] = get_values(cond_prevalence[condition], 0)
p[f'{condition}_symptoms'] = get_values(cond_symptoms[condition], 1)
p[f'{condition}_hsi'] = get_values(cond_hsi[condition], 1)
p['hypertension_testing'] = get_values(cond_testing['hypertension'], 1)
for event in self.events:
p[f'{event}_onset'] = get_values(events_onset[event], 1)
p[f'{event}_death'] = get_values(events_death[event], 1)
p[f'{event}_symptoms'] = get_values(events_symptoms[event], 1)
p[f'{event}_hsi'] = get_values(events_hsi[event], 1)
# Set the interval (in months) between the polls
p['interval_between_polls'] = 3
# Set the probability of an individual losing weight in the CardioMetabolicDisordersWeightLossEvent (this value
# doesn't vary by condition)
p['pr_bmi_reduction'] = 0.1
# Check that every value has been read-in successfully
for param_name in self.PARAMETERS:
assert self.parameters[param_name] is not None, f'Parameter "{param_name}" has not been set.'
# -------------------------------------------- SYMPTOMS -------------------------------------------------------
# Retrieve symptom probabilities
for condition in self.conditions:
if not self.parameters[f'{condition}_symptoms'].empty:
self.prob_symptoms[condition] = self.parameters[f'{condition}_symptoms']
else:
self.prob_symptoms[condition] = {}
for event in self.events:
if not self.parameters[f'{event}_symptoms'].empty:
self.prob_symptoms[event] = self.parameters[f'{event}_symptoms']
else:
self.prob_symptoms[event] = {}
# Register symptoms for conditions and give non-generic symptom 'average' healthcare seeking, except for
# chronic lower back pain, which has no healthcare seeking in adults
for symptom_name in self.symptoms:
if symptom_name == "chronic_lower_back_pain_symptoms":
self.sim.modules['SymptomManager'].register_symptom(
Symptom(name=symptom_name,
no_healthcareseeking_in_adults=True)
)
else:
self.sim.modules['SymptomManager'].register_symptom(
Symptom(name=symptom_name,
odds_ratio_health_seeking_in_adults=1.0)
)
# Register symptoms from events and make them emergencies
for event in self.events:
self.sim.modules['SymptomManager'].register_symptom(
Symptom.emergency(
name=f'{event}_damage', which='adults'
),
)
def initialise_population(self, population):
"""
Set property values for the initial population.
"""
self.age_cats = self.sim.modules['Demography'].AGE_RANGE_CATEGORIES
df = population.props
men = df.is_alive & (df.sex == 'M')
women = df.is_alive & (df.sex == 'F')
def sample_eligible(_filter, _p, _condition):
"""uses filter to get eligible population and samples individuals for condition using p"""
eligible = df.index[_filter]
init_prev = self.rng.choice([True, False], size=len(eligible), p=[_p, 1 - _p])
if sum(init_prev):
df.loc[eligible[init_prev], f'nc_{_condition}'] = True
def sample_eligible_diagnosis_medication(_filter, _p, _condition):
"""uses filter to get eligible population and samples individuals for prior diagnosis & medication use
using p"""
eligible = df.index[_filter]
init_diagnosis = self.rng.choice([True, False], size=len(eligible), p=[_p, 1 - _p])
if sum(init_diagnosis):
df.loc[eligible[init_diagnosis], f'nc_{_condition}_ever_diagnosed'] = True
df.loc[eligible[init_diagnosis], f'nc_{_condition}_date_diagnosis'] = self.sim.date
df.loc[eligible[init_diagnosis], f'nc_{_condition}_date_last_test'] = self.sim.date
df.loc[eligible[init_diagnosis], f'nc_{_condition}_on_medication'] = True
def sample_eligible_treatment_success(_filter, _p, _condition):
"""uses filter to get eligible population and samples individuals for prior diagnosis & medication use
using p"""
eligible = df.index[_filter]
init_treatment_works = self.rng.choice([True, False], size=len(eligible), p=[_p, 1 - _p])
if sum(init_treatment_works):
df.loc[eligible[init_treatment_works], f'nc_{_condition}_medication_prevents_death'] = True
for condition in self.conditions:
p = self.parameters[f'{condition}_initial_prev']
# Men & women without condition
men_wo_cond = men & ~df[f'nc_{condition}']
women_wo_cond = women & ~df[f'nc_{condition}']
for _age_range in self.age_cats:
# Select all eligible individuals (men & women w/o condition and in age range)
sample_eligible(men_wo_cond & (df.age_range == _age_range), p[f'm_{_age_range}'], condition)
sample_eligible(women_wo_cond & (df.age_range == _age_range), p[f'f_{_age_range}'], condition)
# ----- Set variables to false / NaT for everyone
df.loc[df.is_alive, f'nc_{condition}_date_last_test'] = pd.NaT
df.loc[df.is_alive, f'nc_{condition}_ever_diagnosed'] = False
df.loc[df.is_alive, f'nc_{condition}_date_diagnosis'] = pd.NaT
df.loc[df.is_alive, f'nc_{condition}_on_medication'] = False
df.loc[df.is_alive, f'nc_{condition}_medication_prevents_death'] = False
# ----- Sample among eligible population who have condition to set initial proportion diagnosed & on
# medication
p_initial_diagnosis = self.parameters[f'{condition}_hsi']['pr_diagnosed']
# Population with condition
w_cond = df[f'nc_{condition}']
sample_eligible_diagnosis_medication(w_cond, p_initial_diagnosis, condition)
# For those on medication, sample to set initial proportion for whom medication prevents death
p_treatment_works = self.parameters[f'{condition}_hsi']['pr_treatment_works']
# Population already on medication
on_med = df[f'nc_{condition}_on_medication']
sample_eligible_treatment_success(on_med, p_treatment_works, condition)
# ----- Impose the symptom on random sample of those with each condition to have:
# TODO: @britta make linear model data-specific and add in needed complexity
for symptom in self.prob_symptoms[condition].keys():
lm_init_symptoms = LinearModel(
LinearModelType.MULTIPLICATIVE,
self.prob_symptoms[condition].get(f'{symptom}'),
Predictor(
f'nc_{condition}',
conditions_are_mutually_exclusive=True,
conditions_are_exhaustive=True
)
.when(True, 1.0)
.when(False, 0.0))
has_symptom_at_init = lm_init_symptoms.predict(df.loc[df.is_alive], self.rng)
self.sim.modules['SymptomManager'].change_symptom(
person_id=has_symptom_at_init.index[has_symptom_at_init].tolist(),
symptom_string=f'{symptom}',
add_or_remove='+',
disease_module=self)
# ----- Set ever_diagnosed, date of diagnosis, and on_medication to false / NaT
# for everyone
for event in self.events:
df.loc[df.is_alive, f'nc_{event}'] = False
df.loc[df.is_alive, f'nc_{event}_date_last_event'] = pd.NaT
df.loc[df.is_alive, f'nc_{event}_ever_diagnosed'] = False
df.loc[df.is_alive, f'nc_{event}_date_diagnosis'] = pd.NaT
df.loc[df.is_alive, f'nc_{event}_on_medication'] = False
df.loc[df.is_alive, f'nc_{event}_scheduled_date_death'] = pd.NaT
df.loc[df.is_alive, f'nc_{event}_medication_prevents_death'] = False
# ----- Generate the initial "risk score" for the population based on exercise, diet, tobacco, alcohol, BMI
self.update_risk_score()
# ----- Set all other parameters to False / NaT
df.loc[df.is_alive, 'nc_ever_weight_loss_treatment'] = False
df.loc[df.is_alive, 'nc_weight_loss_worked'] = False
def initialise_simulation(self, sim):
"""Schedule:
* Main Polling Event
* Main Logging Event
* Build the LinearModels for the onset/removal of each condition:
"""
sim.schedule_event(CardioMetabolicDisorders_MainPollingEvent(self, self.parameters['interval_between_polls']),
sim.date)
sim.schedule_event(CardioMetabolicDisorders_LoggingEvent(self), sim.date)
# Get DALY weights
if 'HealthBurden' in self.sim.modules.keys():
self.daly_wts['daly_diabetes_uncomplicated'] = \
self.sim.modules['HealthBurden'].get_daly_weight(sequlae_code=971)
self.daly_wts['daly_diabetes_complicated'] = \
self.sim.modules['HealthBurden'].get_daly_weight(sequlae_code=970)
self.daly_wts['daly_chronic_kidney_disease_moderate'] = \
self.sim.modules['HealthBurden'].get_daly_weight(sequlae_code=978)
self.daly_wts['daly_chronic_ischemic_hd'] = \
self.sim.modules['HealthBurden'].get_daly_weight(sequlae_code=697)
self.daly_wts['daly_chronic_lower_back_pain'] = \
self.sim.modules['HealthBurden'].get_daly_weight(sequlae_code=1180)
self.daly_wts['daly_stroke'] = self.sim.modules['HealthBurden'].get_daly_weight(sequlae_code=701)
self.daly_wts['daly_heart_attack_acute_days_1_2'] = \
self.sim.modules['HealthBurden'].get_daly_weight(sequlae_code=689)
self.daly_wts['daly_heart_attack_acute_days_3_28'] = \
self.sim.modules['HealthBurden'].get_daly_weight(sequlae_code=693)
# Days 1-2 of heart attack have a more severe weight than days 3028; the average of these is what is used
# for the DALY weight for any heart attack event
self.daly_wts['daly_heart_attack_avg'] = (
(2 / 28) * self.daly_wts['daly_heart_attack_acute_days_1_2']
+ (26 / 28) * self.daly_wts['daly_heart_attack_acute_days_3_28']
)
self.trackers = {
'onset_condition': Tracker(conditions=self.conditions, age_groups=self.age_cats),
'incident_event': Tracker(conditions=self.events, age_groups=self.age_cats),
'prevalent_event': Tracker(conditions=self.events, age_groups=self.age_cats),
}
# Build the LinearModel for onset/removal/deaths for each condition
# Baseline probability of condition onset, removal, and death are annual; in LinearModel, rates are adjusted to
# be consistent with the polling interval
for condition in self.conditions:
self.lms_onset[condition] = self.build_linear_model(condition, self.parameters['interval_between_polls'],
lm_type='onset')
self.lms_removal[condition] = self.build_linear_model(condition, self.parameters['interval_between_polls'],
lm_type='removal')
self.lms_death[condition] = self.build_linear_model(condition, self.parameters['interval_between_polls'],
lm_type='death')
self.lms_symptoms[condition] = self.build_linear_model_symptoms(condition, self.parameters[
'interval_between_polls'])
# Hypertension is the only condition for which we assume some community-based testing occurs; build LM based on
# age / sex
self.lms_testing['hypertension'] = self.build_linear_model('hypertension', self.parameters[
'interval_between_polls'], lm_type='testing')
for event in self.events:
self.lms_event_onset[event] = self.build_linear_model(event, self.parameters['interval_between_polls'],
lm_type='onset')
self.lms_event_death[event] = self.build_linear_model(event, self.parameters['interval_between_polls'],
lm_type='death')
self.lms_event_symptoms[event] = self.build_linear_model_symptoms(event, self.parameters[
'interval_between_polls'])
# ------------------------------------------ DEFINE THE TESTS -------------------------------------------------
# Create the diagnostic representing the assessment for whether a person is diagnosed with diabetes
# NB. Sensitivity/specificity is assumed to be 100%
self.sim.modules['HealthSystem'].dx_manager.register_dx_test(
assess_diabetes=DxTest(
property='nc_diabetes',
item_codes=self.parameters['diabetes_hsi']['test_item_code'].astype(int)
)
)
# Create the diagnostic representing the assessment for whether a person is diagnosed with hypertension:
# blood pressure measurement
self.sim.modules['HealthSystem'].dx_manager.register_dx_test(
assess_hypertension=DxTest(
property='nc_hypertension'
)
)
# Create the diagnostic representing the assessment for whether a person is diagnosed with back pain
self.sim.modules['HealthSystem'].dx_manager.register_dx_test(
assess_chronic_lower_back_pain=DxTest(
property='nc_chronic_lower_back_pain'
)
)
# Create the diagnostic representing the assessment for whether a person is diagnosed with CKD
self.sim.modules['HealthSystem'].dx_manager.register_dx_test(
assess_chronic_kidney_disease=DxTest(
property='nc_chronic_kidney_disease',
item_codes=self.parameters['chronic_kidney_disease_hsi']['test_item_code'].astype(int)
)
)
# Create the diagnostic representing the assessment for whether a person is diagnosed with CIHD
self.sim.modules['HealthSystem'].dx_manager.register_dx_test(
assess_chronic_ischemic_hd=DxTest(
property='nc_chronic_ischemic_hd'
)
)
# Create the diagnostic representing the assessment for whether a person is diagnosed with stroke
self.sim.modules['HealthSystem'].dx_manager.register_dx_test(
assess_ever_stroke=DxTest(
property='nc_ever_stroke'
)
)
# Create the diagnostic representing the assessment for whether a person is diagnosed with heart attack
self.sim.modules['HealthSystem'].dx_manager.register_dx_test(
assess_ever_heart_attack=DxTest(
property='nc_ever_heart_attack'
)
)
def build_linear_model(self, condition, interval_between_polls, lm_type):
"""
Build a linear model for the risk of onset, removal, or death from a condition, occurrence or death from an
event, and community-based testing for hypertension.
:param condition: the condition or event to build the linear model for
:param interval_between_polls: the duration (in months) between the polls
:param lm_type: whether or not the lm is for onset, removal, death, or event in order to select the correct
parameter set below
:return: a linear model
"""
# Load parameters for correct condition/event
p = self.parameters[f'{condition}_{lm_type}']
# For events, probability of death does not need to be standardised to poll interval since event is a discrete
# occurrence
if condition.startswith('ever_') and lm_type == 'death':
baseline_annual_probability = p['baseline_annual_probability']
else:
baseline_annual_probability = 1 - math.exp(-interval_between_polls / 12 * p['baseline_annual_probability'])
# LinearModel expects native python types - if it's numpy type, convert it
baseline_annual_probability = float(baseline_annual_probability)
predictors = [
Predictor('sex').when('M', p['rr_male']),
Predictor(
'age_years',
conditions_are_mutually_exclusive=True,
conditions_are_exhaustive=True
)
.when('.between(0, 4)', p['rr_0_4'])
.when('.between(5, 9)', p['rr_5_9'])
.when('.between(10, 14)', p['rr_10_14'])
.when('.between(15, 19)', p['rr_15_19'])
.when('.between(20, 24)', p['rr_20_24'])
.when('.between(25, 29)', p['rr_25_29'])
.when('.between(30, 34)', p['rr_30_34'])
.when('.between(35, 39)', p['rr_35_39'])
.when('.between(40, 44)', p['rr_40_44'])
.when('.between(45, 49)', p['rr_45_49'])
.when('.between(50, 54)', p['rr_50_54'])
.when('.between(55, 59)', p['rr_55_59'])
.when('.between(60, 64)', p['rr_60_64'])
.when('.between(65, 69)', p['rr_65_69'])
.when('.between(70, 74)', p['rr_70_74'])
.when('.between(75, 79)', p['rr_75_79'])
.when('.between(80, 84)', p['rr_80_84'])
.when('.between(85, 89)', p['rr_85_89'])
.when('.between(90, 94)', p['rr_90_94'])
.when('.between(95, 99)', p['rr_95_99'])
.when('>= 100', p['rr_100']),
Predictor('li_urban').when(True, p['rr_urban']),
Predictor(
'li_wealth',
conditions_are_mutually_exclusive=True,
conditions_are_exhaustive=True,
)
.when('1', p['rr_wealth_1'])
.when('2', p['rr_wealth_2'])
.when('3', p['rr_wealth_3'])
.when('4', p['rr_wealth_4'])
.when('5', p['rr_wealth_5']),
Predictor(
'li_bmi',
conditions_are_mutually_exclusive=True,
conditions_are_exhaustive=True
)
.when('1', p['rr_bmi_1'])
.when('2', p['rr_bmi_2'])
.when('3', p['rr_bmi_3'])
.when('4', p['rr_bmi_4'])
.when('5', p['rr_bmi_5']),
Predictor('li_low_ex').when(True, p['rr_low_exercise']),
Predictor('li_high_salt').when(True, p['rr_high_salt']),
Predictor('li_high_sugar').when(True, p['rr_high_sugar']),
Predictor('li_tob').when(True, p['rr_tobacco']),
Predictor('li_ex_alc').when(True, p['rr_alcohol']),
Predictor(
'li_mar_stat',
conditions_are_mutually_exclusive=True,
conditions_are_exhaustive=True
)
.when('1', p['rr_marital_status_1'])
.when('2', p['rr_marital_status_2'])
.when('3', p['rr_marital_status_3']),
Predictor('li_in_ed').when(True, p['rr_in_education']),
Predictor(
'li_ed_lev',
conditions_are_mutually_exclusive=True,
conditions_are_exhaustive=True
)
.when('1', p['rr_current_education_level_1'])
.when('2', p['rr_current_education_level_2'])
.when('3', p['rr_current_education_level_3']),
Predictor('li_unimproved_sanitation').when(True, p['rr_unimproved_sanitation']),
Predictor('li_no_access_handwashing').when(True, p['rr_no_access_handwashing']),
Predictor('li_no_clean_drinking_water').when(True, p['rr_no_clean_drinking_water']),
Predictor('li_wood_burn_stove').when(True, p['rr_wood_burning_stove']),
Predictor('nc_diabetes').when(True, p['rr_diabetes']),
Predictor('nc_hypertension').when(True, p['rr_hypertension']),
Predictor('de_depr').when(True, p['rr_depression']),
Predictor('nc_chronic_kidney_disease').when(True, p['rr_chronic_kidney_disease']),
Predictor('nc_chronic_lower_back_pain').when(True, p['rr_chronic_lower_back_pain']),
Predictor('nc_chronic_ischemic_hd').when(True, p['rr_chronic_ischemic_heart_disease']),
Predictor('nc_ever_stroke').when(True, p['rr_ever_stroke']),
Predictor('nc_ever_heart_attack').when(True, p['rr_ever_heart_attack']),
Predictor('nc_diabetes_on_medication').when(True, p['rr_diabetes_on_medication']),
Predictor('nc_hypertension_on_medication').when(True, p['rr_hypertension_on_medication']),
Predictor('nc_chronic_lower_back_pain_on_medication').when(True, p[
'rr_chronic_lower_back_pain_on_medication']),
Predictor('nc_chronic_kidney_disease_on_medication').when(True, p[
'rr_chronic_kidney_disease_on_medication']),
Predictor('nc_chronic_ischemic_hd_on_medication').when(True, p[
'rr_chronic_ischemic_heart_disease_on_medication']),
Predictor('nc_ever_stroke_on_medication').when(True, p['rr_stroke_on_medication']),
Predictor('nc_ever_heart_attack_on_medication').when(True, p['rr_heart_attack_on_medication'])
]
conditional_predictors = [
Predictor().when('hv_inf & '
'(hv_art != "on_VL_suppressed")', p['rr_hiv']),
] if "Hiv" in self.sim.modules else []
linearmodel = LinearModel(
LinearModelType.MULTIPLICATIVE,
baseline_annual_probability,
*(predictors + conditional_predictors)
)
return linearmodel
def build_linear_model_symptoms(self, condition, interval_between_polls):
"""
Build a linear model for the risk of symptoms from a condition.
:param condition: the condition to build the linear model for
:param interval_between_polls: the duration (in months) between the polls
:return: a linear model
"""
# Use temporary empty dict to save results
lms_symptoms_dict = dict()
lms_symptoms_dict[condition] = {}
# Load parameters for correct condition
p = self.prob_symptoms[condition]
for symptom in p.keys():
p_symptom_onset = 1 - math.exp(-interval_between_polls / 12 * p.get(f'{symptom}'))
lms_symptoms_dict[condition][f'{symptom}'] = LinearModel(LinearModelType.MULTIPLICATIVE,
p_symptom_onset, Predictor(f'nc_{condition}')
.when(True, 1.0).otherwise(0.0))
return lms_symptoms_dict[condition]
def on_birth(self, mother_id, child_id):
"""Initialise our properties for a newborn individual.
:param mother_id: the mother for this child
:param child_id: the new child
"""
df = self.sim.population.props
for condition in self.conditions:
df.at[child_id, f'nc_{condition}'] = False
df.at[child_id, f'nc_{condition}_ever_diagnosed'] = False
df.at[child_id, f'nc_{condition}_date_diagnosis'] = pd.NaT
df.at[child_id, f'nc_{condition}_date_last_test'] = pd.NaT
df.at[child_id, f'nc_{condition}_on_medication'] = False
df.at[child_id, f'nc_{condition}_medication_prevents_death'] = False
for event in self.events:
df.at[child_id, f'nc_{event}'] = False
df.at[child_id, f'nc_{event}_date_last_event'] = pd.NaT
df.at[child_id, f'nc_{event}_ever_diagnosed'] = False
df.at[child_id, f'nc_{event}_on_medication'] = False
df.at[child_id, f'nc_{event}_date_diagnosis'] = pd.NaT
df.at[child_id, f'nc_{event}_scheduled_date_death'] = pd.NaT
df.at[child_id, f'nc_{event}_medication_prevents_death'] = False
df.at[child_id, 'nc_risk_score'] = 0
def update_risk_score(self):
"""
Generates or updates the risk score for individuals at initialisation of population or at each polling event
"""
df = self.sim.population.props
df.loc[df.is_alive, 'nc_risk_score'] = (df[[
'li_low_ex', 'li_high_salt', 'li_high_sugar', 'li_tob', 'li_ex_alc']] > 0).sum(1)
df.loc[df['li_bmi'] >= 3, ['nc_risk_score']] += 1
def report_daly_values(self):
"""Report disability weight (average values for the last month) to the HealthBurden module"""
def left_censor(obs, window_open):
return obs.apply(lambda x: max(x, window_open) if pd.notnull(x) else pd.NaT)
df = self.sim.population.props
dw = pd.DataFrame(data=0.0, index=df.index[df.is_alive], columns=self.CAUSES_OF_DISABILITY.keys())
# Diabetes: give everyone who is diagnosed or on medication uncomplicated diabetes daly weight
dw['diabetes'].loc[df['nc_diabetes_ever_diagnosed'] | df['nc_diabetes_on_medication']] = self.daly_wts[
'daly_diabetes_uncomplicated']
# Diabetes: overwrite those with symptoms to have complicated diabetes daly weight
dw['diabetes'].loc[
self.sim.modules['SymptomManager'].who_has('diabetes_symptoms')] = self.daly_wts[
'daly_diabetes_complicated']
# Chronic Lower Back Pain: give those who have symptoms moderate weight
dw['lower_back_pain'].loc[
self.sim.modules['SymptomManager'].who_has('chronic_lower_back_pain_symptoms')] = self.daly_wts[
'daly_chronic_lower_back_pain']
# Chronic Kidney Disease: give those who have symptoms moderate weight
dw['chronic_kidney_disease'].loc[
self.sim.modules['SymptomManager'].who_has('chronic_kidney_disease_symptoms')] = self.daly_wts[
'daly_chronic_kidney_disease_moderate']
# Stroke: give everyone moderate long-term consequences daly weight
dw['stroke'].loc[df.nc_ever_stroke] = self.daly_wts['daly_stroke']
# Chronic Ischemic Heart Disease: give everyone with CIHD symptoms moderate CIHD daly weight
dw['chronic_ischemic_hd'].loc[
self.sim.modules['SymptomManager'].who_has('chronic_ischemic_hd_symptoms')] = self.daly_wts[
'daly_chronic_ischemic_hd']
# Heart Attack: first calculate proportion of month spent following heart attack, then attach weighted daly
# Calculate fraction of the last month that was spent after having a heart attack
days_in_last_month = (self.sim.date - (self.sim.date - DateOffset(months=1))).days
start_heart_attack = left_censor(
df.loc[df.is_alive, 'nc_ever_heart_attack_date_last_event'], self.sim.date - DateOffset(months=1)
)
dur_heart_attack_in_days = (self.sim.date - start_heart_attack).dt.days.clip(
lower=0, upper=days_in_last_month).fillna(0.0)
fraction_of_month_heart_attack = dur_heart_attack_in_days / days_in_last_month
dw['heart_attack'] = fraction_of_month_heart_attack * self.daly_wts['daly_heart_attack_avg']
return dw
def on_hsi_alert(self, person_id, treatment_id):
"""
This is called whenever there is an HSI event commissioned by one of the other disease modules.
"""
pass
def do_at_generic_first_appt(
self,
patient_id: int,
patient_details: PatientDetails,
symptoms: List[str],
**kwargs
) -> IndividualPropertyUpdates:
# This is called by the HSI generic first appts module whenever a
# person attends an appointment and determines if the person will
# be tested for one or more conditions.
# A maximum of one instance of `HSI_CardioMetabolicDisorders_Investigations`
# is created for the person, during which multiple conditions can
# be investigated.
if patient_details.age_years <= 5:
return {}
# The list of conditions that will be investigated in follow-up HSI
conditions_to_investigate = []
# Marker for whether the person has any symptoms of interest
has_any_cmd_symptom = False
# Determine if there are any conditions that should be investigated:
for condition in self.conditions:
is_already_diagnosed = getattr(patient_details, f"nc_{condition}_ever_diagnosed")
has_symptom = f"{condition}_symptoms" in symptoms
date_of_last_test = getattr(
patient_details, f"nc_{condition}_date_last_test"
)
next_test_due = (
pd.isnull(date_of_last_test)
or (self.sim.date - date_of_last_test).days > DAYS_IN_YEAR / 2
)
p_assess_if_no_symptom = self.parameters[f"{condition}_hsi"].get(
"pr_assessed_other_symptoms"
)
if (not is_already_diagnosed) and (
has_symptom
or (
next_test_due
and (self.rng.random_sample() < p_assess_if_no_symptom)
)
):
# If the person is not already diagnosed, and either
# has the symptom or is due a routine check,
# ... add this condition to be investigated in the appointment.
conditions_to_investigate.append(condition)
if (not is_already_diagnosed) and has_symptom:
has_any_cmd_symptom = True
# Schedule follow-up HSI *if* there are any conditions to investigate:
if conditions_to_investigate:
event = HSI_CardioMetabolicDisorders_Investigations(
module=self,
person_id=patient_id,
conditions_to_investigate=conditions_to_investigate,
has_any_cmd_symptom=has_any_cmd_symptom,
)
self.healthsystem.schedule_hsi_event(event, topen=self.sim.date, priority=0)
def do_at_generic_first_appt_emergency(
self,
patient_id: int,
patient_details: PatientDetails = None,
symptoms: List[str] = None,
**kwargs,
) -> IndividualPropertyUpdates:
# This is called by the HSI generic first appts module whenever
# a person attends an emergency appointment and determines if they
# will receive emergency care based on the duration of time since
# symptoms have appeared. A maximum of one instance of
# `HSI_CardioMetabolicDisorders_SeeksEmergencyCareAndGetsTreatment`
# is created for the person, during which multiple events can be
# investigated.
ev_to_investigate = []
for ev in self.events:
# If the person has symptoms of damage from within the last 3 days,
# schedule them for emergency care
if f"{ev}_damage" in symptoms and (
(
self.sim.date
- getattr(patient_details, f"nc_{ev}_date_last_event")
).days
<= 3
):
ev_to_investigate.append(ev)
if ev_to_investigate:
event = HSI_CardioMetabolicDisorders_SeeksEmergencyCareAndGetsTreatment(
module=self,
person_id=patient_id,
events_to_investigate=ev_to_investigate,
)
self.healthsystem.schedule_hsi_event(event, topen=self.sim.date, priority=1)
class Tracker:
"""Helper class for keeping a count with respect to some conditions and some age-groups"""
def __init__(self, conditions: list, age_groups: list):
self._conditions = conditions
self._age_groups = age_groups
self._tracker = self._make_new_tracker()
def _make_new_tracker(self):
return {c: {a: 0.0 for a in self._age_groups} for c in self._conditions}
def reset(self):
self._tracker = self._make_new_tracker()
def add(self, condition: str, _to_add: dict):
for _a in _to_add:
if _a in self._tracker[condition]:
self._tracker[condition][_a] += _to_add[_a]
def report(self):
return self._tracker
# ---------------------------------------------------------------------------------------------------------
# DISEASE MODULE EVENTS
#
# The regular event that actually changes individuals' condition or event status, occurring every 3 months
# and synchronously for all persons.
# Individual level events (HSI, death or cardio-metabolic events) may occur at other times.
# ---------------------------------------------------------------------------------------------------------
class CardioMetabolicDisorders_MainPollingEvent(RegularEvent, PopulationScopeEventMixin):
"""The Main Polling Event.
* Establishes onset of each condition
* Establishes removal of each condition
* Schedules events that arise, according the condition.
"""
def __init__(self, module, interval_between_polls):
"""The Main Polling Event of the CardioMetabolicDisorders Module
:param module: the module that created this event
"""
super().__init__(module, frequency=DateOffset(months=interval_between_polls))
assert isinstance(module, CardioMetabolicDisorders)
def apply(self, population):
"""Apply this event to the population.
:param population: the current population
"""
df = population.props
m = self.module
rng = m.rng
# Function to schedule deaths on random day throughout polling period
def schedule_death_to_occur_before_next_poll(p_id, cond):
self.sim.schedule_event(
CardioMetabolicDisordersDeathEvent(self.module, p_id, cond),
random_date(self.sim.date, self.sim.date + self.frequency - pd.DateOffset(days=1), m.rng)
)
# -------------------------------- COMMUNITY SCREENING FOR HYPERTENSION ---------------------------------------
# A subset of individuals aged >= 50 will receive a blood pressure measurement without directly presenting to
# the healthcare system
eligible_population = df.is_alive & ~df['nc_hypertension_ever_diagnosed']
will_test = self.module.lms_testing['hypertension'].predict(
df.loc[eligible_population], rng, squeeze_single_row_output=False)
idx_will_test = will_test[will_test].index
# Schedule persons for community testing before next polling event
for person_id in idx_will_test:
self.sim.modules['HealthSystem'].schedule_hsi_event(
hsi_event=HSI_CardioMetabolicDisorders_CommunityTestingForHypertension(person_id=person_id,
module=self.module),
priority=1,
topen=random_date(self.sim.date, self.sim.date + self.frequency - pd.DateOffset(days=2), m.rng),
tclose=self.sim.date + self.frequency - pd.DateOffset(days=1) # (to occur before next polling)
)
# ------------------------------ DETERMINE ONSET / REMOVAL OF CONDITIONS -------------------------------------
for condition in self.module.conditions:
# Onset:
eligible_population = df.is_alive & ~df[f'nc_{condition}']
acquires_condition = self.module.lms_onset[condition].predict(
df.loc[eligible_population], rng, squeeze_single_row_output=False)