statspai.epi¶
epi ¶
Epidemiology domain primitives (sp.epi).
Fills the gap the article calls out — statspai already has the heavy epidemiological causal machinery (IPW, G-formula, MSM, target trial), but lacked the entry-level statistical primitives that clinicians, epidemiologists, and public-health researchers reach for first.
Modelled after R's epiR, epitools, and fmsb.
import statspai as sp round(sp.epi.odds_ratio(50, 20, 30, 40).estimate, 3) 3.333 round(sp.epi.relative_risk(50, 950, 10, 990).estimate, 3) 5.0 tables_2x2xK = [[[10, 20], [5, 25]], [[8, 15], [6, 30]]] sp.epi.mantel_haenszel(tables_2x2xK).n_strata 2 events, pop = [10, 20, 30], [1000, 2000, 1500] standard_weights = [0.3, 0.4, 0.3] round(sp.epi.direct_standardize(events, pop, standard_weights).rate, 4) 0.013 sp.epi.bradford_hill(strength=1.0, temporality=1.0, consistency=0.5).total 2.5
DiagnosticTestResult
dataclass
¶
Bases: ResultProtocolMixin
Container for binary diagnostic-test performance metrics.
Returned by :func:sensitivity_specificity / :func:diagnostic_test.
Holds sensitivity and specificity (with Wilson-score or exact CIs), predictive
values (ppv, npv), likelihood ratios (lr_pos, lr_neg),
prevalence, and the raw confusion cells tp/fp/fn/tn.
Examples:
>>> import statspai as sp
>>> res = sp.sensitivity_specificity(tp=90, fn=10, fp=5, tn=95)
>>> isinstance(res, sp.DiagnosticTestResult)
True
>>> res.sensitivity
0.9
>>> res.tp
90
KappaResult
dataclass
¶
Bases: ResultProtocolMixin
Container for Cohen's kappa inter-rater agreement.
Returned by :func:cohen_kappa. Holds kappa with its standard
error and CI, the observed and expected agreement, the number of
n_categories, the weights scheme, and the z/p inference. The
:meth:interpretation method maps kappa to a Landis-Koch label.
Examples:
>>> import statspai as sp
>>> a = [0, 1, 2, 0, 1, 2]
>>> b = [0, 1, 2, 0, 1, 2] # perfect agreement
>>> k = sp.cohen_kappa(a, b)
>>> isinstance(k, sp.KappaResult)
True
>>> k.kappa
1.0
>>> k.interpretation()
'almost perfect agreement'
ROCResult
dataclass
¶
Bases: ResultProtocolMixin
Container for ROC-curve coordinates and AUC inference.
Returned by :func:roc_curve. Holds the sweep thresholds and the
corresponding true/false positive rates (tpr, fpr), plus the
auc with its standard error (auc_se; Hanley-McNeil or DeLong,
see se_method) and CI (auc_ci).
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> y = np.array([0, 0, 0, 1, 1, 1])
>>> s = np.array([0.1, 0.2, 0.3, 0.7, 0.8, 0.9]) # perfectly separable
>>> roc = sp.roc_curve(y, s)
>>> isinstance(roc, sp.ROCResult)
True
>>> roc.auc
1.0
ARResult
dataclass
¶
Bases: ResultProtocolMixin
Attributable-risk quantities (Levin 1953, Miettinen 1974).
IRRResult
dataclass
¶
Bases: ResultProtocolMixin
Incidence rate ratio from person-time data.
NNTResult
dataclass
¶
Bases: ResultProtocolMixin
Number needed to treat (benefit) or harm.
estimate is 1 / |RD| and ci the interval for that
magnitude. ci_signed holds the reciprocals 1 / RD of the
risk-difference limits in ascending order (what R epiR reports).
When the risk-difference interval contains zero, ci_disjoint is
True: the compatible NNTs are 1 / |L| or more on the benefit side
and 1 / U or more on the harm side (Altman's "NNTB x to infinity
to NNTH y"), ci_signed holds (1/L, 1/U) and ci is
(1 / max(|L|, U), inf).
OR2x2Result
dataclass
¶
Bases: ResultProtocolMixin
Result of a 2x2 odds-ratio calculation.
RD2x2Result
dataclass
¶
Bases: ResultProtocolMixin
Result of a 2x2 risk-difference calculation.
RR2x2Result
dataclass
¶
Bases: ResultProtocolMixin
Result of a 2x2 relative-risk (risk-ratio) calculation.
auc ¶
cohen_kappa ¶
cohen_kappa(rater_a: Any, rater_b: Any, *, weights: str = 'unweighted', alpha: float = 0.05) -> KappaResult
Cohen's (1960) kappa for two raters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rater_a
|
array - like
|
Same-length sequences of category labels from two raters. |
required |
rater_b
|
array - like
|
Same-length sequences of category labels from two raters. |
required |
weights
|
('unweighted', 'linear', 'quadratic')
|
Weighting scheme for disagreements across an ordered category scale. "unweighted" recovers the classic Cohen kappa. |
"unweighted"
|
Examples:
>>> import statspai as sp
>>> a = [0, 1, 2, 0, 1, 2]
>>> b = [0, 1, 2, 0, 1, 2] # perfect agreement
>>> k = sp.cohen_kappa(a, b)
>>> k.kappa
1.0
>>> k.n_categories
3
>>> c = [0, 1, 2, 0, 2, 1] # two disagreements vs a
>>> kc = sp.cohen_kappa(a, c)
>>> bool(kc.kappa < 1.0)
True
References
[@cohen1960coefficient]
diagnostic_test ¶
diagnostic_test(*args: Any, **kwargs: Any) -> DiagnosticTestResult
ndcg ¶
Normalised discounted cumulative gain of the top-ranked cases.
For a rare outcome (accounting fraud, default) the question is not how
well a score ranks everyone, which is what the AUC answers, but how many
true cases sit at the very top of the list an examiner would work
through. NDCG at k looks at the k highest scores only, credits a
true case at rank i with 1 / log2(i + 1), and divides by the
credit of a perfect ranking, so 1 means the top of the list is all true
cases (or all of them, when there are fewer than k).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y_true
|
array - like
|
0/1 outcome. |
required |
scores
|
array - like
|
Predicted score; higher means more likely. |
required |
k
|
float
|
A fraction in (0, 1): the top |
0.01
|
Returns:
| Type | Description |
|---|---|
float
|
NDCG at |
Raises:
| Type | Description |
|---|---|
MethodIncompatibility
|
(a |
Notes
Tied scores share their credit: every rank a group of tied cases
occupies earns the group's share of true cases, so the value is the
average of NDCG over all orderings of the ties and does not depend on
the order of the rows (sklearn.metrics.ndcg_score with its default
ignore_ties=False). Without ties this is the plain ranked sum, and
equals farr::ndcg, which breaks ties in data order. Until 1.40.2
ties were broken in data order here too.
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> y = np.array([1, 0, 1, 0, 0, 0, 0, 0, 0, 0])
>>> s = np.array([.9, .8, .7, .6, .5, .4, .3, .2, .1, .0])
>>> round(sp.ndcg(y, s, k=3), 4) # hits at ranks 1 and 3 of top 3
0.9197
>>> sp.ndcg(y, s, k=1)
1.0
References
jarvelin2002cumulated, bao2020detecting
roc_curve ¶
roc_curve(y_true: Any, scores: Any, *, alpha: float = 0.05, se_method: str = 'delong', weights: Any = None) -> ROCResult
ROC curve with the AUC and its standard error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y_true
|
array-like of {0, 1}
|
|
required |
scores
|
array-like of continuous predictions (higher = more "positive")
|
|
required |
alpha
|
float
|
|
0.05
|
weights
|
array - like
|
Non-negative observation weights. The rates become weighted
shares and the AUC the weighted Mann-Whitney probability
|
None
|
se_method
|
('delong', 'hanley', 'hanley-empirical')
|
.. versionchanged:: 1.30.0
The default moved from |
"delong"
|
Notes
The curve is traced over the distinct score values: thresholds holds
them in decreasing order and tpr / fpr the rates of the rule
"positive when score >= threshold". The AUC is therefore the
Mann-Whitney probability P(S+ > S-) + P(S+ = S-)/2 exactly, with
tied scores counted as half. The confidence interval is
AUC +/- z se truncated to [0, 1] (as pROC::ci.auc does).
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> y = np.array([0, 0, 0, 1, 1, 1])
>>> s = np.array([0.1, 0.2, 0.3, 0.7, 0.8, 0.9]) # perfectly separable
>>> roc = sp.roc_curve(y, s)
>>> roc.auc
1.0
>>> bool(roc.auc_ci[0] <= roc.auc <= roc.auc_ci[1])
True
References
[@hanley1982meaning]
sensitivity_specificity ¶
sensitivity_specificity(y_true: Any = None, y_pred: Any = None, *, tp: Optional[int] = None, fn: Optional[int] = None, fp: Optional[int] = None, tn: Optional[int] = None, alpha: float = 0.05, ci_method: str = 'wilson') -> DiagnosticTestResult
Sensitivity and specificity with Wilson-score (or exact) CIs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y_true
|
array - like
|
Reference and predicted binary labels (0/1). |
None
|
y_pred
|
array - like
|
Reference and predicted binary labels (0/1). |
None
|
tp
|
int
|
Pre-computed confusion cells. Use instead of |
None
|
fn
|
int
|
Pre-computed confusion cells. Use instead of |
None
|
fp
|
int
|
Pre-computed confusion cells. Use instead of |
None
|
tn
|
int
|
Pre-computed confusion cells. Use instead of |
None
|
alpha
|
float
|
|
0.05
|
ci_method
|
('wilson', 'exact')
|
Interval for sensitivity and specificity: Wilson score (R
|
"wilson"
|
Examples:
>>> import statspai as sp
>>> res = sp.sensitivity_specificity(tp=90, fn=10, fp=5, tn=95)
>>> res.sensitivity # 90 / (90 + 10)
0.9
>>> res.specificity # 95 / (95 + 5)
0.95
>>> res.ppv # 90 / (90 + 5)
0.9473684210526315
References
[@altman1994statistics]
attributable_risk ¶
attributable_risk(a: Any, b: Optional[float] = None, c: Optional[float] = None, d: Optional[float] = None, *, alpha: float = 0.05, prevalence: str = 'estimated') -> ARResult
Attributable fractions in exposed + in population (Levin 1953).
Computes, from the raw counts: - AF_exposed = (RR - 1) / RR - PAF = P_e * (RR - 1) / [1 + P_e * (RR - 1)] = (R - R_0) / R
where P_e is the proportion exposed, R the overall risk and R_0 the
risk among the unexposed (Stata cs / csi).
The PAF interval is the delta method on log(1 - PAF), where
1 - PAF = R_0 / R. prevalence says whether the exposure
prevalence P_e = (a + b) / N is an estimate or a design constant:
"estimated"(default): the table is one random sample (a cross-sectional survey, or a cohort enrolled without regard to exposure), soP_eis estimated from it and its sampling error enters the interval. The variance is the multinomial delta method on the four cells. This is Statapunaf, vce(unconditional)afterglm y x, family(binomial) link(log) vce(robust)except for the robustN / (N - 1)factor, which is left out here."fixed": the exposure groups were sampled separately with sizes fixed by design andP_eis known. The variance is the Katz variance oflog RRscaled byd log(1 - PAF) / d log RR, as Statapunafwith the default conditionalmarginsvariance.
Under random sampling the "fixed" interval is too narrow when the
outcome is common: with P_e = 0.3, R_0 = 0.45, RR = 2 and
N = 400 it covered 91.4% in a simulation against 94.7% for
"estimated".
The interval needs events in both exposure groups. With a = 0 or
c = 0 the point estimates are still the plug-in values (PAF = 1
when no unexposed case occurred) but paf_ci is (nan, nan) with
a warning; Stata cs reports no interval there either.
.. versionchanged:: 1.42
prevalence="estimated" is the default; the interval used to
hold P_e fixed in every case ("fixed" keeps it).
Examples:
incidence_rate_ratio ¶
incidence_rate_ratio(events_exposed: float, pt_exposed: float, events_unexposed: float, pt_unexposed: float, *, alpha: float = 0.05, method: str = 'exact') -> IRRResult
Person-time incidence rate ratio with exact Poisson CI.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
events_exposed
|
float
|
Event counts. |
required |
events_unexposed
|
float
|
Event counts. |
required |
pt_exposed
|
float
|
Person-time at risk in each group (any time unit, as long as consistent). |
required |
pt_unexposed
|
float
|
Person-time at risk in each group (any time unit, as long as consistent). |
required |
method
|
('exact', 'wald')
|
"exact": the exact conditional (Clopper-Pearson) interval and the
exact conditional binomial test, as R |
"exact"
|
Examples:
number_needed_to_treat ¶
number_needed_to_treat(a: Any, b: Optional[float] = None, c: Optional[float] = None, d: Optional[float] = None, *, alpha: float = 0.05) -> NNTResult
Number needed to treat (or harm), defined as |1 / RD|.
Propagates the Wald CI for RD. Interpretation convention: negative RD -> NNT-Benefit (treatment reduces risk); positive RD -> NNT-Harm.
When the RD interval (L, U) excludes zero, ci is
(1/|U|, 1/|L|) sorted, matching R epiR::epi.2by2. When it
includes zero the set of compatible NNTs is disjoint: 1/|L| or more
on one side and 1/U or more on the other, reported by Altman
(1998) as "NNTB 1/|L| to infinity to NNTH 1/U". ci_disjoint is then
True, ci_signed is (1/L, 1/U) (epiR's pair) and ci is the
interval for the magnitude, (1 / max(|L|, U), inf).
Examples:
>>> import statspai as sp
>>> res = sp.epi.number_needed_to_treat(20, 80, 40, 60)
>>> round(res.estimate, 2)
5.0
>>> round(res.risk_difference, 3) # treated arm has lower risk
-0.2
References
Altman, D. G. (1998). Confidence intervals for the number needed to treat. BMJ 317(7168), 1309-1312. doi:10.1136/bmj.317.7168.1309
odds_ratio ¶
odds_ratio(a: Any, b: Optional[float] = None, c: Optional[float] = None, d: Optional[float] = None, *, method: str = 'woolf', alpha: float = 0.05, correction: Optional[str] = None) -> OR2x2Result
Odds ratio from a 2x2 table.
The standard epidemiology 2x2 layout is::
Outcome+ Outcome-
Exposed a b
Unexposed c d
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
float
|
Cell counts, or pass a 2x2 array-like as |
required |
b
|
float
|
Cell counts, or pass a 2x2 array-like as |
required |
c
|
float
|
Cell counts, or pass a 2x2 array-like as |
required |
d
|
float
|
Cell counts, or pass a 2x2 array-like as |
required |
method
|
('woolf', 'exact')
|
Inference method. "woolf" uses the asymptotic log-OR standard
error and the Wald test; "exact" uses the exact conditional
(noncentral hypergeometric) interval and Fisher's exact test.
The point estimate |
"woolf"
|
alpha
|
float
|
|
0.05
|
correction
|
(None, 'haldane')
|
Only used by .. versionchanged:: 1.42
The Haldane correction used to be applied by default, so a
zero cell moved the point estimate ( |
None
|
Returns:
| Type | Description |
|---|---|
OR2x2Result
|
With |
Notes
The exact limits solve the two tail equations of the noncentral
hypergeometric distribution to machine precision. R fisher.test
and Stata cci, exact solve the same equations with looser root
finders, so their limits differ from these in the fourth or fifth
significant digit (R up to 1.2e-3 relative, Stata up to 2.2e-4 on the
tables in the tests); R's conditional MLE differs by about 1e-5.
Examples:
prevalence_ratio ¶
prevalence_ratio(*args: Any, **kwargs: Any) -> RR2x2Result
Prevalence ratio (cross-sectional RR); mathematically identical
to :func:relative_risk when called on a 2x2 prevalence table.
Distinguished for semantic clarity in cross-sectional studies.
Examples:
>>> import statspai as sp
>>> res = sp.epi.prevalence_ratio(40, 60, 20, 80)
>>> round(res.estimate, 3)
2.0
>>> res.method
'prevalence-ratio'
References
zou2004modified
relative_risk ¶
relative_risk(a: Any, b: Optional[float] = None, c: Optional[float] = None, d: Optional[float] = None, *, alpha: float = 0.05, correction: Optional[str] = None) -> RR2x2Result
Relative risk (risk ratio) with Katz log-RR confidence interval.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
float
|
Cell counts (exposed cases, exposed non-cases, unexposed cases,
unexposed non-cases), or pass a 2x2 array-like as |
required |
b
|
float
|
Cell counts (exposed cases, exposed non-cases, unexposed cases,
unexposed non-cases), or pass a 2x2 array-like as |
required |
c
|
float
|
Cell counts (exposed cases, exposed non-cases, unexposed cases,
unexposed non-cases), or pass a 2x2 array-like as |
required |
d
|
float
|
Cell counts (exposed cases, exposed non-cases, unexposed cases,
unexposed non-cases), or pass a 2x2 array-like as |
required |
alpha
|
float
|
|
0.05
|
correction
|
(None, 'haldane')
|
.. versionchanged:: 1.42
The Haldane correction used to be applied by default, so a
zero cell moved the point estimate ( |
None
|
Examples:
risk_difference ¶
risk_difference(a: Any, b: Optional[float] = None, c: Optional[float] = None, d: Optional[float] = None, *, method: str = 'wald', alpha: float = 0.05) -> RD2x2Result
Risk difference with Wald or Newcombe CI.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
('wald', 'newcombe')
|
Newcombe's hybrid score CI avoids the Wald overshoot problem near 0 or 1. |
"wald"
|
Examples:
direct_standardize ¶
direct_standardize(events: Sequence[float], population: Sequence[float], standard_weights: Sequence[float], *, alpha: float = 0.05, ci_method: str = 'gamma', variance: str = 'poisson') -> StandardizedRateResult
Direct standardization of a rate.
The standardized rate is:
r_std = sum_k (w_k * events_k / population_k)
where w_k are the relative weights of a standard population
(they are normalized internally to sum to 1).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
events
|
array - like
|
Event counts in each stratum of the study population. |
required |
population
|
array - like
|
Denominator (person-time or population size) in each stratum. |
required |
standard_weights
|
array - like
|
Standard population size or proportion per stratum. Will be normalized to sum to 1. |
required |
alpha
|
float
|
|
0.05
|
ci_method
|
('gamma', 'lognormal', 'normal')
|
.. versionchanged:: 1.30.0
The default moved from |
"gamma"
|
variance
|
('poisson', 'binomial')
|
|
"poisson"
|
Returns:
| Type | Description |
|---|---|
StandardizedRateResult
|
|
Notes
The standard error is the square root of the chosen variance of the
weighted sum of stratum rates; .se stores it.
Examples:
indirect_standardize ¶
indirect_standardize(observed: float, events_reference: Sequence[float], population_reference: Sequence[float], population_study: Sequence[float], *, alpha: float = 0.05, ci_method: str = 'exact') -> SMRResult
Indirect standardization -> Standardized Morbidity/Mortality Ratio.
Expected events = sum_k (rate_ref_k * pop_study_k), where rate_ref_k = events_reference_k / population_reference_k. SMR = observed / expected.
ci_method="exact" (default) is the exact Poisson (Garwood)
interval for the observed count divided by the expected count, as
Stata istdize; "lognormal" is SMR exp(+/- z / sqrt(O)), as R
epitools::ageadjust.indirect. p_value is the exact two-sided
Poisson test of SMR = 1: twice the smaller exact tail, capped at 1.
Examples:
breslow_day_test ¶
breslow_day_test(tables: Union[Sequence, ndarray], *, tarone_correction: bool = True) -> tuple[float, float]
Breslow-Day test for homogeneity of the odds ratio across strata.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tables
|
array-like, shape ``(K, 2, 2)``
|
|
required |
tarone_correction
|
bool
|
Apply Tarone's correction (recommended; Tarone 1985). |
True
|
Returns:
| Name | Type | Description |
|---|---|---|
chi2 |
float
|
|
p_value |
float
|
|
Examples:
Test whether the odds ratio is constant across two strata. Here the per-stratum ORs are close, so the test does not reject homogeneity:
>>> import statspai as sp
>>> tables = [
... [[10, 90], [5, 95]],
... [[20, 80], [12, 88]],
... ]
>>> chi2, p = sp.breslow_day_test(tables)
>>> bool(chi2 >= 0.0)
True
>>> bool(0.0 <= p <= 1.0)
True
>>> bool(p > 0.05) # fail to reject OR homogeneity across strata
True
References
breslow1980statistical tarone1985heterogeneity
mantel_haenszel ¶
mantel_haenszel(tables: Union[Sequence, ndarray], *, measure: str = 'OR', alpha: float = 0.05) -> MantelHaenszelResult
Mantel-Haenszel pooled OR / RR across K strata.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tables
|
array-like, shape ``(K, 2, 2)``
|
Each stratum's 2x2 table with layout
|
required |
measure
|
('OR', 'RR')
|
Pooled measure. Use :func: |
"OR"
|
alpha
|
float
|
Two-sided CI level. |
0.05
|
Returns:
| Type | Description |
|---|---|
MantelHaenszelResult
|
|
Examples:
Pool the exposure-outcome odds ratio across two confounder strata, each
a 2x2 table [[exposed-case, exposed-noncase], [unexposed-case,
unexposed-noncase]]:
>>> import statspai as sp
>>> tables = [
... [[10, 90], [5, 95]],
... [[20, 80], [12, 88]],
... ]
>>> res = sp.mantel_haenszel(tables, measure="OR")
>>> type(res).__name__
'MantelHaenszelResult'
>>> res.measure
'OR'
>>> res.n_strata
2
>>> bool(res.estimate > 1.0) # exposure raises the odds in both strata
True
>>> bool(res.ci[0] <= res.estimate <= res.ci[1])
True
References
mantel1959statistical