SVSTR-Score / score_svstr.py
khyeom's picture
Release v1.0: HPRC-trained 35/21-feature calibrated SV+STR models (#1)
3c7d0d1
Raw
History Blame Contribute Delete
3.67 kB
#!/usr/bin/env python3
"""
score_svstr.py — apply the SVSTR-Score confidence model to short-read SV or STR
calls and emit a per-call calibrated confidence score (CS) and tier.
Inference entry point for the released models (sv_model.joblib / str_model.joblib,
each paired with an isotonic calibrator). It loads the trained random forest + its
isotonic calibrator + the feature/config sidecar, then scores a tabular feature
matrix produced by `feature_builder.py` from the caller VCF (short-read VCF +
reference FASTA + static annotation BEDs):
CS = isotonic_calibrator( RF.predict_proba(X)[:, 1] ) # P(concordant)
TIERS:
HIGH CS >= 0.70 (candidate-triage filter)
MODERATE 0.50 <= CS < 0.70
WARNING 0.30 <= CS < 0.50
LOW CS < 0.30
The score is isotonic-calibrated, so the tier is a pure bucket of the calibrated
CS — there are no heuristic override rules, and STR needs no per-locus catalogue
lookup (its features are self-contained). Missing features (fields a merged or
filtered callset may not carry) are filled with the -99999 sentinel that the
trees were trained to split on.
USAGE
python score_svstr.py --variant sv --model-dir . --features sv_features.tsv --out sv_scored.tsv
python score_svstr.py --variant str --model-dir . --features str_features.tsv --out str_scored.tsv
Requires the versions in requirements.txt (scikit-learn==1.7.1). Licence: MIT.
"""
import argparse, json, os, sys
import numpy as np
import pandas as pd
import joblib
MISSING = -99999.0
TIER_EDGES = (0.30, 0.50, 0.70)
TIER_NAMES = ("LOW", "WARNING", "MODERATE", "HIGH")
def to_tier(cs):
return np.asarray(TIER_NAMES)[np.digitize(np.asarray(cs, float), TIER_EDGES, right=False)]
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--variant", choices=["sv", "str"], required=True)
ap.add_argument("--model-dir", default=".", help="dir with *_config.json + *.joblib")
ap.add_argument("--features", required=True,
help="feature table from feature_builder.py (tsv/csv[.gz])")
ap.add_argument("--out", required=True)
ap.add_argument("--raw", action="store_true", help="also emit the uncalibrated RF score (CS_raw)")
a = ap.parse_args()
cfg = json.load(open(os.path.join(a.model_dir, f"{a.variant}_config.json")))
model = joblib.load(os.path.join(a.model_dir, cfg["model_file"]))
cal = joblib.load(os.path.join(a.model_dir, cfg["calibrator_file"]))
feats = cfg["features"]
sep = "\t" if a.features.endswith((".tsv", ".txt", ".gz")) else ","
df = pd.read_csv(a.features, sep=sep)
absent = [f for f in feats if f not in df.columns]
if absent:
print(f"[warn] {len(absent)} model features absent -> -99999 sentinel: {absent}",
file=sys.stderr)
for f in absent:
df[f] = MISSING
X = df[feats].astype("float32")
p = model.predict_proba(X)
raw = p[:, list(model.classes_).index(1)] if p.shape[1] > 1 else p[:, 0]
cs = np.clip(cal.predict(raw), 0.0, 1.0)
out = df.copy()
if a.raw:
out["CS_raw"] = np.round(raw, 4)
out["CS"] = np.round(cs, 4)
out["tier"] = to_tier(cs)
out.to_csv(a.out, sep="\t", index=False)
n = len(out)
vc = pd.Series(out["tier"]).value_counts()
hi = int(vc.get("HIGH", 0))
print(f"{a.variant.upper()}: scored {n:,} calls; HIGH {hi:,} ({hi/n:.1%}) | "
+ " ".join(f"{t}:{int(vc.get(t,0)):,}" for t in TIER_NAMES), file=sys.stderr)
print(f"wrote {a.out}", file=sys.stderr)
if __name__ == "__main__":
main()