vivarium-public-health 4.2.6__py3-none-any.whl → 4.3.1__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.
@@ -3,339 +3,32 @@
3
3
  Risk Effect Models
4
4
  ==================
5
5
 
6
- This module contains tools for modeling the relationship between risk
7
- exposure models and disease models.
8
-
9
6
  """
7
+
10
8
  from collections.abc import Callable
11
- from importlib import import_module
12
9
  from typing import Any
13
10
 
14
11
  import numpy as np
15
12
  import pandas as pd
16
13
  import scipy
17
- from layered_config_tree import ConfigurationError
18
- from vivarium import Component
19
14
  from vivarium.framework.engine import Builder
20
- from vivarium.framework.values import Pipeline
21
15
 
22
- from vivarium_public_health.risks import Risk
23
- from vivarium_public_health.risks.data_transformations import (
24
- load_exposure_data,
25
- pivot_categorical,
26
- )
27
- from vivarium_public_health.risks.distributions import MissingDataError
28
- from vivarium_public_health.utilities import EntityString, TargetString, get_lookup_columns
16
+ from vivarium_public_health.exposure import ExposureEffect
17
+ from vivarium_public_health.exposure.distributions import MissingDataError
18
+ from vivarium_public_health.utilities import EntityString, TargetString
29
19
 
30
20
 
31
- class RiskEffect(Component):
21
+ class RiskEffect(ExposureEffect):
32
22
  """A component to model the effect of a risk factor on an affected entity's target rate.
33
23
 
34
24
  This component can source data either from builder.data or from parameters
35
25
  supplied in the configuration.
36
26
 
