WoFS-StormCal / scripts /result.py
zhangrenchao's picture
Publish WoFS-StormCal engineering reproduction
fa2b79f verified
Raw
History Blame Contribute Delete
6.94 kB
"""Compute paper verification metrics and plot performance and reliability diagrams."""
import json
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import yaml
from sklearn.metrics import average_precision_score, roc_auc_score
ROOT = Path(__file__).resolve().parents[1]
def curves(probability, target, threshold_count):
thresholds = np.linspace(0, 1, threshold_count)
pod, pofd, sr, far, csi, bias = [], [], [], [], [], []
for threshold in thresholds:
forecast = probability >= threshold
event = target == 1
hits = np.sum(forecast & event); false_alarms = np.sum(forecast & ~event)
misses = np.sum(~forecast & event); negatives = np.sum(~forecast & ~event)
pod.append(hits / max(hits + misses, 1)); pofd.append(false_alarms / max(false_alarms + negatives, 1))
sr.append(hits / max(hits + false_alarms, 1)); far.append(false_alarms / max(hits + false_alarms, 1))
csi.append(hits / max(hits + false_alarms + misses, 1)); bias.append((hits + false_alarms) / max(hits + misses, 1))
return {key: np.asarray(value) for key, value in (("threshold", thresholds), ("pod", pod), ("pofd", pofd),
("sr", sr), ("far", far), ("csi", csi), ("bias", bias))}
def reliability(probability, target, bins):
edges = np.linspace(0, 1, bins + 1)
index = np.minimum(np.digitize(probability, edges[1:-1]), bins - 1)
records, component = [], 0.0
for bin_index in range(bins):
mask = index == bin_index
if not mask.any():
continue
forecast_mean, observed_frequency = float(probability[mask].mean()), float(target[mask].mean())
component += mask.mean() * (forecast_mean - observed_frequency) ** 2
records.append({"count": int(mask.sum()), "forecast_probability": forecast_mean,
"observed_frequency": observed_frequency})
return records, float(component)
def metrics(probability, target, threshold_count, bins):
curve = curves(probability, target, threshold_count)
prevalence = float(target.mean())
auc = float(roc_auc_score(target, probability)) if 0 < target.sum() < len(target) else 0.5
aupdc = float(average_precision_score(target, probability)) if target.sum() else 0.0
positives, negatives = int(target.sum()), int(len(target) - target.sum())
minimum_aupdc = float(np.mean(np.arange(1, positives + 1) / (np.arange(1, positives + 1) + negatives))) if positives else 0.0
best = int(np.argmax(curve["csi"])); max_csi = float(curve["csi"][best])
brier = float(np.mean((probability - target) ** 2))
reference = prevalence * (1 - prevalence)
records, reliability_component = reliability(probability, target, bins)
return {"samples": len(target), "event_rate": prevalence, "auc": auc, "aupdc": aupdc,
"minimum_aupdc": minimum_aupdc, "naupdc": (aupdc - minimum_aupdc) / max(1 - minimum_aupdc, 1e-12),
"max_csi": max_csi, "ncsi": (max_csi - prevalence) / max(1 - prevalence, 1e-12),
"max_csi_threshold": float(curve["threshold"][best]), "pod": float(curve["pod"][best]),
"pofd": float(curve["pofd"][best]), "sr": float(curve["sr"][best]), "far": float(curve["far"][best]),
"csi": max_csi, "bias": float(curve["bias"][best]), "brier_score": brier,
"brier_skill_score": 1 - brier / reference if reference > 0 else 0.0,
"reliability_component": reliability_component, "reliability_bins": records}, curve
def main():
config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
data = np.load(ROOT / config["paths"]["inference_dir"] / "predictions.npz")
if str(data["format_version"]) != config["data"]["format_version"]:
raise ValueError("incompatible prediction format")
probabilities, targets, groups = data["probabilities"], data["targets"], data["lead_group"]
if probabilities.shape != targets.shape or probabilities.shape[1:] != (3,):
raise ValueError("probabilities and targets must have shape [N,3]")
hazards, group_names = data["hazards"].tolist(), data["lead_group_names"].tolist()
report, all_curves = {}, {}
for group_index, group_name in enumerate(group_names):
report[group_name] = {}
mask = groups == group_index
for hazard_index, hazard in enumerate(hazards):
result, curve = metrics(probabilities[mask, hazard_index], targets[mask, hazard_index],
int(config["evaluation"]["threshold_count"]), int(config["evaluation"]["probability_bins"]))
report[group_name][hazard] = result; all_curves[(group_name, hazard)] = curve
numeric = [value for group in report.values() for hazard in group.values() for value in hazard.values()
if isinstance(value, (int, float))]
if not np.isfinite(numeric).all():
raise FloatingPointError("evaluation contains NaN or Inf")
output = ROOT / config["paths"]["evaluation_dir"]
output.mkdir(parents=True, exist_ok=True)
(output / "metrics.json").write_text(json.dumps({"by_lead_group_and_hazard": report,
"metric_protocol": "Flora et al. AUC, weighted-average-precision AUPDC, minimum-AUPDC NAUPDC, climatology-normalized max CSI, and Brier verification"}, indent=2) + "\n")
colors = {"tornado": "#b3261e", "hail": "#2e7d32", "wind": "#1565c0"}
figure, axes = plt.subplots(1, 2, figsize=(10, 4.5), sharex=True, sharey=True)
for axis, group_name in zip(axes, group_names):
for hazard in hazards:
curve = all_curves[(group_name, hazard)]
axis.plot(curve["sr"], curve["pod"], color=colors[hazard], label=hazard)
axis.set(xlim=(0, 1), ylim=(0, 1), xlabel="Success ratio", ylabel="Probability of detection", title=group_name.replace("_", " ").title())
axis.grid(alpha=0.25); axis.legend()
figure.tight_layout(); figure.savefig(output / "performance.png", dpi=160); plt.close(figure)
figure, axes = plt.subplots(1, 2, figsize=(10, 4.5), sharex=True, sharey=True)
for axis, group_name in zip(axes, group_names):
axis.plot((0, 1), (0, 1), "k--", linewidth=1, label="perfect")
for hazard in hazards:
bins = report[group_name][hazard]["reliability_bins"]
axis.plot([item["forecast_probability"] for item in bins], [item["observed_frequency"] for item in bins],
marker="o", color=colors[hazard], label=hazard)
axis.set(xlim=(0, 1), ylim=(0, 1), xlabel="Forecast probability", ylabel="Observed frequency", title=group_name.replace("_", " ").title())
axis.grid(alpha=0.25); axis.legend()
figure.tight_layout(); figure.savefig(output / "reliability.png", dpi=160); plt.close(figure)
print(f"evaluation={output.relative_to(ROOT)} groups={len(group_names)} hazards={len(hazards)}")
if __name__ == "__main__":
main()