tracegrpo / src /data /dedup.py
Dhruv1000's picture
Add files using upload-large-folder tool
5bfc214 verified
Raw History Blame Contribute Delete
3.15 kB
"""De-duplication (spec §3.3.5): exact dedup on normalized function text, then
near-duplicate detection via 5-gram Jaccard >= 0.9 using MinHash/LSH.
Operates at the pair level (a pair is one dedup unit — dropping only one half
of a pair would violate pair integrity), keyed by each pair's func_vuln text.
"""
from __future__ import annotations
from collections import defaultdict
from datasketch import MinHash, MinHashLSH
from src.data.normalize_text import shingles, strip_comments_and_whitespace
MINHASH_PERM = 128
JACCARD_THRESHOLD = 0.9
def exact_dedup_pairs(rows: list[dict]) -> tuple[list[dict], dict]:
"""Drops whole pairs whose func_vuln normalizes to a text seen before
(first pair with that text wins). Returns (kept_rows, stats)."""
by_pair: dict[str, list[dict]] = defaultdict(list)
for r in rows:
by_pair[r["pair_id"]].append(r)
seen_norm_text: set[str] = set()
kept_pair_ids: set[str] = set()
n_dropped_pairs = 0
for pair_id, pair_rows in by_pair.items():
vuln_row = next((r for r in pair_rows if r["target"] == 1), pair_rows[0])
norm = strip_comments_and_whitespace(vuln_row["func_vuln"])
if norm in seen_norm_text:
n_dropped_pairs += 1
continue
seen_norm_text.add(norm)
kept_pair_ids.add(pair_id)
kept_rows = [r for r in rows if r["pair_id"] in kept_pair_ids]
stats = {
"n_pairs_before": len(by_pair),
"n_pairs_dropped_exact": n_dropped_pairs,
"n_pairs_after": len(by_pair) - n_dropped_pairs,
}
return kept_rows, stats
def near_dup_pair_groups(rows: list[dict], threshold: float = JACCARD_THRESHOLD) -> list[set[str]]:
"""Returns groups (as sets of pair_id) of pairs whose func_vuln text is a
near-duplicate of another's (Jaccard over 5-gram shingles >= threshold),
via MinHash/LSH. Does not drop anything itself — spec §3.3.5 only asks to
"log how many samples were removed" and spec §3.3's near-dup check is
primarily used as a cross-partition leakage check in the split builder
(`tests/test_splits.py`: "no near-dup across partitions (spot-check)"),
not necessarily a blanket corpus-wide drop.
"""
by_pair: dict[str, list[dict]] = defaultdict(list)
for r in rows:
by_pair[r["pair_id"]].append(r)
lsh = MinHashLSH(threshold=threshold, num_perm=MINHASH_PERM)
minhashes: dict[str, MinHash] = {}
for pair_id, pair_rows in by_pair.items():
vuln_row = next((r for r in pair_rows if r["target"] == 1), pair_rows[0])
norm = strip_comments_and_whitespace(vuln_row["func_vuln"])
sh = shingles(norm, n=5)
mh = MinHash(num_perm=MINHASH_PERM)
for s in sh:
mh.update(s.encode("utf-8"))
minhashes[pair_id] = mh
lsh.insert(pair_id, mh)
visited: set[str] = set()
groups: list[set[str]] = []
for pair_id, mh in minhashes.items():
if pair_id in visited:
continue
matches = set(lsh.query(mh))
if len(matches) > 1:
groups.append(matches)
visited.update(matches)
return groups