fix(eda): bugs de bajo riesgo del benchmark (H1,H5,H12,H13,H14) + tests faltantes
- H1: render_eda_markdown ya no aplica doble x100 a outlier_pct (336% -> real) - H5: profile_database filtra base_tables_only (excluye VIEWs; sakila 21->16) - H12: suggest_reexpression salta columnas no-continuas - H13: to_returns/profile_table elige retornos (financiera) vs diferencias (fisica) - H14: test de regresion ATTACH sqlite via information_schema - +8 tests de las funciones eda nuevas (acf_pacf, adf_kpss, ...). 77 tests verdes - L/M (H2,H3,H4,H6,H7,H8,H9,H10,H11) quedan en issues 0174-0177 para revision Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,57 @@ _DATETIME_SEMANTIC = ("datetime_iso", "date_eu")
|
||||
# promocion a numeric (evita promocionar columnas mayormente no parseables).
|
||||
_PROMOTE_MIN_PARSE = 0.8
|
||||
|
||||
# Cardinalidad maxima (distinct_count) por debajo de la cual una columna numerica
|
||||
# se trata como NO continua (binaria / ordinal de pocos niveles) y, por tanto, no
|
||||
# es candidata a re-expresion de Tukey (la escalera de potencias no aplica a una
|
||||
# variable con pocos niveles discretos).
|
||||
_REEXPR_MIN_DISTINCT = 12
|
||||
|
||||
# Tokens en el nombre (o semantic_type currency) que sugieren que una serie de
|
||||
# niveles es FINANCIERA (precios/volumen): en ese caso la transformacion adecuada
|
||||
# son los retornos. Para magnitudes fisicas (temperatura, caudal) la transformacion
|
||||
# correcta son las diferencias, no los retornos.
|
||||
_FINANCIAL_TOKENS = (
|
||||
"price", "close", "open", "high", "low", "volume", "adj", "vwap",
|
||||
"bid", "ask", "return", "precio", "cierre", "apertura", "cotiz", "retorno",
|
||||
)
|
||||
|
||||
|
||||
def _is_continuous_for_reexpr(col: dict, vals_float: list) -> bool:
|
||||
"""True si la columna numerica es continua y justifica sugerir re-expresion.
|
||||
|
||||
Se saltan (devuelve False):
|
||||
- binarias / ordinales de baja cardinalidad (``distinct_count`` <= umbral):
|
||||
la escalera de potencias de Tukey no tiene sentido sobre pocos niveles
|
||||
discretos (p.ej. ``Survived`` 0/1, ``Pclass`` 1/2/3).
|
||||
- identificadores enteros (flag ``possible_id`` y todos los valores enteros):
|
||||
re-expresar un id (p.ej. ``PassengerId`` 1..n) no aporta nada.
|
||||
Los floats continuos de alta cardinalidad (precios, medidas) NO se saltan
|
||||
aunque lleven ``possible_id``, porque tienen parte decimal (no son enteros).
|
||||
"""
|
||||
dc = col.get("distinct_count")
|
||||
if isinstance(dc, int) and not isinstance(dc, bool) and dc <= _REEXPR_MIN_DISTINCT:
|
||||
return False
|
||||
flags = col.get("flags") or []
|
||||
if "possible_id" in flags and vals_float and all(
|
||||
float(f).is_integer() for f in vals_float
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _looks_financial(col: dict) -> bool:
|
||||
"""True si la columna parece una serie financiera (precio/volumen/divisa).
|
||||
|
||||
Heuristica por nombre (tokens OHLCV típicos) o ``semantic_type == currency``.
|
||||
Decide si una serie de niveles se debe transformar a retornos (financiera) o a
|
||||
diferencias (no financiera, p.ej. temperatura).
|
||||
"""
|
||||
name = (col.get("name") or "").lower()
|
||||
if any(tok in name for tok in _FINANCIAL_TOKENS):
|
||||
return True
|
||||
return (col.get("semantic_type") or "").lower() == "currency"
|
||||
|
||||
|
||||
def _to_float(value):
|
||||
"""Parsea un valor a float limpiando simbolos de moneda y separadores.
|
||||
@@ -175,8 +226,12 @@ def _build_series_block(query_fn, table: str, col: dict, order_col, sample: int)
|
||||
"stl": stl_decompose(series_vals),
|
||||
}
|
||||
|
||||
# Sugerencia de retornos solo si la columna parece de niveles: estrictamente
|
||||
# positiva y con veredicto de estacionariedad NO confirmado.
|
||||
# Sugerencia de transformacion solo si la columna parece de niveles:
|
||||
# estrictamente positiva y con veredicto de estacionariedad NO confirmado.
|
||||
# La transformacion adecuada depende de la SEMANTICA: retornos para series
|
||||
# financieras (precios/volumen), diferencias para magnitudes fisicas
|
||||
# (temperatura, caudal). Aplicar "retornos" a una temperatura no tiene sentido
|
||||
# fisico; la primera diferencia si la estaciona.
|
||||
nb = col.get("numeric") or {}
|
||||
minimum = nb.get("min")
|
||||
verdict = (block["stationarity"] or {}).get("verdict")
|
||||
@@ -186,13 +241,22 @@ def _build_series_block(query_fn, table: str, col: dict, order_col, sample: int)
|
||||
and minimum > 0
|
||||
and verdict in ("non_stationary", "inconclusive")
|
||||
):
|
||||
block["to_returns"] = to_returns(series_vals, method="log")
|
||||
block["levels_suggested"] = True
|
||||
block["levels_reason"] = (
|
||||
"columna estrictamente positiva y no claramente estacionaria: parece una "
|
||||
"serie de niveles (precios); trabajar sobre retornos evita correlacion "
|
||||
"espuria (Granger-Newbold)."
|
||||
)
|
||||
if _looks_financial(col):
|
||||
block["levels_kind"] = "returns"
|
||||
block["to_returns"] = to_returns(series_vals, method="log")
|
||||
block["levels_reason"] = (
|
||||
"columna financiera estrictamente positiva y no claramente "
|
||||
"estacionaria (serie de niveles/precios): trabajar sobre retornos "
|
||||
"evita correlacion espuria (Granger-Newbold)."
|
||||
)
|
||||
else:
|
||||
block["levels_kind"] = "differences"
|
||||
block["levels_reason"] = (
|
||||
"serie de niveles no financiera y no claramente estacionaria: la "
|
||||
"primera diferencia la estaciona; los retornos no tienen sentido en "
|
||||
"magnitudes fisicas (p.ej. temperatura)."
|
||||
)
|
||||
else:
|
||||
block["levels_suggested"] = False
|
||||
|
||||
@@ -296,8 +360,11 @@ def profile_table(
|
||||
vals_float = [f for f in (_to_float(v) for v in vals) if f is not None]
|
||||
col["numeric"] = describe_numeric(vals_float)
|
||||
# Re-expresion sugerida (escalera de Tukey): que transformacion
|
||||
# simetriza mejor la columna a partir de su skew/dominio.
|
||||
col["reexpression"] = suggest_reexpression(col["numeric"])
|
||||
# simetriza mejor la columna a partir de su skew/dominio. Solo para
|
||||
# columnas CONTINUAS: no aplica a binarias/ordinales de baja
|
||||
# cardinalidad ni a identificadores enteros (la fila seria ruido).
|
||||
if _is_continuous_for_reexpr(col, vals_float):
|
||||
col["reexpression"] = suggest_reexpression(col["numeric"])
|
||||
elif inferred in ("categorical", "text"):
|
||||
col["categorical"] = summarize_categorical(vals)
|
||||
# Para columnas no promovidas que ya eran categorical/text y no
|
||||
|
||||
Reference in New Issue
Block a user