MeteoNorm-RF / scripts /result.py
zhangrenchao's picture
Publish MeteoNorm-RF reproduction
b11ef36 verified
Raw
History Blame Contribute Delete
4.36 kB
"""Compute paper metrics, Theil-Sen trends, and the comparison figure."""
import json
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import yaml
ROOT = Path(__file__).resolve().parents[1]
def metrics(observed, predicted):
error = predicted - observed
denominator = np.maximum(np.sum(observed, axis=0), 1e-12)
r2_denominator = np.maximum(np.sum((observed - observed.mean(0)) ** 2, axis=0), 1e-12)
ioa_denominator = np.maximum(np.sum((np.abs(predicted - observed.mean(0)) +
np.abs(observed - observed.mean(0))) ** 2, axis=0), 1e-12)
return {"RMSE": np.sqrt(np.mean(error ** 2, axis=0)),
"r2": 1 - np.sum(error ** 2, axis=0) / r2_denominator,
"FAC2": np.mean((predicted / np.maximum(observed, 1e-12) >= .5) &
(predicted / np.maximum(observed, 1e-12) <= 2), axis=0),
"MB": np.mean(error, axis=0), "MGE": np.mean(np.abs(error), axis=0),
"NMB": np.sum(error, axis=0) / denominator,
"NMGE": np.sum(np.abs(error), axis=0) / denominator,
"COE": 1 - np.sum(np.abs(error), axis=0) /
np.maximum(np.sum(np.abs(observed - observed.mean(0)), axis=0), 1e-12),
"IOA": 1 - np.sum(error ** 2, axis=0) / ioa_denominator}
def theil_sen(x, y, max_pairs=200000, seed=0):
"""Median pairwise slope, with deterministic subsampling for large series."""
x, y = np.asarray(x, float), np.asarray(y, float)
total = len(x) * (len(x) - 1) // 2
if total <= max_pairs:
slopes = [(y[j] - y[i]) / (x[j] - x[i]) for i in range(len(x) - 1)
for j in range(i + 1, len(x)) if x[j] != x[i]]
else:
rng = np.random.default_rng(seed)
left = rng.integers(0, len(x) - 1, max_pairs)
right = rng.integers(left + 1, len(x), max_pairs)
valid = x[right] != x[left]
slopes = (y[right[valid]] - y[left[valid]]) / (x[right[valid]] - x[left[valid]])
slope = float(np.median(slopes))
return slope, float(np.median(y - slope * x))
def main():
config = yaml.safe_load((ROOT / "conf/config.yaml").read_text())
data = np.load(ROOT / config["paths"]["inference"])
names = [str(value) for value in data["pollutant_names"]]
scores = metrics(data["observed"], data["predicted"])
report = {"metrics": {name: {metric: float(values[i]) for metric, values in scores.items()}
for i, name in enumerate(names)}, "theil_sen_per_year": {}}
trends = []
for i, name in enumerate(names):
raw = theil_sen(data["ttrend"], data["observed"][:, i], seed=i)
normalized = theil_sen(data["ttrend"], data["normalized"][:, i], seed=100 + i)
report["theil_sen_per_year"][name] = {"observed_slope": raw[0],
"normalized_slope": normalized[0]}
trends.append((raw[0], normalized[0]))
output = ROOT / config["paths"]["evaluation_dir"]
output.mkdir(parents=True, exist_ok=True)
(output / "metrics.json").write_text(json.dumps(report, indent=2) + "\n")
figure, axes = plt.subplots(2, 1, figsize=(11, 8), gridspec_kw={"height_ratios": [2, 1]})
order = np.argsort(data["ttrend"])
sample = order[::max(1, len(order) // 600)]
axes[0].plot(data["ttrend"][sample], data["observed"][sample, 0], ".", alpha=.25, label="observed PM2.5")
axes[0].plot(data["ttrend"][sample], data["normalized"][sample, 0], ".", alpha=.4, label="weather-normalized PM2.5")
axes[0].set(ylabel="Concentration (ug m-3)", title="Observed and meteorologically normalized test samples")
axes[0].legend()
positions = np.arange(len(names)); width = .36
axes[1].bar(positions - width / 2, [v[0] for v in trends], width, label="observed")
axes[1].bar(positions + width / 2, [v[1] for v in trends], width, label="normalized")
axes[1].axhline(0, color="black", linewidth=.7); axes[1].set_xticks(positions, names)
axes[1].set(ylabel="Theil-Sen slope per year", title="Robust concentration trends")
axes[1].legend(); figure.tight_layout(); figure.savefig(output / "comparison.png", dpi=160)
plt.close(figure)
print(f"evaluation={output.relative_to(ROOT)} pollutants={len(names)}")
if __name__ == "__main__":
main()