Download eval/tools/cases.py from DataEyond/Agentic-Service-Data-Eyond-Catalog: direct link, hf CLI and curl.
- Browser
- Download file 19.2 kB
-
https://huggingface.co/spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/resolve/main/eval/tools/cases.py
- Command line
-
hf download hf://spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/eval/tools/cases.py
-
curl -L -o cases.py https://huggingface.co/spaces/DataEyond/Agentic-Service-Data-Eyond-Catalog/resolve/main/eval/tools/cases.py
19.2 kB
| """Tool-output eval cases — one property-assertion per behaviour that must stay true. | |
| PR-2 Layer 2 (docs/plans/PR2_EVALUATION.md §4). These are the committed, shared | |
| version of the fixtures Sofhia wrote during PR-0/PR-1 (which live gitignored under | |
| `tests/`), plus one basic truth assertion for every other ACTIVE analytics tool so | |
| the whole surface has a net. | |
| The rule (§4): **assert a PROPERTY, not a golden float.** k-means init and ETS | |
| optimisation have legitimate freedom, so cases assert recovered signal, correct | |
| ordering, or bounds — never an exact fitted number. A case fails by raising | |
| AssertionError (or any exception); the runner catches it and records the message. | |
| NO LLM and NO DB are touched. sklearn/statsmodels fits are fine (they are not | |
| models we call over the network) — they are what makes the ML tools' claims real. | |
| """ | |
| from __future__ import annotations | |
| from collections.abc import Awaitable, Callable | |
| from dataclasses import dataclass | |
| import numpy as np | |
| import pandas as pd | |
| from src.tools.analytics.ab_test import analyze_ab_test | |
| from src.tools.analytics.aggregation import analyze_aggregate | |
| from src.tools.analytics.anomaly import _ANOMALY_RATE_CEILING, _flag_rate_quality, analyze_anomaly | |
| from src.tools.analytics.cluster import analyze_cluster | |
| from src.tools.analytics.data_quality import analyze_data_quality | |
| from src.tools.analytics.descriptive import ColumnNotFoundError, analyze_descriptive | |
| from src.tools.analytics.forecast import analyze_forecast | |
| from src.tools.analytics.importance import _infer_task, analyze_importance | |
| from src.tools.analytics.merge import analyze_merge | |
| from src.tools.analytics.pareto import analyze_pareto | |
| from src.tools.analytics.relationship import NonNumericColumnError, analyze_correlation | |
| from src.tools.analytics.segmentation import analyze_segmentation | |
| from src.tools.analytics.temporal import analyze_trend | |
| from src.tools.analytics.visualization import render_chart | |
| class EvalCase: | |
| """One property assertion against one tool. | |
| `fn` builds a fixture with a property planted, calls the tool, and asserts the | |
| tool reports it. It raises on failure and returns None on success. | |
| """ | |
| id: str | |
| tool: str | |
| prop: str | |
| fn: Callable[[], Awaitable[None]] | |
| def _assert(cond: bool, msg: str) -> None: | |
| if not cond: | |
| raise AssertionError(msg) | |
| # --------------------------------------------------------------------------- # | |
| # analyze_data_quality (D-1) # | |
| # --------------------------------------------------------------------------- # | |
| async def _dq_zero_dup_when_none() -> None: | |
| df = pd.DataFrame({"id": range(5), "v": [1, 2, 3, 4, 5]}) # all rows unique | |
| out = analyze_data_quality(df) | |
| _assert(out["duplicate_rows"]["count"] == 0, "unique rows must report 0 duplicates") | |
| _assert(out["duplicate_rows"]["rate"] == 0.0, "0 duplicates -> rate 0.0") | |
| _assert(out["issues"]["duplicate_row_count"] == 0, "issues must carry 0, not absent") | |
| async def _dq_counts_planted_dups() -> None: | |
| df = pd.DataFrame({"a": [1, 1, 1, 2, 3], "b": ["x", "x", "x", "y", "z"]}) | |
| out = analyze_data_quality(df) | |
| _assert(out["duplicate_rows"]["count"] == 2, "3 identical rows -> 2 redundant copies") | |
| _assert(abs(out["duplicate_rows"]["rate"] - 2 / 5) < 1e-9, "rate = 2/5") | |
| async def _dq_subset_candidate_key() -> None: | |
| df = pd.DataFrame({"id": [1, 2, 3], "grp": ["A", "A", "B"]}) | |
| out = analyze_data_quality(df, column_ids=["grp"]) | |
| _assert(out["duplicate_rows"]["count"] == 1, "subset dup: second 'A' is one duplicate") | |
| # --------------------------------------------------------------------------- # | |
| # analyze_anomaly (D-3) # | |
| # --------------------------------------------------------------------------- # | |
| def _outlier_frame(n: int = 60) -> pd.DataFrame: | |
| rng = np.random.RandomState(0) | |
| x, y = rng.normal(0, 1, n), rng.normal(0, 1, n) | |
| x[:3] = [50.0, -48.0, 55.0] | |
| y[:3] = [50.0, -47.0, 60.0] | |
| return pd.DataFrame({"x": x, "y": y}) | |
| async def _an_names_rows_via_label() -> None: | |
| df = _outlier_frame(60) | |
| df["eqm_no"] = [f"UNIT-{i:03d}" for i in range(60)] | |
| out = await analyze_anomaly(df, ["x", "y"], contamination=0.1, label_columns=["eqm_no"]) | |
| _assert(out["n_anomalies"] > 0, "planted outliers must be flagged") | |
| for rec in out["anomalies"]: | |
| _assert(str(rec["eqm_no"]).startswith("UNIT-"), "each record must name its row") | |
| _assert("x" in rec and "y" in rec, "scored values must still be present") | |
| named = {rec["eqm_no"] for rec in out["anomalies"]} | |
| _assert(bool({"UNIT-000", "UNIT-001", "UNIT-002"} & named), "planted units must appear") | |
| async def _an_missing_label_raises() -> None: | |
| df = _outlier_frame(30) | |
| try: | |
| await analyze_anomaly(df, ["x", "y"], label_columns=["ghost_id"]) | |
| except ColumnNotFoundError: | |
| return | |
| raise AssertionError("a missing label column must raise ColumnNotFoundError") | |
| async def _an_quality_present_not_weak() -> None: | |
| out = await analyze_anomaly(_outlier_frame(60), ["x", "y"], contamination=0.1) | |
| q = out["quality"] | |
| _assert(q["metric"] == "anomaly_rate", "quality metric must be anomaly_rate") | |
| _assert(q["floor"] == _ANOMALY_RATE_CEILING, "floor must be the ceiling constant") | |
| _assert(q["value"] == round(out["anomaly_rate"], 4), "quality value echoes the rate") | |
| _assert(q["weak"] is False, "a ~0.1 flag rate is a normal run, not weak") | |
| async def _an_flag_rate_branches() -> None: | |
| _assert(_flag_rate_quality(0.22)["weak"] is False, "clean-data floor 0.22 is not weak") | |
| _assert(_flag_rate_quality(0.50)["weak"] is False, "boundary is strict > 0.5") | |
| _assert(_flag_rate_quality(0.857)["weak"] is True, "0.86 flagged is weak") | |
| _assert("noise" in _flag_rate_quality(0.857)["note"], "weak note names it as noise") | |
| # --------------------------------------------------------------------------- # | |
| # analyze_importance (D-4) # | |
| # --------------------------------------------------------------------------- # | |
| async def _imp_infer_non_numeric() -> None: | |
| classify, reason = _infer_task(pd.Series(["a", "b", "a", "c"])) | |
| _assert(classify is True and "categor" in reason.lower(), "text target -> classification") | |
| async def _imp_infer_float() -> None: | |
| classify, reason = _infer_task(pd.Series([1.5, 2.25, 3.75, 0.1])) | |
| _assert(classify is False and "continuous" in reason.lower(), "float target -> regression") | |
| async def _imp_infer_ordinal_integer() -> None: | |
| classify, reason = _infer_task(pd.Series(list(range(15)) * 3)) | |
| _assert(classify is False and "ordinal" in reason.lower(), "dense 15-int -> ordinal regression") | |
| async def _imp_infer_binary_flag() -> None: | |
| classify, reason = _infer_task(pd.Series([0, 1, 1, 0, 1, 0])) | |
| _assert(classify is True and "flag" in reason.lower(), "0/1 -> binary classification") | |
| async def _imp_infer_sparse_codes() -> None: | |
| classify, reason = _infer_task(pd.Series([10, 20, 700, 10, 20, 700])) | |
| _assert(classify is True and "categor" in reason.lower(), "sparse codes -> classification") | |
| async def _imp_infer_many_integers() -> None: | |
| classify, _ = _infer_task(pd.Series(range(50))) | |
| _assert(classify is False, "50 distinct integers -> regression") | |
| async def _imp_infer_all_null() -> None: | |
| classify, reason = _infer_task(pd.Series([None, None, None], dtype="float64")) | |
| _assert("no non-null" in reason.lower(), "all-null column is empty, not 'non-numeric'") | |
| async def _imp_recovers_planted_signal() -> None: | |
| rng = np.random.RandomState(0) | |
| n = 120 | |
| driver = rng.normal(0, 1, n) | |
| noise = rng.normal(0, 1, n) | |
| target = 5.0 * driver + 0.1 * noise + rng.normal(0, 0.05, n) | |
| df = pd.DataFrame({"driver": driver, "noise": noise, "target": target}) | |
| out = await analyze_importance(df, "target", feature_columns=["driver", "noise"]) | |
| _assert(out["task"] == "regression", "continuous target -> regression") | |
| top = out["features"][0]["feature"] | |
| _assert(top == "driver", f"planted driver must rank first, got {top}") | |
| _assert(out["quality"]["weak"] is False, "a strong linear signal must not be weak") | |
| # --------------------------------------------------------------------------- # | |
| # analyze_pareto (D-6) # | |
| # --------------------------------------------------------------------------- # | |
| def _revenue() -> pd.DataFrame: | |
| return pd.DataFrame({"region": ["A", "B", "C", "D", "A"], "revenue": [50, 10, 5, 5, 30]}) | |
| async def _pareto_additive_no_caveat() -> None: | |
| out = analyze_pareto(_revenue(), "region", "revenue", agg="sum") | |
| _assert(out["caveats"] == [], "sum on non-negative values -> no honesty caveat") | |
| _assert(out["categories_for_80pct"] == 1, "A alone is 80% -> categories_for_80pct == 1") | |
| async def _pareto_non_additive_warns() -> None: | |
| out = analyze_pareto(_revenue(), "region", "revenue", agg="mean") | |
| _assert(any("not additive" in c for c in out["caveats"]), "mean must be flagged non-additive") | |
| async def _pareto_negative_warns() -> None: | |
| df = pd.DataFrame({"region": ["A", "B", "C"], "delta": [50, -25, 10]}) | |
| out = analyze_pareto(df, "region", "delta", agg="sum") | |
| _assert(any("negative" in c for c in out["caveats"]), "negative values must be flagged") | |
| # --------------------------------------------------------------------------- # | |
| # analyze_trend (D-7) # | |
| # --------------------------------------------------------------------------- # | |
| def _months(values: list[float]) -> pd.DataFrame: | |
| dates = pd.date_range("2024-01-01", periods=len(values), freq="MS") | |
| return pd.DataFrame({"date": dates, "v": values}) | |
| async def _trend_clean_agrees() -> None: | |
| out = analyze_trend(_months([10, 20, 30, 40]), "date", "v", freq="month") | |
| _assert(out["direction"] == "up" and out["slope"] > 0, "clean rise -> up, positive slope") | |
| _assert(out["r_squared"] == 1.0, "perfect line -> R^2 == 1") | |
| _assert(not any("Mixed signal" in c for c in out["caveats"]), "agreement -> no mixed caveat") | |
| async def _trend_disagreement_flagged() -> None: | |
| out = analyze_trend(_months([100, 40, 30, 20, 10, 110]), "date", "v", freq="month") | |
| _assert(out["direction"] == "up", "endpoints go up (ends high)") | |
| _assert(out["slope"] < 0, "OLS slope goes down") | |
| _assert(any("Mixed signal" in c for c in out["caveats"]), "disagreement must be flagged") | |
| # --------------------------------------------------------------------------- # | |
| # analyze_cluster (D-10) # | |
| # --------------------------------------------------------------------------- # | |
| def _blobs(per: int = 12) -> pd.DataFrame: | |
| rng = np.random.RandomState(0) | |
| pts = np.vstack( | |
| [rng.normal(c, 0.2, (per, 2)) for c in (0, 10, 20)] | |
| ) | |
| return pd.DataFrame(pts, columns=["x", "y"]) | |
| async def _cluster_override_visible() -> None: | |
| df = pd.DataFrame({"a": [1.0] * 25, "b": [2.0] * 25}) # identical -> collapse | |
| out = await analyze_cluster(df, ["a", "b"], k=5) | |
| _assert(out["requested_k"] == 5, "requested_k must echo the explicit k") | |
| _assert(out["k"] != 5, "collapse must fall back to a different k") | |
| _assert(any("Requested k=5" in c for c in out["caveats"]), "override must be visible") | |
| async def _cluster_k_honoured() -> None: | |
| out = await analyze_cluster(_blobs(10), ["x", "y"], k=3) | |
| _assert(out["k"] == 3, "separable blobs -> explicit k=3 honoured") | |
| _assert(not any("fell back" in c for c in out["caveats"]), "no fallback on a good fit") | |
| async def _cluster_auto_finds_3() -> None: | |
| out = await analyze_cluster(_blobs(12), ["x", "y"]) # no k | |
| _assert(out["requested_k"] is None, "auto must report requested_k None") | |
| _assert(out["k"] == 3, "silhouette must recover the 3 planted blobs") | |
| # --------------------------------------------------------------------------- # | |
| # Basic coverage — one truth assertion for every other ACTIVE tool # | |
| # --------------------------------------------------------------------------- # | |
| async def _descriptive_basic() -> None: | |
| out = analyze_descriptive(pd.DataFrame({"revenue": [10, 20, 30, 40]}), ["revenue"])["revenue"] | |
| _assert(out["mean"] == 25 and out["median"] == 25, "mean/median of 10..40 = 25") | |
| _assert(out["min"] == 10 and out["max"] == 40, "min/max = 10/40") | |
| async def _aggregate_groupby_sum() -> None: | |
| df = pd.DataFrame({"region": ["A", "A", "B", "B"], "revenue": [10, 20, 100, 200]}) | |
| rows = analyze_aggregate(df, {"revenue": ["sum"]}, group_by=["region"]) | |
| by = {r["region"]: r for r in rows} | |
| _assert(by["A"]["revenue_sum"] == 30 and by["B"]["revenue_sum"] == 300, "per-group sums") | |
| async def _correlation_recovers_positive() -> None: | |
| out = analyze_correlation(pd.DataFrame({"x": [1, 2, 3, 4], "y": [2, 4, 6, 8]}), ["x", "y"]) | |
| _assert(abs(out["matrix"]["x"]["y"] - 1.0) < 1e-9, "y=2x -> correlation 1.0") | |
| async def _correlation_refuses_non_numeric() -> None: | |
| df = pd.DataFrame({"x": [1, 2], "label": ["a", "b"]}) | |
| try: | |
| analyze_correlation(df, ["x", "label"]) | |
| except NonNumericColumnError: | |
| return | |
| raise AssertionError("a non-numeric column must be refused") | |
| async def _merge_aligns_two_metrics() -> None: | |
| left = pd.DataFrame({"section": ["S1", "S2"], "pa": [90.0, 80.0]}) | |
| right = pd.DataFrame({"section": ["S1", "S2"], "backlog": [3, 7]}) | |
| rows = analyze_merge(left, right, on=["section"], how="inner") | |
| by = {r["section"]: r for r in rows} | |
| _assert(len(rows) == 2, "inner join on matched keys -> 2 rows") | |
| _assert(by["S1"]["pa"] == 90.0 and by["S1"]["backlog"] == 3, "both metrics aligned per key") | |
| def _ab(a_vals: list[float], b_vals: list[float]) -> pd.DataFrame: | |
| return pd.DataFrame({"grp": ["A"] * len(a_vals) + ["B"] * len(b_vals), "val": a_vals + b_vals}) | |
| async def _ab_significant_difference() -> None: | |
| out = analyze_ab_test(_ab([10, 11, 9, 10, 12], [20, 21, 19, 20, 22]), "grp", "val", "A", "B") | |
| _assert(out["comparison"] == "higher", "B > A") | |
| _assert(out["test"]["significant"] is True, "clearly separated groups are significant") | |
| async def _ab_overlap_not_significant() -> None: | |
| out = analyze_ab_test(_ab([10, 11, 9, 10], [10, 12, 9, 11]), "grp", "val", "A", "B") | |
| _assert(out["test"]["significant"] is not True, "overlapping groups are not significant") | |
| async def _segmentation_quantile_balanced() -> None: | |
| df = pd.DataFrame({"age": list(range(12))}) | |
| out = analyze_segmentation(df, "age", bins=4, method="quantile") | |
| counts = [s["count"] for s in out["segments"]] | |
| _assert(len(out["segments"]) == 4, "4 quantile bins") | |
| _assert(max(counts) - min(counts) <= 1, "quantile bins must be balanced (counts within 1)") | |
| async def _forecast_trend_beats_naive() -> None: | |
| dates = pd.date_range("2024-01-01", periods=18, freq="MS") | |
| df = pd.DataFrame({"month": dates, "v": [100 + 10 * i for i in range(18)]}) | |
| out = await analyze_forecast(df, "month", "v", horizon=3, freq="month") | |
| _assert(len(out["forecast"]) == 3 and out["direction"] == "up", "rising trend projected up") | |
| q = out["quality"] | |
| _assert(q["value"] is not None and q["value"] < 1.0, "clean trend -> MASE < 1") | |
| _assert(q["weak"] is False, "a validated sub-1 MASE is not weak") | |
| async def _forecast_short_series_not_validated() -> None: | |
| dates = pd.date_range("2024-01-01", periods=6, freq="MS") | |
| df = pd.DataFrame({"month": dates, "v": [100 + 5 * i for i in range(6)]}) | |
| out = await analyze_forecast(df, "month", "v", horizon=2, freq="month") | |
| q = out["quality"] | |
| _assert(q["value"] is None, "too-short series cannot be validated -> no MASE number") | |
| _assert("not validated" in q["note"], "must say 'not validated', not a silent number") | |
| _assert(q["weak"] is True, "an unvalidated forecast is weak evidence") | |
| # --------------------------------------------------------------------------- # | |
| # render_chart — computes no numbers; its truth is a valid envelope mapping # | |
| # the requested columns # | |
| # --------------------------------------------------------------------------- # | |
| async def _chart_builds_envelope() -> None: | |
| df = pd.DataFrame({"region": ["A", "B", "C"], "revenue": [30, 20, 10]}) | |
| env = render_chart(df, "bar", x="region", y="revenue") | |
| _assert(env["schema"] == "dataeyond.chart.v1", "must be a dataeyond.chart.v1 envelope") | |
| _assert(env["chart_type"] == "bar", "chart_type echoes the request") | |
| trace = env["plotly"]["data"][0] | |
| _assert(list(trace["x"]) == ["A", "B", "C"], "x maps the requested column") | |
| _assert(list(trace["y"]) == [30, 20, 10], "y maps the requested column") | |
| async def _chart_missing_column_raises() -> None: | |
| df = pd.DataFrame({"region": ["A"], "revenue": [1]}) | |
| try: | |
| render_chart(df, "bar", x="region", y="ghost") | |
| except ColumnNotFoundError: | |
| return | |
| raise AssertionError("a missing chart column must raise ColumnNotFoundError") | |
| def _mk(tool: str, *fns: Callable[[], Awaitable[None]]) -> list[EvalCase]: | |
| """Build cases for one tool; id + prop derive from each function's name.""" | |
| return [ | |
| EvalCase(f.__name__.lstrip("_"), tool, f.__name__.lstrip("_").replace("_", " "), f) | |
| for f in fns | |
| ] | |
| CASES: list[EvalCase] = [ | |
| *_mk( | |
| "analyze_data_quality", | |
| _dq_zero_dup_when_none, | |
| _dq_counts_planted_dups, | |
| _dq_subset_candidate_key, | |
| ), | |
| *_mk( | |
| "analyze_anomaly", | |
| _an_names_rows_via_label, | |
| _an_missing_label_raises, | |
| _an_quality_present_not_weak, | |
| _an_flag_rate_branches, | |
| ), | |
| *_mk( | |
| "analyze_importance", | |
| _imp_infer_non_numeric, | |
| _imp_infer_float, | |
| _imp_infer_ordinal_integer, | |
| _imp_infer_binary_flag, | |
| _imp_infer_sparse_codes, | |
| _imp_infer_many_integers, | |
| _imp_infer_all_null, | |
| _imp_recovers_planted_signal, | |
| ), | |
| *_mk( | |
| "analyze_pareto", | |
| _pareto_additive_no_caveat, | |
| _pareto_non_additive_warns, | |
| _pareto_negative_warns, | |
| ), | |
| *_mk( | |
| "analyze_trend", | |
| _trend_clean_agrees, | |
| _trend_disagreement_flagged, | |
| ), | |
| *_mk( | |
| "analyze_cluster", | |
| _cluster_override_visible, | |
| _cluster_k_honoured, | |
| _cluster_auto_finds_3, | |
| ), | |
| *_mk("analyze_descriptive", _descriptive_basic), | |
| *_mk("analyze_aggregate", _aggregate_groupby_sum), | |
| *_mk( | |
| "analyze_correlation", | |
| _correlation_recovers_positive, | |
| _correlation_refuses_non_numeric, | |
| ), | |
| *_mk("analyze_merge", _merge_aligns_two_metrics), | |
| *_mk( | |
| "analyze_ab_test", | |
| _ab_significant_difference, | |
| _ab_overlap_not_significant, | |
| ), | |
| *_mk("analyze_segmentation", _segmentation_quantile_balanced), | |
| *_mk( | |
| "analyze_forecast", | |
| _forecast_trend_beats_naive, | |
| _forecast_short_series_not_validated, | |
| ), | |
| *_mk( | |
| "render_chart", | |
| _chart_builds_envelope, | |
| _chart_missing_column_raises, | |
| ), | |
| ] | |