---
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.
