hssling commited on
Commit
e2b2c19
·
verified ·
1 Parent(s): 121602b

Deploy EPISIM v0.12.4 all-design research hardening

Browse files
episim/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
  """EPISIM — Epidemiology Platform for In Silico Methods."""
2
 
3
- __version__ = "0.12.3a1"
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 = dict(design.parameters)
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 get_design, list_designs, run_design, study_preview
 
 
 
 
 
 
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 = dict(design.parameters)
134
- params["seed_value"] = seed_value
135
- params.update(parameter_overrides)
 
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 = "Physical activity intervention"
 
 
 
 
 
 
 
324
  return f"{label} assignment" if family == "experimental" else label
 
 
 
 
 
 
325
  if family == "experimental":
326
  return "Simulated intervention assignment"
327
- if "ai" in q or "algorithm" in q:
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 ("ai" in q or "algorithm" in q):
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 in {"rct_parallel", "rct_cluster", "stepped_wedge"}:
 
 
 
 
 
 
 
 
 
1552
  n_total = results.get("n_total", len(study.data))
1553
- n_treatment = results.get("n_treatment", results.get("n_treated", "not recorded"))
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, "hazard_ratio")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1582
  return (
1583
- f"The time-to-event simulation estimated a hazard-ratio-style effect of "
1584
- f"{hazard_ratio} for {plan.outcomes[0]}."
 
 
 
 
 
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.3a1"
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"}]