Files
fn_registry/python/functions/datascience/summarize_table_pg.md
T
egutierrez 32c7336bf6 feat(infra): auto-commit con 56 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-21 14:22:55 +02:00

7.4 KiB

name, kind, lang, domain, version, purity, signature, description, tags, params, output, uses_functions, uses_types, returns, returns_optional, error_type, imports, tested, tests, test_file_path, file_path
name kind lang domain version purity signature description tags params output uses_functions uses_types returns returns_optional error_type imports tested tests test_file_path file_path
summarize_table_pg function py datascience 1.0.0 impure def summarize_table_pg(dsn: str, table: str, schema: str = "public", high_card_ratio: float = 0.9) -> dict Adaptador PostgreSQL del perfilado base del grupo eda: espejo de summarize_table_duckdb. Perfila una tabla PostgreSQL con SQL push-down (count, count(DISTINCT), min/max/avg/stddev_samp, percentile_cont) sin traer filas a RAM, y devuelve EXACTAMENTE el mismo esqueleto TableProfile (mismas claves) para que el resto del grupo eda lo consuma igual con fuente PostgreSQL. dict-no-throw.
eda
postgres
postgresql
profiling
datascience
exploratory-data-analysis
table-profile
name desc
dsn Cadena de conexion PostgreSQL en formato postgresql://user:pass@host:port/dbname. Un DSN invalido o servidor inalcanzable devuelve {status:'error'} sin lanzar (se propaga el error de pg_query).
name desc
table Nombre de la tabla a perfilar. Se valida contra ^[A-Za-z_][A-Za-z0-9_]*$ y se cita en el SQL (los identificadores no son parametrizables en el cuerpo del SELECT).
name desc
schema Schema PostgreSQL donde vive la tabla (default 'public'). Se valida con el mismo patron y se cita.
name desc
high_card_ratio Umbral de unicidad (unique_pct, 0-1) a partir del cual una columna categorical recibe el flag high_cardinality. Default 0.9.
dict dict-no-throw. En exito {status:'ok', profile: TableProfile} con source='postgres' y el MISMO shape que summarize_table_duckdb (n_rows/n_cols, type_breakdown, constant_cols, all_null_cols, null_cell_pct y columns[] de ColumnProfile con name/physical_type/inferred_type/semantic_type/count/null_count/null_pct/distinct_count/unique_pct/flags y sub-dict numeric con min,max,mean,std,p25,p50,p75 y el resto en None). En error {status:'error', error:str}. Claves estadisticas finas (skew, kurtosis, histograma, percentiles finos, moda, outliers, correlaciones, key_candidates, quality_score) quedan en None/[] para que otras funciones del grupo eda las completen.
pg_query_py_infra
false error_go_core
true
test_shape_y_metadatos_tabla
test_column_profile_shape
test_null_pct_total
test_distinct_no_excede_filas
test_type_breakdown
test_tabla_invalida_devuelve_error
test_schema_invalido_devuelve_error
test_tabla_inexistente_devuelve_error
test_error_de_lectura_pg_se_propaga
python/functions/datascience/summarize_table_pg_test.py python/functions/datascience/summarize_table_pg.py

Ejemplo

import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from datascience import summarize_table_pg

# Perfila la tabla `trends` del PostgreSQL del proyecto captacion_clientes
# (la misma base que alimenta Metabase).
res = summarize_table_pg(
    dsn="postgresql://captacion:secret@localhost:5433/trends",
    table="amazon_bestsellers",
    schema="public",
    high_card_ratio=0.9,
)

if res["status"] == "ok":
    p = res["profile"]
    print(f"{p['table']}: {p['n_rows']} filas x {p['n_cols']} cols  (source={p['source']})")
    print("type_breakdown:", p["type_breakdown"])
    for col in p["columns"]:
        print(col["name"], col["inferred_type"], "nulls=", col["null_pct"], col["flags"])
else:
    print("error:", res["error"])

