a69d14d38e
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>
110 lines
3.5 KiB
Python
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
|