Source code for pystatsbio.diagnostic._common

"""Shared result types for diagnostic accuracy analysis."""

from __future__ import annotations

from dataclasses import dataclass

import numpy as np
from numpy.typing import NDArray


[docs] @dataclass(frozen=True) class ROCResult: """Result of ROC analysis. Attributes ---------- thresholds : array Thresholds at which TPR/FPR are evaluated. Includes ``-inf`` and ``+inf`` so the curve always passes through (0,0) and (1,1). tpr : array True positive rate (sensitivity) at each threshold. fpr : array False positive rate (1 − specificity) at each threshold. auc : float Area under the ROC curve (Mann-Whitney U / (n1*n0)). auc_se : float DeLong standard error of the AUC. auc_ci_lower, auc_ci_upper : float Confidence interval for AUC (logit-transformed DeLong). conf_level : float Confidence level used for CI. n_positive, n_negative : int Number of positive (case) and negative (control) observations. direction : str ``'<'`` (controls < cases) or ``'>'`` (controls > cases). """ thresholds: NDArray[np.floating] tpr: NDArray[np.floating] # sensitivity / true positive rate fpr: NDArray[np.floating] # 1 - specificity / false positive rate auc: float auc_se: float # DeLong standard error auc_ci_lower: float auc_ci_upper: float conf_level: float n_positive: int n_negative: int direction: str # '<' or '>'
[docs] def summary(self) -> str: """Human-readable summary.""" lines = [ "ROC Analysis", "=" * 40, f"Direction : controls {self.direction} cases", f"AUC : {self.auc:.4f}", f"DeLong SE : {self.auc_se:.4f}", f"{self.conf_level:.0%} CI : [{self.auc_ci_lower:.4f}, {self.auc_ci_upper:.4f}]", f"n positive : {self.n_positive}", f"n negative : {self.n_negative}", f"n thresholds: {len(self.thresholds)}", ] return "\n".join(lines)
[docs] @dataclass(frozen=True) class DiagnosticResult: """Result of diagnostic accuracy evaluation at a fixed cutoff. All CIs use the method specified in ``method`` (e.g. ``'clopper-pearson'`` for exact binomial CIs). """ cutoff: float sensitivity: float sensitivity_ci: tuple[float, float] specificity: float specificity_ci: tuple[float, float] ppv: float npv: float lr_positive: float lr_negative: float dor: float # diagnostic odds ratio dor_ci: tuple[float, float] prevalence: float conf_level: float method: str # CI method, e.g. 'clopper-pearson'
[docs] def summary(self) -> str: """Human-readable summary.""" lines = [ "Diagnostic Accuracy", "=" * 40, f"Cutoff : {self.cutoff:.4g}", f"Sensitivity : {self.sensitivity:.4f} " f"({self.conf_level:.0%} CI: {self.sensitivity_ci[0]:.4f}{self.sensitivity_ci[1]:.4f})", f"Specificity : {self.specificity:.4f} " f"({self.conf_level:.0%} CI: {self.specificity_ci[0]:.4f}{self.specificity_ci[1]:.4f})", f"PPV : {self.ppv:.4f}", f"NPV : {self.npv:.4f}", f"LR+ : {self.lr_positive:.4f}", f"LR− : {self.lr_negative:.4f}", f"DOR : {self.dor:.4f} " f"({self.conf_level:.0%} CI: {self.dor_ci[0]:.4f}{self.dor_ci[1]:.4f})", f"Prevalence : {self.prevalence:.4f}", f"CI method : {self.method}", ] return "\n".join(lines)