"""Tests para stl_decompose.""" import numpy as np from stl_decompose import stl_decompose def _serie_estacional(n: int, period: int, trend: float, amp: float, seed: int) -> list: rng = np.random.default_rng(seed) return [ trend * i + amp * np.sin(2 * np.pi * i / period) + rng.normal(0, 1) for i in range(n) ] def test_serie_con_tendencia_y_estacionalidad(): serie = _serie_estacional(n=120, period=12, trend=0.3, amp=10.0, seed=0) res = stl_decompose(serie, period=12) assert res["period"] == 12 assert res["trend_strength"] > 0.5 assert res["seasonal_strength"] > 0.5 assert len(res["trend"]["values"]) == 120 def test_fuerza_estacional_alta_con_estacionalidad_fuerte(): # Amplitud estacional grande, ruido pequeno => seasonal_strength cercano a 1. serie = _serie_estacional(n=120, period=12, trend=0.05, amp=20.0, seed=1) res = stl_decompose(serie, period=12) assert res["seasonal_strength"] > 0.9 def test_infiere_periodo_si_none(): serie = _serie_estacional(n=120, period=12, trend=0.1, amp=10.0, seed=2) res = stl_decompose(serie) # period=None assert res.get("period_inferred") is True assert res["period"] is not None def test_serie_corta_devuelve_nota(): # period=12 pero solo 20 puntos (< 2*period=24): nota, no descompone. serie = _serie_estacional(n=20, period=12, trend=0.1, amp=5.0, seed=3) res = stl_decompose(serie, period=12) assert "note" in res assert res["trend_strength"] is None def test_muestra_insuficiente_devuelve_nota(): res = stl_decompose([1, 2, 3, 4, 5]) assert res["n"] == 5 assert res["note"] == "datos insuficientes" assert res["seasonal_strength"] is None def test_descarta_none_y_nan(): serie = _serie_estacional(n=120, period=12, trend=0.2, amp=8.0, seed=4) sucio = [] for i, v in enumerate(serie): sucio.append(v) if i % 30 == 0: sucio.append(None) sucio.append(float("nan")) res = stl_decompose(sucio, period=12) assert res["n"] == 120 def test_serie_larga_resume_sin_values(): # >200 puntos: las componentes vienen resumidas sin 'values'. serie = _serie_estacional(n=300, period=12, trend=0.1, amp=10.0, seed=5) res = stl_decompose(serie, period=12) assert res["trend"]["values"] is None assert "mean" in res["trend"] assert "note" in res["trend"] # --- H2: deteccion de periodo robusta a tendencia (detrend) ------------------ def test_h2_infer_period_detrend_con_tendencia_fuerte(): # Golden: serie con tendencia FUERTE + estacionalidad de periodo 12. Sin detrend # la autocorrelacion cruda decae monotonamente y el lag minimo (2) gana siempre # (period=2 espurio). Con el detrend lineal el lag ganador es el periodo real. from stl_decompose import _infer_period serie = _serie_estacional(n=120, period=12, trend=0.8, amp=10.0, seed=0) arr = np.asarray(serie, dtype=float) assert _infer_period(arr, max_period=60) == 12 def test_h2_auto_period_no_degenera_a_2(): # End-to-end: stl_decompose(period=None) sobre serie con tendencia fuerte detecta # estacionalidad real en vez de reportar period=2 y seasonal_strength ~ 0 # (el falso negativo de estacionalidad que motivo el fix H2). serie = _serie_estacional(n=120, period=12, trend=0.8, amp=10.0, seed=0) res = stl_decompose(serie) assert res["period"] != 2 assert res["seasonal_strength"] > 0.5 def test_h2_serie_sin_estacionalidad_no_inventa_periodo(): # Edge: serie con SOLO tendencia (sin componente estacional) no debe inventar un # periodo; tras el detrend el residuo es ruido sin pico de autocorrelacion claro. rng = np.random.default_rng(7) serie = [0.5 * i + rng.normal(0, 1) for i in range(120)] res = stl_decompose(serie) # Sin periodo fiable: nota explicita, nunca seasonal_strength=0 como conclusion. assert res["trend_strength"] is None assert "note" in res