37
- For a risk named 'risk' that affects 'affected_risk' and 'affected_cause',
38
- the configuration would look like:
39
-
40
- .. code-block:: yaml
41
-
42
- configuration:
43
- risk_effect.risk_name_on_affected_target:
44
- exposure_parameters: 2
45
- incidence_rate: 10
46
-
47
27
  """
48
28
 
49
- ###############
50
- # Properties #
51
- ##############
52
-
53
- @property
54
- def name(self) -> str:
55
- return self.get_name(self.risk, self.target)
56
-
57
- @staticmethod
58
- def get_name(risk: EntityString, target: TargetString) -> str:
29
+ def get_name(self, risk: EntityString, target: TargetString) -> str:
59
30
  return f"risk_effect.{risk.name}_on_{target}"
60
31
 
61
- @property
62
- def configuration_defaults(self) -> dict[str, Any]:
63
- """Default values for any configurations managed by this component."""
64
- return {
65
- self.name: {
66
- "data_sources": {
67
- "relative_risk": f"{self.risk}.relative_risk",
68
- "population_attributable_fraction": f"{self.risk}.population_attributable_fraction",
69
- },
70
- "data_source_parameters": {
71
- "relative_risk": {},
72
- },
73
- }
74
- }
75
-
76
- @property
77
- def is_exposure_categorical(self) -> bool:
78
- return self._exposure_distribution_type in [
79
- "dichotomous",
80
- "ordered_polytomous",
81
- "unordered_polytomous",
82
- ]
83
-
84
- #####################
85
- # Lifecycle methods #
86
- #####################
87
-
88
- def __init__(self, risk: str, target: str):
89
- """
90
-
91
- Parameters
92
- ----------
93
- risk
94
- Type and name of risk factor, supplied in the form
95
- "risk_type.risk_name" where risk_type should be singular (e.g.,
96
- risk_factor instead of risk_factors).
97
- target
98
- Type, name, and target rate of entity to be affected by risk factor,
99
- supplied in the form "entity_type.entity_name.measure"
100
- where entity_type should be singular (e.g., cause instead of causes).
101
- """
102
- super().__init__()
103
- self.risk = EntityString(risk)
104
- self.target = TargetString(target)
105
-
106
- self._exposure_distribution_type = None
107
-
108
- self.exposure_pipeline_name = f"{self.risk.name}.exposure"
109
- self.target_pipeline_name = f"{self.target.name}.{self.target.measure}"
110
- self.target_paf_pipeline_name = f"{self.target_pipeline_name}.paf"
111
-
112
- # noinspection PyAttributeOutsideInit
113
- def setup(self, builder: Builder) -> None:
114
- self.exposure = self.get_risk_exposure(builder)
115
-
116
- self._relative_risk_source = self.get_relative_risk_source(builder)
117
- self.relative_risk = self.get_relative_risk_pipeline(builder)
118
-
119
- self.register_target_modifier(builder)
120
- self.register_paf_modifier(builder)
121
-
122
- #################
123
- # Setup methods #
124
- #################
125
-
126
- def build_all_lookup_tables(self, builder: Builder) -> None:
127
- self._exposure_distribution_type = self.get_distribution_type(builder)
128
-
129
- rr_data = self.load_relative_risk(builder)
130
- rr_value_cols = None
131
- if self.is_exposure_categorical:
132
- rr_data, rr_value_cols = self.process_categorical_data(builder, rr_data)
133
- self.lookup_tables["relative_risk"] = self.build_lookup_table(
134
- builder, rr_data, rr_value_cols
135
- )
136
-
137
- paf_data = self.get_filtered_data(
138
- builder, self.configuration.data_sources.population_attributable_fraction
139
- )
140
- self.lookup_tables["population_attributable_fraction"] = self.build_lookup_table(
141
- builder, paf_data
142
- )
143
-
144
- def get_distribution_type(self, builder: Builder) -> str:
145
- """Get the distribution type for the risk from the configuration."""
146
- risk_exposure_component = self._get_risk_exposure_class(builder)
147
- if risk_exposure_component.distribution_type:
148
- return risk_exposure_component.distribution_type
149
- return risk_exposure_component.get_distribution_type(builder)
150
-
151
- def load_relative_risk(
152
- self,
153
- builder: Builder,
154
- configuration=None,
155
- ) -> str | float | pd.DataFrame:
156
- if configuration is None:
157
- configuration = self.configuration
158
-
159
- rr_source = configuration.data_sources.relative_risk
160
- rr_dist_parameters = configuration.data_source_parameters.relative_risk.to_dict()
161
-
162
- if isinstance(rr_source, str):
163
- try:
164
- distribution = getattr(import_module("scipy.stats"), rr_source)
165
- rng = np.random.default_rng(builder.randomness.get_seed(self.name))
166
- rr_data = distribution(**rr_dist_parameters).ppf(rng.random())
167
- except AttributeError:
168
- rr_data = self.get_filtered_data(builder, rr_source)
169
- except TypeError:
170
- raise ConfigurationError(
171
- f"Parameters {rr_dist_parameters} are not valid for distribution {rr_source}."
172
- )
173
- else:
174
- rr_data = self.get_filtered_data(builder, rr_source)
175
- return rr_data
176
-
177
- def get_filtered_data(
178
- self, builder: "Builder", data_source: str | float | pd.DataFrame
179
- ) -> float | pd.DataFrame:
180
- data = super().get_data(builder, data_source)
181
-
182
- if isinstance(data, pd.DataFrame):
183
- # filter data to only include the target entity and measure
184
- correct_target_mask = True
185
- columns_to_drop = []
186
- if "affected_entity" in data.columns:
187
- correct_target_mask &= data["affected_entity"] == self.target.name
188
- columns_to_drop.append("affected_entity")
189
- if "affected_measure" in data.columns:
190
- correct_target_mask &= data["affected_measure"] == self.target.measure
191
- columns_to_drop.append("affected_measure")
192
- data = data[correct_target_mask].drop(columns=columns_to_drop)
193
- return data
194
-
195
- def process_categorical_data(
196
- self, builder: Builder, rr_data: str | float | pd.DataFrame
197
- ) -> tuple[str | float | pd.DataFrame, list[str]]:
198
- if not isinstance(rr_data, pd.DataFrame):
199
- cat1 = builder.data.load("population.demographic_dimensions")
200
- cat1["parameter"] = "cat1"
201
- cat1["value"] = rr_data
202
- cat2 = cat1.copy()
203
- cat2["parameter"] = "cat2"
204
- cat2["value"] = 1
205
- rr_data = pd.concat([cat1, cat2], ignore_index=True)
206
- if "parameter" in rr_data.index.names:
207
- rr_data = rr_data.reset_index("parameter")
208
-
209
- rr_value_cols = list(rr_data["parameter"].unique())
210
- rr_data = pivot_categorical(builder, self.risk, rr_data, "parameter")
211
- return rr_data, rr_value_cols
212
-
213
- # todo currently this isn't being called. we need to properly set rrs if
214
- # the exposure has been rebinned
215
- def rebin_relative_risk_data(
216
- self, builder, relative_risk_data: pd.DataFrame
217
- ) -> pd.DataFrame:
218
- """Rebin relative risk data.
219
-
220
- When the polytomous risk is rebinned, matching relative risk needs to be rebinned.
221
- After rebinning, rr for both exposed and unexposed categories should be the weighted sum of relative risk
222
- of the component categories where weights are relative proportions of exposure of those categories.
223
- For example, if cat1, cat2, cat3 are exposed categories and cat4 is unexposed with exposure [0.1,0.2,0.3,0.4],
224
- for the matching rr = [rr1, rr2, rr3, 1], rebinned rr for the rebinned cat1 should be:
225
- (0.1 *rr1 + 0.2 * rr2 + 0.3* rr3) / (0.1+0.2+0.3)
226
- """
227
- if not self.risk in builder.configuration.to_dict():
228
- return relative_risk_data
229
-
230
- rebin_exposed_categories = set(builder.configuration[self.risk]["rebinned_exposed"])
231
-
232
- if rebin_exposed_categories:
233
- # todo make sure this works
234
- exposure_data = load_exposure_data(builder, self.risk)
235
- relative_risk_data = self._rebin_relative_risk_data(
236
- relative_risk_data, exposure_data, rebin_exposed_categories
237
- )
238
-
239
- return relative_risk_data
240
-
241
- def _rebin_relative_risk_data(
242
- self,
243
- relative_risk_data: pd.DataFrame,
244
- exposure_data: pd.DataFrame,
245
- rebin_exposed_categories: set,
246
- ) -> pd.DataFrame:
247
- cols = list(exposure_data.columns.difference(["value"]))
248
-
249
- relative_risk_data = relative_risk_data.merge(exposure_data, on=cols)
250
- relative_risk_data["value_x"] = relative_risk_data.value_x.multiply(
251
- relative_risk_data.value_y
252
- )
253
- relative_risk_data.parameter = relative_risk_data["parameter"].map(
254
- lambda p: "cat1" if p in rebin_exposed_categories else "cat2"
255
- )
256
- relative_risk_data = relative_risk_data.groupby(cols).sum().reset_index()
257
- relative_risk_data["value"] = relative_risk_data.value_x.divide(
258
- relative_risk_data.value_y
259
- ).fillna(0)
260
- return relative_risk_data.drop(columns=["value_x", "value_y"])
261
-
262
- def get_risk_exposure(self, builder: Builder) -> Callable[[pd.Index], pd.Series]:
263
- return builder.value.get_value(self.exposure_pipeline_name)
264
-
265
- def adjust_target(self, index: pd.Index, target: pd.Series) -> pd.Series:
266
- relative_risk = self.relative_risk(index)
267
- return target * relative_risk
268
-
269
- def get_relative_risk_source(self, builder: Builder) -> Callable[[pd.Index], pd.Series]:
270
-
271
- if not self.is_exposure_categorical:
272
- tmred = builder.data.load(f"{self.risk}.tmred")
273
- tmrel = 0.5 * (tmred["min"] + tmred["max"])
274
- scale = builder.data.load(f"{self.risk}.relative_risk_scalar")
275
-
276
- def generate_relative_risk(index: pd.Index) -> pd.Series:
277
- rr = self.lookup_tables["relative_risk"](index)
278
- exposure = self.exposure(index)
279
- relative_risk = np.maximum(rr.values ** ((exposure - tmrel) / scale), 1)
280
- return relative_risk
281
-
282
- else:
283
- index_columns = ["index", self.risk.name]
284
-
285
- def generate_relative_risk(index: pd.Index) -> pd.Series:
286
- rr = self.lookup_tables["relative_risk"](index)
287
- exposure = self.exposure(index).reset_index()
288
- exposure.columns = index_columns
289
- exposure = exposure.set_index(index_columns)
290
-
291
- relative_risk = rr.stack().reset_index()
292
- relative_risk.columns = index_columns + ["value"]
293
- relative_risk = relative_risk.set_index(index_columns)
294
-
295
- effect = relative_risk.loc[exposure.index, "value"].droplevel(self.risk.name)
296
- return effect
297
-
298
- return generate_relative_risk
299
-
300
- def get_relative_risk_pipeline(self, builder: Builder) -> Pipeline:
301
- return builder.value.register_value_producer(
302
- f"{self.risk.name}_on_{self.target.name}.relative_risk",
303
- self._relative_risk_source,
304
- component=self,
305
- required_resources=[self.exposure],
306
- )
307
-
308
- def register_target_modifier(self, builder: Builder) -> None:
309
- builder.value.register_value_modifier(
310
- self.target_pipeline_name,
311
- modifier=self.adjust_target,
312
- component=self,
313
- required_resources=[self.relative_risk],
314
- )
315
-
316
- def register_paf_modifier(self, builder: Builder) -> None:
317
- required_columns = get_lookup_columns(
318
- [self.lookup_tables["population_attributable_fraction"]]
319
- )
320
- builder.value.register_value_modifier(
321
- self.target_paf_pipeline_name,
322
- modifier=self.lookup_tables["population_attributable_fraction"],
323
- component=self,
324
- required_resources=required_columns,
325
- )
326
-
327
- ##################
328
- # Helper methods #
329
- ##################
330
-
331
- def _get_risk_exposure_class(self, builder: Builder) -> Risk:
332
- risk_exposure_component = builder.components.get_component(self.risk)
333
- if not isinstance(risk_exposure_component, Risk):
334
- raise ValueError(
335
- f"Risk effect model {self.name} requires a Risk component named {self.risk}"
336
- )
337
- return risk_exposure_component
338
-
339
32
 
340
33
  class NonLogLinearRiskEffect(RiskEffect):
341
34
  """A component to model the exposure-parametrized effect of a risk factor.
