7ac69ab4fb
Bloque del grupo eda (sesion ausente EDA-benchmark): - 8 funciones nuevas: adf_kpss_stationarity, acf_pacf, stl_decompose, to_returns, fdr_correction, suggest_reexpression, exploratory_caveats, render_eda_pdf - integracion: profile_table (run_series, emit_pdf), association_matrix (FDR Benjamini-Hochberg), render_eda_markdown (secciones series/reexpresion/caveats) - slash commands /eda y /capitulos - issues 0173-0177: mejoras del /eda derivadas del benchmark sobre 12 datasets reales (outlier_pct x100, periodo estacional, FK inference, render models, tipos id-like) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
247 lines
10 KiB
Python
247 lines
10 KiB
Python
"""Genera las advertencias que recuerdan que un EDA es EXPLORATORIO, no confirmatorio.
|
|
|
|
Funcion pura y determinista: dict (TableProfile del grupo ``eda``) entra, dict con
|
|
una lista de caveats sale. No hace I/O, no muta el input, no lanza excepciones.
|
|
|
|
Doctrina (Tukey, *EDA* 1977; Aronson; López de Prado 2018): el análisis exploratorio
|
|
sirve para GENERAR hipótesis, no para confirmarlas. Lo que se ve mirando todo el
|
|
dataset a la vez —correlaciones, clusters, "significancias", outliers— es un punto de
|
|
partida, no una conclusión: hay que validarlo fuera de muestra con un análisis dirigido.
|
|
Esta función inspecciona qué contiene el perfil y devuelve solo las advertencias que
|
|
aplican a lo que realmente se ha calculado (si hay correlaciones → caveat de
|
|
causalidad; si hay modelos → caveat de overfitting; etc.), además de una advertencia
|
|
general que siempre acompaña a un EDA.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
# Umbrales para disparar caveats dependientes de magnitud.
|
|
_SMALL_SAMPLE_ROWS = 30 # n_rows por debajo de esto -> baja potencia.
|
|
_HIGH_MISSING_FRACTION = 0.2 # null_cell_pct (fracción) por encima -> sesgo MNAR.
|
|
|
|
|
|
def _to_float(v):
|
|
"""Parsea a float; None si es None/bool/no parseable (NaN incluido)."""
|
|
if v is None or isinstance(v, bool):
|
|
return None
|
|
try:
|
|
f = float(v)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
if f != f: # NaN
|
|
return None
|
|
return f
|
|
|
|
|
|
def _correlation_pairs(profile: dict) -> list:
|
|
"""Extrae la lista de pares de correlación del perfil, tolerando varios shapes.
|
|
|
|
``correlations`` puede ser una lista de pares o un dict con ``pairs`` /
|
|
``strongest``. Devuelve siempre una lista (vacía si no hay nada usable).
|
|
"""
|
|
correlations = profile.get("correlations")
|
|
if not correlations:
|
|
return []
|
|
if isinstance(correlations, dict):
|
|
pairs = correlations.get("pairs") or correlations.get("strongest") or []
|
|
else:
|
|
pairs = correlations
|
|
return list(pairs) if isinstance(pairs, (list, tuple)) else []
|
|
|
|
|
|
def _has_models(profile: dict) -> bool:
|
|
"""True si el perfil contiene un bloque de modelos multivariantes ajustados."""
|
|
models = profile.get("models")
|
|
if not isinstance(models, dict):
|
|
return False
|
|
return any(models.get(k) for k in ("pca", "kmeans", "outliers"))
|
|
|
|
|
|
def _has_pvalues(profile: dict) -> bool:
|
|
"""True si el perfil contiene p-values (tests de normalidad o de tendencia)."""
|
|
models = profile.get("models")
|
|
if isinstance(models, dict) and models.get("normality"):
|
|
return True
|
|
# Tests de tendencia adjuntados por columna (trend_slope) también traen p-value.
|
|
for col in profile.get("columns") or []:
|
|
if isinstance(col, dict) and isinstance(col.get("trend"), dict):
|
|
if col["trend"].get("p_value") is not None:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _has_outliers(profile: dict) -> bool:
|
|
"""True si se han detectado outliers (multivariantes o por columna numérica)."""
|
|
models = profile.get("models")
|
|
if isinstance(models, dict) and models.get("outliers"):
|
|
return True
|
|
for col in profile.get("columns") or []:
|
|
if not isinstance(col, dict):
|
|
continue
|
|
num = col.get("numeric")
|
|
if isinstance(num, dict):
|
|
n_out = _to_float(num.get("n_outliers"))
|
|
opct = _to_float(num.get("outlier_pct"))
|
|
if (n_out is not None and n_out > 0) or (opct is not None and opct > 0):
|
|
return True
|
|
return False
|
|
|
|
|
|
def exploratory_caveats(profile: dict) -> dict:
|
|
"""Devuelve las advertencias de que el EDA es exploratorio según lo que contiene.
|
|
|
|
Inspecciona un TableProfile (dict del grupo ``eda``) y arma la lista de caveats
|
|
relevantes. Una advertencia general (la naturaleza exploratoria del EDA) se
|
|
incluye SIEMPRE; el resto solo se añaden cuando el perfil contiene aquello a lo
|
|
que aplican:
|
|
|
|
- correlaciones presentes -> correlación ≠ causalidad.
|
|
- modelos / correlaciones -> riesgo de overfitting in-sample (validar OOS).
|
|
- p-values (normalidad/tendencia) -> no son confirmación sin corregir / IID.
|
|
- ≥2 pares de correlación -> comparaciones múltiples (falsos positivos).
|
|
- outliers detectados -> no implican errores.
|
|
- n_rows pequeño -> baja potencia, estimaciones inestables.
|
|
- muchos faltantes -> posible sesgo si no son aleatorios (MNAR).
|
|
|
|
Es pura, determinista y no lanza excepciones. Un perfil vacío o ``None`` devuelve
|
|
solo el caveat general con una nota.
|
|
|
|
Args:
|
|
profile: TableProfile dict del grupo ``eda``. Se lee todo defensivamente con
|
|
``.get(...)`` porque casi cualquier fase puede faltar.
|
|
|
|
Returns:
|
|
dict con:
|
|
- ``n``: número de caveats devueltos (int).
|
|
- ``caveats``: lista de dicts ``{"id", "topic", "message", "reference"}``,
|
|
empezando por el general ``exploratory_nature``.
|
|
- ``note``: cadena vacía en el caso normal; mensaje cuando el perfil está
|
|
vacío y solo se devuelve la advertencia general.
|
|
"""
|
|
if not isinstance(profile, dict):
|
|
profile = {}
|
|
|
|
caveats: list = []
|
|
|
|
# Caveat general: SIEMPRE presente. El EDA genera hipótesis, no conclusiones.
|
|
caveats.append({
|
|
"id": "exploratory_nature",
|
|
"topic": "naturaleza exploratoria",
|
|
"message": (
|
|
"El EDA genera HIPÓTESIS, no conclusiones. Cada patrón que veas aquí es un "
|
|
"punto de partida para confirmarlo con un análisis dirigido sobre datos "
|
|
"nuevos, no una verdad ya establecida."
|
|
),
|
|
"reference": "Tukey (1977), Exploratory Data Analysis; Aronson",
|
|
})
|
|
|
|
if not profile:
|
|
return {
|
|
"n": len(caveats),
|
|
"caveats": caveats,
|
|
"note": "perfil vacío: solo se devuelve la advertencia general",
|
|
}
|
|
|
|
corr_pairs = _correlation_pairs(profile)
|
|
has_corr = len(corr_pairs) > 0
|
|
has_models = _has_models(profile)
|
|
|
|
# Correlación ≠ causalidad.
|
|
if has_corr:
|
|
caveats.append({
|
|
"id": "correlation_not_causation",
|
|
"topic": "correlación vs causalidad",
|
|
"message": (
|
|
"Las correlaciones son asociaciones, no relaciones causales. Una "
|
|
"correlación fuerte puede venir de una variable de confusión o del "
|
|
"azar; valídala out-of-sample o con un diseño experimental antes de "
|
|
"actuar sobre ella."
|
|
),
|
|
"reference": "Tukey (1977), EDA",
|
|
})
|
|
|
|
# Overfitting in-sample: cualquier patrón ajustado sobre todo el dataset.
|
|
if has_models or has_corr:
|
|
caveats.append({
|
|
"id": "in_sample_overfitting",
|
|
"topic": "overfitting in-sample",
|
|
"message": (
|
|
"Los patrones (modelos, clusters, correlaciones) se han extraído sobre "
|
|
"TODO el dataset. Lo aprendido in-sample puede no replicar fuera de "
|
|
"muestra (overfitting / selección por backtest). Valida con holdout o "
|
|
"walk-forward antes de confiar en ellos."
|
|
),
|
|
"reference": "López de Prado (2018), Advances in Financial Machine Learning",
|
|
})
|
|
|
|
# p-values: no son confirmación sin corregir multiplicidad / sobre datos no-IID.
|
|
if _has_pvalues(profile):
|
|
caveats.append({
|
|
"id": "p_values_not_confirmation",
|
|
"topic": "p-values",
|
|
"message": (
|
|
"Los p-values sin corregir por comparaciones múltiples, o calculados "
|
|
"sobre datos no-IID (series temporales, datos agrupados), no son "
|
|
"confirmación. Trata cualquier 'significancia' vista en exploración "
|
|
"como provisional."
|
|
),
|
|
"reference": "Tukey (1977), EDA",
|
|
})
|
|
|
|
# Comparaciones múltiples: cuantos más pares/columnas miras, más falsos positivos.
|
|
if len(corr_pairs) >= 2:
|
|
caveats.append({
|
|
"id": "multiple_comparisons",
|
|
"topic": "comparaciones múltiples",
|
|
"message": (
|
|
"Al examinar muchos pares/columnas a la vez, algunos parecerán "
|
|
"'significativos' solo por azar (problema de comparaciones múltiples). "
|
|
"Cuantas más combinaciones miras, más falsos positivos esperas."
|
|
),
|
|
"reference": "López de Prado (2018), AFML",
|
|
})
|
|
|
|
# Outliers detectados no implican errores.
|
|
if _has_outliers(profile):
|
|
caveats.append({
|
|
"id": "outliers_not_errors",
|
|
"topic": "outliers",
|
|
"message": (
|
|
"Los outliers detectados son puntos estadísticamente atípicos, NO "
|
|
"necesariamente errores. Pueden ser el dato más interesante (fraude, "
|
|
"evento raro). Investígalos antes de eliminarlos."
|
|
),
|
|
"reference": "Tukey (1977), EDA",
|
|
})
|
|
|
|
# Muestra pequeña: baja potencia, estimaciones inestables.
|
|
n_rows = _to_float(profile.get("n_rows"))
|
|
if n_rows is not None and n_rows < _SMALL_SAMPLE_ROWS:
|
|
caveats.append({
|
|
"id": "small_sample",
|
|
"topic": "muestra pequeña",
|
|
"message": (
|
|
f"Pocas filas (n={int(n_rows)}): la potencia estadística es baja y las "
|
|
"estimaciones (media, correlación, forma de la distribución) son "
|
|
"inestables. Los patrones pueden cambiar con más datos."
|
|
),
|
|
"reference": "Tukey (1977), EDA",
|
|
})
|
|
|
|
# Datos faltantes: posible sesgo si no son aleatorios (MNAR).
|
|
null_frac = _to_float(profile.get("null_cell_pct"))
|
|
all_null_cols = profile.get("all_null_cols") or []
|
|
if (null_frac is not None and null_frac > _HIGH_MISSING_FRACTION) or all_null_cols:
|
|
caveats.append({
|
|
"id": "missing_data_bias",
|
|
"topic": "datos faltantes",
|
|
"message": (
|
|
"Hay un volumen notable de datos faltantes. Si los ausentes no son "
|
|
"aleatorios (MNAR), los estadísticos calculados sobre lo presente "
|
|
"están sesgados; no extrapoles sin entender por qué faltan."
|
|
),
|
|
"reference": "Tukey (1977), EDA",
|
|
})
|
|
|
|
return {"n": len(caveats), "caveats": caveats, "note": ""}
|