Files
fn_registry/python/functions/pipelines/profile_bq_table.py
T
egutierrez 5a4f82cf76 chore: auto-commit (26 archivos)
- python/functions/bigquery/bq_auth.md
- python/functions/bigquery/bq_load_from_file.md
- python/functions/bigquery/bq_load_from_gcs.md
- python/functions/bigquery/client.py
- python/functions/bigquery/queries.py
- python/functions/datascience/__init__.py
- python/functions/datascience/decode_qr_image.py
- python/functions/datascience/load_bq_table_to_duckdb.md
- python/functions/datascience/load_bq_table_to_duckdb.py
- python/functions/pipelines/profile_bq_table.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-02 19:00:13 +02:00

160 lines
6.6 KiB
Python

"""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,
where_sql: str = "",
select_sql: str = "",
) -> 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.
where_sql: Clausula WHERE SQL (sin la palabra WHERE) aplicada al origen y a
su COUNT. Pass-through a `load_bq_table_to_duckdb`. Ej:
"fecha <= CURRENT_DATE() AND venta_n IS NOT NULL". Se interpola tal cual:
no usar con input no confiable.
select_sql: Expresiones del SELECT (sin la palabra SELECT); vacio = `*`.
Pass-through a `load_bq_table_to_duckdb`. Util para castear tipos
problematicos (p. ej. BIGNUMERIC->FLOAT64) antes de perfilar.
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,
where_sql=where_sql,
select_sql=select_sql,
)
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,
}
load_block = {
k: load[k]
for k in (
"n_rows_source", "n_rows_fetched", "sampled", "sample_frac",
"pseudonymized", "table", "streamed",
)
if k in load
}
# Trazabilidad de los filtros de origen (solo si vienen informados).
if where_sql:
load_block["where_sql"] = where_sql
if select_sql:
load_block["select_sql"] = select_sql
return {
"status": "ok",
"table_fqn": table_fqn,
"load": load_block,
"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