|
16 | 16 | import logging |
17 | 17 | import math |
18 | 18 | import sys |
| 19 | +from dataclasses import dataclass |
19 | 20 | from pathlib import Path |
20 | 21 | from typing import Annotated, Any, cast |
21 | 22 |
|
|
45 | 46 | } |
46 | 47 |
|
47 | 48 |
|
| 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 | + |
48 | 68 | class CaseAnalysisRow(BaseModel): |
49 | 69 | suite_id: str | None = None |
50 | 70 | workload_id: str | None = None |
@@ -125,6 +145,22 @@ class CaseAnalysisRow(BaseModel): |
125 | 145 | original_value_leak_count: float | None = None |
126 | 146 | original_value_leak_record_count: int = 0 |
127 | 147 | 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 |
128 | 164 | validation_max_entities_per_call: int | None = None |
129 | 165 | detection_artifact_rows: int = 0 |
130 | 166 | seed_entity_count: float | None = None |
@@ -217,6 +253,22 @@ class GroupAnalysisRow(BaseModel): |
217 | 253 | sum_original_value_leak_count: float | None = None |
218 | 254 | leaking_case_count: int = 0 |
219 | 255 | 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 |
220 | 272 | median_seed_entity_count: float | None = None |
221 | 273 | median_seed_validation_candidate_count: float | None = None |
222 | 274 | median_estimated_seed_validation_chunk_count: float | None = None |
@@ -404,6 +456,7 @@ def _build_case_row( |
404 | 456 | artifact_rows = _rows_for_case(artifacts, case_id) |
405 | 457 | trace_rows = _rows_for_case(traces, case_id) |
406 | 458 | record_rows = _records_of_type(measurement_rows, "record") |
| 459 | + evaluation_rows = _records_of_type(measurement_rows, "evaluation_record") |
407 | 460 | ndd_rows = _records_of_type(measurement_rows, "ndd_workflow") |
408 | 461 | model_rows = _model_workflow_rows(measurement_rows) |
409 | 462 | stage_rows = _records_of_type(measurement_rows, "stage") |
@@ -493,6 +546,7 @@ def _build_case_row( |
493 | 546 | original_value_leak_count=_sum_or_none(record_rows, "original_value_leak_count"), |
494 | 547 | original_value_leak_record_count=_positive_count(record_rows, "original_value_leak_count"), |
495 | 548 | original_value_leak_label_counts=_sum_prefixed_ints(record_rows, "original_value_leak_label_counts."), |
| 549 | + **_case_evaluation_metrics(evaluation_rows), |
496 | 550 | validation_max_entities_per_call=validation_max_entities_per_call, |
497 | 551 | **_case_artifact_metrics( |
498 | 552 | artifact_rows, |
@@ -690,6 +744,43 @@ def _error_status_count(rows: pd.DataFrame) -> int: |
690 | 744 | return int(statuses.isin({"error", "failed"}).sum()) |
691 | 745 |
|
692 | 746 |
|
| 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 | + |
693 | 784 | def _case_artifact_metrics( |
694 | 785 | artifact_rows: pd.DataFrame, |
695 | 786 | *, |
@@ -1187,6 +1278,7 @@ def _build_group_row(keys: tuple[Any, ...], group: pd.DataFrame) -> GroupAnalysi |
1187 | 1278 | relaxed_recall = _safe_ratio(relaxed_gt_found, ground_truth_entity_count) |
1188 | 1279 | label_compatible_precision = _safe_ratio(label_compatible_detected_tp, final_entity_count) |
1189 | 1280 | label_compatible_recall = _safe_ratio(label_compatible_gt_found, ground_truth_entity_count) |
| 1281 | + evaluation_metrics = _group_evaluation_metrics(group) |
1190 | 1282 | return GroupAnalysisRow( |
1191 | 1283 | workload_id=_none_if_nan(workload_id), |
1192 | 1284 | workload_category=_none_if_nan(workload_category), |
@@ -1287,6 +1379,7 @@ def _build_group_row(keys: tuple[Any, ...], group: pd.DataFrame) -> GroupAnalysi |
1287 | 1379 | sum_original_value_leak_count=_sum_or_none(group, "original_value_leak_count"), |
1288 | 1380 | leaking_case_count=_positive_count(group, "original_value_leak_count"), |
1289 | 1381 | median_original_value_leak_count=_median_or_none(group, "original_value_leak_count"), |
| 1382 | + **evaluation_metrics, |
1290 | 1383 | median_seed_entity_count=_median_or_none(group, "seed_entity_count"), |
1291 | 1384 | median_seed_validation_candidate_count=_median_or_none(group, "seed_validation_candidate_count"), |
1292 | 1385 | 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: |
1317 | 1410 | return int(dataframe[column].fillna(False).astype(bool).sum()) |
1318 | 1411 |
|
1319 | 1412 |
|
| 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 | + |
1320 | 1425 | def _sum_int_or_none(dataframe: pd.DataFrame, column: str) -> int | None: |
1321 | 1426 | value = _sum_or_none(dataframe, column) |
1322 | 1427 | return int(value) if value is not None else None |
|
0 commit comments