feat(eda): series temporales + rigor anti-data-mining + PDF movil + /eda + benchmark issues
Bloque del grupo eda (sesion ausente EDA-benchmark): - 8 funciones nuevas: adf_kpss_stationarity, acf_pacf, stl_decompose, to_returns, fdr_correction, suggest_reexpression, exploratory_caveats, render_eda_pdf - integracion: profile_table (run_series, emit_pdf), association_matrix (FDR Benjamini-Hochberg), render_eda_markdown (secciones series/reexpresion/caveats) - slash commands /eda y /capitulos - issues 0173-0177: mejoras del /eda derivadas del benchmark sobre 12 datasets reales (outlier_pct x100, periodo estacional, FK inference, render models, tipos id-like) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
"""Tests de estacionariedad de una serie temporal: ADF + KPSS (grupo eda).
|
||||
|
||||
Funcion pura y determinista que combina dos contrastes de estacionariedad con
|
||||
hipotesis nulas opuestas y emite un veredicto de consenso. Motivada por la
|
||||
necesidad (Hyndman "Forecasting", Hamilton "Time Series Analysis") de saber si
|
||||
una serie es estacionaria ANTES de correlacionarla o modelarla: correlacionar
|
||||
niveles no estacionarios produce correlacion espuria (Granger-Newbold 1974).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import warnings
|
||||
|
||||
from statsmodels.tsa.stattools import adfuller, kpss
|
||||
|
||||
|
||||
def _clean(values: list) -> list[float]:
|
||||
"""Conserva solo valores numericos finitos, descartando None/NaN/no-numericos.
|
||||
|
||||
Los booleanos se excluyen explicitamente: en Python ``bool`` es subclase de
|
||||
``int``, pero tratar True/False como numeros en una serie temporal es casi
|
||||
siempre un error de tipado.
|
||||
"""
|
||||
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 adf_kpss_stationarity(values: list, alpha: float = 0.05) -> dict:
|
||||
"""Evalua la estacionariedad de una serie combinando ADF y KPSS.
|
||||
|
||||
Aplica dos contrastes con hipotesis nulas opuestas:
|
||||
|
||||
- **ADF** (Augmented Dickey-Fuller): H0 = "la serie tiene raiz unitaria"
|
||||
(es NO estacionaria). Si ``p < alpha`` se rechaza H0 -> evidencia de
|
||||
estacionariedad.
|
||||
- **KPSS** (Kwiatkowski-Phillips-Schmidt-Shin): H0 = "la serie es
|
||||
estacionaria (en torno a una tendencia)". Si ``p < alpha`` se rechaza H0
|
||||
-> evidencia de NO estacionariedad.
|
||||
|
||||
Combinar ambos da mas robustez que cualquiera por separado, porque sus
|
||||
hipotesis nulas son contrarias. El veredicto de consenso sigue la
|
||||
interpretacion estandar (Hyndman, "Forecasting: Principles and Practice"):
|
||||
|
||||
- ADF rechaza H0 **y** KPSS no rechaza H0 -> ``"stationary"``.
|
||||
- ADF no rechaza H0 **y** KPSS rechaza H0 -> ``"non_stationary"``.
|
||||
- Ambos coinciden en lo contrario o se contradicen -> ``"inconclusive"``
|
||||
(a menudo indica serie diferenciable o estacionaria en tendencia).
|
||||
|
||||
Funcion pura y determinista: no hace I/O, no muta los inputs.
|
||||
|
||||
Args:
|
||||
values: serie temporal de valores numericos en orden cronologico.
|
||||
None/NaN/infinitos/no-numericos se descartan antes del test.
|
||||
alpha: nivel de significancia para ambos contrastes (default 0.05).
|
||||
|
||||
Returns:
|
||||
Con menos de 8 puntos validos (muestra insuficiente para un test de
|
||||
raiz unitaria fiable) devuelve
|
||||
``{"n": n, "note": "datos insuficientes", "verdict": None}``.
|
||||
|
||||
En otro caso un dict con::
|
||||
|
||||
{
|
||||
"n": int,
|
||||
"alpha": float,
|
||||
"adf": {"stat": float, "p_value": float, "lags": int,
|
||||
"stationary": bool, # rechaza H0 de raiz unitaria
|
||||
"conclusion": str},
|
||||
"kpss": {"stat": float, "p_value": float, "lags": int,
|
||||
"stationary": bool, # NO rechaza H0 de estacionariedad
|
||||
"conclusion": str,
|
||||
"p_value_clipped": bool}, # p en limite de tabla KPSS
|
||||
"verdict": "stationary" | "non_stationary" | "inconclusive",
|
||||
"warning": str | None, # aviso de correlacion espuria si procede
|
||||
}
|
||||
|
||||
``warning`` se rellena cuando el veredicto NO es ``"stationary"`` para
|
||||
recordar que correlacionar/regresionar niveles no estacionarios produce
|
||||
relaciones espurias; conviene pasar a retornos o diferencias.
|
||||
"""
|
||||
clean = _clean(values)
|
||||
n = len(clean)
|
||||
|
||||
if n < 8:
|
||||
return {"n": n, "note": "datos insuficientes", "verdict": None}
|
||||
|
||||
# ADF: H0 = raiz unitaria (no estacionaria). p < alpha => estacionaria.
|
||||
adf_stat, adf_p, adf_lags, _adf_nobs, _adf_crit, _adf_icbest = adfuller(
|
||||
clean, autolag="AIC"
|
||||
)
|
||||
adf_stationary = bool(adf_p < alpha)
|
||||
adf = {
|
||||
"stat": float(adf_stat),
|
||||
"p_value": float(adf_p),
|
||||
"lags": int(adf_lags),
|
||||
"stationary": adf_stationary,
|
||||
"conclusion": (
|
||||
"rechaza H0 de raiz unitaria: evidencia de estacionariedad"
|
||||
if adf_stationary
|
||||
else "no rechaza H0 de raiz unitaria: posible no estacionaria"
|
||||
),
|
||||
}
|
||||
|
||||
# KPSS: H0 = estacionaria en torno a tendencia. p < alpha => NO estacionaria.
|
||||
# statsmodels emite InterpolationWarning cuando el p-valor cae fuera de la
|
||||
# tabla [0.01, 0.10]; lo capturamos para saber si quedo recortado.
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
kpss_stat, kpss_p, kpss_lags, _kpss_crit = kpss(
|
||||
clean, regression="c", nlags="auto"
|
||||
)
|
||||
p_clipped = any("InterpolationWarning" in str(w.category) for w in caught) or any(
|
||||
"p-value" in str(w.message).lower() for w in caught
|
||||
)
|
||||
kpss_stationary = bool(kpss_p >= alpha) # NO rechaza H0 => estacionaria
|
||||
kpss_result = {
|
||||
"stat": float(kpss_stat),
|
||||
"p_value": float(kpss_p),
|
||||
"lags": int(kpss_lags),
|
||||
"stationary": kpss_stationary,
|
||||
"conclusion": (
|
||||
"no rechaza H0 de estacionariedad: evidencia de estacionariedad"
|
||||
if kpss_stationary
|
||||
else "rechaza H0 de estacionariedad: posible no estacionaria"
|
||||
),
|
||||
"p_value_clipped": bool(p_clipped),
|
||||
}
|
||||
|
||||
# Consenso de los dos contrastes.
|
||||
if adf_stationary and kpss_stationary:
|
||||
verdict = "stationary"
|
||||
elif (not adf_stationary) and (not kpss_stationary):
|
||||
verdict = "non_stationary"
|
||||
else:
|
||||
verdict = "inconclusive"
|
||||
|
||||
warning: str | None = None
|
||||
if verdict != "stationary":
|
||||
warning = (
|
||||
"serie no claramente estacionaria: correlacionar o regresionar sus "
|
||||
"niveles puede dar relaciones espurias (Granger-Newbold). Considera "
|
||||
"trabajar sobre retornos o diferencias (ver to_returns)."
|
||||
)
|
||||
|
||||
return {
|
||||
"n": n,
|
||||
"alpha": float(alpha),
|
||||
"adf": adf,
|
||||
"kpss": kpss_result,
|
||||
"verdict": verdict,
|
||||
"warning": warning,
|
||||
}
|
||||
Reference in New Issue
Block a user