"""Normality tests for a numeric column. Pure, deterministic helper that runs a battery of normality hypothesis tests over a numeric sample and reports, per test, whether the data is consistent with a normal distribution at a given significance level. """ from __future__ import annotations import math from scipy import stats def _clean(values: list) -> list[float]: """Keep only finite numeric values, dropping None/NaN/non-numeric. Booleans are excluded explicitly: in Python ``bool`` is a subclass of ``int`` but treating True/False as numbers in a normality test is almost always a data-typing mistake. """ out: list[float] = [] for v in values: if v is None or isinstance(v, bool): continue if not isinstance(v, (int, float)): continue x = float(v) if math.isnan(x) or math.isinf(x): continue out.append(x) return out def normality_tests(values: list, alpha: float = 0.05) -> dict: """Run normality hypothesis tests on a numeric sample. Cleans the input (drops None, NaN, infinities and non-numeric values) and applies up to three normality tests: Jarque-Bera, D'Agostino-Pearson (``scipy.stats.normaltest``) and Shapiro-Wilk. For each test the null hypothesis is "the data comes from a normal distribution", so the sample is flagged ``normal = p > alpha`` (fail to reject the null). Shapiro-Wilk is only applied when ``3 <= n <= 5000``; outside that range its key is ``None``. Args: values: Sample of numeric values. None/NaN/non-numeric are discarded. alpha: Significance level for each test (default 0.05). Returns: For ``n < 8`` (insufficient sample) a dict ``{"n": n, "note": "muestra insuficiente", "is_normal": None}``. Otherwise a dict with:: { "n": int, "jarque_bera": {"stat": float, "p": float, "normal": bool}, "dagostino": {"stat": float, "p": float, "normal": bool}, "shapiro": {"stat": float, "p": float, "normal": bool} | None, "is_normal": bool, # consensus of applicable tests } ``is_normal`` is the consensus (all applicable tests agree the data is normal) over the tests that were actually run. """ clean = _clean(values) n = len(clean) if n < 8: return {"n": n, "note": "muestra insuficiente", "is_normal": None} jb_stat, jb_p = stats.jarque_bera(clean) jb = { "stat": float(jb_stat), "p": float(jb_p), "normal": bool(jb_p > alpha), } da_stat, da_p = stats.normaltest(clean) dagostino = { "stat": float(da_stat), "p": float(da_p), "normal": bool(da_p > alpha), } shapiro: dict | None = None if 3 <= n <= 5000: sw_stat, sw_p = stats.shapiro(clean) shapiro = { "stat": float(sw_stat), "p": float(sw_p), "normal": bool(sw_p > alpha), } applicable = [jb, dagostino] + ([shapiro] if shapiro is not None else []) is_normal = all(t["normal"] for t in applicable) return { "n": n, "jarque_bera": jb, "dagostino": dagostino, "shapiro": shapiro, "is_normal": bool(is_normal), }