Ronda 4 (verificada con re-corrida sobre los datasets afectados): - H2: stl_decompose deriva periodo de la frecuencia del indice (seattle period=365 seasonal_strength=0.84; fin del period=2 espurio) - H3+H10: infer_fk por senal de nombre (<X>Id->X.<X>Id) + excluir no-clave -> chinook 111->9 FK, todas reales, cero absurdas, 16-27x mas rapido; base intacta (flag off->111) - H6: association no computa eta2 si cardinalidad~=n (Ticket-Fare espurio fuera) - H7: id secuencial monotono excluido de correlacion y PCA/KMeans (PassengerId fuera) - H8: correlacion de series no estacionarias marcada espuria / sobre retornos - H11: distribution_type usa modos/cardinalidad/normalidad (quality->discrete) - 66 tests verdes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 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 | |||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| infer_fk_containment_duckdb | function | py | datascience | 1.1.0 | impure | def infer_fk_containment_duckdb(db_path: str, tables: list = None, min_inclusion: float = 0.9, max_card: int = 200000, require_name_signal: bool = True) -> dict | Infiere FOREIGN KEYs candidatas entre tablas DuckDB combinando SEÑAL DE NOMBRE y containment de valores: exige primero que el origen nombre/contenga la tabla destino (patron <X>Id -> X.<X>Id / <x>_id -> x) y que el destino sea su PK nombrada, excluyendo PKs propias y columnas de medida como origen; luego confirma con inclusion(A subseteq B) = |distinct(A) interseccion distinct(B)| / |distinct(A)| >= min_inclusion y B key-ish (distinct/count >= 0.95). El filtro de nombre va ANTES del INTERSECT: mata el 10-20x de falsos positivos de la contencion pura y acelera (menos pares). Degrada a contencion pura si el esquema no usa convencion de nombres. Poda por tipo base y push-down SQL sin traer filas a RAM. Parte del grupo eda (relaciones inter-tabla). |
|
|
dict dict-no-throw. En exito {status:'ok', fk_candidates:[{from_table, from_col, to_table, to_col, inclusion, cardinality, to_is_key, name_match}, ...], tables:[str], skipped:[str], name_signal_enforced:bool} con fk_candidates ordenado por (name_match, inclusion) descendente; name_match indica si la candidata tiene señal de nombre; name_signal_enforced indica si el filtro de nombre se aplico (False si se degrado a contencion pura); cardinality es '1:1' (A casi unica en T1) o 'N:1' (A se repite, apunta a la key de T2). En error {status:'error', error:str}. |
|
false | error_go_core | true |
|
python/functions/datascience/infer_fk_containment_duckdb_test.py | python/functions/datascience/infer_fk_containment_duckdb.py |
Ejemplo
import sys, os, duckdb
sys.path.insert(0, os.path.join("python", "functions"))
from datascience import infer_fk_containment_duckdb
# Base de ejemplo en /tmp: orders.customer_id -> customers.id
path = "/tmp/fk_demo.duckdb"
if os.path.exists(path):
os.remove(path)
con = duckdb.connect(path)
con.execute("CREATE TABLE customers (id INTEGER, region VARCHAR)")
con.execute("INSERT INTO customers VALUES (1,'norte'),(2,'sur'),(3,'este'),(4,'oeste')")
con.execute("CREATE TABLE orders (order_id INTEGER, customer_id INTEGER, total DOUBLE)")
con.execute("INSERT INTO orders VALUES (10,1,99.5),(11,2,12.0),(12,1,45.25),(13,3,7.75),(14,4,60.0)")
con.close()
res = infer_fk_containment_duckdb(path, min_inclusion=0.9)
if res["status"] == "ok":
for fk in res["fk_candidates"]:
print(f"{fk['from_table']}.{fk['from_col']} -> "
f"{fk['to_table']}.{fk['to_col']} "
f"inclusion={fk['inclusion']:.2f} {fk['cardinality']}")
# -> orders.customer_id -> customers.id inclusion=1.00 N:1
else:
print("error:", res["error"])
Cuando usarla
- Cuando exploras un esquema DuckDB que no conoces y quieres descubrir el grafo de relaciones (que tabla referencia a cual) sin que la base haya declarado FKs.
- Como paso del grupo
edaque va mas alla del perfil por tabla (summarize_table_duckdb): aqui se modelan las relaciones INTER-tabla. - Antes de migrar un esquema sin constraints a otro motor (PostgreSQL, etc.) para proponer las FOREIGN KEYs que faltan.
- Para auditar integridad referencial: una inclusion < 1.0 en una FK que crees que deberia ser total indica valores huerfanos (filas de T1 cuyo valor no existe en la key de T2).
Gotchas
- Impura: lee de disco via las primitivas read-only del grupo
duckdb(no crea ni modifica la base). Eldb_pathdebe existir. - Coste O(pares podados): el numero de comparaciones es O(tablas^2 x columnas^2) ANTES de la poda. La poda por tipo base (solo se comparan columnas de la misma clase: ambos enteros, ambos varchar, ...) recorta drasticamente ese espacio, pero en esquemas con muchas tablas y columnas del mismo tipo puede seguir siendo costoso. Cada par evaluado dispara un
INTERSECTen el motor. INTERSECTpuede ser caro en tablas enormes: por esomax_card(default 200000) limita el lado destino. Sicount(T2) > max_card, los pares hacia T2 se saltan y se anota enskipped[]. Subemax_cardcon cuidado: el INTERSECT materializa los distintos de ambos lados.- Señal de nombre obligatoria por defecto (
require_name_signal=True): para emitir una candidata, el origen debe NOMBRAR o CONTENER la tabla destino (AlbumId -> Album,customer_id -> customers,manager_staff_id -> staff) y el destino debe ser su PK nombrada (oid). Esto mata el grueso de falsos de la contencion pura (sin el filtro, chinook daba 111 candidatas vs 9 reales; sakila 565 vs ~21). Limite: una FK con nombre divergente que no contiene la tabla destino (p.ej.Customer.SupportRepId -> Employee.EmployeeId) NO es alcanzable por nombre y se pierde; y las self-FK (Employee.ReportsTo -> Employee) nunca se infieren (la funcion exige T1 != T2). Si tu esquema usa convencion de nombres pero tiene FK con columnas que no terminan enid, esas tambien se pierden en modo enforce. - Degrada a contencion pura sin convencion de nombres: si NINGUNA columna del esquema termina en
id, no se exige señal y se vuelve al comportamiento historico de solo-contencion (name_signal_enforced=Falseen el retorno). Tambien puedes forzarlo conrequire_name_signal=False. - Containment != FK declarada: que A este contenido en B (con B key-ish) es una FK probable, no una garantia. Una columna puede estar contenida por coincidencia (rangos pequenos de enteros, banderas, fechas solapadas) sin ser una relacion real. Revisa siempre las candidatas; trata
inclusionycardinalitycomo senales, no como verdad. - Entero y float NO se mezclan: la poda por tipo pone INTEGER/BIGINT/... en la clase
integery FLOAT/DOUBLE/DECIMAL enfloat, y solo empareja columnas de la misma clase. Una FK entera contra una columna float casi nunca es real, asi que se descarta de entrada. - Solo esquema
maincuandotables=None: hereda el alcance deduckdb_list_tables(esquemamain). - Identificadores interpolados: nombres de tabla/columna se validan contra
^[A-Za-z_][A-Za-z0-9_]*$y se citan (COUNT DISTINCT / INTERSECT no admiten parametros posicionales para identificadores). Una tabla con nombre invalido devuelve{status:'error'}; una columna con nombre invalido se ignora sin abortar. - Direccion: cada candidata es A -> B (A es la FK, B es la key referenciada). El par inverso (B -> A) se evalua por separado y normalmente no pasa el filtro de inclusion o el de key.
Notas
Definicion de containment usada:
inclusion(A subseteq B) = |distinct(A) interseccion distinct(B)| / |distinct(A)|
Criterio de emision de FK candidata A (de T1) -> B (de T2):
- T1 != T2 y
type_class(A) == type_class(B)(poda por clase de tipo base). count(T2) <= max_card(si no, los pares hacia T2 se saltan ->skipped[]).distinct(A) > 0.- B es key-ish:
distinct(B) / count(T2) >= 0.95. inclusion(A subseteq B) >= min_inclusion.
Cardinalidad: si A es (casi) unica en T1 (distinct(A) / count(T1) >= 0.95) ->
1:1; si no -> N:1 (A se repite y apunta a la key de T2).
Todo se calcula con push-down (COUNT(DISTINCT), INTERSECT) — nunca se traen
filas a RAM. Los count(*) por tabla y los distinct por columna se cachean para
no recomputarlos entre pares.
fk_candidate = {
from_table, from_col, to_table, to_col, inclusion, cardinality, to_is_key,
name_match
}
Antes del criterio de containment (pasos 1-5), cuando require_name_signal=True y
el esquema usa convencion de nombres, se aplica un filtro de SEÑAL DE NOMBRE que
recorta los pares evaluados (por eso baja tambien el coste, issue H10):
0a. El origen no puede ser la PK de su propia tabla, detectada SOLO por NOMBRE: el
stem de la columna casa con el nombre de la tabla (Genre.GenreId, film.film_id)
o es el generico id. NO se usa la PRIMARY KEY declarada — asi funciona sobre
tablas materializadas con CREATE TABLE AS (sin PK), donde Track.AlbumId
(stem 'album' != tabla 'track') NO es PK propia y se conserva como FK.
0b. El destino debe ser la PK nombrada de su tabla: to_col nombra to_table
(store_id en store) o es el generico id.
0c. El origen debe nombrar la tabla destino: su stem casa con to_table
(<X>Id -> X) o la contiene como subcadena (manager_staff_id -> staff).
Capability growth log
- v1.1.0 (2026-06-29) — añade
require_name_signal(default True): filtro de señal de nombre ANTES del containment. Corrige falsos positivos masivos de la inferencia por sola contencion (chinook 111->9, sakila 565->21) y acelera (chinook 6.8s->0.4s, sakila 23.4s->0.9s). Issues H3 + H10 del benchmark EDA. Retrocompatible: degrada a contencion pura si el esquema no usa convencion de nombres de clave. Nuevos campos en el retorno:name_matchpor candidata yname_signal_enforced.