Cuando usarla

  • Cuando hagas EDA de una tabla PostgreSQL que no conoces y necesites el esqueleto barato de su perfil (tipos inferidos, nulos, cardinalidad, flags) antes de gastar en estadistica fina. Tipico: las bases PostgreSQL conectadas a Metabase (trends, captacion_clientes, etc.).
  • Como adaptador PostgreSQL del grupo eda: produce el mismo TableProfile que summarize_table_duckdb, de modo que profile_table y el resto del grupo funcionan igual cambiando solo la fuente.
  • Cuando quieras perfilar tablas grandes sin traer filas a RAM: todo se calcula con agregados (count, count(DISTINCT), min/max/avg/stddev_samp, percentile_cont) que hacen push-down en el motor de PostgreSQL.

Gotchas

  • Impura: lee de un servidor PostgreSQL via pg_query (transaccion read-only, nunca escribe). Requiere psycopg2 (ya en python/.venv) y un DSN valido; un servidor inalcanzable devuelve {status:'error'} sin lanzar.
  • distinct_count exacto solo hasta 200000 filas: para n_rows <= 200000 se calcula count(DISTINCT col) EXACTO en la query agregada por columna. Por encima de ese umbral NO se estima (PostgreSQL no trae HyperLogLog de serie sin extension) y distinct_count se capa de forma conservadora a min(count_no_nulo, n_rows). En ambos casos unique_pct = min(distinct_count / n_rows, 1.0), asi que nunca excede 1.0. Por encima de 200k filas los flags possible_id / high_cardinality derivan de esa cota conservadora, no de un distinct real.
  • El shape es identico a summarize_table_duckdb (mismas claves de TableProfile y ColumnProfile, mismo sub-dict numeric) para que profile_table y el grupo eda lo consuman sin distinguir la fuente. source es "postgres" (vs "duckdb"). NO calcula skew, kurtosis, histograma, percentiles finos (p1/p5/p95/p99), moda, outliers, correlaciones, key_candidates ni quality_score: esas claves quedan en None/[] para otras funciones del grupo. El sub-dict numeric solo trae min, max, mean, std, p25, p50, p75 (estos tres ultimos via percentile_cont WITHIN GROUP).
  • Identificadores (schema/tabla/columna) se interpolan citados, no son parametrizables: por eso table y schema se validan contra ^[A-Za-z_][A-Za-z0-9_]*$ antes de citarlos con comillas dobles. Un nombre invalido (con ;, espacios, etc.) devuelve {status:'error'} sin tocar la base. Los valores (schema/table de la query a information_schema) si van por parametros posicionales %s.
  • count del ColumnProfile es el no-nulo (count(col)); null_count = n_rows - count. Una tabla con 0 filas devuelve perfiles con null_pct=0.0 y distinct_count=0.

Notas

Contrato compartido por todo el grupo eda (identico a summarize_table_duckdb, mantener estable):

TableProfile = {
  table, source, profiled_at, n_rows, n_cols, size_bytes, duplicate_rows,
  duplicate_pct, constant_cols:[str], all_null_cols:[str], null_cell_pct,
  type_breakdown:{numeric, categorical, datetime, text, boolean},
  columns:[ColumnProfile], correlations, key_candidates:[str], quality_score,
  llm, models
}
ColumnProfile = {
  name, physical_type, inferred_type, semantic_type, count, n_rows, null_count,
  null_pct, empty_count, empty_pct, distinct_count, unique_pct, flags:[str],
  quality_score, numeric:<sub>|None, categorical:<sub>|None, datetime:<sub>|None
}
numeric_sub = {
  min, max, mean, median, mode, std, variance, cv, p1, p5, p25, p50, p75, p95,
  p99, iqr, skew, kurtosis, n_outliers, outlier_pct, zero_pct, negative_pct,
  distribution_type, histogram
}

Mapeo de data_type (information_schema) PostgreSQL a inferred_type: smallint/integer/bigint/numeric/decimal/real/double precision/serial* -> numeric; date/time*/timestamp* -> datetime; boolean -> boolean; text/varchar/character* -> categorical si distinct_count <= 50 o distinct_count/n_rows < 0.5, si no text; el resto (json, jsonb, uuid, array, bytea, ...) -> text.

Flags por columna: constant (distinct_count<=1), possible_id (unique_pct>=0.99 y null_pct==0), high_cardinality (categorical con unique_pct>=high_card_ratio), mostly_null (null_pct>0.5).