ethibench 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ethibench/__init__.py +7 -0
- ethibench/analysis/__init__.py +0 -0
- ethibench/analysis/duplicates.py +89 -0
- ethibench/analysis/statistics.py +95 -0
- ethibench/analysis/unmatched.py +53 -0
- ethibench/cli.py +772 -0
- ethibench/config.py +31 -0
- ethibench/convert_report.py +86 -0
- ethibench/cumulative_analysis.py +410 -0
- ethibench/datasets.py +69 -0
- ethibench/evaluate.py +593 -0
- ethibench/llm.py +76 -0
- ethibench/metrics.py +193 -0
- ethibench/models.py +67 -0
- ethibench/pairwise.py +242 -0
- ethibench/plots.py +1703 -0
- ethibench/report.py +266 -0
- ethibench/results.py +269 -0
- ethibench-1.0.0.dist-info/METADATA +547 -0
- ethibench-1.0.0.dist-info/RECORD +24 -0
- ethibench-1.0.0.dist-info/WHEEL +5 -0
- ethibench-1.0.0.dist-info/entry_points.txt +2 -0
- ethibench-1.0.0.dist-info/licenses/LICENSE +21 -0
- ethibench-1.0.0.dist-info/top_level.txt +1 -0
ethibench/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Duplicate detection — findings that matched GT in raw matching but were removed by bipartite."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def calculate_duplicates_for_run(raw_matching: dict, matching: dict) -> tuple[int, list[str]]:
|
|
10
|
+
"""Count duplicates and return their UUIDs.
|
|
11
|
+
|
|
12
|
+
A duplicate is a finding with non-empty selected_gt_ids in raw_matching
|
|
13
|
+
but empty selected_gt_ids in the final matching.
|
|
14
|
+
"""
|
|
15
|
+
dup_count = 0
|
|
16
|
+
dup_uuids: list[str] = []
|
|
17
|
+
|
|
18
|
+
for uuid, raw_data in raw_matching.items():
|
|
19
|
+
raw_gt_ids = raw_data.get("selected_gt_ids", [])
|
|
20
|
+
if not raw_gt_ids:
|
|
21
|
+
continue
|
|
22
|
+
final_gt_ids = matching.get(uuid, {}).get("selected_gt_ids", [])
|
|
23
|
+
if not final_gt_ids:
|
|
24
|
+
dup_count += 1
|
|
25
|
+
dup_uuids.append(uuid)
|
|
26
|
+
|
|
27
|
+
return dup_count, dup_uuids
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def extract_duplicates(output_dir: Path, findings_file: Path | None = None) -> dict:
|
|
31
|
+
"""Analyze duplicates across all subsets in an evaluation output directory.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
output_dir: The evaluation_outputs directory containing raw_matchings/ and matchings/.
|
|
35
|
+
findings_file: Optional findings_parsed.jsonl for extracting full finding objects.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Dict mapping subset name → {"count": int, "uuids": list[str], "findings": list[dict]}.
|
|
39
|
+
"""
|
|
40
|
+
raw_dir = output_dir / "raw_matchings"
|
|
41
|
+
match_dir = output_dir / "matchings"
|
|
42
|
+
|
|
43
|
+
if not raw_dir.is_dir() or not match_dir.is_dir():
|
|
44
|
+
logger.warning("No raw_matchings/ or matchings/ found in output_dir.")
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
# Load findings if provided
|
|
48
|
+
findings_by_uuid: dict[str, dict] = {}
|
|
49
|
+
if findings_file and findings_file.exists():
|
|
50
|
+
with open(findings_file) as f:
|
|
51
|
+
for line in f:
|
|
52
|
+
if line.strip():
|
|
53
|
+
item = json.loads(line)
|
|
54
|
+
if "uuid" in item:
|
|
55
|
+
findings_by_uuid[item["uuid"]] = item
|
|
56
|
+
|
|
57
|
+
results: dict[str, dict] = {}
|
|
58
|
+
|
|
59
|
+
for raw_file in sorted(raw_dir.glob("matchings_*.json")):
|
|
60
|
+
subset = raw_file.stem.replace("matchings_", "")
|
|
61
|
+
match_file = match_dir / raw_file.name
|
|
62
|
+
|
|
63
|
+
if not match_file.exists():
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
with open(raw_file) as f:
|
|
67
|
+
raw_list = json.load(f)
|
|
68
|
+
with open(match_file) as f:
|
|
69
|
+
match_list = json.load(f)
|
|
70
|
+
|
|
71
|
+
all_uuids: list[str] = []
|
|
72
|
+
total_dups = 0
|
|
73
|
+
|
|
74
|
+
for raw_m, final_m in zip(raw_list, match_list):
|
|
75
|
+
count, uuids = calculate_duplicates_for_run(raw_m, final_m)
|
|
76
|
+
total_dups += count
|
|
77
|
+
all_uuids.extend(uuids)
|
|
78
|
+
|
|
79
|
+
unique_uuids = list(set(all_uuids))
|
|
80
|
+
dup_findings = [findings_by_uuid[u] for u in unique_uuids if u in findings_by_uuid]
|
|
81
|
+
|
|
82
|
+
results[subset] = {
|
|
83
|
+
"count": total_dups,
|
|
84
|
+
"uuids": unique_uuids,
|
|
85
|
+
"findings": dup_findings,
|
|
86
|
+
}
|
|
87
|
+
logger.info(f" {subset}: {total_dups} duplicates ({len(unique_uuids)} unique)")
|
|
88
|
+
|
|
89
|
+
return results
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Matching statistics — GT coverage, findings-per-GT distribution."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_ground_truths(gt_dir: Path) -> tuple[dict[str, dict], dict[str, list[str]]]:
|
|
11
|
+
"""Load all GT files and return (gt_by_id, gt_ids_by_subset)."""
|
|
12
|
+
gt_by_id: dict[str, dict] = {}
|
|
13
|
+
gt_ids_by_subset: dict[str, list[str]] = defaultdict(list)
|
|
14
|
+
|
|
15
|
+
for gt_file in gt_dir.glob("*_gt.jsonl"):
|
|
16
|
+
with open(gt_file) as f:
|
|
17
|
+
for line in f:
|
|
18
|
+
if not line.strip():
|
|
19
|
+
continue
|
|
20
|
+
gt = json.loads(line)
|
|
21
|
+
gt_id = gt.get("id")
|
|
22
|
+
subset = gt.get("subset_name")
|
|
23
|
+
if gt_id and subset:
|
|
24
|
+
gt_by_id[gt_id] = gt
|
|
25
|
+
gt_ids_by_subset[subset].append(gt_id)
|
|
26
|
+
|
|
27
|
+
return gt_by_id, dict(gt_ids_by_subset)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def compute_statistics(
|
|
31
|
+
output_dir: Path,
|
|
32
|
+
gt_dir: Path,
|
|
33
|
+
) -> dict:
|
|
34
|
+
"""Compute matching statistics from raw_matchings and ground truth.
|
|
35
|
+
|
|
36
|
+
Returns a dict with:
|
|
37
|
+
- total_gt: total ground truth entries
|
|
38
|
+
- total_subsets: number of subsets
|
|
39
|
+
- per_subset: dict of subset → {total_gt, matched_gt, unmatched_gt, findings_per_gt_avg, ...}
|
|
40
|
+
- unmatched_gts: list of GT entries with no findings across any run
|
|
41
|
+
"""
|
|
42
|
+
gt_by_id, gt_ids_by_subset = load_ground_truths(gt_dir)
|
|
43
|
+
raw_dir = output_dir / "raw_matchings"
|
|
44
|
+
|
|
45
|
+
if not raw_dir.is_dir():
|
|
46
|
+
logger.warning("No raw_matchings/ found.")
|
|
47
|
+
return {}
|
|
48
|
+
|
|
49
|
+
# Build gt_id → list of finding UUIDs from raw matchings
|
|
50
|
+
gt_to_findings: dict[str, set[str]] = defaultdict(set)
|
|
51
|
+
|
|
52
|
+
for raw_file in raw_dir.glob("matchings_*.json"):
|
|
53
|
+
with open(raw_file) as f:
|
|
54
|
+
raw_list = json.load(f)
|
|
55
|
+
for raw_m in raw_list:
|
|
56
|
+
for uuid, data in raw_m.items():
|
|
57
|
+
for gt_id in data.get("selected_gt_ids", []):
|
|
58
|
+
gt_to_findings[gt_id].add(uuid)
|
|
59
|
+
|
|
60
|
+
# Per-subset stats
|
|
61
|
+
per_subset: dict[str, dict] = {}
|
|
62
|
+
all_unmatched: list[dict] = []
|
|
63
|
+
|
|
64
|
+
for subset, gt_ids in sorted(gt_ids_by_subset.items()):
|
|
65
|
+
matched = 0
|
|
66
|
+
findings_counts: list[int] = []
|
|
67
|
+
for gt_id in gt_ids:
|
|
68
|
+
n = len(gt_to_findings.get(gt_id, set()))
|
|
69
|
+
findings_counts.append(n)
|
|
70
|
+
if n > 0:
|
|
71
|
+
matched += 1
|
|
72
|
+
else:
|
|
73
|
+
all_unmatched.append(gt_by_id[gt_id])
|
|
74
|
+
|
|
75
|
+
total = len(gt_ids)
|
|
76
|
+
avg = sum(findings_counts) / total if total else 0
|
|
77
|
+
max_f = max(findings_counts) if findings_counts else 0
|
|
78
|
+
min_f = min(findings_counts) if findings_counts else 0
|
|
79
|
+
|
|
80
|
+
per_subset[subset] = {
|
|
81
|
+
"total_gt": total,
|
|
82
|
+
"matched_gt": matched,
|
|
83
|
+
"unmatched_gt": total - matched,
|
|
84
|
+
"findings_per_gt_avg": round(avg, 2),
|
|
85
|
+
"findings_per_gt_max": max_f,
|
|
86
|
+
"findings_per_gt_min": min_f,
|
|
87
|
+
}
|
|
88
|
+
logger.info(f" {subset}: {matched}/{total} GTs matched, avg {avg:.1f} findings/GT")
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
"total_gt": len(gt_by_id),
|
|
92
|
+
"total_subsets": len(gt_ids_by_subset),
|
|
93
|
+
"per_subset": per_subset,
|
|
94
|
+
"unmatched_gts": all_unmatched,
|
|
95
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Extract findings with no ground truth match (false positives)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def extract_unmatched(output_dir: Path, findings_file: Path | None = None) -> dict:
|
|
10
|
+
"""Find findings that had no match in raw_matchings (true false positives).
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
output_dir: The evaluation_outputs directory containing raw_matchings/.
|
|
14
|
+
findings_file: Optional findings_parsed.jsonl for full finding objects.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Dict mapping subset → {"uuids": list[str], "findings": list[dict]}.
|
|
18
|
+
"""
|
|
19
|
+
raw_dir = output_dir / "raw_matchings"
|
|
20
|
+
if not raw_dir.is_dir():
|
|
21
|
+
logger.warning("No raw_matchings/ found in output_dir.")
|
|
22
|
+
return {}
|
|
23
|
+
|
|
24
|
+
findings_by_uuid: dict[str, dict] = {}
|
|
25
|
+
if findings_file and findings_file.exists():
|
|
26
|
+
with open(findings_file) as f:
|
|
27
|
+
for line in f:
|
|
28
|
+
if line.strip():
|
|
29
|
+
item = json.loads(line)
|
|
30
|
+
if "uuid" in item:
|
|
31
|
+
findings_by_uuid[item["uuid"]] = item
|
|
32
|
+
|
|
33
|
+
results: dict[str, dict] = {}
|
|
34
|
+
|
|
35
|
+
for raw_file in sorted(raw_dir.glob("matchings_*.json")):
|
|
36
|
+
subset = raw_file.stem.replace("matchings_", "")
|
|
37
|
+
|
|
38
|
+
with open(raw_file) as f:
|
|
39
|
+
raw_list = json.load(f)
|
|
40
|
+
|
|
41
|
+
unmatched_uuids: set[str] = set()
|
|
42
|
+
for raw_m in raw_list:
|
|
43
|
+
for uuid, data in raw_m.items():
|
|
44
|
+
if not data.get("selected_gt_ids", []):
|
|
45
|
+
unmatched_uuids.add(uuid)
|
|
46
|
+
|
|
47
|
+
uuids = sorted(unmatched_uuids)
|
|
48
|
+
findings = [findings_by_uuid[u] for u in uuids if u in findings_by_uuid]
|
|
49
|
+
|
|
50
|
+
results[subset] = {"uuids": uuids, "findings": findings}
|
|
51
|
+
logger.info(f" {subset}: {len(uuids)} unmatched findings")
|
|
52
|
+
|
|
53
|
+
return results
|