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