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>
135 lines
5.3 KiB
Python
135 lines
5.3 KiB
Python
"""Autocorrelacion (ACF) y autocorrelacion parcial (PACF) de una serie (grupo eda).
|
|
|
|
Funcion pura y determinista que calcula la funcion de autocorrelacion y la
|
|
parcial con sus bandas de confianza, mas el test de Ljung-Box de autocorrelacion
|
|
global. Motivada por Hyndman ("Forecasting") para identificar el orden de un
|
|
modelo ARIMA, y por Lopez de Prado ("Advances in Financial ML"): una serie
|
|
autocorrelacionada viola el supuesto IID, de modo que los p-valores de una
|
|
regresion OLS estandar sobre ella estan inflados (falsos positivos).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
import numpy as np
|
|
from statsmodels.stats.diagnostic import acorr_ljungbox
|
|
from statsmodels.tsa.stattools import acf, pacf
|
|
|
|
|
|
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 no es un valor de serie temporal valido).
|
|
"""
|
|
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 acf_pacf(values: list, nlags: int = 40, alpha: float = 0.05) -> dict:
|
|
"""Calcula ACF, PACF y el test Ljung-Box de una serie temporal.
|
|
|
|
Computa la funcion de autocorrelacion (ACF) y la autocorrelacion parcial
|
|
(PACF) hasta ``nlags`` retardos, con sus bandas de confianza al nivel
|
|
``1 - alpha``, e identifica que retardos son significativos (cuyo intervalo
|
|
de confianza no contiene 0). Ademas corre el test de **Ljung-Box** sobre el
|
|
conjunto de retardos: H0 = "los datos son independientes" (sin
|
|
autocorrelacion); si ``p < alpha`` se rechaza -> la serie esta
|
|
autocorrelacionada.
|
|
|
|
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 calculo.
|
|
nlags: numero maximo de retardos a calcular (default 40). Se recorta
|
|
automaticamente a ``n // 2`` para PACF (statsmodels exige
|
|
``nlags < n/2``) y a ``n - 1`` para ACF.
|
|
alpha: nivel de significancia para las bandas de confianza y para el
|
|
test de Ljung-Box (default 0.05).
|
|
|
|
Returns:
|
|
Con menos de 8 puntos validos devuelve
|
|
``{"n": n, "note": "datos insuficientes", "is_autocorrelated": None}``.
|
|
|
|
En otro caso un dict con::
|
|
|
|
{
|
|
"n": int,
|
|
"nlags": int, # retardos efectivamente calculados
|
|
"acf": [float, ...], # incluye lag 0 (=1.0) en el indice 0
|
|
"pacf": [float, ...],
|
|
"acf_confint": [[low, high], ...], # banda por lag
|
|
"pacf_confint": [[low, high], ...],
|
|
"significant_acf_lags": [int, ...], # lags (>=1) significativos
|
|
"significant_pacf_lags": [int, ...],
|
|
"ljung_box": {"stat": float, "p_value": float, "lags": int},
|
|
"is_autocorrelated": bool, # Ljung-Box rechaza independencia
|
|
}
|
|
|
|
``is_autocorrelated = True`` significa que la serie NO es ruido blanco:
|
|
cuidado al aplicarle inferencia OLS clasica (p-valores inflados).
|
|
"""
|
|
clean = _clean(values)
|
|
n = len(clean)
|
|
|
|
if n < 8:
|
|
return {"n": n, "note": "datos insuficientes", "is_autocorrelated": None}
|
|
|
|
arr = np.asarray(clean, dtype=float)
|
|
|
|
# Recorta nlags a los limites de statsmodels: ACF admite hasta n-1, PACF < n/2.
|
|
eff_lags = min(nlags, n - 1, (n // 2) - 1)
|
|
eff_lags = max(eff_lags, 1)
|
|
|
|
acf_vals, acf_confint = acf(arr, nlags=eff_lags, alpha=alpha, fft=False)
|
|
pacf_vals, pacf_confint = pacf(arr, nlags=eff_lags, alpha=alpha)
|
|
|
|
# Un lag es significativo si su banda de confianza (centrada en el valor) no
|
|
# contiene 0. statsmodels devuelve confint como intervalos centrados en el
|
|
# estimador, asi que comparamos el intervalo desplazado al origen.
|
|
def _significant(vals, confint) -> list[int]:
|
|
out: list[int] = []
|
|
for lag in range(1, len(vals)):
|
|
low = confint[lag][0] - vals[lag]
|
|
high = confint[lag][1] - vals[lag]
|
|
if vals[lag] < low or vals[lag] > high:
|
|
out.append(lag)
|
|
return out
|
|
|
|
significant_acf = _significant(acf_vals, acf_confint)
|
|
significant_pacf = _significant(pacf_vals, pacf_confint)
|
|
|
|
# Ljung-Box sobre el maximo retardo calculado.
|
|
lb = acorr_ljungbox(arr, lags=[eff_lags], return_df=True)
|
|
lb_stat = float(lb["lb_stat"].iloc[0])
|
|
lb_p = float(lb["lb_pvalue"].iloc[0])
|
|
is_autocorrelated = bool(lb_p < alpha)
|
|
|
|
return {
|
|
"n": n,
|
|
"nlags": int(eff_lags),
|
|
"acf": [float(v) for v in acf_vals],
|
|
"pacf": [float(v) for v in pacf_vals],
|
|
"acf_confint": [[float(lo), float(hi)] for lo, hi in acf_confint],
|
|
"pacf_confint": [[float(lo), float(hi)] for lo, hi in pacf_confint],
|
|
"significant_acf_lags": significant_acf,
|
|
"significant_pacf_lags": significant_pacf,
|
|
"ljung_box": {
|
|
"stat": lb_stat,
|
|
"p_value": lb_p,
|
|
"lags": int(eff_lags),
|
|
},
|
|
"is_autocorrelated": is_autocorrelated,
|
|
}
|