---
id: "0174"
title: "EDA series temporales: período estacional roto + correlación de niveles + to_returns ciego"
status: completado
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.


## Resolucion (2026-06-29, sesion /ausente)
Resuelto y verificado con re-corrida del benchmark EDA. Commit principal: e142ef02. Detalle en reports/ausente-eda-benchmark-2026-06-29.md y temp/eda_benchmark/EVALUATION.md.