@@ -366,15 +59,15 @@ class NonLogLinearRiskEffect(RiskEffect):
366
59
  return {
367
60
  self.name: {
368
61
  "data_sources": {
369
- "relative_risk": f"{self.risk}.relative_risk",
370
- "population_attributable_fraction": f"{self.risk}.population_attributable_fraction",
62
+ "relative_risk": f"{self.entity}.relative_risk",
63
+ "population_attributable_fraction": f"{self.entity}.population_attributable_fraction",
371
64
  },
372
65
  }
373
66
  }
374
67
 
375
68
  @property
376
69
  def columns_required(self) -> list[str]:
377
- return [f"{self.risk.name}_exposure"]
70
+ return [f"{self.entity.name}_exposure"]
378
71
 
379
72
  #################
380
73
  # Setup methods #
@@ -414,8 +107,8 @@ class NonLogLinearRiskEffect(RiskEffect):
414
107
  .reset_index()
415
108
  )
416
109
  rr_data = rr_data.drop("parameter", axis=1)
417
- rr_data[f"{self.risk.name}_exposure_start"] = rr_data["left_exposure"]
418
- rr_data[f"{self.risk.name}_exposure_end"] = rr_data["right_exposure"]
110
+ rr_data[f"{self.entity.name}_exposure_start"] = rr_data["left_exposure"]
111
+ rr_data[f"{self.entity.name}_exposure_end"] = rr_data["right_exposure"]
419
112
  # build lookup table
