--- id: "0174" title: "EDA series temporales: período estacional roto + correlación de niveles + to_returns ciego" status: pendiente type: bugfix domain: - registry-quality scope: registry-only priority: alta depends: [] blocks: [] related: ["0173", "0175", "0176", "0177"] created: 2026-06-29 updated: 2026-06-29 tags: [eda, datascience, stl_decompose, profile_table, to_returns, series, benchmark] --- # 0174 — EDA series temporales: período estacional + correlación de niveles ## Contexto El benchmark `/eda` (29/06/2026, `temp/eda_benchmark/EVALUATION.md`) confirmó que la estacionariedad (ADF+KPSS), la autocorrelación (Ljung-Box) y el aviso de espuriedad Granger-Newbold están **bien** (verificados a mano con `statsmodels`). Pero el **detector de período estacional está roto**, lo que produce falsos negativos de estacionalidad, y la correlación de precios se calcula sobre niveles (espuria para uso financiero). Hallazgos cubiertos: | Hallazgo | Severidad | Evidencia del benchmark | |---|---|---| | H2 — período estacional sale `2` casi siempre → `seasonal_strength=0` | crítico | seattle `temp_max` reporta "sin estacionalidad" (`period=2`); STL real con `period=365` da fuerza estacional **0.843**. UNRATE (mensual) debería usar 12, no 2 | | H8 — correlación de precios sobre niveles marcada `sig=sí` | medio-alto | aapl/btc `Close–Open=0.998 sig=sí`: espuria por construcción (niveles autocorrelados no estacionarios) | | H13 — `to_returns` sugerido ciegamente a temperatura (sin sentido físico) | bajo | seattle `temp_max`: "convertir a retornos"; debería ser "diferencias" | ### Causa raíz H2 (verificada en código, READ-ONLY) `python/functions/datascience/stl_decompose.py:34-58` (`_infer_period`) busca el lag entre 2 y `max_period` que maximiza la autocorrelación **cruda** de la serie. En cualquier serie con tendencia (precios, temperatura), la autocorrelación decae monótonamente desde el lag mínimo, así que **el lag 2 casi siempre gana** → `period=2` espurio y un STL con componente estacional que es ruido (`seasonal_strength≈0`). Además, `python/functions/pipelines/profile_table.py:175` (`_build_series_block`) llama `stl_decompose(series_vals)` **sin pasar el período**, pese a que el pipeline ya conoce la columna de orden temporal (`order_col`) y podría derivar la frecuencia. ## Tareas 1. **H2 — arreglar la inferencia de período** en `stl_decompose.py:34-58`. Opciones (preferir la robusta): (a) detrend antes de autocorrelar; (b) buscar picos en el periodograma/FFT en vez del primer lag; (c) **derivar el período de la frecuencia del índice datetime** (mensual→12, diario→7 y/o 365) — la señal más fiable. 2. **H2 — pasar el período desde el pipeline:** en `profile_table.py:_build_series_block`, cuando exista `order_col` datetime, inferir la frecuencia del índice y pasar `period=` explícito a `stl_decompose`. Si no se puede determinar un período fiable, que `stl_decompose` **no reporte `seasonal_strength=0`** como conclusión: devolver `note` "período no determinado" (ya hay una rama así en `:139-145`; extenderla a los casos que hoy caen en `period=2`). 3. **H8 — correlación sobre retornos para series no estacionarias:** en la sección de correlaciones de `profile_table.py:346-384`, cuando una columna sea una serie no estacionaria de niveles (verdict `non_stationary`/`inconclusive`, ya detectado), correlacionar sobre retornos/diferencias (`to_returns`, ya importado) o marcar esos pares de niveles como "posible espuria" junto a la tabla. El aviso global existe pero está lejos de los números. 4. **H13 — retornos vs diferencias por semántica:** en `profile_table.py:189` / `to_returns.py`, elegir "retornos" (financiero, estrictamente positivo multiplicativo) vs "diferencias" (físico, aditivo) según la naturaleza, o usar "diferencias" por defecto cuando no haya señal financiera. 5. Tests: `stl_decompose_test.py` (serie sintética mensual con estacionalidad anual → período correcto y `seasonal_strength` alta; serie con tendencia sin estacionalidad → nota, no `period=2`); cobertura de `_build_series_block` con `order_col` datetime. ## Definition of Done | Escenario | Tipo | Comando / evidencia | Resultado esperado | |---|---|---|---| | Golden: estacionalidad anual | e2e | re-correr `profile_table` con `run_series=True` sobre seattle `temp_max` | `seasonal_strength ≈ 0.84` con período ≈ 365 (NO "sin estacionalidad", NO `period=2`) | | Edge: serie mensual | unit | `stl_decompose_test.py` serie mensual sintética con ciclo 12 | período inferido 12 y fuerza estacional alta | | Edge: sin estacionalidad | unit | `stl_decompose_test.py` serie con solo tendencia | `note` "período no determinado", NO `seasonal_strength=0` como conclusión | | Error: serie corta | unit | `stl_decompose([...]<2*period)` | nota "serie corta", sin crash (contrato actual) | | H8 | e2e | re-correr `profile_table` sobre aapl/btc | pares de niveles no estacionarios marcados como posible espuria o correlación sobre retornos | | Mecánica | — | `./fn run stl_decompose_py_datascience`; `fn index` | tests verdes; índice limpio | Re-correr el benchmark sobre seattle, fred-unrate, aapl y btc y confirmar que la estacionalidad se detecta donde existe y no se inventa donde no. ## Notas Issue derivado de `temp/eda_benchmark/EDA_ISSUES.md`. H2 es el segundo bloqueante de fiabilidad: un "sin estacionalidad" donde la hay es un falso negativo que un decisor creería. La estacionariedad ya funciona — no tocarla. Hermanos: 0173, 0175, 0176, 0177.