sensor-modeling 0.2.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.
Files changed (114) hide show
  1. sensor_modeling/__init__.py +45 -0
  2. sensor_modeling/alerts/__init__.py +26 -0
  3. sensor_modeling/alerts/alert.py +532 -0
  4. sensor_modeling/analysis/__init__.py +43 -0
  5. sensor_modeling/analysis/_frame.py +19 -0
  6. sensor_modeling/analysis/behavioral_analysis.py +57 -0
  7. sensor_modeling/analysis/behavioral_metrics.py +66 -0
  8. sensor_modeling/analysis/comparison.py +164 -0
  9. sensor_modeling/analysis/dependency_network.py +408 -0
  10. sensor_modeling/analysis/granger_causality.py +314 -0
  11. sensor_modeling/analysis/pipeline.py +168 -0
  12. sensor_modeling/analysis/reporting.py +109 -0
  13. sensor_modeling/baseline/__init__.py +30 -0
  14. sensor_modeling/baseline/adaptive.py +520 -0
  15. sensor_modeling/baseline/features.py +224 -0
  16. sensor_modeling/change_point/__init__.py +13 -0
  17. sensor_modeling/change_point/_validation.py +31 -0
  18. sensor_modeling/change_point/adaptive_normalization.py +55 -0
  19. sensor_modeling/change_point/embedding_cpd.py +60 -0
  20. sensor_modeling/change_point/energy_efficient.py +57 -0
  21. sensor_modeling/change_point/genetic_optimization.py +65 -0
  22. sensor_modeling/cli.py +416 -0
  23. sensor_modeling/context/__init__.py +33 -0
  24. sensor_modeling/context/occupancy.py +529 -0
  25. sensor_modeling/data/__init__.py +5 -0
  26. sensor_modeling/data/loaders.py +146 -0
  27. sensor_modeling/data/preprocessing.py +83 -0
  28. sensor_modeling/data/synthetic.py +121 -0
  29. sensor_modeling/data/validation.py +81 -0
  30. sensor_modeling/evaluation/__init__.py +92 -0
  31. sensor_modeling/evaluation/ablation.py +303 -0
  32. sensor_modeling/evaluation/attribution.py +474 -0
  33. sensor_modeling/evaluation/detection.py +297 -0
  34. sensor_modeling/evaluation/metrics.py +541 -0
  35. sensor_modeling/evaluation/provenance.py +309 -0
  36. sensor_modeling/examples/__init__.py +1 -0
  37. sensor_modeling/examples/demos/__init__.py +1 -0
  38. sensor_modeling/examples/demos/ambient_pipeline_demo.py +418 -0
  39. sensor_modeling/examples/demos/bernoulli_ar_demo.py +356 -0
  40. sensor_modeling/examples/demos/cpd_ar_demo.py +25 -0
  41. sensor_modeling/examples/demos/cpd_benchmark.py +42 -0
  42. sensor_modeling/examples/demos/hmm_granger_demo.py +30 -0
  43. sensor_modeling/examples/demos/nhpp_pelt_demo.py +80 -0
  44. sensor_modeling/examples/tutorials/__init__.py +1 -0
  45. sensor_modeling/fusion/__init__.py +46 -0
  46. sensor_modeling/fusion/defaults.py +296 -0
  47. sensor_modeling/fusion/emissions.py +339 -0
  48. sensor_modeling/fusion/estimate.py +375 -0
  49. sensor_modeling/fusion/filter.py +323 -0
  50. sensor_modeling/health/__init__.py +31 -0
  51. sensor_modeling/health/monitor.py +590 -0
  52. sensor_modeling/health/status.py +74 -0
  53. sensor_modeling/hmm/__init__.py +15 -0
  54. sensor_modeling/hmm/adaptive_hmm.py +22 -0
  55. sensor_modeling/hmm/base.py +134 -0
  56. sensor_modeling/hmm/circadian_hmm.py +22 -0
  57. sensor_modeling/hmm/heterogeneous_hmm.py +22 -0
  58. sensor_modeling/hmm/hierarchical_hmm.py +35 -0
  59. sensor_modeling/hmm/scaled_dirichlet_hmm.py +23 -0
  60. sensor_modeling/interop/__init__.py +57 -0
  61. sensor_modeling/interop/fhir.py +418 -0
  62. sensor_modeling/interop/privacy.py +308 -0
  63. sensor_modeling/models/__init__.py +12 -0
  64. sensor_modeling/models/bernoulli_ar/__init__.py +6 -0
  65. sensor_modeling/models/bernoulli_ar/base_model.py +569 -0
  66. sensor_modeling/models/bernoulli_ar/multivariate_model.py +411 -0
  67. sensor_modeling/models/change_point_detection/__init__.py +10 -0
  68. sensor_modeling/models/change_point_detection/deep.py +65 -0
  69. sensor_modeling/models/change_point_detection/pelt.py +159 -0
  70. sensor_modeling/models/nhpp_pelt/__init__.py +5 -0
  71. sensor_modeling/models/nhpp_pelt/bspline.py +96 -0
  72. sensor_modeling/models/nhpp_pelt/cli.py +243 -0
  73. sensor_modeling/models/nhpp_pelt/diagnostics.py +234 -0
  74. sensor_modeling/models/nhpp_pelt/io.py +58 -0
  75. sensor_modeling/models/nhpp_pelt/model.py +408 -0
  76. sensor_modeling/models/nhpp_pelt/optimizer.py +142 -0
  77. sensor_modeling/models/nhpp_pelt/plotting.py +218 -0
  78. sensor_modeling/models/nhpp_pelt/quad.py +72 -0
  79. sensor_modeling/models/nhpp_pelt/regularization.py +121 -0
  80. sensor_modeling/models/nhpp_pelt/utils.py +174 -0
  81. sensor_modeling/observations/__init__.py +59 -0
  82. sensor_modeling/observations/adapters.py +195 -0
  83. sensor_modeling/observations/ingest.py +269 -0
  84. sensor_modeling/observations/observation.py +270 -0
  85. sensor_modeling/observations/registry.py +262 -0
  86. sensor_modeling/observations/stream.py +342 -0
  87. sensor_modeling/observations/types.py +107 -0
  88. sensor_modeling/observations/units.py +117 -0
  89. sensor_modeling/online/__init__.py +36 -0
  90. sensor_modeling/online/benchmarks.py +242 -0
  91. sensor_modeling/online/pipeline.py +485 -0
  92. sensor_modeling/simulation/__init__.py +54 -0
  93. sensor_modeling/simulation/faults.py +191 -0
  94. sensor_modeling/simulation/household.py +862 -0
  95. sensor_modeling/states/__init__.py +23 -0
  96. sensor_modeling/states/markov.py +105 -0
  97. sensor_modeling/states/ontology.py +238 -0
  98. sensor_modeling/utils/__init__.py +41 -0
  99. sensor_modeling/utils/data_io.py +199 -0
  100. sensor_modeling/utils/logging_config.py +10 -0
  101. sensor_modeling/utils/missing.py +188 -0
  102. sensor_modeling/utils/plotting.py +98 -0
  103. sensor_modeling/utils/validation.py +117 -0
  104. sensor_modeling/visualization/__init__.py +3 -0
  105. sensor_modeling/visualization/clinical.py +67 -0
  106. sensor_modeling/visualization/interactive.py +208 -0
  107. sensor_modeling/visualization/research.py +60 -0
  108. sensor_modeling/visualization/web_app.py +137 -0
  109. sensor_modeling-0.2.0.dist-info/METADATA +683 -0
  110. sensor_modeling-0.2.0.dist-info/RECORD +114 -0
  111. sensor_modeling-0.2.0.dist-info/WHEEL +5 -0
  112. sensor_modeling-0.2.0.dist-info/entry_points.txt +18 -0
  113. sensor_modeling-0.2.0.dist-info/licenses/LICENSE +21 -0
  114. sensor_modeling-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,297 @@