420
113
  rr_value_cols = ["left_exposure", "left_rr", "right_exposure", "right_rr"]
421
114
  self.lookup_tables["relative_risk"] = self.build_lookup_table(
@@ -438,17 +131,19 @@ class NonLogLinearRiskEffect(RiskEffect):
438
131
  configuration = self.configuration
439
132
 
440
133
  # get TMREL
441
- tmred = builder.data.load(f"{self.risk}.tmred")
134
+ tmred = builder.data.load(f"{self.entity}.tmred")
442
135
  if tmred["distribution"] == "uniform":
443
136
  draw = builder.configuration.input_data.input_draw_number
444
137
  rng = np.random.default_rng(builder.randomness.get_seed(self.name + str(draw)))
445
138
  self.tmrel = rng.uniform(tmred["min"], tmred["max"])
446
139
  elif tmred["distribution"] == "draws": # currently only for iron deficiency
447
140
  raise MissingDataError(
448
- f"This data has draw-level TMRELs. You will need to contact the research team that models {self.risk.name} to get this data."
141
+ f"This data has draw-level TMRELs. You will need to contact the research team that models {self.entity.name} to get this data."
449
142
  )
450
143
  else:
451
- raise MissingDataError(f"No TMRED found in gbd_mapping for risk {self.risk.name}")
144
+ raise MissingDataError(
145
+ f"No TMRED found in gbd_mapping for risk {self.entity.name}"
146
+ )
452
147
 
453
148
  # calculate RR at TMREL
454
149
  rr_source = configuration.data_sources.relative_risk
@@ -489,7 +184,7 @@ class NonLogLinearRiskEffect(RiskEffect):
489
184
  def get_relative_risk_source(self, builder: Builder) -> Callable[[pd.Index], pd.Series]:
490
185
  def generate_relative_risk(index: pd.Index) -> pd.Series:
491
186
  rr_intervals = self.lookup_tables["relative_risk"](index)
492
- exposure = self.population_view.get(index)[f"{self.risk.name}_exposure"]
187
+ exposure = self.population_view.get(index)[f"{self.entity.name}_exposure"]
493
188
  x1, x2 = (
494
189
  rr_intervals["left_exposure"].values,
495
190
  rr_intervals["right_exposure"].values,
@@ -512,7 +207,7 @@ class NonLogLinearRiskEffect(RiskEffect):
512
207
  parameter_data_is_numeric = rr_data["parameter"].dtype.kind in "biufc"
513
208
  if not parameter_data_is_numeric:
514
209
  raise ValueError(
515
- f"The parameter column in your {self.risk.name} relative risk data must contain numeric data. Its dtype is {rr_data['parameter'].dtype} instead."
210
+ f"The parameter column in your {self.entity.name} relative risk data must contain numeric data. Its dtype is {rr_data['parameter'].dtype} instead."
516
211
  )
517
212
 
518
213
  # and that these RR values are monotonically increasing within each demographic group
@@ -22,12 +22,12 @@ from vivarium.framework.population import SimulantData
22
22
  from vivarium.framework.resource import Resource
23
23
  from vivarium.framework.values import Pipeline
24
24
 
25
+ from vivarium_public_health.exposure.distributions import PolytomousDistribution
25
26
  from vivarium_public_health.risks import Risk, RiskEffect
26
27
  from vivarium_public_health.risks.data_transformations import (
27
28
  get_exposure_post_processor,
28
29
  pivot_categorical,
29
30
  )
30
- from vivarium_public_health.risks.distributions import PolytomousDistribution
31
31
  from vivarium_public_health.utilities import EntityString, get_lookup_columns, to_snake_case
32
32
 
33
33
  CATEGORICAL = "categorical"
@@ -71,7 +71,10 @@ class LBWSGDistribution(PolytomousDistribution):
71
71
 
72
72
  if isinstance(birth_exposure_data, pd.DataFrame):
73
73
  birth_exposure_data = pivot_categorical(
74
- builder, self.risk, birth_exposure_data, "parameter"
74
+ builder,
75
+ self.exposure_component.entity,
76
+ birth_exposure_data,
77
+ "parameter",
75
78
  )
76
79
 
77
80
  self.lookup_tables["birth_exposure"] = self.build_lookup_table(
@@ -118,7 +121,9 @@ class LBWSGDistribution(PolytomousDistribution):
118
121
  -------
119
122
  The intervals for each category.
120
123
  """
121
- categories: dict[str, str] = builder.data.load(f"{self.risk}.categories")
124
+ categories: dict[str, str] = builder.data.load(
125
+ f"{self.exposure_component.entity}.categories"
126
+ )
122
127
  category_intervals = {
123
128
  axis: {
124
129
  category: self._parse_description(axis, description)
@@ -271,7 +276,7 @@ class LBWSGRisk(Risk):
271
276
  # Add birth exposure data source
272
277
  configuration_defaults[self.name]["data_sources"][
273
278
  "birth_exposure"
274
- ] = f"{self.risk}.birth_exposure"
279
+ ] = f"{self.entity}.birth_exposure"
275
280
  configuration_defaults[self.name]["distribution_type"] = "lbwsg"
276
281
  return configuration_defaults
277
282
 
@@ -300,7 +305,7 @@ class LBWSGRisk(Risk):
300
305
  # Propensity only used on initialization; not being saved to avoid a cycle
301
306
  return None
302
307
 
303
- def get_exposure_pipeline(self, builder: Builder) -> Pipeline | None:
308
+ def get_exposure_callable(self, builder: Builder) -> Pipeline | None:
304
309
  # Exposure only used on initialization; not being saved to avoid a cycle
305
310
  return None
306
311
 
@@ -359,7 +364,7 @@ class LBWSGRisk(Risk):
359
364
 
360
365
  def get_current_exposure(self, index: pd.Index) -> pd.DataFrame:
361
366
  raise LifeCycleError(
362
- f"The {self.risk.name} exposure pipeline should not be called. You probably want to"
367
+ f"The {self.entity.name} exposure pipeline should not be called. You probably want to"
363
368
  f" refer directly one of the exposure columns. During simulant initialization the birth"
364
369
  f" exposure pipelines should be used instead."
365
370
  )
@@ -400,12 +405,12 @@ class LBWSGRiskEffect(RiskEffect):
400
405
  LBWSGRisk.get_exposure_column_name(axis) for axis in LBWSGRisk.AXES
401
406
  ]
402
407
  self.relative_risk_pipeline_name = (
403
- f"effect_of_{self.risk.name}_on_{self.target.name}.relative_risk"
408
+ f"effect_of_{self.entity.name}_on_{self.target.name}.relative_risk"
404
409
  )
405
410
 
406
411
  def relative_risk_column_name(self, age_group_id: str) -> str:
407
412
  return (
408
- f"effect_of_{self.risk.name}_on_{age_group_id}_{self.target.name}_relative_risk"
413
+ f"effect_of_{self.entity.name}_on_{age_group_id}_{self.target.name}_relative_risk"
409
414
  )
410
415
 
411
416
  # noinspection PyAttributeOutsideInit
@@ -426,7 +431,7 @@ class LBWSGRiskEffect(RiskEffect):
426
431
  builder, paf_data, paf_value_cols
427
432
  )
428
433
 
429
- def get_risk_exposure(self, builder: Builder) -> Callable[[pd.Index], pd.DataFrame]:
434
+ def get_exposure_callable(self, builder: Builder) -> Callable[[pd.Index], pd.DataFrame]:
430
435
  def exposure(index: pd.Index) -> pd.DataFrame:
431
436
  return self.population_view.subview(self.lbwsg_exposure_column_names).get(index)
432
437
 
@@ -435,7 +440,7 @@ class LBWSGRiskEffect(RiskEffect):
435
440
  def get_population_attributable_fraction_source(
436
441
  self, builder: Builder
437
442
  ) -> tuple[pd.DataFrame, list[str]]:
438
- paf_key = f"{self.risk}.population_attributable_fraction"
443
+ paf_key = f"{self.entity}.population_attributable_fraction"
439
444
  paf_data = builder.data.load(paf_key)
440
445
  return paf_data, builder.data.value_columns()(paf_key)
441
446
 
@@ -449,7 +454,7 @@ class LBWSGRiskEffect(RiskEffect):
449
454
 
450
455
  def get_age_intervals(self, builder: Builder) -> dict[str, pd.Interval]:
451
456
  age_bins = builder.data.load("population.age_bins").set_index("age_start")
452
- relative_risks = builder.data.load(f"{self.risk}.relative_risk")
457
+ relative_risks = builder.data.load(f"{self.entity}.relative_risk")
453
458
  exposed_age_group_starts = (
454
459
  relative_risks.groupby("age_start")["value"].any().reset_index()["age_start"]
455
460
  )
@@ -476,7 +481,7 @@ class LBWSGRiskEffect(RiskEffect):
476
481
  }
477
482
 
478
483
  # get relative risk data for target
479
- interpolators = builder.data.load(f"{self.risk}.relative_risk_interpolator")
484
+ interpolators = builder.data.load(f"{self.entity}.relative_risk_interpolator")
480
485
  interpolators = (
481
486
  # isolate RRs for target and drop non-neonatal age groups since they have RR == 1.0
482
487
  interpolators[
@@ -1,3 +1,4 @@
1
+ from vivarium_public_health.treatment.intervention import Intervention, InterventionEffect
1
2
  from vivarium_public_health.treatment.magic_wand import AbsoluteShift
2
3
  from vivarium_public_health.treatment.scale_up import LinearScaleUp
3
4
  from vivarium_public_health.treatment.therapeutic_inertia import TherapeuticInertia
@@ -0,0 +1,85 @@
1
+ from typing import NamedTuple
2
+
3
+ from vivarium_public_health.exposure import Exposure
4
+ from vivarium_public_health.exposure.effect import ExposureEffect
5
+ from vivarium_public_health.utilities import EntityString, TargetString
6
+
7
+
8
+ class Intervention(Exposure):
9
+ """A model for an intervention defined by coverage (access to intervention).
10
+
11
+ This component models access to an intervention as a dichotomous exposure where
12
+ simulants are either covered or uncovered by the intervention.
13
+
14
+ For example,
15
+
16
+ #. vaccination coverage where simulants are either vaccinated (covered) or
17
+ unvaccinated (uncovered).
18
+ #. treatment access where simulants either have access to treatment (covered)
19
+ or do not have access (uncovered).
20
+
21
+ This component can source data either from a key in an Artifact
22
+ ("intervention.intervention_name.coverage") or from parameters
23
+ supplied in the configuration. If data is derived from the configuration, it
24
+ must be an integer or float expressing the desired coverage level or a
25
+ covariate name that is intended to be used as a proxy. For example, for an
26
+ intervention named "intervention", the configuration could look like this:
27
+
28
+ .. code-block:: yaml
29
+
30
+ configuration:
31
+ intervention.intervention_name:
32
+ coverage: 0.8
33
+
34
+ Interventions should be configured with names in the format of
35
+ "intervention.intervention_name".
36
+
37
+ """
38
+
39
+ @property
40
+ def exposure_type(self) -> str:
41
+ """The measure of the intervention access."""
42
+ return "coverage"
43
+
44
+ @property
45
+ def dichotomous_exposure_category_names(self) -> NamedTuple:
46
+ """The name of the covered and uncovered categories for this intervention."""
47
+
48
+ class __Categories(NamedTuple):
49
+ exposed: str = "covered"
50
+ unexposed: str = "uncovered"
51
+
52
+ categories = __Categories()
53
+ return categories
54
+
55
+
56
+ class InterventionEffect(ExposureEffect):
57
+ """A component to model the effect of an intervention on an affected entity's target rate.
58
+
59
+ This component models how intervention coverage affects the rate of some target
60
+ entity (e.g., disease incidence, mortality, disability). The effect is typically
61
+ protective, reducing the target rate for covered simulants compared to uncovered
62
+ simulants.
63
+
64
+ This component can source data either from builder.data or from parameters
65
+ supplied in the configuration. The data should specify the relative risk or
66
+ rate ratio associated with intervention coverage.
67
+
68
+ For example, an intervention effect might model how vaccination coverage affects
69
+ disease incidence, where vaccinated individuals have a lower risk of disease
70
+ compared to unvaccinated individuals.
71
+
72
+ For an exposure named 'exposure_name' that affects 'affected_entity' and 'affected_cause',
73
+ the configuration would look like:
74
+
75
+ .. code-block:: yaml
76
+
77
+ configuration:
78
+ intervention_effect.exposure_name_on_affected_target:
79
+ exposure_parameters: 2
80
+ incidence_rate: 10
81
+
82
+ """
83
+
84
+ def get_name(self, intervention: EntityString, target: TargetString) -> str:
85
+ return f"intervention_effect.{intervention.name}_on_{target}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vivarium_public_health
3
- Version: 4.2.6
3
+ Version: 4.3.1
4
4
  Summary: Components for modelling diseases, risks, and interventions with ``vivarium``
5
5
  Home-page: https://github.com/ihmeuw/vivarium_public_health
6
6
  Author: The vivarium developers