--- name: suggest_intratable_fk_candidates kind: function lang: py domain: datascience version: "1.0.0" purity: pure signature: "def suggest_intratable_fk_candidates(profile: dict, max_candidates: int = 20) -> list" description: "Sobre el TableProfile de UNA tabla (el dict de profile_table), sugiere por heuristica de nombre + cardinalidad que columnas PARECEN una clave foranea hacia otra tabla, cuando no hay relaciones inter-tabla que medir (una sola tabla). Es una SUGERENCIA, no una afirmacion: el ref_table_guess es el stem del nombre (customer_id -> customer) y NO confirma containment. Pura: solo lee el dict, sin I/O; nunca lanza (devuelve [])." tags: [eda, datascience, relationships, foreign-key, fk, heuristic, schema, python] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "" imports: [] params: - name: profile desc: "TableProfile (dict que produce profile_table / summarize_table_*). Se leen de forma defensiva `columns` (lista de ColumnProfile con name/inferred_type/physical_type/distinct_count/unique_pct/flags), `n_rows` (int) y `key_candidates` (lista de nombres de columna ya candidatos a PK, que se excluyen). Si no es dict o no trae columns -> []." - name: max_candidates desc: "Tope de sugerencias devueltas (default 20). Las columnas candidatas se ordenan por distinct_count descendente (mas informativas primero) antes de cortar a este maximo." output: "list (posiblemente vacia) de dicts, uno por columna sugerida, con claves: `column` (nombre), `ref_table_guess` (tabla conjeturada por el stem del nombre, p.ej. customer_id -> 'customer'), `reason` (frase humana que deja claro que es heuristica sin confirmar containment), `distinct_count` (int|None), `unique_pct` (float|None, fraccion 0-1 tal como viene del profile), `inferred_type` (str), `physical_type` (str). Nunca lanza." tested: true tests: ["test_golden_customer_id_detectado_otras_no", "test_camelcase_albumid_detectado", "test_constante_status_id_no_aparece", "test_profile_vacio_y_none_devuelven_lista_vacia", "test_category_id_casi_unico_parece_pk_no_aparece", "test_ref_table_guess_multitoken_y_orden_por_distinct", "test_max_candidates_corta_la_lista", "test_id_generico_solo_nunca_es_fk"] test_file_path: "python/functions/datascience/suggest_intratable_fk_candidates_test.py" file_path: "python/functions/datascience/suggest_intratable_fk_candidates.py" --- ## Ejemplo ```python from datascience import suggest_intratable_fk_candidates # TableProfile de UNA tabla (tipo titanic): customer_id es FK N:1; id es la PK; # amount es una medida float; name es categorica sin sufijo de id. profile = { "n_rows": 891, "key_candidates": ["id"], "columns": [ {"name": "id", "inferred_type": "numeric", "physical_type": "BIGINT", "distinct_count": 891, "unique_pct": 1.0, "flags": ["possible_id"]}, {"name": "customer_id", "inferred_type": "numeric", "physical_type": "BIGINT", "distinct_count": 137, "unique_pct": 0.15, "flags": []}, {"name": "amount", "inferred_type": "numeric", "physical_type": "DOUBLE", "distinct_count": 400, "unique_pct": 0.45, "flags": []}, {"name": "name", "inferred_type": "categorical", "physical_type": "VARCHAR", "distinct_count": 700, "unique_pct": 0.78, "flags": []}, ], } out = suggest_intratable_fk_candidates(profile) [c["column"] for c in out] # -> ["customer_id"] out[0]["ref_table_guess"] # -> "customer" out[0]["reason"] # -> "el nombre termina en '_id' y es N:1 (137 valores distintos < 891 filas): # parece (heuristica por nombre, sin confirmar containment) una referencia a # una tabla «customer»" ``` ## Cuando usarla Cuando el EDA tiene SOLO UNA tabla y, por tanto, no se puede inferir una FK inter-tabla por containment (no hay otra tabla cuyos valores contener). Es el plan B del capitulo RELACIONES de AutomaticEDA: en vez de medir solapamiento de valores entre tablas (lo correcto cuando hay varias, ver `infer_fk_containment_duckdb` / `build_join_graph`), conjetura por el NOMBRE de la columna (`_id`) y por su CARDINALIDAD N:1 que columnas parecen apuntar a una entidad externa. Usala para enriquecer el reporte con "estas columnas parecen referencias a otras tablas" sin prometer que esa tabla exista. NO la uses si tienes varias tablas: ahi mide containment de verdad. ## Gotchas - Es **heuristica**, no una verdad: produce **falsos positivos** (una columna `period_id` que en realidad es un codigo libre, no una FK) y **falsos negativos** (una FK que no se llama `*_id`, p.ej. `parent`, `owner`, `sku`). No la trates como una afirmacion de esquema. - `ref_table_guess` es una **conjetura por el nombre** (el stem sin el sufijo id): `customer_id` -> `customer`, `AlbumId` -> `album`, `manager_staff_id` -> `manager_staff`. Puede no coincidir con el nombre real de la tabla (plurales, prefijos, alias). Es una pista, no un join garantizado. - **NO confirma containment**: no comprueba que los valores de la columna existan en ninguna otra tabla (no puede — solo recibe el perfil de una tabla). Para confirmar una FK real con varias tablas usa `infer_fk_containment_duckdb`. - Excluye deliberadamente: el `id`/`Id`/`ID` generico a secas (suele ser la PK propia, no una referencia), las columnas constantes, las que parecen unicas (`unique_pct >= 0.99`, mas PK que FK) y los tipos no-clave (float/decimal son medidas; date/time/timestamp y boolean no son claves). En camelCase, `paid`, `valid`, `grid` (con `id` en minuscula y sin separador) NO se confunden con FK. - `unique_pct` se interpreta como **fraccion 0-1** (tal como la emite el profile), no como porcentaje 0-100.