| """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() |
|
|