feat(eda): pipeline BQ-EDA sobre tablas BigQuery (grupo eda)
Añade el conector y el pipeline para hacer EDA automático sobre tablas/vistas de BigQuery, reutilizando profile_table del grupo eda sin duplicar profiling: - load_bq_table_to_duckdb (datascience): trae una tabla BQ a DuckDB con seudonimización SHA-1 de columnas PII y normalización de dtypes. Por defecto carga el total de filas (sample_frac=None); el muestreo es opt-in explícito. - profile_bq_table (pipeline): orquesta load -> profile_table -> render report (JSON + Markdown + PDF/PPTX). Full por defecto. Ambas tageadas eda+bigquery, v1.1.0. El default full responde a la preferencia del operador: los EDA se corren sobre el total salvo indicación contraria. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
---
|
||||
name: profile_bq_table
|
||||
kind: pipeline
|
||||
lang: py
|
||||
domain: pipelines
|
||||
purity: impure
|
||||
version: "1.1.0"
|
||||
signature: "def profile_bq_table(table_fqn: str, sample_frac: float = None, max_rows: int = 0, pseudonymize_cols: list = None, run_models: bool = True, run_series: bool = False, run_llm: bool = False, project_id: str = \"\", report_dir: str = \"reports\", duckdb_path: str = \"\", keep_duckdb: bool = False) -> dict"
|
||||
description: "EDA one-shot de una tabla o vista de BigQuery: materializa el origen COMPLETO por defecto (todas las filas; muestreo opt-in con sample_frac; seudonimizacion PII opcional, LOPDGDD/RGPD) a un DuckDB local con load_bq_table_to_duckdb y lo perfila end-to-end con profile_table del grupo de capacidad eda, emitiendo el informe AutomaticEDA (PDF A5 movil + PPTX 16:9), Markdown y JSON sidecar. Es el adaptador BigQuery que faltaba en el grupo eda, resuelto por composicion (BigQuery -> DuckDB local -> profile_table) sin duplicar la logica de perfilado ni de render. Es el hazme un EDA de esta tabla BigQuery en una sola llamada, sobre el total de filas por defecto."
|
||||
tags: [eda, bigquery, launcher]
|
||||
uses_functions:
|
||||
- load_bq_table_to_duckdb_py_datascience
|
||||
- profile_table_py_pipelines
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/pipelines/profile_bq_table.py"
|
||||
params:
|
||||
- name: table_fqn
|
||||
desc: "FQN de la tabla/vista BigQuery: `project.dataset.table`."
|
||||
- name: sample_frac
|
||||
desc: "None (DEFAULT) = FULL, perfila TODAS las filas del origen. Un float en (0,1) activa el muestreo opt-in (`WHERE rand() < frac`, ~frac del total)."
|
||||
- name: max_rows
|
||||
desc: "Tope duro opcional de filas (LIMIT). 0 (DEFAULT) = sin tope. Se combina con sample_frac si ambos se pasan."
|
||||
- name: pseudonymize_cols
|
||||
desc: "Columnas PII a seudonimizar (hash) antes de materializar (LOPDGDD/RGPD). Preserva nulos y cardinalidad."
|
||||
- name: run_models
|
||||
desc: "PCA/KMeans/IsolationForest/normalidad sobre numericas. Default True (informe AutomaticEDA completo)."
|
||||
- name: run_series
|
||||
desc: "Analisis de serie temporal por columna numerica. Default False."
|
||||
- name: run_llm
|
||||
desc: "1 llamada LLM sobre el perfil agregado (nunca filas crudas). Default False."
|
||||
- name: project_id
|
||||
desc: "Proyecto GCP de facturacion. Vacio = primer segmento del FQN."
|
||||
- name: report_dir
|
||||
desc: "Directorio de salida de los reports. Default 'reports' (artefacto local gitignored)."
|
||||
- name: duckdb_path
|
||||
desc: "Ruta DuckDB a usar. Vacio = temporal autogestionado."
|
||||
- name: keep_duckdb
|
||||
desc: "Si True conserva el DuckDB materializado (para el notebook Jupyter). Default False."
|
||||
output: "dict dict-no-throw. En exito {status:'ok', table_fqn, load:{n_rows_source,n_rows_fetched,sampled,sample_frac,pseudonymized,table}, duckdb_path, report_md_path, report_json_path, aeda_pdf_path, aeda_pptx_path, aeda_manifest_path, profile}. En error {status:'error', error, stage}."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
from pipelines.profile_bq_table import profile_bq_table
|
||||
|
||||
# FULL por defecto: EDA sobre TODAS las filas de la vista (3,8M).
|
||||
r = profile_bq_table(
|
||||
"autingo-159109.customer_marts.customer_profile",
|
||||
pseudonymize_cols=["document_number", "full_name", "email", "phone", "postal_code", "salesforce_customer_id"],
|
||||
run_models=True,
|
||||
)
|
||||
print(r["load"]["n_rows_fetched"], "filas perfiladas, sampled=", r["load"]["sampled"])
|
||||
print(r["aeda_pdf_path"]); print(r["aeda_pptx_path"]); print(r["report_md_path"])
|
||||
|
||||
# Muestreo opt-in: EDA sobre ~5 % de las filas (tabla enorme / iteracion rapida).
|
||||
r = profile_bq_table(
|
||||
"autingo-159109.customer_marts.customer_profile",
|
||||
sample_frac=0.05,
|
||||
pseudonymize_cols=["document_number", "full_name", "email", "phone", "postal_code", "salesforce_customer_id"],
|
||||
)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando pidan un EDA de una tabla o vista de BigQuery ("hazme un EDA de esta
|
||||
tabla BigQuery"). Es el adaptador BigQuery del grupo de capacidad `eda` por
|
||||
composicion: trae el origen COMPLETO (todas las filas, por defecto) a un DuckDB
|
||||
local y delega todo el perfilado y render en `profile_table`, sin adaptador
|
||||
BigQuery nativo ni logica de EDA duplicada. Usala como primer paso al recibir un
|
||||
dataset BigQuery desconocido, antes de modelar o limpiar, o para auditar la
|
||||
calidad de una vista ya productiva. Para iteracion rapida o tablas que no quepan
|
||||
en RAM, pasa `sample_frac` (muestreo opt-in).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: requiere ADC de BigQuery configurado (Application Default Credentials)
|
||||
para que `load_bq_table_to_duckdb` autentique contra el proyecto.
|
||||
- FULL por defecto: `sample_frac=None` perfila TODAS las filas del origen. Una
|
||||
vista de millones de filas se trae entera a RAM (varios GB posibles) antes de
|
||||
materializar en DuckDB; el fetch usa el BigQuery Storage Read API (Arrow) cuando
|
||||
esta disponible, mucho mas rapido que REST. Para acotar coste/memoria, pasa
|
||||
`sample_frac` in (0,1) (muestreo opt-in) o `max_rows` (tope duro). Si por limite
|
||||
de recursos no cabe el total, dilo explicito con el maximo que si se cargo.
|
||||
- Seudonimiza PII con `pseudonymize_cols` para cumplir LOPDGDD/RGPD ANTES de
|
||||
escribir a disco: nombres, DNI/NIE, email, telefono, direccion, IDs de cliente,
|
||||
etc. Se hashean preservando nulos y cardinalidad. Sin seudonimizar, la muestra
|
||||
materializada (DuckDB + reports) contiene datos personales reales [POL-MMNSEG-001-1.0].
|
||||
- El DuckDB temporal se borra al terminar salvo `keep_duckdb=True` (pasalo para
|
||||
seguir explorando la muestra desde un notebook Jupyter). Si pasas `duckdb_path`
|
||||
explicito, la ruta se respeta y solo se conserva con `keep_duckdb=True`.
|
||||
- Escribe reports a `report_dir` (default 'reports', artefacto local gitignored):
|
||||
Markdown + JSON sidecar + PDF A5 movil + PPTX 16:9 del informe AutomaticEDA.
|
||||
- `run_llm=True` gasta tokens (haiku) pero solo envia el perfil agregado, nunca
|
||||
filas crudas ni datos personales.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.1.0 (2026-07-01) — FULL pasa a ser el DEFAULT del pipeline: se sustituye `max_rows=300000, sample=True` por `sample_frac=None` (None = perfila todas las filas) + `max_rows=0` (tope duro opcional). El muestreo es opt-in explicito (`sample_frac`). Alinea con la preferencia estandar del usuario: los EDA se corren sobre el total salvo que se pida lo contrario. Hereda el fetch acelerado (Arrow/bqstorage) de `load_bq_table_to_duckdb` v1.1.0.
|
||||
@@ -0,0 +1,138 @@
|
||||
"""profile_bq_table — EDA one-shot de una tabla/vista BigQuery con el grupo `eda`.
|
||||
|
||||
Pipeline impuro: materializa una tabla o vista de BigQuery (por defecto COMPLETA —
|
||||
todas las filas — o una muestra si se pasa `sample_frac`, con seudonimizacion PII
|
||||
opcional, LOPDGDD/RGPD) a un DuckDB local con `load_bq_table_to_duckdb`, y la
|
||||
perfila end-to-end con `profile_table` del grupo de capacidad `eda`, emitiendo el
|
||||
informe AutomaticEDA (PDF A5 movil + PPTX 16:9), Markdown y JSON sidecar. Es el
|
||||
adaptador BigQuery que faltaba en el grupo `eda`, resuelto por composicion
|
||||
(BigQuery -> DuckDB local -> profile_table) sin duplicar la logica de perfilado ni
|
||||
de render.
|
||||
|
||||
Modo por defecto = FULL: `sample_frac=None` perfila TODAS las filas del origen
|
||||
(preferencia estandar del usuario: los EDA se corren sobre el total salvo que se
|
||||
pida lo contrario). El muestreo es opt-in explicito: `sample_frac=0.05` perfila
|
||||
~5 % de las filas; `max_rows` es un tope duro opcional (0 = sin tope).
|
||||
|
||||
Funciones del registry compuestas (NO se reimplementa su logica):
|
||||
- load_bq_table_to_duckdb : trae la tabla/vista BigQuery a un DuckDB local
|
||||
(completa por defecto, o muestra si sample_frac).
|
||||
- profile_table : orquestador one-shot del grupo `eda` que perfila la
|
||||
DuckDB materializada y emite el informe AutomaticEDA.
|
||||
|
||||
Estilo dict-no-throw del grupo `eda`: nunca lanza; devuelve {status:'error', ...}.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from datascience import load_bq_table_to_duckdb
|
||||
from pipelines.profile_table import profile_table
|
||||
|
||||
|
||||
def profile_bq_table(
|
||||
table_fqn: str,
|
||||
sample_frac: float = None,
|
||||
max_rows: int = 0,
|
||||
pseudonymize_cols: list = None,
|
||||
run_models: bool = True,
|
||||
run_series: bool = False,
|
||||
run_llm: bool = False,
|
||||
project_id: str = "",
|
||||
report_dir: str = "reports",
|
||||
duckdb_path: str = "",
|
||||
keep_duckdb: bool = False,
|
||||
) -> dict:
|
||||
"""EDA one-shot de una tabla/vista BigQuery.
|
||||
|
||||
Por defecto perfila TODAS las filas del origen (`sample_frac=None`, modo FULL).
|
||||
Materializa el origen (con seudonimizacion PII opcional) a un DuckDB local y lo
|
||||
perfila con `profile_table` del grupo `eda`, emitiendo el informe AutomaticEDA
|
||||
(PDF A5 movil + PPTX 16:9) + Markdown + JSON sidecar.
|
||||
|
||||
Args:
|
||||
table_fqn: FQN de la tabla/vista BigQuery ("project.dataset.table").
|
||||
sample_frac: None (default) = FULL, perfila todas las filas. Un float en
|
||||
(0,1) activa el muestreo opt-in (`WHERE rand() < frac`, ~frac del total).
|
||||
max_rows: Tope duro opcional de filas (LIMIT). 0 (default) = sin tope.
|
||||
pseudonymize_cols: Columnas PII a seudonimizar (hash) antes de materializar.
|
||||
run_models: Modelos baratos (PCA/KMeans/IsolationForest/normalidad).
|
||||
run_series: Analisis de serie temporal por columna numerica.
|
||||
run_llm: 1 llamada LLM sobre el perfil agregado (nunca filas crudas).
|
||||
project_id: Proyecto GCP de facturacion. Vacio = primer segmento del FQN.
|
||||
report_dir: Directorio de salida de los reports.
|
||||
duckdb_path: Ruta DuckDB a usar. Vacio = temporal autogestionado.
|
||||
keep_duckdb: Si True conserva el DuckDB materializado.
|
||||
|
||||
Returns:
|
||||
dict dict-no-throw con el resultado del pipeline (ver output del .md).
|
||||
"""
|
||||
tmp_created = False
|
||||
try:
|
||||
# DuckDB temporal si no se pasa ruta.
|
||||
if not duckdb_path:
|
||||
fd, duckdb_path = tempfile.mkstemp(prefix="eda_bq_", suffix=".duckdb")
|
||||
os.close(fd)
|
||||
os.remove(duckdb_path) # que lo cree DuckDB limpio
|
||||
tmp_created = True
|
||||
|
||||
load = load_bq_table_to_duckdb(
|
||||
table_fqn,
|
||||
duckdb_path,
|
||||
sample_frac=sample_frac,
|
||||
max_rows=max_rows,
|
||||
project_id=project_id,
|
||||
pseudonymize_cols=pseudonymize_cols,
|
||||
)
|
||||
if load.get("status") != "ok":
|
||||
return {
|
||||
"status": "error",
|
||||
"error": load.get("error", "load fallo"),
|
||||
"stage": "load",
|
||||
}
|
||||
|
||||
prof = profile_table(
|
||||
duckdb_path,
|
||||
load["table"],
|
||||
backend="duckdb",
|
||||
run_models=run_models,
|
||||
run_series=run_series,
|
||||
run_llm=run_llm,
|
||||
emit_automatic=True, # PDF A5 movil + PPTX 16:9
|
||||
emit_pdf=False,
|
||||
write_report=True, # Markdown + JSON sidecar
|
||||
report_dir=report_dir,
|
||||
)
|
||||
if prof.get("status") != "ok":
|
||||
return {
|
||||
"status": "error",
|
||||
"error": prof.get("error", "profile fallo"),
|
||||
"stage": "profile",
|
||||
"load": load,
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"table_fqn": table_fqn,
|
||||
"load": {
|
||||
k: load[k]
|
||||
for k in ("n_rows_source", "n_rows_fetched", "sampled", "sample_frac", "pseudonymized", "table")
|
||||
if k in load
|
||||
},
|
||||
"duckdb_path": duckdb_path if keep_duckdb else None,
|
||||
"report_md_path": prof.get("report_md_path"),
|
||||
"report_json_path": prof.get("report_json_path"),
|
||||
"aeda_pdf_path": prof.get("aeda_pdf_path"),
|
||||
"aeda_pptx_path": prof.get("aeda_pptx_path"),
|
||||
"aeda_manifest_path": prof.get("aeda_manifest_path"),
|
||||
"profile": prof.get("profile"),
|
||||
}
|
||||
except Exception as e: # noqa: BLE001
|
||||
return {"status": "error", "error": str(e)}
|
||||
finally:
|
||||
# Limpia el DuckDB temporal salvo que se pida conservarlo.
|
||||
if tmp_created and not keep_duckdb and duckdb_path and os.path.exists(duckdb_path):
|
||||
try:
|
||||
os.remove(duckdb_path)
|
||||
except OSError:
|
||||
pass
|
||||
Reference in New Issue
Block a user