Download src/decision.py from HarryDataScience/sentinel-fraud-detection: direct link, hf CLI and curl.
- Browser
- Download file 2.29 kB
-
https://huggingface.co/spaces/HarryDataScience/sentinel-fraud-detection/resolve/main/src/decision.py
- Command line
-
hf download hf://spaces/HarryDataScience/sentinel-fraud-detection/src/decision.py
-
curl -L -o decision.py https://huggingface.co/spaces/HarryDataScience/sentinel-fraud-detection/resolve/main/src/decision.py
2.29 kB
| """Cost-based decision threshold and risk bands.""" | |
| import numpy as np | |
| def optimal_threshold(y_true, proba, cost_fn: float, cost_fp: float) -> dict: | |
| """Pick the threshold that minimises FN*cost_fn + FP*cost_fp. | |
| A transaction is flagged when proba >= threshold. Candidate thresholds are | |
| the boundaries between distinct score values (midpoints), so the chosen | |
| threshold is not glued to a single validation score. | |
| """ | |
| y = np.asarray(y_true).astype(int) | |
| p = np.asarray(proba, dtype=float) | |
| if y.shape != p.shape or y.size == 0: | |
| raise ValueError("y_true and proba must be non-empty and the same length") | |
| n_pos = int(y.sum()) | |
| baseline = n_pos * cost_fn # cost of flagging nothing | |
| order = np.argsort(-p, kind="stable") | |
| ps, ys = p[order], y[order] | |
| tp = np.cumsum(ys) | |
| fp = np.cumsum(1 - ys) | |
| last_of_group = np.where(np.r_[ps[1:] != ps[:-1], True])[0] | |
| cost = (n_pos - tp[last_of_group]) * cost_fn + fp[last_of_group] * cost_fp | |
| best = int(np.argmin(cost)) | |
| if cost[best] >= baseline: | |
| # Flagging anything costs more than it saves: flag nothing. | |
| return {"threshold": 1.0, "expected_cost": float(baseline), "baseline_cost": float(baseline)} | |
| idx = last_of_group[best] | |
| if idx + 1 < len(ps): | |
| threshold = float((ps[idx] + ps[idx + 1]) / 2.0) | |
| else: | |
| threshold = float(ps[idx] / 2.0) | |
| return {"threshold": threshold, "expected_cost": float(cost[best]), "baseline_cost": float(baseline)} | |
| def expected_cost(y_true, flagged, cost_fn: float, cost_fp: float) -> float: | |
| y = np.asarray(y_true).astype(int) | |
| f = np.asarray(flagged).astype(bool) | |
| fn = int(((y == 1) & ~f).sum()) | |
| fp = int(((y == 0) & f).sum()) | |
| return float(fn * cost_fn + fp * cost_fp) | |
| def risk_band_edges(threshold: float) -> dict: | |
| """Risk bands derive from the decision threshold t: | |
| LOW < t/2 <= MEDIUM < t <= HIGH. HIGH is exactly the 'flagged as fraud' set. | |
| """ | |
| return {"medium_from": threshold * 0.5, "high_from": threshold} | |
| def risk_level(probability: float, threshold: float) -> str: | |
| edges = risk_band_edges(threshold) | |
| if probability >= edges["high_from"]: | |
| return "HIGH" | |
| if probability >= edges["medium_from"]: | |
| return "MEDIUM" | |
| return "LOW" | |