Skip to content

Commit 6f843af

Browse files
committed
Add evaluation rollups to benchmark analysis
Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
1 parent 2a49d18 commit 6f843af

3 files changed

Lines changed: 210 additions & 0 deletions

File tree

tests/tools/test_benchmark_output_analysis.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,104 @@ def test_analyze_benchmark_output_counts_generic_model_workflow_records(tmp_path
187187
assert result.model_usage_groups[0].sum_observed_total_tokens == 42
188188

189189

190+
def test_analyze_benchmark_output_rolls_up_evaluation_records(tmp_path: Path) -> None:
191+
tool = load_tool(
192+
"measurement_benchmark_output_analysis_evaluation_rollups",
193+
REPO_ROOT / "tools/measurement/analyze_benchmark_output.py",
194+
)
195+
benchmark_dir = tmp_path / "benchmark"
196+
benchmark_dir.mkdir()
197+
_write_jsonl(
198+
benchmark_dir / "measurements.jsonl",
199+
[
200+
{
201+
"record_type": "evaluation_record",
202+
"run_id": "bio__substitute__r000",
203+
"detection_valid": True,
204+
"detection_invalid_entity_count": 0,
205+
"type_fidelity_valid": True,
206+
"type_fidelity_invalid_replacement_count": 0,
207+
"relational_consistency_valid": False,
208+
"relational_consistency_invalid_relation_count": 2,
209+
"attribute_fidelity_valid": True,
210+
"attribute_fidelity_invalid_entity_count": 0,
211+
"run_tags": {
212+
"workload_id": "bio",
213+
"config_id": "substitute",
214+
"case_id": "bio__substitute__r000",
215+
},
216+
},
217+
{
218+
"record_type": "evaluation_record",
219+
"run_id": "bio__substitute__r000",
220+
"detection_valid": False,
221+
"detection_invalid_entity_count": 3,
222+
"type_fidelity_valid": True,
223+
"type_fidelity_invalid_replacement_count": 0,
224+
"relational_consistency_valid": True,
225+
"relational_consistency_invalid_relation_count": 0,
226+
"attribute_fidelity_valid": None,
227+
"attribute_fidelity_invalid_entity_count": 0,
228+
"run_tags": {
229+
"workload_id": "bio",
230+
"config_id": "substitute",
231+
"case_id": "bio__substitute__r000",
232+
},
233+
},
234+
{
235+
"record_type": "evaluation_record",
236+
"run_id": "bio__substitute__r001",
237+
"detection_valid": True,
238+
"detection_invalid_entity_count": 1,
239+
"type_fidelity_valid": False,
240+
"type_fidelity_invalid_replacement_count": 4,
241+
"relational_consistency_valid": True,
242+
"relational_consistency_invalid_relation_count": 0,
243+
"attribute_fidelity_valid": False,
244+
"attribute_fidelity_invalid_entity_count": 5,
245+
"run_tags": {
246+
"workload_id": "bio",
247+
"config_id": "substitute",
248+
"case_id": "bio__substitute__r001",
249+
},
250+
},
251+
],
252+
)
253+
254+
result = tool.analyze_benchmark_output(benchmark_dir)
255+
256+
cases = {row.case_id: row for row in result.cases}
257+
first_case = cases["bio__substitute__r000"]
258+
assert first_case.detection_judged_record_count == 2
259+
assert first_case.detection_valid_record_count == 1
260+
assert first_case.detection_valid_rate == pytest.approx(0.5)
261+
assert first_case.detection_invalid_entity_count == 3
262+
assert first_case.relational_consistency_judged_record_count == 2
263+
assert first_case.relational_consistency_valid_rate == pytest.approx(0.5)
264+
assert first_case.attribute_fidelity_judged_record_count == 1
265+
assert first_case.attribute_fidelity_valid_rate == pytest.approx(1.0)
266+
267+
second_case = cases["bio__substitute__r001"]
268+
assert second_case.type_fidelity_judged_record_count == 1
269+
assert second_case.type_fidelity_valid_record_count == 0
270+
assert second_case.type_fidelity_valid_rate == pytest.approx(0.0)
271+
assert second_case.type_fidelity_invalid_replacement_count == 4
272+
273+
group = result.groups[0]
274+
assert group.sum_detection_judged_record_count == 3
275+
assert group.sum_detection_valid_record_count == 2
276+
assert group.micro_detection_valid_rate == pytest.approx(2 / 3)
277+
assert group.sum_detection_invalid_entity_count == 4
278+
assert group.sum_type_fidelity_judged_record_count == 3
279+
assert group.sum_type_fidelity_valid_record_count == 2
280+
assert group.micro_type_fidelity_valid_rate == pytest.approx(2 / 3)
281+
assert group.sum_type_fidelity_invalid_replacement_count == 4
282+
assert group.sum_attribute_fidelity_judged_record_count == 2
283+
assert group.sum_attribute_fidelity_valid_record_count == 1
284+
assert group.micro_attribute_fidelity_valid_rate == pytest.approx(0.5)
285+
assert group.sum_attribute_fidelity_invalid_entity_count == 5
286+
287+
190288
def test_analyze_benchmark_output_accepts_detection_artifact_override(tmp_path: Path) -> None:
191289
tool = load_tool(
192290
"measurement_benchmark_output_analysis_artifact_override",

tools/measurement/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,10 @@ Replace judge evaluation:
429429
`attribute_fidelity_invalid_entity_count`: counts of invalid judge findings.
430430
These fields count structures returned by the judges but do not include raw
431431
values, replacement strings, or judge reasoning text.
432+
- `case_analysis` also includes per-case rollups for each judge family:
433+
`{family}_judged_record_count`, `{family}_valid_record_count`,
434+
`{family}_valid_rate`, and the corresponding invalid-count field.
435+
- `group_analysis` includes grouped micro-rate rollups:
436+
`sum_{family}_judged_record_count`, `sum_{family}_valid_record_count`,
437+
`micro_{family}_valid_rate`, and `sum_{invalid_count_field}`. These rates are
438+
computed from summed counts, not medians of case-level rates.

tools/measurement/analyze_benchmark_output.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import logging
1717
import math
1818
import sys
19+
from dataclasses import dataclass
1920
from pathlib import Path
2021
from typing import Annotated, Any, cast
2122

@@ -45,6 +46,25 @@
4546
}
4647

4748

49+
@dataclass(frozen=True)
50+
class _EvaluationRollup:
51+
prefix: str
52+
valid_column: str
53+
invalid_count_column: str
54+
55+
56+
_EVALUATION_ROLLUPS = (
57+
_EvaluationRollup("detection", "detection_valid", "detection_invalid_entity_count"),
58+
_EvaluationRollup("type_fidelity", "type_fidelity_valid", "type_fidelity_invalid_replacement_count"),
59+
_EvaluationRollup(
60+
"relational_consistency",
61+
"relational_consistency_valid",
62+
"relational_consistency_invalid_relation_count",
63+
),
64+
_EvaluationRollup("attribute_fidelity", "attribute_fidelity_valid", "attribute_fidelity_invalid_entity_count"),
65+
)
66+
67+
4868
class CaseAnalysisRow(BaseModel):
4969
suite_id: str | None = None
5070
workload_id: str | None = None
@@ -125,6 +145,22 @@ class CaseAnalysisRow(BaseModel):
125145
original_value_leak_count: float | None = None
126146
original_value_leak_record_count: int = 0
127147
original_value_leak_label_counts: dict[str, int] = Field(default_factory=dict)
148+
detection_judged_record_count: int = 0
149+
detection_valid_record_count: int = 0
150+
detection_valid_rate: float | None = None
151+
detection_invalid_entity_count: int = 0
152+
type_fidelity_judged_record_count: int = 0
153+
type_fidelity_valid_record_count: int = 0
154+
type_fidelity_valid_rate: float | None = None
155+
type_fidelity_invalid_replacement_count: int = 0
156+
relational_consistency_judged_record_count: int = 0
157+
relational_consistency_valid_record_count: int = 0
158+
relational_consistency_valid_rate: float | None = None
159+
relational_consistency_invalid_relation_count: int = 0
160+
attribute_fidelity_judged_record_count: int = 0
161+
attribute_fidelity_valid_record_count: int = 0
162+
attribute_fidelity_valid_rate: float | None = None
163+
attribute_fidelity_invalid_entity_count: int = 0
128164
validation_max_entities_per_call: int | None = None
129165
detection_artifact_rows: int = 0
130166
seed_entity_count: float | None = None
@@ -217,6 +253,22 @@ class GroupAnalysisRow(BaseModel):
217253
sum_original_value_leak_count: float | None = None
218254
leaking_case_count: int = 0
219255
median_original_value_leak_count: float | None = None
256+
sum_detection_judged_record_count: int = 0
257+
sum_detection_valid_record_count: int = 0
258+
micro_detection_valid_rate: float | None = None
259+
sum_detection_invalid_entity_count: int = 0
260+
sum_type_fidelity_judged_record_count: int = 0
261+
sum_type_fidelity_valid_record_count: int = 0
262+
micro_type_fidelity_valid_rate: float | None = None
263+
sum_type_fidelity_invalid_replacement_count: int = 0
264+
sum_relational_consistency_judged_record_count: int = 0
265+
sum_relational_consistency_valid_record_count: int = 0
266+
micro_relational_consistency_valid_rate: float | None = None
267+
sum_relational_consistency_invalid_relation_count: int = 0
268+
sum_attribute_fidelity_judged_record_count: int = 0
269+
sum_attribute_fidelity_valid_record_count: int = 0
270+
micro_attribute_fidelity_valid_rate: float | None = None
271+
sum_attribute_fidelity_invalid_entity_count: int = 0
220272
median_seed_entity_count: float | None = None
221273
median_seed_validation_candidate_count: float | None = None
222274
median_estimated_seed_validation_chunk_count: float | None = None
@@ -404,6 +456,7 @@ def _build_case_row(
404456
artifact_rows = _rows_for_case(artifacts, case_id)
405457
trace_rows = _rows_for_case(traces, case_id)
406458
record_rows = _records_of_type(measurement_rows, "record")
459+
evaluation_rows = _records_of_type(measurement_rows, "evaluation_record")
407460
ndd_rows = _records_of_type(measurement_rows, "ndd_workflow")
408461
model_rows = _model_workflow_rows(measurement_rows)
409462
stage_rows = _records_of_type(measurement_rows, "stage")
@@ -493,6 +546,7 @@ def _build_case_row(
493546
original_value_leak_count=_sum_or_none(record_rows, "original_value_leak_count"),
494547
original_value_leak_record_count=_positive_count(record_rows, "original_value_leak_count"),
495548
original_value_leak_label_counts=_sum_prefixed_ints(record_rows, "original_value_leak_label_counts."),
549+
**_case_evaluation_metrics(evaluation_rows),
496550
validation_max_entities_per_call=validation_max_entities_per_call,
497551
**_case_artifact_metrics(
498552
artifact_rows,
@@ -690,6 +744,43 @@ def _error_status_count(rows: pd.DataFrame) -> int:
690744
return int(statuses.isin({"error", "failed"}).sum())
691745

692746

747+
def _case_evaluation_metrics(evaluation_rows: pd.DataFrame) -> dict[str, int | float | None]:
748+
metrics: dict[str, int | float | None] = {}
749+
for rollup in _EVALUATION_ROLLUPS:
750+
judged_count, valid_count = _evaluation_judged_and_valid_counts(evaluation_rows, rollup.valid_column)
751+
metrics[f"{rollup.prefix}_judged_record_count"] = judged_count
752+
metrics[f"{rollup.prefix}_valid_record_count"] = valid_count
753+
metrics[f"{rollup.prefix}_valid_rate"] = _safe_ratio(valid_count, judged_count)
754+
metrics[rollup.invalid_count_column] = _sum_int_or_zero(evaluation_rows, rollup.invalid_count_column)
755+
return metrics
756+
757+
758+
def _evaluation_judged_and_valid_counts(evaluation_rows: pd.DataFrame, valid_column: str) -> tuple[int, int]:
759+
if valid_column not in evaluation_rows.columns:
760+
return 0, 0
761+
verdicts = [_optional_bool(value) for value in evaluation_rows[valid_column].tolist()]
762+
judged_count = sum(verdict is not None for verdict in verdicts)
763+
valid_count = sum(verdict is True for verdict in verdicts)
764+
return judged_count, valid_count
765+
766+
767+
def _optional_bool(value: object) -> bool | None:
768+
if value is None or pd.isna(value):
769+
return None
770+
if isinstance(value, bool):
771+
return value
772+
if isinstance(value, str):
773+
normalized = value.strip().lower()
774+
if normalized in {"true", "1", "yes"}:
775+
return True
776+
if normalized in {"false", "0", "no"}:
777+
return False
778+
return None
779+
if isinstance(value, int | float):
780+
return bool(value)
781+
return None
782+
783+
693784
def _case_artifact_metrics(
694785
artifact_rows: pd.DataFrame,
695786
*,
@@ -1187,6 +1278,7 @@ def _build_group_row(keys: tuple[Any, ...], group: pd.DataFrame) -> GroupAnalysi
11871278
relaxed_recall = _safe_ratio(relaxed_gt_found, ground_truth_entity_count)
11881279
label_compatible_precision = _safe_ratio(label_compatible_detected_tp, final_entity_count)
11891280
label_compatible_recall = _safe_ratio(label_compatible_gt_found, ground_truth_entity_count)
1281+
evaluation_metrics = _group_evaluation_metrics(group)
11901282
return GroupAnalysisRow(
11911283
workload_id=_none_if_nan(workload_id),
11921284
workload_category=_none_if_nan(workload_category),
@@ -1287,6 +1379,7 @@ def _build_group_row(keys: tuple[Any, ...], group: pd.DataFrame) -> GroupAnalysi
12871379
sum_original_value_leak_count=_sum_or_none(group, "original_value_leak_count"),
12881380
leaking_case_count=_positive_count(group, "original_value_leak_count"),
12891381
median_original_value_leak_count=_median_or_none(group, "original_value_leak_count"),
1382+
**evaluation_metrics,
12901383
median_seed_entity_count=_median_or_none(group, "seed_entity_count"),
12911384
median_seed_validation_candidate_count=_median_or_none(group, "seed_validation_candidate_count"),
12921385
median_estimated_seed_validation_chunk_count=_median_or_none(group, "estimated_seed_validation_chunk_count"),
@@ -1317,6 +1410,18 @@ def _sum_bool_or_zero(dataframe: pd.DataFrame, column: str) -> int:
13171410
return int(dataframe[column].fillna(False).astype(bool).sum())
13181411

13191412

1413+
def _group_evaluation_metrics(group: pd.DataFrame) -> dict[str, int | float | None]:
1414+
metrics: dict[str, int | float | None] = {}
1415+
for rollup in _EVALUATION_ROLLUPS:
1416+
judged_count = _sum_int_or_zero(group, f"{rollup.prefix}_judged_record_count")
1417+
valid_count = _sum_int_or_zero(group, f"{rollup.prefix}_valid_record_count")
1418+
metrics[f"sum_{rollup.prefix}_judged_record_count"] = judged_count
1419+
metrics[f"sum_{rollup.prefix}_valid_record_count"] = valid_count
1420+
metrics[f"micro_{rollup.prefix}_valid_rate"] = _safe_ratio(valid_count, judged_count)
1421+
metrics[f"sum_{rollup.invalid_count_column}"] = _sum_int_or_zero(group, rollup.invalid_count_column)
1422+
return metrics
1423+
1424+
13201425
def _sum_int_or_none(dataframe: pd.DataFrame, column: str) -> int | None:
13211426
value = _sum_or_none(dataframe, column)
13221427
return int(value) if value is not None else None

0 commit comments

Comments
 (0)