Download src/data/build_splits.py from Dhruv1000/tracegrpo: direct link, hf CLI and curl.
- Browser
- Download file 6.05 kB
-
https://huggingface.co/Dhruv1000/tracegrpo/resolve/main/src/data/build_splits.py
- Command line
-
hf download hf://Dhruv1000/tracegrpo/src/data/build_splits.py
-
curl -L -o build_splits.py https://huggingface.co/Dhruv1000/tracegrpo/resolve/main/src/data/build_splits.py
6.05 kB
| """Phase 1 §3.3: builds the three split regimes (random, chronological, | |
| repo-disjoint) plus the OOD manifest placeholder, at the pair level so the | |
| pair-integrity constraint (§3.3.4: a pair_id never crosses partitions) holds | |
| by construction. | |
| Each split is written to data/splits/<regime>/{train,valid,test}.json as a | |
| list of pair_id (not sample_id — both halves of a pair always travel | |
| together, so pair_id is the right unit of assignment and the smaller file). | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import random | |
| from collections import defaultdict | |
| from pathlib import Path | |
| import pandas as pd | |
| SPLITS_DIR = Path("data/splits") | |
| def _pair_table(df: pd.DataFrame) -> pd.DataFrame: | |
| """One row per pair_id, carrying the fields splits are built from.""" | |
| vuln_rows = df[df["target"] == 1].drop_duplicates("pair_id") | |
| return vuln_rows[["pair_id", "project", "source", "commit_date"]].reset_index(drop=True) | |
| def build_random_split( | |
| pairs: pd.DataFrame, seed: int = 42, ratios: tuple[float, float, float] = (0.8, 0.1, 0.1) | |
| ) -> dict[str, list[str]]: | |
| """80/10/10 by pair, stratified by source so each split keeps a | |
| comparable primevul/secvuleval mix (spec §3.3.1).""" | |
| rng = random.Random(seed) | |
| splits: dict[str, list[str]] = {"train": [], "valid": [], "test": []} | |
| for _source, group in pairs.groupby("source"): | |
| ids = group["pair_id"].tolist() | |
| rng.shuffle(ids) | |
| n = len(ids) | |
| n_train = int(n * ratios[0]) | |
| n_valid = int(n * ratios[1]) | |
| splits["train"].extend(ids[:n_train]) | |
| splits["valid"].extend(ids[n_train : n_train + n_valid]) | |
| splits["test"].extend(ids[n_train + n_valid :]) | |
| return splits | |
| def build_chronological_split( | |
| pairs: pd.DataFrame, | |
| train_before: str, | |
| valid_before: str, | |
| ) -> tuple[dict[str, list[str]], dict]: | |
| """Train on commit_date < train_before, valid in [train_before, valid_before), | |
| test >= valid_before (spec §3.3.2). Only pairs with a known commit_date are | |
| eligible; pairs without one are returned separately as `undated` so callers | |
| can decide whether to fold them into train or exclude them (we exclude by | |
| default — undated data must not leak into a label that claims to be | |
| time-ordered).""" | |
| dated = pairs[pairs["commit_date"].notna()].copy() | |
| # format="mixed": commit_date mixes two real formats across sources — | |
| # PrimeVul/Big-Vul gives plain "YYYY-MM-DD", SecVulEval/GitHub gives full | |
| # ISO-8601 with time+"Z". Without format="mixed", pandas infers ONE format | |
| # from the data and silently coerces every row in the other format to | |
| # NaT — found empirically: this was dropping 1,891 valid PrimeVul-dated | |
| # pairs (all plain-date strings) from the chronological split entirely, | |
| # not just mis-reporting a coverage stat. See DECISIONS.md. | |
| dated["commit_date"] = pd.to_datetime(dated["commit_date"], utc=True, format="mixed", errors="coerce") | |
| dated = dated[dated["commit_date"].notna()] | |
| train_cut = pd.Timestamp(train_before, tz="UTC") | |
| valid_cut = pd.Timestamp(valid_before, tz="UTC") | |
| train_ids = dated[dated["commit_date"] < train_cut]["pair_id"].tolist() | |
| valid_ids = dated[(dated["commit_date"] >= train_cut) & (dated["commit_date"] < valid_cut)]["pair_id"].tolist() | |
| test_ids = dated[dated["commit_date"] >= valid_cut]["pair_id"].tolist() | |
| stats = { | |
| "n_total_pairs": len(pairs), | |
| "n_dated_pairs": len(dated), | |
| "n_undated_pairs_excluded": len(pairs) - len(dated), | |
| "n_train": len(train_ids), | |
| "n_valid": len(valid_ids), | |
| "n_test": len(test_ids), | |
| "train_before": train_before, | |
| "valid_before": valid_before, | |
| } | |
| return {"train": train_ids, "valid": valid_ids, "test": test_ids}, stats | |
| def build_repo_disjoint_folds( | |
| pairs: pd.DataFrame, n_folds: int = 5, seed: int = 42 | |
| ) -> list[dict[str, list[str]]]: | |
| """Assigns every pair of a project to exactly one fold (greedy balanced by | |
| vulnerable-sample count per project; `pairs` here is already the | |
| one-row-per-pair, target=1 table, so "vulnerable-sample count" == pair | |
| count for that project). Returns `n_folds` {train, valid, test} splits, | |
| each using one fold as test, the next as valid, the rest as train | |
| (spec §3.3.3: "5 folds stored for cross-validation").""" | |
| by_project: dict[str, list[str]] = defaultdict(list) | |
| for row in pairs.itertuples(): | |
| by_project[row.project].append(row.pair_id) | |
| rng = random.Random(seed) | |
| projects = list(by_project.keys()) | |
| rng.shuffle(projects) | |
| projects.sort(key=lambda p: -len(by_project[p])) # largest first, greedy balance | |
| fold_sizes = [0] * n_folds | |
| fold_projects: list[list[str]] = [[] for _ in range(n_folds)] | |
| for project in projects: | |
| i = min(range(n_folds), key=lambda k: fold_sizes[k]) | |
| fold_projects[i].append(project) | |
| fold_sizes[i] += len(by_project[project]) | |
| fold_pair_ids = [ | |
| [pid for project in fp for pid in by_project[project]] for fp in fold_projects | |
| ] | |
| folds = [] | |
| for k in range(n_folds): | |
| test_ids = fold_pair_ids[k] | |
| valid_ids = fold_pair_ids[(k + 1) % n_folds] | |
| train_ids = [ | |
| pid for j in range(n_folds) if j not in (k, (k + 1) % n_folds) for pid in fold_pair_ids[j] | |
| ] | |
| folds.append({"train": train_ids, "valid": valid_ids, "test": test_ids}) | |
| return folds | |
| def assert_pair_integrity(split: dict[str, list[str]]) -> None: | |
| train, valid, test = set(split["train"]), set(split["valid"]), set(split["test"]) | |
| assert not (train & valid), "pair_id leaked between train/valid" | |
| assert not (train & test), "pair_id leaked between train/test" | |
| assert not (valid & test), "pair_id leaked between valid/test" | |
| def write_split(name: str, split: dict[str, list[str]], out_dir: Path = SPLITS_DIR) -> None: | |
| d = out_dir / name | |
| d.mkdir(parents=True, exist_ok=True) | |
| for part, ids in split.items(): | |
| (d / f"{part}.json").write_text(json.dumps(sorted(ids))) | |