"""Tests para summarize_outlier_dims.""" from isolation_forest_outliers import isolation_forest_outliers from summarize_outlier_dims import summarize_outlier_dims # Dataset compartido: 3 columnas, 13 filas. La fila ORIGINAL 6 tiene None en "a" # (se descarta), de modo que la fila ORIGINAL 10 -- con un valor extremo en "c" # -- queda en el indice VALIDO 9 (no 10). Esto verifica el salto de None. A = [0.1, 0.2, -0.1, 0.0, 0.3, -0.2, None, 0.15, -0.05, 0.25, 0.2, -0.3, 0.1] B = [1.0, 1.1, 0.9, 1.2, 0.8, 1.0, 1.3, 1.1, 0.95, 1.05, 0.9, 1.15, 1.0] C = [5.0, 5.2, 4.8, 5.1, 4.9, 5.0, 5.3, 4.95, 5.05, 4.9, 500.0, 5.1, 5.0] RAW = {"a": A, "b": B, "c": C} # Mapa original -> valido (saltando original 6): # orig: 0 1 2 3 4 5 7 8 9 10 11 12 # valid: 0 1 2 3 4 5 6 7 8 9 10 11 # => el extremo en "c" (original 10) esta en el indice valido 9. EXTREME_VALID_INDEX = 9 def test_row_index_skips_none_rows(): # Mapeo directo (sin depender de la aleatoriedad de IsolationForest): el # indice valido 9 debe corresponder a la fila con c == 500 -> el None de la # fila original 6 se salto correctamente. summary = summarize_outlier_dims( RAW, [{"row_index": EXTREME_VALID_INDEX, "score": -0.5}], top_k=3 ) assert len(summary) == 1 entry = summary[0] assert entry["row_index"] == EXTREME_VALID_INDEX assert entry["score"] == -0.5 # La dimension dominante es "c", con su valor extremo y |z| alto. top = entry["dims"][0] assert top["col"] == "c" assert top["value"] == 500.0 assert abs(top["z"]) > 2.0 # top_k respetado: como mucho 3 dims. assert len(entry["dims"]) <= 3 def test_extreme_row_flagged_via_isolation(): # Integracion real: detectar outliers y explicarlos. result = isolation_forest_outliers(RAW, contamination=0.1) assert "note" not in result outlier_rows = result["outlier_rows"] assert outlier_rows # al menos un outlier summary = summarize_outlier_dims(RAW, outlier_rows, top_k=3) # Paralela a outlier_rows (todos los indices estan en rango). assert len(summary) == len(outlier_rows) by_index = {e["row_index"]: e for e in summary} # El punto extremo debe estar entre los outliers detectados... assert EXTREME_VALID_INDEX in by_index # ...y su dimension top debe ser "c" (donde se desvia ~muchas sigmas). extreme = by_index[EXTREME_VALID_INDEX] assert extreme["dims"][0]["col"] == "c" assert abs(extreme["dims"][0]["z"]) > 2.0 def test_out_of_range_row_index_is_ignored(): # Indices fuera de rango se omiten en lugar de petar. summary = summarize_outlier_dims( RAW, [ {"row_index": 999, "score": -1.0}, {"row_index": -1, "score": -1.0}, {"row_index": EXTREME_VALID_INDEX, "score": -0.5}, ], top_k=2, ) # Solo sobrevive el indice valido; los otros dos se descartan. assert len(summary) == 1 assert summary[0]["row_index"] == EXTREME_VALID_INDEX assert len(summary[0]["dims"]) <= 2 def test_degrades_to_empty_on_invalid_inputs(): # raw_numeric vacio + outlier_rows vacio. assert summarize_outlier_dims({}, [], 3) == [] # raw_numeric no es dict. assert summarize_outlier_dims("not a dict", [{"row_index": 0}], 3) == [] # outlier_rows no es lista. assert summarize_outlier_dims(RAW, "not a list", 3) == [] # Sin columnas numericas (todas con strings) -> []. assert summarize_outlier_dims( {"s": ["x", "y", "z"]}, [{"row_index": 0, "score": -1.0}], 3 ) == [] # Entradas malformadas dentro de outlier_rows se ignoran (no petan). assert summarize_outlier_dims( RAW, ["nope", 42, {"no_row_index": 1}], 3 ) == []