Spaces:
Sleeping
Sleeping
Deploy EPISIM v0.12.4 all-design research hardening
Browse files- episim/__init__.py +1 -1
- episim/lab/__init__.py +2 -1
- episim/lab/runner.py +22 -3
- episim/research/pipeline.py +197 -15
- pyproject.toml +1 -1
episim/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
"""EPISIM — Epidemiology Platform for In Silico Methods."""
|
| 2 |
|
| 3 |
-
__version__ = "0.12.
|
| 4 |
__all__ = ["__version__"]
|
|
|
|
| 1 |
"""EPISIM — Epidemiology Platform for In Silico Methods."""
|
| 2 |
|
| 3 |
+
__version__ = "0.12.4a1"
|
| 4 |
__all__ = ["__version__"]
|
episim/lab/__init__.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
| 1 |
"""Lab-facing APIs for cataloguing and running EPISIM experiments."""
|
| 2 |
|
| 3 |
from episim.lab.registry import DesignSpec, get_design, list_designs
|
| 4 |
-
from episim.lab.runner import run_design, study_preview
|
| 5 |
|
| 6 |
__all__ = [
|
| 7 |
"DesignSpec",
|
| 8 |
"get_design",
|
| 9 |
"list_designs",
|
|
|
|
| 10 |
"run_design",
|
| 11 |
"study_preview",
|
| 12 |
]
|
|
|
|
| 1 |
"""Lab-facing APIs for cataloguing and running EPISIM experiments."""
|
| 2 |
|
| 3 |
from episim.lab.registry import DesignSpec, get_design, list_designs
|
| 4 |
+
from episim.lab.runner import resolve_design_parameters, run_design, study_preview
|
| 5 |
|
| 6 |
__all__ = [
|
| 7 |
"DesignSpec",
|
| 8 |
"get_design",
|
| 9 |
"list_designs",
|
| 10 |
+
"resolve_design_parameters",
|
| 11 |
"run_design",
|
| 12 |
"study_preview",
|
| 13 |
]
|
episim/lab/runner.py
CHANGED
|
@@ -1,19 +1,27 @@
|
|
| 1 |
"""Helpers for running EPISIM lab experiments."""
|
| 2 |
from __future__ import annotations
|
| 3 |
|
|
|
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
import pandas as pd
|
| 7 |
|
| 8 |
from episim.core.reproducibility import Study
|
| 9 |
-
from episim.lab.registry import get_design
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
def run_design(key: str, **overrides: Any) -> Study:
|
| 13 |
"""Run a registered design with parameter overrides."""
|
| 14 |
design = get_design(key)
|
| 15 |
-
params =
|
| 16 |
-
params.update(overrides)
|
| 17 |
return design.runner(**params)
|
| 18 |
|
| 19 |
|
|
@@ -25,3 +33,14 @@ def study_preview(study: Study, rows: int = 8) -> tuple[pd.DataFrame, pd.DataFra
|
|
| 25 |
else:
|
| 26 |
result_preview = pd.DataFrame({"message": ["No summary metrics recorded."]})
|
| 27 |
return data_preview, result_preview
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""Helpers for running EPISIM lab experiments."""
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
+
from inspect import Parameter, signature
|
| 5 |
from typing import Any
|
| 6 |
|
| 7 |
import pandas as pd
|
| 8 |
|
| 9 |
from episim.core.reproducibility import Study
|
| 10 |
+
from episim.lab.registry import DesignSpec, get_design
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def resolve_design_parameters(design: DesignSpec, overrides: dict[str, Any]) -> dict[str, Any]:
|
| 14 |
+
"""Return defaults plus runner-compatible overrides for a registered design."""
|
| 15 |
+
params = dict(design.parameters)
|
| 16 |
+
accepted = _accepted_parameter_names(design)
|
| 17 |
+
params.update({key: value for key, value in overrides.items() if key in accepted})
|
| 18 |
+
return params
|
| 19 |
|
| 20 |
|
| 21 |
def run_design(key: str, **overrides: Any) -> Study:
|
| 22 |
"""Run a registered design with parameter overrides."""
|
| 23 |
design = get_design(key)
|
| 24 |
+
params = resolve_design_parameters(design, overrides)
|
|
|
|
| 25 |
return design.runner(**params)
|
| 26 |
|
| 27 |
|
|
|
|
| 33 |
else:
|
| 34 |
result_preview = pd.DataFrame({"message": ["No summary metrics recorded."]})
|
| 35 |
return data_preview, result_preview
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def _accepted_parameter_names(design: DesignSpec) -> set[str]:
|
| 39 |
+
sig = signature(design.runner)
|
| 40 |
+
if any(param.kind == Parameter.VAR_KEYWORD for param in sig.parameters.values()):
|
| 41 |
+
return set(design.parameters)
|
| 42 |
+
return {
|
| 43 |
+
name
|
| 44 |
+
for name, param in sig.parameters.items()
|
| 45 |
+
if param.kind in {Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY}
|
| 46 |
+
}
|
episim/research/pipeline.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
import json
|
|
|
|
| 5 |
import sqlite3
|
| 6 |
from dataclasses import asdict, dataclass
|
| 7 |
from pathlib import Path
|
|
@@ -13,7 +14,13 @@ import pandas as pd
|
|
| 13 |
|
| 14 |
from episim import __version__
|
| 15 |
from episim.core.reproducibility import Study
|
| 16 |
-
from episim.lab import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
from episim.reporting.ai_disclosure import block as ai_disclosure
|
| 18 |
|
| 19 |
|
|
@@ -130,9 +137,10 @@ def plan_research(
|
|
| 130 |
clean_question = _clean_question(question)
|
| 131 |
selected_design = design_key or _infer_design_key(clean_question)
|
| 132 |
design = get_design(selected_design)
|
| 133 |
-
params =
|
| 134 |
-
|
| 135 |
-
|
|
|
|
| 136 |
title = _title_from_question(clean_question, design.title)
|
| 137 |
target = _target_phrase(clean_question)
|
| 138 |
exposure = _exposure_phrase(clean_question, design.family)
|
|
@@ -316,18 +324,35 @@ def _exposure_phrase(question: str, family: str) -> str:
|
|
| 316 |
if "lifestyle" in q:
|
| 317 |
label = "Structured lifestyle intervention"
|
| 318 |
return f"{label} assignment" if family == "experimental" else label
|
| 319 |
-
if "diet" in q:
|
| 320 |
label = "Dietary or nutrition-related intervention"
|
| 321 |
return f"{label} assignment" if family == "experimental" else label
|
|
|
|
|
|
|
| 322 |
if "exercise" in q or "physical activity" in q:
|
| 323 |
-
label =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
return f"{label} assignment" if family == "experimental" else label
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
if family == "experimental":
|
| 326 |
return "Simulated intervention assignment"
|
| 327 |
-
if
|
| 328 |
return "AI-enabled exposure or decision-support process"
|
| 329 |
if "policy" in q:
|
| 330 |
return "Policy or implementation exposure"
|
|
|
|
|
|
|
| 331 |
return "Primary exposure or intervention specified by the research question"
|
| 332 |
|
| 333 |
|
|
@@ -345,11 +370,21 @@ def _outcome_phrases(question: str, design_key: str) -> tuple[str, ...]:
|
|
| 345 |
q = question.lower()
|
| 346 |
if design_key == "qualitative_mixed_methods":
|
| 347 |
return ("theme saturation", "acceptability score", "integrated interpretation")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
if "frailty" in q:
|
| 349 |
return ("12-month frailty event", "risk ratio", "risk difference")
|
| 350 |
if "cognitive" in q or "cognition" in q:
|
| 351 |
return ("cognitive decline event", "risk ratio", "risk difference")
|
| 352 |
-
if "trust" in q and (
|
| 353 |
return ("trust and acceptability outcome", "theme saturation", "survey score")
|
| 354 |
if "mortality" in q or design_key == "survival_cox":
|
| 355 |
return ("time-to-event outcome", "censoring status", "hazard ratio")
|
|
@@ -360,6 +395,15 @@ def _outcome_phrases(question: str, design_key: str) -> tuple[str, ...]:
|
|
| 360 |
return ("primary health or social outcome", "effect estimate", "uncertainty interval")
|
| 361 |
|
| 362 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
def _hypotheses(
|
| 364 |
exposure: str,
|
| 365 |
outcomes: tuple[str, ...],
|
|
@@ -1548,9 +1592,18 @@ def _make_declarations(plan: ResearchPlan) -> pd.DataFrame:
|
|
| 1548 |
|
| 1549 |
def _primary_result_paragraph(plan: ResearchPlan, study: Study) -> str:
|
| 1550 |
results = study.results
|
| 1551 |
-
if plan.design_key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1552 |
n_total = results.get("n_total", len(study.data))
|
| 1553 |
-
n_treatment = results.get("n_treatment",
|
| 1554 |
n_control = results.get("n_control", "not recorded")
|
| 1555 |
treated_rate = _percent(results.get("event_rate_treatment"))
|
| 1556 |
control_rate = _percent(results.get("event_rate_control"))
|
|
@@ -1563,6 +1616,29 @@ def _primary_result_paragraph(plan: ResearchPlan, study: Study) -> str:
|
|
| 1563 |
f"and {control_rate} in the comparator arm. The estimated risk ratio was "
|
| 1564 |
f"{risk_ratio}, and the absolute risk difference was {risk_difference}."
|
| 1565 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1566 |
if plan.design_key == "cohort":
|
| 1567 |
risk_ratio = _estimate_with_ci(results, "risk_ratio")
|
| 1568 |
risk_difference = _estimate_with_ci(results, "risk_difference", percent=True)
|
|
@@ -1575,13 +1651,118 @@ def _primary_result_paragraph(plan: ResearchPlan, study: Study) -> str:
|
|
| 1575 |
odds_ratio = _estimate_with_ci(results, "odds_ratio")
|
| 1576 |
return (
|
| 1577 |
f"The case-control simulation estimated an odds ratio of {odds_ratio} for "
|
| 1578 |
-
f"{plan.exposure_or_intervention.lower()} and {plan.outcomes[0]}."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1579 |
)
|
| 1580 |
if plan.design_key == "survival_cox":
|
| 1581 |
-
hazard_ratio = _estimate_with_ci(results, "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1582 |
return (
|
| 1583 |
-
f"The
|
| 1584 |
-
f"{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1585 |
)
|
| 1586 |
numeric_results = [
|
| 1587 |
f"{metric.replace('_', ' ')}={value}"
|
|
@@ -1648,10 +1829,11 @@ def _estimate_with_ci(
|
|
| 1648 |
results: dict[str, Any],
|
| 1649 |
metric: str,
|
| 1650 |
*,
|
|
|
|
| 1651 |
percent: bool = False,
|
| 1652 |
) -> str:
|
| 1653 |
value = results.get(metric)
|
| 1654 |
-
ci = results.get(f"{metric}_ci")
|
| 1655 |
if value is None:
|
| 1656 |
return "not estimated"
|
| 1657 |
if percent and isinstance(value, int | float):
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
import json
|
| 5 |
+
import re
|
| 6 |
import sqlite3
|
| 7 |
from dataclasses import asdict, dataclass
|
| 8 |
from pathlib import Path
|
|
|
|
| 14 |
|
| 15 |
from episim import __version__
|
| 16 |
from episim.core.reproducibility import Study
|
| 17 |
+
from episim.lab import (
|
| 18 |
+
get_design,
|
| 19 |
+
list_designs,
|
| 20 |
+
resolve_design_parameters,
|
| 21 |
+
run_design,
|
| 22 |
+
study_preview,
|
| 23 |
+
)
|
| 24 |
from episim.reporting.ai_disclosure import block as ai_disclosure
|
| 25 |
|
| 26 |
|
|
|
|
| 137 |
clean_question = _clean_question(question)
|
| 138 |
selected_design = design_key or _infer_design_key(clean_question)
|
| 139 |
design = get_design(selected_design)
|
| 140 |
+
params = resolve_design_parameters(
|
| 141 |
+
design,
|
| 142 |
+
{"seed_value": seed_value, **parameter_overrides},
|
| 143 |
+
)
|
| 144 |
title = _title_from_question(clean_question, design.title)
|
| 145 |
target = _target_phrase(clean_question)
|
| 146 |
exposure = _exposure_phrase(clean_question, design.family)
|
|
|
|
| 324 |
if "lifestyle" in q:
|
| 325 |
label = "Structured lifestyle intervention"
|
| 326 |
return f"{label} assignment" if family == "experimental" else label
|
| 327 |
+
if "diet" in q or "nutrition" in q:
|
| 328 |
label = "Dietary or nutrition-related intervention"
|
| 329 |
return f"{label} assignment" if family == "experimental" else label
|
| 330 |
+
if "physical inactivity" in q or "inactive" in q:
|
| 331 |
+
return "Physical inactivity exposure"
|
| 332 |
if "exercise" in q or "physical activity" in q:
|
| 333 |
+
label = (
|
| 334 |
+
"Physical activity intervention"
|
| 335 |
+
if family == "experimental"
|
| 336 |
+
else "Physical activity exposure"
|
| 337 |
+
)
|
| 338 |
+
return f"{label} assignment" if family == "experimental" else label
|
| 339 |
+
if "preventive intervention" in q or "prevention" in q:
|
| 340 |
+
label = "Preventive intervention"
|
| 341 |
return f"{label} assignment" if family == "experimental" else label
|
| 342 |
+
if "treatment intensity" in q:
|
| 343 |
+
return "Treatment intensity"
|
| 344 |
+
if "eligibility" in q or "age 65" in q:
|
| 345 |
+
return "Eligibility-threshold assignment"
|
| 346 |
+
if "misinformation" in q:
|
| 347 |
+
return "Health-misinformation exposure"
|
| 348 |
if family == "experimental":
|
| 349 |
return "Simulated intervention assignment"
|
| 350 |
+
if _mentions_ai(q):
|
| 351 |
return "AI-enabled exposure or decision-support process"
|
| 352 |
if "policy" in q:
|
| 353 |
return "Policy or implementation exposure"
|
| 354 |
+
if "intervention" in q:
|
| 355 |
+
return "Proposed intervention or implementation exposure"
|
| 356 |
return "Primary exposure or intervention specified by the research question"
|
| 357 |
|
| 358 |
|
|
|
|
| 370 |
q = question.lower()
|
| 371 |
if design_key == "qualitative_mixed_methods":
|
| 372 |
return ("theme saturation", "acceptability score", "integrated interpretation")
|
| 373 |
+
if design_key == "cross_sectional" and "prevalence" in q and "frailty" in q:
|
| 374 |
+
return ("frailty prevalence", "odds ratio", "exposure rate")
|
| 375 |
+
if design_key in {"markov_decision", "microsimulation_lifetable"}:
|
| 376 |
+
return ("QALYs and costs", "incremental cost-effectiveness", "mortality")
|
| 377 |
+
if "misinformation" in q:
|
| 378 |
+
return ("contagion adoption", "peak spread", "final attack rate")
|
| 379 |
+
if "fall" in q:
|
| 380 |
+
return ("monthly fall rate", "level change", "slope change")
|
| 381 |
+
if "hospital" in q:
|
| 382 |
+
return ("preventable hospitalization", "threshold effect", "local contrast")
|
| 383 |
if "frailty" in q:
|
| 384 |
return ("12-month frailty event", "risk ratio", "risk difference")
|
| 385 |
if "cognitive" in q or "cognition" in q:
|
| 386 |
return ("cognitive decline event", "risk ratio", "risk difference")
|
| 387 |
+
if "trust" in q and _mentions_ai(q):
|
| 388 |
return ("trust and acceptability outcome", "theme saturation", "survey score")
|
| 389 |
if "mortality" in q or design_key == "survival_cox":
|
| 390 |
return ("time-to-event outcome", "censoring status", "hazard ratio")
|
|
|
|
| 395 |
return ("primary health or social outcome", "effect estimate", "uncertainty interval")
|
| 396 |
|
| 397 |
|
| 398 |
+
def _mentions_ai(text: str) -> bool:
|
| 399 |
+
return bool(
|
| 400 |
+
re.search(
|
| 401 |
+
r"\b(ai|a\.i\.|artificial intelligence|algorithm|algorithmic)\b",
|
| 402 |
+
text.lower(),
|
| 403 |
+
)
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
|
| 407 |
def _hypotheses(
|
| 408 |
exposure: str,
|
| 409 |
outcomes: tuple[str, ...],
|
|
|
|
| 1592 |
|
| 1593 |
def _primary_result_paragraph(plan: ResearchPlan, study: Study) -> str:
|
| 1594 |
results = study.results
|
| 1595 |
+
if plan.design_key == "cross_sectional":
|
| 1596 |
+
prevalence = _estimate_with_ci(results, "prevalence", percent=True)
|
| 1597 |
+
odds_ratio = _estimate_with_ci(results, "odds_ratio")
|
| 1598 |
+
exposure_rate = _percent(results.get("exposure_rate"))
|
| 1599 |
+
return (
|
| 1600 |
+
f"The cross-sectional sample included {len(study.data)} simulated records. "
|
| 1601 |
+
f"Estimated {plan.outcomes[0]} was {prevalence}, exposure prevalence was "
|
| 1602 |
+
f"{exposure_rate}, and the exposure-outcome odds ratio was {odds_ratio}."
|
| 1603 |
+
)
|
| 1604 |
+
if plan.design_key == "rct_parallel":
|
| 1605 |
n_total = results.get("n_total", len(study.data))
|
| 1606 |
+
n_treatment = results.get("n_treatment", "not recorded")
|
| 1607 |
n_control = results.get("n_control", "not recorded")
|
| 1608 |
treated_rate = _percent(results.get("event_rate_treatment"))
|
| 1609 |
control_rate = _percent(results.get("event_rate_control"))
|
|
|
|
| 1616 |
f"and {control_rate} in the comparator arm. The estimated risk ratio was "
|
| 1617 |
f"{risk_ratio}, and the absolute risk difference was {risk_difference}."
|
| 1618 |
)
|
| 1619 |
+
if plan.design_key == "rct_cluster":
|
| 1620 |
+
risk_ratio = _estimate_with_ci(results, "risk_ratio")
|
| 1621 |
+
risk_difference = _estimate_with_ci(results, "risk_difference", percent=True)
|
| 1622 |
+
return (
|
| 1623 |
+
f"The cluster trial simulated {results.get('n_clusters', 'not recorded')} "
|
| 1624 |
+
f"clusters and {results.get('n_total', len(study.data))} participants. "
|
| 1625 |
+
f"Cluster-level event rates were "
|
| 1626 |
+
f"{_percent(results.get('treated_cluster_event_rate'))} in treated clusters "
|
| 1627 |
+
f"and {_percent(results.get('control_cluster_event_rate'))} in control "
|
| 1628 |
+
f"clusters; the risk ratio was {risk_ratio} and the risk difference was "
|
| 1629 |
+
f"{risk_difference}."
|
| 1630 |
+
)
|
| 1631 |
+
if plan.design_key == "stepped_wedge":
|
| 1632 |
+
return (
|
| 1633 |
+
f"The stepped-wedge simulation generated {results.get('n_total', len(study.data))} "
|
| 1634 |
+
f"cluster-period participant records across rollout periods "
|
| 1635 |
+
f"{results.get('rollout_period_min', 'not recorded')} to "
|
| 1636 |
+
f"{results.get('rollout_period_max', 'not recorded')}. Event rates were "
|
| 1637 |
+
f"{_percent(results.get('treated_event_rate'))} under intervention exposure "
|
| 1638 |
+
f"and {_percent(results.get('control_event_rate'))} under control exposure; "
|
| 1639 |
+
f"the marginal risk difference was "
|
| 1640 |
+
f"{_percent(results.get('marginal_risk_difference'))}."
|
| 1641 |
+
)
|
| 1642 |
if plan.design_key == "cohort":
|
| 1643 |
risk_ratio = _estimate_with_ci(results, "risk_ratio")
|
| 1644 |
risk_difference = _estimate_with_ci(results, "risk_difference", percent=True)
|
|
|
|
| 1651 |
odds_ratio = _estimate_with_ci(results, "odds_ratio")
|
| 1652 |
return (
|
| 1653 |
f"The case-control simulation estimated an odds ratio of {odds_ratio} for "
|
| 1654 |
+
f"{plan.exposure_or_intervention.lower()} and {plan.outcomes[0]}. "
|
| 1655 |
+
f"The analytic sample contained {results.get('sampled_cases', 'not recorded')} "
|
| 1656 |
+
f"cases and {results.get('sampled_controls', 'not recorded')} controls."
|
| 1657 |
+
)
|
| 1658 |
+
if plan.design_key == "interrupted_time_series":
|
| 1659 |
+
return (
|
| 1660 |
+
f"The interrupted time-series simulation estimated a baseline rate of "
|
| 1661 |
+
f"{results.get('estimated_baseline_rate', 'not recorded')} and a post-policy "
|
| 1662 |
+
f"level change of {results.get('estimated_level_change', 'not recorded')}. "
|
| 1663 |
+
f"The pre-period mean rate was {results.get('pre_mean_rate', 'not recorded')} "
|
| 1664 |
+
f"and the post-period mean rate was "
|
| 1665 |
+
f"{results.get('post_mean_rate', 'not recorded')}."
|
| 1666 |
+
)
|
| 1667 |
+
if plan.design_key == "regression_discontinuity":
|
| 1668 |
+
return (
|
| 1669 |
+
f"The regression-discontinuity analysis used "
|
| 1670 |
+
f"{results.get('local_n', 'not recorded')} observations inside the bandwidth. "
|
| 1671 |
+
f"The estimated threshold jump was "
|
| 1672 |
+
f"{results.get('estimated_jump', 'not recorded')}, with mean outcomes of "
|
| 1673 |
+
f"{results.get('left_mean_outcome', 'not recorded')} below and "
|
| 1674 |
+
f"{results.get('right_mean_outcome', 'not recorded')} above the cutoff."
|
| 1675 |
+
)
|
| 1676 |
+
if plan.design_key == "instrumental_variables":
|
| 1677 |
+
return (
|
| 1678 |
+
f"The instrumental-variable simulation estimated a first stage of "
|
| 1679 |
+
f"{results.get('first_stage', 'not recorded')}, reduced form of "
|
| 1680 |
+
f"{results.get('reduced_form', 'not recorded')}, Wald local average "
|
| 1681 |
+
f"treatment effect of {results.get('wald_late', 'not recorded')}, and naive "
|
| 1682 |
+
f"difference of {results.get('naive_difference', 'not recorded')}."
|
| 1683 |
+
)
|
| 1684 |
+
if plan.design_key == "propensity_score":
|
| 1685 |
+
return (
|
| 1686 |
+
f"The propensity-score simulation estimated a naive difference of "
|
| 1687 |
+
f"{results.get('naive_difference', 'not recorded')} and an IPTW-adjusted "
|
| 1688 |
+
f"difference of {results.get('iptw_difference', 'not recorded')}. The mean "
|
| 1689 |
+
f"propensity score was {results.get('mean_propensity', 'not recorded')} and "
|
| 1690 |
+
f"maximum pre-weighting standardized mean difference was "
|
| 1691 |
+
f"{results.get('max_unweighted_smd', 'not recorded')}."
|
| 1692 |
)
|
| 1693 |
if plan.design_key == "survival_cox":
|
| 1694 |
+
hazard_ratio = _estimate_with_ci(results, "estimated_hazard_ratio")
|
| 1695 |
+
return (
|
| 1696 |
+
f"The time-to-event simulation estimated a hazard ratio of {hazard_ratio} "
|
| 1697 |
+
f"for {plan.outcomes[0]}. The event fraction was "
|
| 1698 |
+
f"{_percent(results.get('event_fraction'))}, with event rates of "
|
| 1699 |
+
f"{_percent(results.get('treated_event_rate'))} in the treated group and "
|
| 1700 |
+
f"{_percent(results.get('control_event_rate'))} in the comparator group."
|
| 1701 |
+
)
|
| 1702 |
+
if plan.design_key == "meta_analysis":
|
| 1703 |
+
pooled = _estimate_with_ci(results, "pooled_effect", ci_key="pooled_ci")
|
| 1704 |
+
return (
|
| 1705 |
+
f"The random-effects meta-analysis pooled {len(study.data)} study-level "
|
| 1706 |
+
f"effects. The pooled effect was {pooled}, tau-squared was "
|
| 1707 |
+
f"{results.get('tau2', 'not recorded')}, I-squared was "
|
| 1708 |
+
f"{_percent(results.get('i2'))}, and Cochran's Q was "
|
| 1709 |
+
f"{results.get('q', 'not recorded')}."
|
| 1710 |
+
)
|
| 1711 |
+
if plan.design_key == "agent_based_seir":
|
| 1712 |
+
return (
|
| 1713 |
+
f"The SEIR simulation peaked at {results.get('peak_infectious', 'not recorded')} "
|
| 1714 |
+
f"infectious agents on day {results.get('peak_day', 'not recorded')}. "
|
| 1715 |
+
f"Final recovered count was {results.get('final_recovered', 'not recorded')} "
|
| 1716 |
+
f"and the attack rate was {_percent(results.get('attack_rate'))}."
|
| 1717 |
+
)
|
| 1718 |
+
if plan.design_key == "microsimulation_lifetable":
|
| 1719 |
+
return (
|
| 1720 |
+
f"The microsimulation estimated mean QALYs of "
|
| 1721 |
+
f"{results.get('mean_qaly', 'not recorded')} and mean cost of "
|
| 1722 |
+
f"{results.get('mean_cost', 'not recorded')}. Disease prevalence was "
|
| 1723 |
+
f"{_percent(results.get('disease_prevalence'))} and mortality fraction was "
|
| 1724 |
+
f"{_percent(results.get('mortality_fraction'))}."
|
| 1725 |
+
)
|
| 1726 |
+
if plan.design_key == "markov_decision":
|
| 1727 |
+
return (
|
| 1728 |
+
f"The Markov model estimated prevention costs of "
|
| 1729 |
+
f"{results.get('prevention_cost', 'not recorded')} and QALYs of "
|
| 1730 |
+
f"{results.get('prevention_qaly', 'not recorded')}, compared with standard "
|
| 1731 |
+
f"care costs of {results.get('standard_cost', 'not recorded')} and QALYs of "
|
| 1732 |
+
f"{results.get('standard_qaly', 'not recorded')}. The ICER was "
|
| 1733 |
+
f"{results.get('icer', 'not recorded')} per QALY gained."
|
| 1734 |
+
)
|
| 1735 |
+
if plan.design_key == "network_contagion":
|
| 1736 |
+
return (
|
| 1737 |
+
f"The network-contagion simulation peaked at "
|
| 1738 |
+
f"{results.get('peak_infectious', 'not recorded')} active cases on day "
|
| 1739 |
+
f"{results.get('peak_day', 'not recorded')}. The final attack rate was "
|
| 1740 |
+
f"{_percent(results.get('final_attack_rate'))} across a network with observed "
|
| 1741 |
+
f"mean degree {results.get('mean_degree_observed', 'not recorded')}."
|
| 1742 |
+
)
|
| 1743 |
+
if plan.design_key == "qualitative_mixed_methods":
|
| 1744 |
+
return (
|
| 1745 |
+
f"The mixed-methods simulation reached saturation by interview "
|
| 1746 |
+
f"{results.get('saturation_interview', 'not recorded')}, identified "
|
| 1747 |
+
f"{results.get('themes_identified', 'not recorded')} themes, and estimated a "
|
| 1748 |
+
f"survey acceptability difference of "
|
| 1749 |
+
f"{results.get('survey_acceptability_difference', 'not recorded')}. The "
|
| 1750 |
+
f"integrated inference was "
|
| 1751 |
+
f"{results.get('mixed_methods_inference', 'not recorded')}."
|
| 1752 |
+
)
|
| 1753 |
+
if plan.design_key == "ecological_peai":
|
| 1754 |
+
phase3 = results.get("phase3", {})
|
| 1755 |
+
phase4 = results.get("phase4", {})
|
| 1756 |
+
validity = phase3.get("criterion_validity", {}) if isinstance(phase3, dict) else {}
|
| 1757 |
+
gbt = validity.get("peai_gbt", {}) if isinstance(validity, dict) else {}
|
| 1758 |
return (
|
| 1759 |
+
f"The PEAI simulation retained "
|
| 1760 |
+
f"{phase4.get('n_prospective_after_attrition', len(study.data))} prospective "
|
| 1761 |
+
f"records after attrition. Maximum frailty AUROC was "
|
| 1762 |
+
f"{phase4.get('max_frailty_auroc', 'not recorded')}, maximum ascertainment "
|
| 1763 |
+
f"attenuation was {phase4.get('max_ascertainment_attenuation', 'not recorded')}, "
|
| 1764 |
+
f"and the GBT PEAI Spearman correlation with frailty was "
|
| 1765 |
+
f"{gbt.get('spearman_vs_frailty', 'not recorded')}."
|
| 1766 |
)
|
| 1767 |
numeric_results = [
|
| 1768 |
f"{metric.replace('_', ' ')}={value}"
|
|
|
|
| 1829 |
results: dict[str, Any],
|
| 1830 |
metric: str,
|
| 1831 |
*,
|
| 1832 |
+
ci_key: str | None = None,
|
| 1833 |
percent: bool = False,
|
| 1834 |
) -> str:
|
| 1835 |
value = results.get(metric)
|
| 1836 |
+
ci = results.get(ci_key or f"{metric}_ci")
|
| 1837 |
if value is None:
|
| 1838 |
return "not estimated"
|
| 1839 |
if percent and isinstance(value, int | float):
|
pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "episim"
|
| 7 |
-
version = "0.12.
|
| 8 |
description = "Epidemiology Platform for In Silico Methods"
|
| 9 |
readme = "README.md"
|
| 10 |
authors = [{name = "Siddalingaiah H. S.", email = "hssling@gmail.com"}]
|
|
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "episim"
|
| 7 |
+
version = "0.12.4a1"
|
| 8 |
description = "Epidemiology Platform for In Silico Methods"
|
| 9 |
readme = "README.md"
|
| 10 |
authors = [{name = "Siddalingaiah H. S.", email = "hssling@gmail.com"}]
|