feat(eda): histograma sin outliers (vista central) en num_distr
describe_numeric emite una nueva clave aditiva histogram_clipped: un segundo histograma re-binado sobre el rango de vallas de Tukey [p25-1.5*IQR, p75+1.5*IQR], reutilizando los percentiles ya calculados. Es [] cuando el recorte no excluye nada (sin outliers), la columna es constante (iqr==0) o la sub-muestra recortada pierde dispersion, de modo que el renderer no duplica el histograma completo. El capitulo num_distr consume histogram_clipped como una segunda figura DENTRO del mismo grupo keep-together de la columna: la vista central se lee cuando una cola larga aplasta la escala del histograma completo. Bump describe_numeric 1.0.0->1.1.0 (aditivo) y CHAPTER_VERSION num_distr 1.3.0->1.4.0. Tests: golden (recorta la cola), edges (sin outliers -> [], constante -> []), contrato de claves y smoke e2e de render.
This commit is contained in:
@@ -13,6 +13,7 @@ _EXPECTED_KEYS = {
|
||||
"p1", "p5", "p25", "p50", "p75", "p95", "p99", "iqr",
|
||||
"skew", "kurtosis", "n_outliers", "outlier_pct",
|
||||
"zero_pct", "negative_pct", "distribution_type", "histogram",
|
||||
"histogram_clipped",
|
||||
}
|
||||
|
||||
|
||||
@@ -61,9 +62,10 @@ def test_lista_vacia_todo_none():
|
||||
result = describe_numeric([None, "abc", float("nan")])
|
||||
|
||||
assert set(result.keys()) == _EXPECTED_KEYS
|
||||
for key in _EXPECTED_KEYS - {"histogram"}:
|
||||
for key in _EXPECTED_KEYS - {"histogram", "histogram_clipped"}:
|
||||
assert result[key] is None, f"{key} debe ser None"
|
||||
assert result["histogram"] == []
|
||||
assert result["histogram_clipped"] == []
|
||||
|
||||
|
||||
def test_cv_none_cuando_mean_cero():
|
||||
@@ -83,3 +85,56 @@ def test_iqr_y_percentiles():
|
||||
assert result["p1"] <= result["p25"] <= result["p50"] <= result["p75"] <= result["p99"]
|
||||
assert result["min"] == 1.0
|
||||
assert result["max"] == 100.0
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# histogram_clipped: second view of the central mass, outliers trimmed.
|
||||
# --------------------------------------------------------------------------- #
|
||||
def test_histogram_clipped_trims_the_tail():
|
||||
"""Golden: with a long high tail, the clipped histogram excludes the outliers.
|
||||
|
||||
A tight cluster in [1, 5] plus a handful of extreme values. The full histogram
|
||||
stretches to the extreme (min..max); the clipped one is re-binned over the
|
||||
Tukey inner fences, so its upper edge stays far below the extreme and it holds
|
||||
fewer values than the full sample.
|
||||
"""
|
||||
cluster = [1, 2, 3, 4, 5] * 20 # 100 values in [1, 5]
|
||||
values = cluster + [500, 800, 1000] # 3 far outliers
|
||||
result = describe_numeric(values)
|
||||
|
||||
full = result["histogram"]
|
||||
clipped = result["histogram_clipped"]
|
||||
assert full and clipped # both present
|
||||
for bucket in clipped:
|
||||
assert "lo" in bucket and "hi" in bucket and "count" in bucket
|
||||
|
||||
# The full histogram reaches the extreme; the clipped one does not.
|
||||
assert full[-1]["hi"] >= 900
|
||||
assert clipped[-1]["hi"] < 100
|
||||
|
||||
# The clip removed the tail: fewer values counted than the full sample.
|
||||
total_full = sum(b["count"] for b in full)
|
||||
total_clipped = sum(b["count"] for b in clipped)
|
||||
assert total_full == 103
|
||||
assert total_clipped < total_full
|
||||
assert total_clipped >= 100 # the whole cluster survives the clip
|
||||
|
||||
|
||||
def test_histogram_clipped_empty_when_no_outliers():
|
||||
"""Edge: a clean spread with no fence outliers yields an empty clipped view.
|
||||
|
||||
When the inner-fence range already covers every value, there is nothing to
|
||||
trim, so histogram_clipped is [] and the renderer skips the redundant second
|
||||
view instead of duplicating the full histogram.
|
||||
"""
|
||||
result = describe_numeric(list(range(1, 101))) # uniform 1..100, no outliers
|
||||
assert result["n_outliers"] == 0
|
||||
assert result["histogram"] # full histogram present
|
||||
assert result["histogram_clipped"] == [] # nothing trimmed
|
||||
|
||||
|
||||
def test_histogram_clipped_empty_when_constant():
|
||||
"""Edge: a constant column (iqr == 0) never produces a clipped view."""
|
||||
result = describe_numeric([7] * 30)
|
||||
assert result["iqr"] == 0
|
||||
assert result["histogram_clipped"] == []
|
||||
|
||||
Reference in New Issue
Block a user