1
+ """Measuring whether behavioural change detection is usable.
2
+
3
+ Four numbers decide whether a monitoring system is worth deploying, and they
4
+ trade against each other rather than improving together:
5
+
6
+ .. code-block:: text
7
+
8
+ detection delay how long a real change goes unreported
9
+ false alert burden how often a carer is contacted for nothing
10
+ missed changes how often a real change is never reported
11
+ calibration whether the confidence attached means anything
12
+
13
+ A system tuned to detect everything floods its recipients; one tuned for
14
+ silence misses what matters. This module measures the trade rather than
15
+ asserting a favourable point on it, and includes arms where nothing at all has
16
+ changed, because a detector's behaviour on a stable record is as informative
17
+ as its behaviour on a changed one.
18
+
19
+ Arms are paired by seed, so the stable and changed runs of a given seed differ
20
+ only in the injected change.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+ from collections.abc import Iterable, Sequence
27
+ from dataclasses import dataclass, field, replace
28
+ from datetime import date, timedelta
29
+ from typing import Any
30
+
31
+ import numpy as np
32
+
33
+ from ..alerts.alert import AlertKind
34
+ from ..online.pipeline import BehaviouralSensingPipeline, PipelineConfig, collect_alerts
35
+ from ..simulation.faults import DegradationConfig, degrade
36
+ from ..simulation.household import BehaviourShift, HouseholdConfig, simulate
37
+ from .metrics import DetectionMetrics, detection_metrics
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ #: Feature the injected changes act on. Sleep is used because the simulator
42
+ #: can shift it cleanly and because it is the quantity most ambient-monitoring
43
+ #: studies report on.
44
+ TRACKED_FEATURE = "sleeping_hours"
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class ChangeArm:
49
+ """One configuration of injected change and sensor degradation."""
50
+
51
+ name: str
52
+ shift: BehaviourShift | None
53
+ degradation: DegradationConfig | None = None
54
+ description: str = ""
55
+
56
+ @property
57
+ def has_change(self) -> bool:
58
+ """Whether a real behavioural change was injected."""
59
+ return self.shift is not None
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class ArmOutcome:
64
+ """What one arm produced on one seed."""
65
+
66
+ arm: str
67
+ seed: int
68
+ metrics: DetectionMetrics
69
+ behavioural_alerts: int
70
+ person_days: float
71
+ mean_alert_confidence: float
72
+
73
+ @property
74
+ def alerts_per_person_day(self) -> float:
75
+ """Total behavioural alert burden, matched or not."""
76
+ return self.behavioural_alerts / self.person_days if self.person_days else 0.0
77
+
78
+ def to_dict(self) -> dict[str, object]:
79
+ """Return a serialisable form of the outcome."""
80
+ return {
81
+ "arm": self.arm,
82
+ "seed": self.seed,
83
+ "behavioural_alerts": self.behavioural_alerts,
84
+ "alerts_per_person_day": self.alerts_per_person_day,
85
+ "mean_alert_confidence": self.mean_alert_confidence,
86
+ "detection": self.metrics.to_dict(),
87
+ }
88
+
89
+
90
+ @dataclass
91
+ class DetectionStudy:
92
+ """Detection outcomes across arms and seeds."""
93
+
94
+ outcomes: list[ArmOutcome] = field(default_factory=list)
95
+
96
+ def arms(self) -> list[str]:
97
+ """Arm names in first-seen order."""
98
+ seen: dict[str, None] = {}
99
+ for outcome in self.outcomes:
100
+ seen.setdefault(outcome.arm, None)
101
+ return list(seen)
102
+
103
+ def for_arm(self, arm: str) -> list[ArmOutcome]:
104
+ """Every outcome for one arm, ordered by seed."""
105
+ return sorted((o for o in self.outcomes if o.arm == arm), key=lambda o: o.seed)
106
+
107
+ def summary(self, arm: str) -> dict[str, float]:
108
+ """Aggregate one arm across its seeds."""
109
+ outcomes = self.for_arm(arm)
110
+ if not outcomes:
111
+ raise KeyError(f"no outcomes for arm '{arm}'")
112
+ # Pool the individual delays rather than averaging each seed's median.
113
+ # A mean of medians is not a median, it weights a seed that detected one
114
+ # change as heavily as a seed that detected twenty, and it is not the
115
+ # quantity the provenance record defines "median_delay_days" to be.
116
+ delays = [delay for o in outcomes for delay in o.metrics.delays_days]
117
+ seed_medians = [
118
+ o.metrics.median_delay_days
119
+ for o in outcomes
120
+ if not np.isnan(o.metrics.median_delay_days)
121
+ ]
122
+ return {
123
+ "seeds": float(len(outcomes)),
124
+ "recall": float(np.mean([o.metrics.recall for o in outcomes])),
125
+ "median_delay_days": float(np.median(delays)) if delays else float("nan"),
126
+ "mean_seed_median_delay_days": (
127
+ float(np.mean(seed_medians)) if seed_medians else float("nan")
128
+ ),
129
+ "detected_changes": float(len(delays)),
130
+ "alerts_per_person_day": float(
131
+ np.mean([o.alerts_per_person_day for o in outcomes])
132
+ ),
133
+ "false_positives_per_person_day": float(
134
+ np.mean([o.metrics.false_positives_per_person_day for o in outcomes])
135
+ ),
136
+ "mean_alert_confidence": float(
137
+ np.mean([o.mean_alert_confidence for o in outcomes])
138
+ ),
139
+ }
140
+
141
+ def to_dict(self) -> dict[str, object]:
142
+ """Return a serialisable form of the study."""
143
+ return {
144
+ "arms": {arm: self.summary(arm) for arm in self.arms()},
145
+ "outcomes": [o.to_dict() for o in self.outcomes],
146
+ }
147
+
148
+
149
+ def standard_arms(
150
+ days: int = 70, change_day: int = 40, magnitude: float = 1.6
151
+ ) -> list[ChangeArm]:
152
+ """Build the arms needed to characterise the detection trade-off.
153
+
154
+ Every arm that injects a change is matched by one that does not, so the
155
+ alert burden attributable to the change is separable from the burden the
156
+ detector produces anyway.
157
+ """
158
+ step = BehaviourShift(
159
+ start_day=change_day,
160
+ sleep_delta_hours=magnitude,
161
+ night_bathroom_extra=1.2,
162
+ )
163
+ gradual = BehaviourShift(
164
+ start_day=change_day // 2,
165
+ sleep_delta_hours=magnitude,
166
+ night_bathroom_extra=1.2,
167
+ ramp_days=max(days - change_day // 2 - 5, 5),
168
+ )
169
+ missing = DegradationConfig(missing_rate=0.3, seed=1)
170
+
171
+ return [
172
+ ChangeArm(
173
+ "stable",
174
+ None,
175
+ description="Nothing changed. Every alert here is a false alarm.",
176
+ ),
177
+ ChangeArm(
178
+ "abrupt_change",
179
+ step,
180
+ description="A step change in sleep on a known day.",
181
+ ),
182
+ ChangeArm(
183
+ "gradual_change",
184
+ gradual,
185
+ description="The same magnitude reached slowly, which no single "
186
+ "day's deviation can reveal.",
187
+ ),
188
+ ChangeArm(
189
+ "stable_degraded",
190
+ None,
191
+ missing,
192
+ description="Nothing changed, and a third of records are lost. "
193
+ "Missingness must not manufacture findings.",
194
+ ),
195
+ ChangeArm(
196
+ "abrupt_change_degraded",
197
+ step,
198
+ missing,
199
+ description="A real change seen through a degraded record.",
200
+ ),
201
+ ]
202
+
203
+
204
+ def _evaluate(
205
+ arm: ChangeArm,
206
+ seed: int,
207
+ *,
208
+ days: int,
209
+ change_day: int,
210
+ step: timedelta,
211
+ max_delay_days: float,
212
+ ) -> ArmOutcome:
213
+ """Run one arm on one seed and score the alerts it delivered."""
214
+ household = HouseholdConfig(days=days, seed=seed, shift=arm.shift)
215
+ result = simulate(household)
216
+
217
+ observations: Sequence[Any] = result.observations
218
+ if arm.degradation is not None:
219
+ observations, _ = degrade(observations, replace(arm.degradation, seed=seed))
220
+
221
+ pipeline = BehaviouralSensingPipeline(
222
+ result.registry, config=PipelineConfig(tz=result.config.tz, step=step)
223
+ )
224
+ steps = pipeline.run(observations)
225
+ steps.extend(pipeline.close(result.end))
226
+
227
+ alerts = [
228
+ alert
229
+ for alert in collect_alerts(steps)
230
+ if alert.kind is AlertKind.BEHAVIOURAL_CHANGE
231
+ and TRACKED_FEATURE in str(alert.subject)
232
+ ]
233
+ detected: list[date] = [alert.at.date() for alert in alerts]
234
+
235
+ true_changes: list[date] = []
236
+ if arm.shift is not None:
237
+ true_changes.append(household.start + timedelta(days=arm.shift.start_day))
238
+
239
+ metrics = detection_metrics(
240
+ detected,
241
+ true_changes,
242
+ person_days=float(days),
243
+ max_delay_days=max_delay_days,
244
+ )
245
+ return ArmOutcome(
246
+ arm=arm.name,
247
+ seed=seed,
248
+ metrics=metrics,
249
+ behavioural_alerts=len(alerts),
250
+ person_days=float(days),
251
+ mean_alert_confidence=(
252
+ float(np.mean([a.confidence for a in alerts])) if alerts else float("nan")
253
+ ),
254
+ )
255
+
256
+
257
+ def run_detection_study(
258
+ arms: Iterable[ChangeArm] | None = None,
259
+ *,
260
+ seeds: Iterable[int] = (11, 22, 33),
261
+ days: int = 70,
262
+ change_day: int = 40,
263
+ step: timedelta = timedelta(minutes=15),
264
+ max_delay_days: float = 21.0,
265
+ ) -> DetectionStudy:
266
+ """Characterise detection delay against alert burden across arms.
267
+
268
+ A gradual arm's change is dated from where it *begins*, not from where it
269
+ becomes visible, so its reported delay includes the time the trend spent
270
+ too small to detect. That is the honest accounting: the resident was
271
+ already declining.
272
+ """
273
+ selected = list(arms) if arms is not None else standard_arms(days, change_day)
274
+ if not selected:
275
+ raise ValueError("at least one arm is required")
276
+
277
+ study = DetectionStudy()
278
+ for seed in seeds:
279
+ for arm in selected:
280
+ outcome = _evaluate(
281
+ arm,
282
+ seed,
283
+ days=days,
284
+ change_day=change_day,
285
+ step=step,
286
+ max_delay_days=max_delay_days,
287
+ )
288
+ study.outcomes.append(outcome)
289
+ logger.info(
290
+ "seed %s | %-24s | alerts %d | recall %.2f | delay %.1f",
291
+ seed,
292
+ arm.name,
293
+ outcome.behavioural_alerts,
294
+ outcome.metrics.recall,
295
+ outcome.metrics.median_delay_days,
296
+ )
297
+ return study