---
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 (`<algo>_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.
