6f88f184f1
Nuevo capítulo dedicado `outliers` para el motor AutomaticEDA que reúne y profundiza en un solo sitio el análisis de valores atípicos, hoy disperso entre `num_distr` (conteo por columna) y `modelos` (IsolationForest). Se registra en `chapters_registry.py` entre `missingness` y `correlacion` (bloque de calidad de datos: calidad → missingness → outliers). Contenido del capítulo: - Resumen univariante por columna: nº y % de atípicos por Tukey (1.5·IQR) y por z-score (|z| > 3), con vallas inferior/superior y valores extremos. Ordenado por contaminación y marcando las columnas más afectadas. Reusa las funciones del registry `build_boxplot_stats` (vallas desde los percentiles del profile) y `detect_outliers` (regla z-score sobre la muestra cruda de `ctx`). - Boxplots de Tukey de las columnas más contaminadas (caja, bigotes y puntos atípicos), delegados a la función nueva `build_boxplots_figure`. - Multivariante: filas anómalas considerando todas las columnas a la vez con `isolation_forest_outliers` — nº y % de filas, las más anómalas con su score y las dimensiones que las hacen raras (top columnas por |z|, vía la función nueva `summarize_outlier_dims`). El detector se corre en vivo sobre `raw_numeric` para que el indexado de filas coincida exactamente con el de las dimensiones; cae al bloque precomputado del perfil cuando no hay muestra cruda (preset lite). - Interpretación exploratoria: un atípico no es necesariamente un error (distingue error de dato vs dato real extremo) y recomendaciones (revisar, winsorizar o re-expresar, enlazando con la re-expresión de Tukey del perfil). Términos clicables registrados en el glosario compartido: `outlier`, `tukey_fence`, `zscore`, `isolation_forest`. Funciones nuevas del registry (dominio datascience, grupo eda): - `build_boxplots_figure_py_datascience` (figure helper, impura) - `summarize_outlier_dims_py_datascience` (pura) El capítulo se activa con ≥1 columna numérica y devuelve None en su ausencia; lee todo defensivo y nunca lanza. Tests: capítulo (golden + edges + error path + render PDF/PPTX) y ambas funciones nuevas. Suite de no-regresión de AutomaticEDA verde. Verificado end-to-end con el dataset Titanic (Fare/Parch/SibSp como las columnas más contaminadas). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
110 lines
3.3 KiB
Python
110 lines
3.3 KiB
Python
"""Tests para build_boxplots_figure (boxplots horizontales de Tukey, grupo eda).
|
|
|
|
Usa el backend Agg sin display; no muestra ni guarda figuras. Cada test cierra
|
|
explícitamente la Figure construida (matplotlib.pyplot.close) para no acumular
|
|
estado entre tests.
|
|
"""
|
|
|
|
import matplotlib
|
|
|
|
matplotlib.use("Agg")
|
|
|
|
import matplotlib.pyplot as plt # noqa: E402
|
|
from matplotlib.figure import Figure # noqa: E402
|
|
|
|
from build_boxplots_figure import build_boxplots_figure
|
|
|
|
|
|
def _box(name, q1, median, q3, mn, mx, low=False, high=False, fliers=None):
|
|
"""Construye una entrada {name, box, fliers} con un box estilo build_boxplot_stats."""
|
|
iqr = q3 - q1
|
|
return {
|
|
"name": name,
|
|
"box": {
|
|
"q1": q1,
|
|
"median": median,
|
|
"q3": q3,
|
|
"iqr": iqr,
|
|
"lower_fence": q1 - 1.5 * iqr,
|
|
"upper_fence": q3 + 1.5 * iqr,
|
|
"whisker_lo": max(mn, q1 - 1.5 * iqr),
|
|
"whisker_hi": min(mx, q3 + 1.5 * iqr),
|
|
"min": mn,
|
|
"max": mx,
|
|
"has_low_outliers": low,
|
|
"has_high_outliers": high,
|
|
"n_outliers": 0,
|
|
},
|
|
"fliers": fliers,
|
|
}
|
|
|
|
|
|
def test_returns_figure_with_axes():
|
|
boxes = [
|
|
_box("edad", 10.0, 25.0, 40.0, 1.0, 100.0, high=True),
|
|
_box("ingresos", 100.0, 200.0, 300.0, 50.0, 400.0),
|
|
_box("score", -1.0, 0.0, 1.0, -5.0, 5.0, low=True, high=True),
|
|
]
|
|
fig = build_boxplots_figure(boxes, title="Boxplots", max_boxes=12)
|
|
assert isinstance(fig, Figure)
|
|
assert len(fig.axes) >= 1
|
|
# Tres cajas -> tres etiquetas en el eje Y.
|
|
ax = fig.axes[0]
|
|
assert len(ax.get_yticks()) == 3
|
|
plt.close(fig)
|
|
|
|
|
|
def test_empty_list_returns_placeholder_figure():
|
|
fig = build_boxplots_figure([], title="vacío")
|
|
assert isinstance(fig, Figure)
|
|
assert len(fig.axes) >= 1
|
|
plt.close(fig)
|
|
|
|
|
|
def test_invalid_box_is_skipped_not_raised():
|
|
boxes = [
|
|
{"name": "rota", "box": {"q1": None, "median": None, "q3": None}},
|
|
{"name": "sin_box"}, # falta la clave box.
|
|
"no_es_dict", # entrada no-dict.
|
|
_box("buena", 1.0, 2.0, 3.0, 0.0, 10.0, high=True),
|
|
]
|
|
fig = build_boxplots_figure(boxes)
|
|
assert isinstance(fig, Figure)
|
|
ax = fig.axes[0]
|
|
# Solo la caja válida sobrevive al filtrado.
|
|
assert len(ax.get_yticks()) == 1
|
|
plt.close(fig)
|
|
|
|
|
|
def test_all_invalid_returns_placeholder():
|
|
boxes = [
|
|
{"name": "a", "box": {"q1": None, "median": 1.0, "q3": 2.0}},
|
|
{"name": "b"},
|
|
]
|
|
fig = build_boxplots_figure(boxes)
|
|
assert isinstance(fig, Figure)
|
|
assert len(fig.axes) >= 1
|
|
plt.close(fig)
|
|
|
|
|
|
def test_raw_fliers_are_drawn():
|
|
boxes = [
|
|
_box("con_fliers", 10.0, 20.0, 30.0, 5.0, 200.0,
|
|
high=True, fliers=[150.0, 180.0, 200.0]),
|
|
]
|
|
fig = build_boxplots_figure(boxes)
|
|
assert isinstance(fig, Figure)
|
|
assert len(fig.axes) >= 1
|
|
plt.close(fig)
|
|
|
|
|
|
def test_max_boxes_truncates_and_does_not_raise():
|
|
boxes = [_box(f"c{i}", float(i), float(i + 1), float(i + 2),
|
|
float(i - 5), float(i + 10)) for i in range(20)]
|
|
fig = build_boxplots_figure(boxes, title="muchos", max_boxes=5)
|
|
assert isinstance(fig, Figure)
|
|
ax = fig.axes[0]
|
|
# Solo se dibujan las primeras 5 cajas.
|
|
assert len(ax.get_yticks()) == 5
|
|
plt.close(fig)
|