Files
fn_registry/python/functions/datascience/extract_timeseries_raw_test.py
T
egutierrez a69d14d38e feat(eda): capítulo TIMESERIES del AutomaticEDA (evolución + análisis de serie)
Capítulo nuevo build_timeseries(profile, ctx) -> Chapter|None del motor
AutomaticEDA. Cuando la tabla tiene columna de fecha/datetime, grafica la
evolución de cada columna numérica por periodo (valor agregado + conteo de filas)
y los paneles de descomposición STL y autocorrelación (ACF), con el análisis de
la serie: estacionariedad (ADF+KPSS), autocorrelación (Ljung-Box), fuerzas de
tendencia/estacionalidad (Hyndman) y la transformación sugerida (retornos o
diferencias) para evitar correlaciones espurias. Sin columna temporal devuelve
None. Consolida series OHLC casi idénticas en un único gráfico conservando el
análisis de cada columna.

La serie cruda llega por ctx['timeseries_raw'] (mismo patrón que modelos con
raw_numeric); las figuras son perezosas (Figure.make) y el paginador del núcleo
garantiza no-corte en PDF y PPTX. CHAPTER_VERSION 1.0.0.

Cubre los MUST del diseño (report 2043): MUST-9.1 (línea valor-vs-tiempo + conteo
por periodo), MUST-9.2 (paneles STL + ACF), MUST-9.3 (perfil datetime +
consolidación OHLC).

Funciones nuevas del registry (grupo eda), delegadas a fn-constructor, no inline:
- detect_time_column (pure): detecta la columna temporal y las numéricas
- profile_datetime (pure): rango/frecuencia/regularidad/huecos de la fecha
- resample_timeseries (pure): agrega la serie por periodo + conteo
- extract_timeseries_raw (impure): lee la serie cruda ordenada de DuckDB/PG

Verificación: 69 tests verdes (capítulo 9 + funciones 28 + núcleo/renderers);
golden real sobre seattle-weather (estacional) y aapl (OHLC) con PDF+PPTX sin
cortar nada (cols_cortadas=[]).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:35:42 +02:00

110 lines
3.5 KiB
Python

"""Tests para extract_timeseries_raw.
No usa DuckDB real: inyecta un query_fn FAKE (closure) que devuelve filas
predefinidas y, opcionalmente, captura el SQL recibido para verificar la query
generada (ORDER BY por la columna temporal + LIMIT). Asi el test es
autocontenido y no depende de ningun backend.
"""
import os
import sys
sys.path.insert(0, os.path.dirname(__file__))
from extract_timeseries_raw import extract_timeseries_raw
def _fake_query(rows, captured=None, status="ok", error=None):
"""Crea un query_fn FAKE.
`captured` (lista opcional) recibe el SQL ejecutado para poder inspeccionarlo.
`status`/`error` permiten simular un fallo del backend.
"""
def _q(sql):
if captured is not None:
captured.append(sql)
if status != "ok":
return {"status": "error", "error": error or "boom"}
return {"status": "ok", "rows": rows}
return _q
def test_golden_t_y_series_alineadas():
"""Golden: t y series alineadas, floats convertidos, n correcto."""
rows = [
{"fecha": "2024-01-01", "ventas": "10", "stock": 5},
{"fecha": "2024-01-02", "ventas": "20.5", "stock": 7},
{"fecha": "2024-01-03", "ventas": 30, "stock": 9},
]
res = extract_timeseries_raw(_fake_query(rows), "t", "fecha", ["ventas", "stock"])
assert res["status"] == "ok"
assert res["n"] == 3
assert res["time_col"] == "fecha"
assert res["t"] == ["2024-01-01", "2024-01-02", "2024-01-03"]
assert res["series"]["ventas"] == [10.0, 20.5, 30.0]
assert res["series"]["stock"] == [5.0, 7.0, 9.0]
def test_valor_no_convertible_da_none():
"""Valor no convertible a float -> None en la serie (alineacion preservada)."""
rows = [
{"fecha": "2024-01-01", "ventas": "abc"},
{"fecha": "2024-01-02", "ventas": None},
{"fecha": "2024-01-03", "ventas": "12.5"},
]
res = extract_timeseries_raw(_fake_query(rows), "t", "fecha", ["ventas"])
assert res["status"] == "ok"
assert res["series"]["ventas"] == [None, None, 12.5]
assert res["n"] == 3
def test_value_cols_vacia_status_error():
"""value_cols vacia -> status error con t/series/n vacios."""
res = extract_timeseries_raw(_fake_query([]), "t", "fecha", [])
assert res["status"] == "error"
assert "value_cols" in res["error"]
assert res["t"] == []
assert res["series"] == {}
assert res["n"] == 0
def test_query_fn_status_error_propaga():
"""query_fn que devuelve status != ok -> se propaga como error."""
res = extract_timeseries_raw(
_fake_query([], status="error", error="db locked"),
"t",
"fecha",
["ventas"],
)
assert res["status"] == "error"
assert "db locked" in res["error"]
assert res["n"] == 0
def test_query_fn_none_da_error_sin_reventar():
"""query_fn None -> error degradado, sin excepcion."""
res = extract_timeseries_raw(None, "t", "fecha", ["ventas"])
assert res["status"] == "error"
assert res["t"] == []
assert res["n"] == 0
def test_sql_contiene_order_by_y_limit():
"""La query generada ordena por time_col y aplica el LIMIT sobre la tabla."""
captured = []
rows = [{"fecha": "2024-01-01", "ventas": 1}]
extract_timeseries_raw(
_fake_query(rows, captured),
"ventas_tbl",
"fecha",
["ventas"],
max_rows=123,
)
assert len(captured) == 1
sql = captured[0]
assert 'ORDER BY "fecha"' in sql
assert "LIMIT 123" in sql
assert 'FROM "ventas_tbl"' in sql