--- name: extract_timeseries_raw kind: function lang: py domain: datascience version: "1.0.0" purity: impure signature: "def extract_timeseries_raw(query_fn, table: str, time_col: str, value_cols: list, max_rows: int = 5000) -> dict" description: "Extrae la serie temporal CRUDA (fechas + una o varias columnas numericas) de una tabla, ordenada cronologicamente, para alimentar el render del capitulo TIMESERIES de AutomaticEDA (linea valor-vs-tiempo + conteo por periodo). Recibe un lector read-only inyectado `query_fn(sql) -> dict` (mismo contrato que duckdb_query_readonly / pg_query / el `_q` de profile_table) y NO abre ninguna conexion por su cuenta. Construye UNA sola query con identificadores escapados, ORDER BY por la columna temporal y LIMIT. Devuelve dict dict-no-throw: t (fechas ISO string), series (lista paralela float|None por columna) y n. El capitulo no toca la BD: recibe esto en ctx['timeseries_raw']. Reutilizable tambien por profile_table en una fase futura." tags: [eda, timeseries, datascience, automatic-eda, extraction, read-only, duckdb, postgres, python] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: [datetime] params: - name: query_fn desc: "callable lector read-only del backend activo. Recibe un string SQL y devuelve un dict {'status':'ok','rows':[{col:val,...},...]} (mismo contrato que duckdb_query_readonly o el `_q` de profile_table). NO se abre ninguna conexion dentro de la funcion: toda la lectura pasa por query_fn. Si es None -> error." - name: table desc: "nombre de la tabla de la que extraer la serie. Se escapa con comillas dobles en la query." - name: time_col desc: "nombre de la columna de orden temporal. Se usa en ORDER BY (cronologico ascendente) y se filtra IS NOT NULL. Sus valores se devuelven en `t` como string ISO." - name: value_cols desc: "lista de nombres de columnas numericas a extraer. Cada una produce una entrada en `series` con una lista paralela a `t`. Vacia o None -> status error." - name: max_rows desc: "limite de filas a leer (clausula LIMIT). Default 5000. Protege el render frente a tablas enormes." output: "dict (nunca lanza). En exito: {'status':'ok','time_col':str,'t':[str,...] (fechas ISO en orden),'series':{col:[float|None,...],...} (paralela a t por value_col, None si el valor no es convertible a float),'n':int}. En error (sin lanzar): {'status':'error','error':str,'time_col':str,'t':[],'series':{},'n':0}. Errores: query_fn None, value_cols vacia, table/time_col vacios, o query_fn devuelve status!='ok' (se propaga su error)." tested: true tests: ["test_golden_t_y_series_alineadas", "test_valor_no_convertible_da_none", "test_value_cols_vacia_status_error", "test_query_fn_status_error_propaga", "test_query_fn_none_da_error_sin_reventar", "test_sql_contiene_order_by_y_limit"] test_file_path: "python/functions/datascience/extract_timeseries_raw_test.py" file_path: "python/functions/datascience/extract_timeseries_raw.py" --- ## Ejemplo ```python import sys, os sys.path.insert(0, os.path.join("python", "functions")) from datascience import extract_timeseries_raw from infra import duckdb_query_readonly # El lector read-only se inyecta como closure (igual que el `_q` de profile_table). db = "data/ventas.duckdb" def _q(sql): return duckdb_query_readonly(db, sql) res = extract_timeseries_raw(_q, "ventas_diarias", "fecha", ["importe", "unidades"]) # res == { # "status": "ok", # "time_col": "fecha", # "t": ["2024-01-01", "2024-01-02", ...], # "series": {"importe": [1234.5, 980.0, ...], "unidades": [12.0, 9.0, ...]}, # "n": 365, # } # Se entrega al capitulo TIMESERIES sin que este toque la BD: ctx = {"timeseries_raw": res} ``` ## Cuando usarla Cuando el capitulo TIMESERIES de AutomaticEDA necesita pintar una serie valor-vs-tiempo (o conteo por periodo) y NO debe abrir la base de datos por su cuenta: extraes aqui las fechas + columnas numericas ordenadas y se las pasas en `ctx['timeseries_raw']`. Usala tambien siempre que quieras la secuencia cruda ordenada cronologicamente de una o varias columnas para alimentar otros contrastes de serie (ADF/KPSS, ACF/PACF, STL) reutilizando un unico lector read-only inyectado, en vez de hacer N muestreos a mano. ## Gotchas - **Impura**: lee de la base de datos a traves de `query_fn`. No abre conexiones por su cuenta — depende por completo del lector inyectado. Sigue el estilo dict-no-throw del grupo `eda`: nunca lanza; ante cualquier fallo devuelve `{"status":"error","error":...}` con `t=[]`, `series={}`, `n=0`. - **`error_type` en el frontmatter es `error_go_core` por convencion del registry** (toda funcion impura debe declararlo y el indexer lo exige), pero el codigo NO lanza esa excepcion: degrada al dict de error. Es metadata, no comportamiento. - **No loguear los datos crudos**: `t`/`series` pueden contener datos sensibles (igual que un HAR). No volcar el dict completo a logs ni a telemetria; en trazas usa solo `n` y los nombres de columna. - **Alineacion por fila**: `series[col][i]` corresponde a `t[i]`. Un valor no convertible a float se guarda como `None` (no se descarta la fila) para no romper la alineacion temporal. - **Orden**: el orden cronologico depende del `ORDER BY "time_col"` del backend. Si `time_col` esta guardada como texto con formato no lexicograficamente ordenable (p.ej. `DD/MM/YYYY`), el orden no sera el real — normaliza la columna a date/timestamp antes, o pasa una columna ya ordenable. - **`max_rows`**: con LIMIT, si la tabla supera `max_rows` obtienes solo el primer tramo cronologico, no un muestreo uniforme. Sube `max_rows` si necesitas el rango completo.