Source code for scitex_seizure_metrics.report
"""MetricsReport — frozen dataclass that carries a full evaluation result.
Designed so the same object can be:
- serialised to JSON for `stx.io.save(rep.to_dict(), "metrics.json")`
- materialised as a one-row pandas DataFrame for stacking across patients/folds
- pretty-printed for terminal logs.
"""
from __future__ import annotations
import json
from dataclasses import asdict, dataclass, field
from typing import Any
import pandas as pd
[docs]
@dataclass
class MetricsReport:
"""Single-row evaluation result.
Fields are deliberately flat so the report stacks cleanly across
patients/folds via `pd.concat([r.to_frame() for r in reports])`.
"""
# Identification
name: str = "" # e.g. "P03_fold0"
regime: str = "" # "detection" | "forecasting"
# Detection-style (continuous prediction → ranking metrics)
roc_auc: float | None = None
pr_auc: float | None = None
brier: float | None = None
balanced_accuracy: float | None = None
mcc: float | None = None
# Sample/event scoring (timescoring engine)
sensitivity: float | None = None
precision: float | None = None
f1: float | None = None
fp_per_day: float | None = None
fp_per_hour: float | None = None
n_ref_events: int | None = None
n_tp: int | None = None
n_fp: int | None = None
# Forecasting (alarm-based)
sph_seconds: float | None = None
sop_seconds: float | None = None
time_in_warning_frac: float | None = None
ioc: float | None = None # Improvement over Chance (vs surrogate)
surrogate_sensitivity: float | None = None
# Free-form extras (per-class scores, calibration table, etc.)
extras: dict[str, Any] = field(default_factory=dict)
[docs]
def to_dict(self) -> dict[str, Any]:
return asdict(self)
[docs]
def to_frame(self) -> pd.DataFrame:
d = self.to_dict()
d.pop("extras")
return pd.DataFrame([d])
[docs]
def to_json(self) -> str:
return json.dumps(self.to_dict(), indent=2, default=str)
def __str__(self) -> str:
d = {k: v for k, v in self.to_dict().items()
if v not in (None, {}, "")}
lines = [f"MetricsReport({d.get('name', '?')}, {d.get('regime', '?')})"]
for k, v in d.items():
if k in ("name", "regime"):
continue
if isinstance(v, float):
lines.append(f" {k:<25} {v:.4f}")
else:
lines.append(f" {k:<25} {v}")
return "\n".join(lines)