7ac69ab4fb
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>
163 lines
6.2 KiB
Python
163 lines
6.2 KiB
Python
"""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,
|
|
}
|