vivarium-public-health 4.2.6__py3-none-any.whl → 4.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,139 @@
1
+ """
2
+ ======================
3
+ Intervention Observers
4
+ ======================
5
+
6
+ This module contains tools for observing risk exposure during the simulation.
7
+
8
+ """
9
+
10
+ import pandas as pd
11
+ from vivarium.framework.engine import Builder
12
+
13
+ from vivarium_public_health.results.columns import COLUMNS
14
+ from vivarium_public_health.results.observer import PublicHealthObserver
15
+ from vivarium_public_health.utilities import to_years
16
+
17
+
18
+ class CategoricalInterventionObserver(PublicHealthObserver):
19
+ """
20
+ A class for observering interventions. This class has the same implementation as
21
+ the 'CategoricalRiskObserver' class.
22
+
23
+ """
24
+
25
+ @property
26
+ def columns_required(self) -> list[str] | None:
27
+ """The columns required by this observer."""
28
+ return ["alive"]
29
+
30
+ #####################
31
+ # Lifecycle methods #
32
+ #####################
33
+
34
+ def __init__(self, intervention: str) -> None:
35
+ """Constructor for this observer.
36
+
37
+ Parameters
38
+ ----------
39
+ intervention
40
+ The name of the intervention being observed
41
+ """
42
+ super().__init__()
43
+ self.intervention = intervention
44
+ self.coverage_pipeline_name = f"{self.intervention}.coverage"
45
+
46
+ #################
47
+ # Setup methods #
48
+ #################
49
+
50
+ def setup(self, builder: Builder) -> None:
51
+ """Set up the observer."""
52
+ self.step_size = builder.time.step_size()
53
+ self.categories = builder.data.load(f"intervention.{self.intervention}.categories")
54
+
55
+ def get_configuration_name(self) -> str:
56
+ return self.intervention
57
+
58
+ def register_observations(self, builder: Builder) -> None:
59
+ """Register a stratification and observation.
60
+
61
+ Notes
62
+ -----
63
+ While it's typical for all stratification registrations to be encapsulated
64
+ in a single class (i.e. the
65
+ :class:ResultsStratifier <vivarium_public_health.results.stratification.ResultsStratifier),
66
+ this observer registers an additional one. While it could be registered
67
+ in the ``ResultsStratifier`` as well, it is specific to this observer and
68
+ so it is registered here while we have easy access to the required categories
69
+ and value names.
70
+ """
71
+ builder.results.register_stratification(
72
+ f"{self.intervention}",
73
+ list(self.categories.keys()),
74
+ requires_values=[self.coverage_pipeline_name],
75
+ )
76
+ self.register_adding_observation(
77
+ builder=builder,
78
+ name=f"person_time_{self.intervention}",
79
+ pop_filter=f'alive == "alive" and tracked==True',
80
+ when="time_step__prepare",
81
+ requires_columns=["alive"],
82
+ requires_values=[self.coverage_pipeline_name],
83
+ additional_stratifications=self.configuration.include + [self.intervention],
84
+ excluded_stratifications=self.configuration.exclude,
85
+ aggregator=self.aggregate_intervention_category_person_time,
86
+ )
87
+
88
+ ###############
89
+ # Aggregators #
90
+ ###############
91
+
92
+ def aggregate_intervention_category_person_time(self, x: pd.DataFrame) -> float:
93
+ """Aggregate the person time for this time step."""
94
+ return len(x) * to_years(self.step_size())
95
+
96
+ ##############################
97
+ # Results formatting methods #
98
+ ##############################
99
+
100
+ def format(self, measure: str, results: pd.DataFrame) -> pd.DataFrame:
101
+ """Rename the appropriate column to 'sub_entity'.
102
+
103
+ The primary thing this method does is rename the risk column
104
+ to 'sub_entity'. We do this here instead of the 'get_sub_entity_column'
105
+ method simply because we do not want the risk column at all. If we keep
106
+ it here and then return it as the sub-entity column later, the final
107
+ results would have both.
108
+
109
+ Parameters
110
+ ----------
111
+ measure
112
+ The measure.
113
+ results
114
+ The results to format.
115
+
116
+ Returns
117
+ -------
118
+ The formatted results.
119
+ """
120
+ results = results.reset_index()
121
+ results.rename(columns={self.intervention: COLUMNS.SUB_ENTITY}, inplace=True)
122
+ return results
123
+
124
+ def get_measure_column(self, measure: str, results: pd.DataFrame) -> pd.Series:
125
+ """Get the 'measure' column values."""
126
+ return pd.Series("person_time", index=results.index)
127
+
128
+ def get_entity_type_column(self, measure: str, results: pd.DataFrame) -> pd.Series:
129
+ """Get the 'entity_type' column values."""
130
+ return pd.Series("rei", index=results.index)
131
+
132
+ def get_entity_column(self, measure: str, results: pd.DataFrame) -> pd.Series:
133
+ """Get the 'entity' column values."""
134
+ return pd.Series(self.intervention, index=results.index)
135
+
136
+ def get_sub_entity_column(self, measure: str, results: pd.DataFrame) -> pd.Series:
137
+ """Get the 'sub_entity' column values."""
138
+ # The sub-entity col was created in the 'format' method
139
+ return results[COLUMNS.SUB_ENTITY]
@@ -7,30 +7,14 @@ This module contains tools for modeling categorical and continuous risk
7
7
  exposure.
