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>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
---
|
||||
id: build_column_dictionary_py_datascience
|
||||
name: build_column_dictionary
|
||||
kind: function
|
||||
lang: py
|
||||
domain: datascience
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def build_column_dictionary(db_profile: dict) -> dict"
|
||||
description: "Construye el diccionario de columnas BUSCABLE de una base entera a partir del DatabaseProfile que emite profile_database (grupo eda). Aplana db_profile['table_profiles'] (lista de TableProfile con table y columns) en una entrada por columna con tabla, tipo inferido, tipo semantico, marca de PII (RGPD/LOPDGDD), %null, cardinalidad y valores top. Responde a nivel de base 'donde esta el customer_id / telefono / IBAN'. Emite tambien pii_columns y un markdown grep-able ordenado por columna, precedido de las columnas compartidas por nombre entre tablas (candidatas a join key cross-tabla). Funcion pura, dict-no-throw, no muta el input."
|
||||
tags: [eda, relations]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
example: |
|
||||
from datascience import build_column_dictionary
|
||||
db_profile = {"table_profiles": [
|
||||
{"table": "clientes", "columns": [
|
||||
{"name": "email", "inferred_type": "text", "semantic_type": "email",
|
||||
"null_pct": 0.05, "distinct_count": 990}]}]}
|
||||
res = build_column_dictionary(db_profile)
|
||||
# res["pii_columns"] -> [{"table": "clientes", "column": "email", "is_pii": True, ...}]
|
||||
tested: true
|
||||
tests:
|
||||
- "test_golden_flattens_two_tables"
|
||||
- "test_pii_flagged_from_semantic_type"
|
||||
- "test_empty_semantic_type_maps_to_none_and_not_pii"
|
||||
- "test_shared_column_names_detected_as_join_keys"
|
||||
- "test_top_values_from_categorical_block"
|
||||
- "test_empty_profile_returns_empty_ok"
|
||||
- "test_malformed_input_returns_empty_ok"
|
||||
- "test_missing_keys_read_defensively"
|
||||
- "test_does_not_mutate_input"
|
||||
test_file_path: "python/functions/datascience/build_column_dictionary_test.py"
|
||||
file_path: "python/functions/datascience/build_column_dictionary.py"
|
||||
params:
|
||||
- name: db_profile
|
||||
desc: >
|
||||
DatabaseProfile del grupo eda tal como lo devuelve profile_database en su
|
||||
clave db_profile (el dict con table_profiles). table_profiles es una lista
|
||||
de TableProfile; de cada uno se leen table (nombre) y columns (lista de
|
||||
ColumnProfile). De cada ColumnProfile se leen defensivamente con .get(...):
|
||||
name, inferred_type (numeric|categorical|datetime|text|boolean),
|
||||
semantic_type ("" que se normaliza a None; los que emite infer_semantic_type:
|
||||
email, iban, credit_card, phone_intl, postal_code_es, ...), null_pct
|
||||
(fraccion 0-1), distinct_count (cardinalidad, expuesta como n_distinct) y el
|
||||
bloque categorical.top (para top_values). Una entrada vacia, None o
|
||||
malformada produce el resultado vacio en estado ok (nunca lanza).
|
||||
output: >
|
||||
dict dict-no-throw con status ("ok" siempre), n_tables (int, tablas con columnas
|
||||
procesadas), n_columns (int total de columnas), entries (list[dict] una por
|
||||
columna con table, column, inferred_type, semantic_type|None, is_pii (bool),
|
||||
null_pct (float 0-1|None), n_distinct (int|None), top_values (list[str]|None)),
|
||||
pii_columns (subconjunto de entries con is_pii=True: dato personal segun
|
||||
[POL-MMNSEG-001-1.0]) y markdown (str, tabla grep-able ordenada por nombre de
|
||||
columna precedida de las columnas compartidas por nombre entre tablas). Entrada
|
||||
vacia o malformada -> n_tables/n_columns 0, listas vacias, markdown "".
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
from datascience import build_column_dictionary
|
||||
|
||||
# db_profile minimo de juguete (forma de la clave db_profile de profile_database).
|
||||
db_profile = {
|
||||
"table_profiles": [
|
||||
{
|
||||
"table": "clientes",
|
||||
"columns": [
|
||||
{"name": "customer_id", "inferred_type": "numeric",
|
||||
"semantic_type": "", "null_pct": 0.0, "distinct_count": 1000},
|
||||
{"name": "email", "inferred_type": "text",
|
||||
"semantic_type": "email", "null_pct": 0.05, "distinct_count": 990},
|
||||
{"name": "ciudad", "inferred_type": "categorical",
|
||||
"semantic_type": "", "null_pct": 0.0, "distinct_count": 3,
|
||||
"categorical": {"top": [
|
||||
{"value": "Madrid", "count": 5, "pct": 0.5},
|
||||
{"value": "Bilbao", "count": 3, "pct": 0.3}]}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"table": "pedidos",
|
||||
"columns": [
|
||||
{"name": "customer_id", "inferred_type": "numeric",
|
||||
"semantic_type": "", "null_pct": 0.0, "distinct_count": 800},
|
||||
{"name": "iban", "inferred_type": "text",
|
||||
"semantic_type": "iban", "null_pct": 0.1, "distinct_count": 795},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
res = build_column_dictionary(db_profile)
|
||||
print(res["n_tables"], res["n_columns"]) # 2 5
|
||||
print([(e["table"], e["column"]) for e in res["pii_columns"]])
|
||||
# [('clientes', 'email'), ('pedidos', 'iban')]
|
||||
print(res["markdown"]) # tabla grep-able + seccion de join keys (customer_id)
|
||||
```
|
||||
|
||||
Uso real componiendo con `profile_database` (perfila la base y construye el diccionario):
|
||||
|
||||
```python
|
||||
from pipelines.profile_database import profile_database
|
||||
from datascience import build_column_dictionary
|
||||
|
||||
r = profile_database("mi_base.duckdb", write_report=False)
|
||||
if r["status"] == "ok":
|
||||
dicc = build_column_dictionary(r["db_profile"])
|
||||
# grep sobre dicc["markdown"] para localizar donde vive cada dato,
|
||||
# dicc["pii_columns"] para el inventario RGPD de la base.
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Usala cuando necesites un indice tabla.columna de una base ENTERA: para localizar
|
||||
por busqueda "donde esta el customer_id / telefono / IBAN" antes de escribir un
|
||||
join, para descubrir claves de join cross-tabla (columnas con el mismo nombre en
|
||||
varias tablas) o para levantar un inventario de columnas con datos personales
|
||||
(RGPD/LOPDGDD) sobre el que auditar. Es el paso natural despues de
|
||||
`profile_database`: toma su `db_profile` y lo convierte en diccionario buscable.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El criterio de PII se basa SOLO en el `semantic_type` que hoy emite el grupo
|
||||
`eda` (`infer_semantic_type`): se marcan email, phone_intl, iban, credit_card y
|
||||
postal_code_es. El catalogo de regex NO detecta hoy nombre de persona ni DNI/NIE,
|
||||
asi que esas columnas caen como texto/categorico y NO se marcan automaticamente.
|
||||
Politica [POL-MMNSEG-001-1.0]: ante cualquier duda sobre si una columna contiene
|
||||
datos personales, tratala como PII y avisa antes de exponerla; `pii_columns` es
|
||||
una ayuda, no un inventario RGPD exhaustivo.
|
||||
- `n_distinct` se lee de la clave `distinct_count` del ColumnProfile (no de
|
||||
`categorical.n_distinct`); en tablas grandes puede venir de `approx_unique`
|
||||
(HyperLogLog) capado a n_rows, no exacto.
|
||||
- `top_values` solo se rellena si la columna trae bloque `categorical` (lo pone
|
||||
`profile_table` para columnas categorical/text); las numericas/datetime lo
|
||||
dejan en None.
|
||||
- Funcion pura: no toca disco ni muta el input. NO perfila la base — eso lo hace
|
||||
`profile_database`; aqui solo se APLANA su salida.
|
||||
Reference in New Issue
Block a user