|
| 1 | +from tlo.notify import notifier |
| 2 | + |
| 3 | +from pathlib import Path |
| 4 | +from typing import Optional |
| 5 | +from tlo import Module, logging, population |
| 6 | +from tlo.population import Population |
| 7 | +import pandas as pd |
| 8 | + |
| 9 | +from tlo.util import convert_chain_links_into_EAV |
| 10 | + |
| 11 | +import copy |
| 12 | + |
| 13 | +logger = logging.getLogger(__name__) |
| 14 | +logger.setLevel(logging.INFO) |
| 15 | + |
| 16 | +class CollectEventChains(Module): |
| 17 | + |
| 18 | + def __init__(self, name=None): |
| 19 | + super().__init__(name) |
| 20 | + |
| 21 | + # This is how I am passing data from fnc taking place before event to the one after |
| 22 | + # It doesn't seem very elegant but not sure how else to go about it |
| 23 | + self.print_chains = False |
| 24 | + self.df_before = [] |
| 25 | + self.row_before = pd.Series() |
| 26 | + self.mni_instances_before = False |
| 27 | + self.mni_row_before = {} |
| 28 | + self.entire_mni_before = {} |
| 29 | + |
| 30 | + def initialise_simulation(self, sim): |
| 31 | + notifier.add_listener("event_about_to_run", self.on_notification_event_about_to_run) |
| 32 | + notifier.add_listener("event_has_just_ran", self.on_notification_event_has_just_ran) |
| 33 | + |
| 34 | + def read_parameters(self, resourcefilepath: Optional[Path] = None): |
| 35 | + pass |
| 36 | + |
| 37 | + def initialise_population(self, population): |
| 38 | + pass |
| 39 | + |
| 40 | + def on_birth(self, mother, child): |
| 41 | + pass |
| 42 | + |
| 43 | + def on_notification_sim_about_to_start(self,data): |
| 44 | + pass |
| 45 | + |
| 46 | + def on_notification_event_about_to_run(self, data): |
| 47 | + """Do this when notified that an event is about to run. This function checks whether this event should be logged as part of the event chains, and if so stored required information before the event has occurred. """ |
| 48 | + print("This is the data I received ", data) |
| 49 | + |
| 50 | + # Initialise these variables |
| 51 | + self.print_chains = False |
| 52 | + self.df_before = [] |
| 53 | + self.row_before = pd.Series() |
| 54 | + self.mni_instances_before = False |
| 55 | + self.mni_row_before = {} |
| 56 | + self.entire_mni_before = {} |
| 57 | + |
| 58 | + print("My Modules") |
| 59 | + print(self.sim.modules.keys()) |
| 60 | + # Only print event if it belongs to modules of interest and if it is not in the list of events to ignore |
| 61 | + if all(sub not in str(data['EventName']) for sub in self.sim.generate_event_chains_ignore_events): |
| 62 | + |
| 63 | + # Will eventually use this once I can actually GET THE NAME OF THE SELF |
| 64 | + #if not set(self.sim.generate_event_chains_ignore_events).intersection(str(self)): |
| 65 | + |
| 66 | + self.print_chains = True |
| 67 | + |
| 68 | + # Target is single individual |
| 69 | + if not isinstance(data["target"], Population): |
| 70 | + |
| 71 | + # Save row for comparison after event has occurred |
| 72 | + self.row_before = self.sim.population.props.loc[abs(data['target'])].copy().fillna(-99999) |
| 73 | + |
| 74 | + # Check if individual is already in mni dictionary, if so copy her original status |
| 75 | + if 'PregnancySupervisor' in self.sim.modules: |
| 76 | + mni = self.sim.modules['PregnancySupervisor'].mother_and_newborn_info |
| 77 | + if data['target'] in mni: |
| 78 | + self.mni_instances_before = True |
| 79 | + self.mni_row_before = mni[data['target']].copy() |
| 80 | + else: |
| 81 | + self.mni_row_before = None |
| 82 | + |
| 83 | + else: |
| 84 | + |
| 85 | + # This will be a population-wide event. In order to find individuals for which this led to |
| 86 | + # a meaningful change, make a copy of the while pop dataframe/mni before the event has occurred. |
| 87 | + self.df_before = self.sim.population.props.copy() |
| 88 | + if 'PregnancySupervisor' in self.sim.modules: |
| 89 | + self.entire_mni_before = copy.deepcopy(self.sim.modules['PregnancySupervisor'].mother_and_newborn_info) |
| 90 | + else: |
| 91 | + self.entire_mni_before = None |
| 92 | + |
| 93 | + return |
| 94 | + |
| 95 | + |
| 96 | + def on_notification_event_has_just_ran(self, data): |
| 97 | + """ If print_chains=True, this function logs the event and identifies and logs the any property changes that have occured to one or multiple individuals as a result of the event taking place. """ |
| 98 | + print("This is the data I received ", data) |
| 99 | + |
| 100 | + chain_links = {} |
| 101 | + |
| 102 | + # Target is single individual |
| 103 | + if not isinstance(data["target"], Population): |
| 104 | + |
| 105 | + # Copy full new status for individual |
| 106 | + row_after = self.sim.population.props.loc[abs(data['target'])].fillna(-99999) |
| 107 | + |
| 108 | + # Check if individual is in mni after the event |
| 109 | + mni_instances_after = False |
| 110 | + if 'PregnancySupervisor' in self.sim.modules: |
| 111 | + mni = self.sim.modules['PregnancySupervisor'].mother_and_newborn_info |
| 112 | + if data['target'] in mni: |
| 113 | + mni_instances_after = True |
| 114 | + else: |
| 115 | + mni_instances_after = None |
| 116 | + |
| 117 | + # Create and store event for this individual, regardless of whether any property change occurred |
| 118 | + link_info = { |
| 119 | + 'EventName' : data['EventName'], |
| 120 | + } |
| 121 | + |
| 122 | + # Store (if any) property changes as a result of the event for this individual |
| 123 | + for key in self.row_before.index: |
| 124 | + if self.row_before[key] != row_after[key]: # Note: used fillna previously, so this is safe |
| 125 | + link_info[key] = row_after[key] |
| 126 | + |
| 127 | + if 'PregnancySupervisor' in self.sim.modules: |
| 128 | + # Now check and store changes in the mni dictionary, accounting for following cases: |
| 129 | + # Individual is in mni dictionary before and after |
| 130 | + if self.mni_instances_before and mni_instances_after: |
| 131 | + for key in self.mni_row_before: |
| 132 | + if self.mni_values_differ(mni_row_before[key], mni[data['target']][key]): |
| 133 | + link_info[key] = mni[data['target']][key] |
| 134 | + # Individual is only in mni dictionary before event |
| 135 | + elif self.mni_instances_before and not mni_instances_after: |
| 136 | + default = self.sim.modules['PregnancySupervisor'].default_all_mni_values |
| 137 | + for key in self.mni_row_before: |
| 138 | + if self.mni_values_differ(mni_row_before[key], default[key]): |
| 139 | + link_info[key] = default[key] |
| 140 | + # Individual is only in mni dictionary after event |
| 141 | + elif mni_instances_after and not self.mni_instances_before: |
| 142 | + default = self.sim.modules['PregnancySupervisor'].default_all_mni_values |
| 143 | + for key in default: |
| 144 | + if self.mni_values_differ(default[key], mni[data['target']][key]): |
| 145 | + link_info[key] = mni[data['target']][key] |
| 146 | + # Else, no need to do anything |
| 147 | + |
| 148 | + # Add individual to the chain links |
| 149 | + chain_links[data['target']] = link_info |
| 150 | + |
| 151 | + else: |
| 152 | + # Target is entire population. Identify individuals for which properties have changed |
| 153 | + # and store their changes. |
| 154 | + |
| 155 | + # Population frame after event |
| 156 | + df_after = self.sim.population.props |
| 157 | + if 'PregnancySupervisor' in self.sim.modules: |
| 158 | + entire_mni_after = copy.deepcopy(self.sim.modules['PregnancySupervisor'].mother_and_newborn_info) |
| 159 | + else: |
| 160 | + entire_mni_after = None |
| 161 | + |
| 162 | + # Create and store the event and dictionary of changes for affected individuals |
| 163 | + chain_links = self.compare_population_dataframe_and_mni(self.df_before, df_after, self.entire_mni_before, entire_mni_after) |
| 164 | + |
| 165 | + if chain_links: |
| 166 | + # Convert chain_links into EAV |
| 167 | + ednav = convert_chain_links_into_EAV(chain_links) |
| 168 | + |
| 169 | + logger.info(key='event_chains', |
| 170 | + data= ednav.to_dict(), |
| 171 | + description='Links forming chains of events for simulated individuals') |
| 172 | + |
| 173 | + # Reset variables |
| 174 | + self.print_chains = False |
| 175 | + self.df_before = [] |
| 176 | + self.row_before = pd.Series() |
| 177 | + self.mni_instances_before = False |
| 178 | + self.mni_row_before = {} |
| 179 | + self.entire_mni_before = {} |
| 180 | + |
| 181 | + return |
| 182 | + |
| 183 | + def mni_values_differ(self, v1, v2): |
| 184 | + |
| 185 | + if isinstance(v1, list) and isinstance(v2, list): |
| 186 | + return v1 != v2 # simple element-wise comparison |
| 187 | + |
| 188 | + if pd.isna(v1) and pd.isna(v2): |
| 189 | + return False # treat both NaT/NaN as equal |
| 190 | + return v1 != v2 |
| 191 | + |
| 192 | + def compare_entire_mni_dicts(self,entire_mni_before, entire_mni_after): |
| 193 | + diffs = {} |
| 194 | + |
| 195 | + all_individuals = set(entire_mni_before.keys()) | set(entire_mni_after.keys()) |
| 196 | + |
| 197 | + for person in all_individuals: |
| 198 | + if person not in entire_mni_before: # but is afterward |
| 199 | + for key in entire_mni_after[person]: |
| 200 | + if self.mni_values_differ(entire_mni_after[person][key],self.sim.modules['PregnancySupervisor'].default_all_mni_values[key]): |
| 201 | + if person not in diffs: |
| 202 | + diffs[person] = {} |
| 203 | + diffs[person][key] = entire_mni_after[person][key] |
| 204 | + |
| 205 | + elif person not in entire_mni_after: # but is beforehand |
| 206 | + for key in entire_mni_before[person]: |
| 207 | + if self.mni_values_differ(entire_mni_before[person][key],self.sim.modules['PregnancySupervisor'].default_all_mni_values[key]): |
| 208 | + if person not in diffs: |
| 209 | + diffs[person] = {} |
| 210 | + diffs[person][key] = self.sim.modules['PregnancySupervisor'].default_all_mni_values[key] |
| 211 | + |
| 212 | + else: # person is in both |
| 213 | + # Compare properties |
| 214 | + for key in entire_mni_before[person]: |
| 215 | + if self.mni_values_differ(entire_mni_before[person][key],entire_mni_after[person][key]): |
| 216 | + if person not in diffs: |
| 217 | + diffs[person] = {} |
| 218 | + diffs[person][key] = entire_mni_after[person][key] |
| 219 | + |
| 220 | + return diffs |
| 221 | + |
| 222 | + def compare_population_dataframe_and_mni(self,df_before, df_after, entire_mni_before, entire_mni_after): |
| 223 | + """ This function compares the population dataframe and mni dictionary before/after a population-wide event has occurred. |
| 224 | + It allows us to identify the individuals for which this event led to a significant (i.e. property) change, and to store the properties which have changed as a result of it. """ |
| 225 | + |
| 226 | + # Create a mask of where values are different |
| 227 | + diff_mask = (df_before != df_after) & ~(df_before.isna() & df_after.isna()) |
| 228 | + if 'PregnancySupervisor' in self.sim.modules: |
| 229 | + diff_mni = self.compare_entire_mni_dicts(entire_mni_before, entire_mni_after) |
| 230 | + else: |
| 231 | + diff_mni = [] |
| 232 | + |
| 233 | + # Create an empty list to store changes for each of the individuals |
| 234 | + chain_links = {} |
| 235 | + len_of_diff = len(diff_mask) |
| 236 | + |
| 237 | + # Loop through each row of the mask |
| 238 | + persons_changed = [] |
| 239 | + |
| 240 | + for idx, row in diff_mask.iterrows(): |
| 241 | + changed_cols = row.index[row].tolist() |
| 242 | + |
| 243 | + if changed_cols: # Proceed only if there are changes in the row |
| 244 | + persons_changed.append(idx) |
| 245 | + # Create a dictionary for this person |
| 246 | + # First add event info |
| 247 | + link_info = { |
| 248 | + 'EventName': type(self).__name__, |
| 249 | + } |
| 250 | + |
| 251 | + # Store the new values from df_after for the changed columns |
| 252 | + for col in changed_cols: |
| 253 | + link_info[col] = df_after.at[idx, col] |
| 254 | + |
| 255 | + if idx in diff_mni: |
| 256 | + # This person has also undergone changes in the mni dictionary, so add these here |
| 257 | + for key in diff_mni[idx]: |
| 258 | + link_info[col] = diff_mni[idx][key] |
| 259 | + |
| 260 | + # Append the event and changes to the individual key |
| 261 | + chain_links[idx] = link_info |
| 262 | + |
| 263 | + if 'PregnancySupervisor' in self.sim.modules: |
| 264 | + # For individuals which only underwent changes in mni dictionary, save changes here |
| 265 | + if len(diff_mni)>0: |
| 266 | + for key in diff_mni: |
| 267 | + if key not in persons_changed: |
| 268 | + # If individual hadn't been previously added due to changes in pop df, add it here |
| 269 | + link_info = { |
| 270 | + 'EventName': type(self).__name__, |
| 271 | + } |
| 272 | + |
| 273 | + for key_prop in diff_mni[key]: |
| 274 | + link_info[key_prop] = diff_mni[key][key_prop] |
| 275 | + |
| 276 | + chain_links[key] = link_info |
| 277 | + |
| 278 | + return chain_links |
| 279 | + |
| 280 | + |
| 281 | + |
0 commit comments