8
8
 
9
9
  """
10
+ from typing import NamedTuple
10
11
 
11
- from typing import Any
12
-
13
- import pandas as pd
14
- from vivarium import Component
15
12
  from vivarium.framework.engine import Builder
16
- from vivarium.framework.event import Event
17
- from vivarium.framework.population import SimulantData
18
- from vivarium.framework.randomness import RandomnessStream
19
- from vivarium.framework.resource import Resource
20
- from vivarium.framework.values import Pipeline
21
13
 
22
- from vivarium_public_health.risks.data_transformations import get_exposure_post_processor
23
- from vivarium_public_health.risks.distributions import (
24
- ContinuousDistribution,
25
- DichotomousDistribution,
26
- EnsembleDistribution,
27
- PolytomousDistribution,
28
- RiskExposureDistribution,
29
- )
30
- from vivarium_public_health.utilities import EntityString, get_lookup_columns
14
+ from vivarium_public_health.exposure import Exposure
31
15
 
32
16
 
33
- class Risk(Component):
17
+ class Risk(Exposure):
34
18
  """A model for a risk factor defined by either a continuous or a categorical value.
35
19
 
36
20
  For example,
@@ -89,217 +73,34 @@ class Risk(Component):
89
73
 
90
74
  """
91
75
 
92
- exposure_distributions = {
93
- "dichotomous": DichotomousDistribution,
94
- "ordered_polytomous": PolytomousDistribution,
95
- "unordered_polytomous": PolytomousDistribution,
96
- "normal": ContinuousDistribution,
97
- "lognormal": ContinuousDistribution,
98
- "ensemble": EnsembleDistribution,
99
- }
100
-
101
- ##############
102
- # Properties #
103
- ##############
104
-
105
76
  @property
106
- def name(self) -> str:
107
- return self.risk
77
+ def exposure_type(self) -> str:
78
+ """The measure of the risk exposure."""
79
+ return "exposure"
108
80
 
109
81
  @property
110
- def configuration_defaults(self) -> dict[str, Any]:
111
- return {
112
- self.name: {
113
- "data_sources": {
114
- "exposure": f"{self.risk}.exposure",
115
- "ensemble_distribution_weights": f"{self.risk}.exposure_distribution_weights",
116
- "exposure_standard_deviation": f"{self.risk}.exposure_standard_deviation",
117
- },
118
- "distribution_type": f"{self.risk}.distribution",
119
- # rebinned_exposed only used for DichotomousDistribution
120
- "rebinned_exposed": [],
121
- "category_thresholds": [],
122
- }
123
- }
82
+ def dichotomous_exposure_category_names(self) -> NamedTuple:
83
+ """The name of the exposed category for this intervention."""
124
84
 
125
- @property
126
- def columns_created(self) -> list[str]:
127
- columns_to_create = [self.propensity_column_name]
128
- if self.create_exposure_column:
129
- columns_to_create.append(self.exposure_column_name)
130
- return columns_to_create
85
+ class __Categories(NamedTuple):
86
+ exposed: str = "exposed"
87
+ unexposed: str = "unexposed"
131
88
 
132
- @property
133
- def initialization_requirements(self) -> list[str | Resource]:
134
- return [self.randomness]
89
+ categories = __Categories()
90
+ return categories
135
91
 
136
92
  #####################
137
93
  # Lifecycle methods #
138
94
  #####################
139
95
 
140
- def __init__(self, risk: str):
141
- """
142
-
143
- Parameters
144
- ----------
145
- risk
146
- the type and name of a risk, specified as "type.name". Type is singular.
147
- """
148
- super().__init__()
149
- self.risk = EntityString(risk)
150
- self.distribution_type = None
151
-
152
- self.randomness_stream_name = f"initial_{self.risk.name}_propensity"
153
- self.propensity_column_name = f"{self.risk.name}_propensity"
154
- self.propensity_pipeline_name = f"{self.risk.name}.propensity"
155
- self.exposure_pipeline_name = f"{self.risk.name}.exposure"
156
- self.exposure_column_name = f"{self.risk.name}_exposure"
157
-
158
- #################
159
- # Setup methods #
160
- #################
161
-
162
- def build_all_lookup_tables(self, builder: "Builder") -> None:
163
- # All lookup tables are built in the exposure distribution
164
- pass
165
-
166
- # noinspection PyAttributeOutsideInit
167
96
  def setup(self, builder: Builder) -> None:
168
- self.distribution_type = self.get_distribution_type(builder)
169
- self.exposure_distribution = self.get_exposure_distribution(builder)
170
-
171
- self.randomness = self.get_randomness_stream(builder)
172
- self.propensity = self.get_propensity_pipeline(builder)
173
- self.exposure = self.get_exposure_pipeline(builder)
174
-
175
- # We want to set this to True iff there is a non-loglinear risk effect
97
+ super().setup(builder)
98
+ # We want to set this to True if there is a non-loglinear risk effect
176
99
  # on this risk instance
177
100
  self.create_exposure_column = bool(
178
101
  [
179
102
  component
180
103
  for component in builder.components.list_components()
181
- if component.startswith(f"non_log_linear_risk_effect.{self.risk.name}_on_")
104
+ if component.startswith(f"non_log_linear_risk_effect.{self.entity.name}_on_")
182
105
  ]
183
106
  )
184
-
185
- def get_distribution_type(self, builder: Builder) -> str:
186
- """Get the distribution type for the risk from the configuration.
187
-
188
- If the configured distribution type is not one of the supported types,
189
- it is assumed to be a data source and the data is retrieved using the
190
- get_data method.
191
-
192
- Parameters
193
- ----------
194
- builder
195
- The builder object.
196
-
197
- Returns
198
- -------
199
- The distribution type.
200
- """
201
- if self.configuration is None:
202
- self.configuration = self.get_configuration(builder)
203
-
204
- distribution_type = self.configuration["distribution_type"]
205
- if distribution_type not in self.exposure_distributions.keys():
206
- # todo deal with incorrect typing
207
- distribution_type = self.get_data(builder, distribution_type)
208
-
209
- if self.configuration["rebinned_exposed"]:
210
- if distribution_type != "dichotomous" or "polytomous" not in distribution_type:
211
- raise ValueError(
212
- f"Unsupported risk distribution type '{distribution_type}' "
213
- f"for {self.name}. Rebinned exposed categories are only "
214
- "supported for dichotomous and polytomous distributions."
215
- )
216
- distribution_type = "dichotomous"
217
- return distribution_type
218
-
219
- def get_exposure_distribution(self, builder: Builder) -> RiskExposureDistribution:
220
- """Creates and sets up the exposure distribution component for the Risk
221
- based on its distribution type.
222
-
223
- Parameters
224
- ----------
225
- builder
226
- The builder object.
227
-
228
- Returns
229
- -------
230
- The exposure distribution.
231
-
232
- Raises
233
- ------
234
- NotImplementedError
235
- If the distribution type is not supported.
236
- """
237
- try:
238
- exposure_distribution = self.exposure_distributions[self.distribution_type](
239
- self.risk, self.distribution_type
240
- )
241
- except KeyError:
242
- raise NotImplementedError(
243
- f"Distribution type {self.distribution_type} is not supported."
244
- )
245
-
246
- exposure_distribution.setup_component(builder)
247
- return exposure_distribution
248
-
249
- def get_randomness_stream(self, builder: Builder) -> RandomnessStream:
250
- return builder.randomness.get_stream(self.randomness_stream_name, component=self)
251
-
252
- def get_propensity_pipeline(self, builder: Builder) -> Pipeline:
253
- return builder.value.register_value_producer(
254
- self.propensity_pipeline_name,
255
- source=lambda index: (
256
- self.population_view.subview([self.propensity_column_name])
257
- .get(index)
258
- .squeeze(axis=1)
259
- ),
260
- component=self,
261
- required_resources=[self.propensity_column_name],
262
- )
263
-
264
- def get_exposure_pipeline(self, builder: Builder) -> Pipeline:
265
- required_columns = get_lookup_columns(
266
- self.exposure_distribution.lookup_tables.values()
267
- )
268
- return builder.value.register_value_producer(
269
- self.exposure_pipeline_name,
270
- source=self.get_current_exposure,
271
- component=self,
272
- required_resources=required_columns
273
- + [
274
- self.propensity,
275
- self.exposure_distribution.exposure_parameters,
276
- ],
277
- preferred_post_processor=get_exposure_post_processor(builder, self.name),
278
- )
279
-
280
- ########################
281
- # Event-driven methods #
282
- ########################
283
-
284
- def on_initialize_simulants(self, pop_data: SimulantData) -> None:
285
- propensity = pd.Series(
286
- self.randomness.get_draw(pop_data.index), name=self.propensity_column_name
287
- )
288
- self.population_view.update(propensity)
289
- self.update_exposure_column(pop_data.index)
290
-
291
- def on_time_step_prepare(self, event: Event) -> None:
292
- self.update_exposure_column(event.index)
293
-
294
- def update_exposure_column(self, index: pd.Index) -> None:
295
- if self.create_exposure_column:
296
- exposure = pd.Series(self.exposure(index), name=self.exposure_column_name)
297
- self.population_view.update(exposure)
298
-
299
- ##################################
300
- # Pipeline sources and modifiers #
301
- ##################################
302
-
303
- def get_current_exposure(self, index: pd.Index) -> pd.Series:
304
- propensity = self.propensity(index)
305
- return pd.Series(self.exposure_distribution.ppf(propensity), index=index)