foo
This commit is contained in:
parent
d50a0fcbb1
commit
94f910d978
3 changed files with 227 additions and 35 deletions
159
metrics_stats.py
159
metrics_stats.py
|
|
@ -3,13 +3,24 @@
|
|||
import json
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from scipy import sparse
|
||||
from scipy.sparse.linalg import spsolve
|
||||
from scipy.stats import pearsonr
|
||||
import rasterio
|
||||
from rasterio.warp import transform as transform_coords
|
||||
|
||||
from metrics_indices import BLUE_BAND, GREEN_BAND, RED_BAND
|
||||
|
||||
WHITTAKER_LAMBDA_DAYS_SQ = 400.0
|
||||
|
||||
|
||||
def _norm_date_key(s):
|
||||
if s is None:
|
||||
return None
|
||||
t = str(s).strip()
|
||||
return t.split("T")[0][:10] if "T" in t else t[:10]
|
||||
|
||||
|
||||
def load_timeseries(filepath):
|
||||
"""Load JSON timeseries and return dict mapping date -> value."""
|
||||
|
|
@ -22,14 +33,24 @@ def load_timeseries(filepath):
|
|||
|
||||
def match_dates(fusion_ts, phenocam_ts):
|
||||
"""Match dates between timeseries, return aligned numpy arrays (filter None values)."""
|
||||
common_dates = set(fusion_ts.keys()) & set(phenocam_ts.keys())
|
||||
|
||||
def _bundle(m):
|
||||
out = {}
|
||||
for k, v in m.items():
|
||||
nk = _norm_date_key(k)
|
||||
if nk and nk not in out:
|
||||
out[nk] = v
|
||||
return out
|
||||
|
||||
fa, pa = _bundle(fusion_ts), _bundle(phenocam_ts)
|
||||
common_dates = set(fa) & set(pa)
|
||||
fusion_vals = []
|
||||
phenocam_vals = []
|
||||
dates = []
|
||||
|
||||
for date in sorted(common_dates):
|
||||
fusion_val = fusion_ts[date]
|
||||
phenocam_val = phenocam_ts[date]
|
||||
fusion_val = fa[date]
|
||||
phenocam_val = pa[date]
|
||||
if fusion_val is not None and phenocam_val is not None:
|
||||
fusion_vals.append(fusion_val)
|
||||
phenocam_vals.append(phenocam_val)
|
||||
|
|
@ -94,19 +115,21 @@ def nse(y_true, y_pred):
|
|||
|
||||
|
||||
def calculate_temporal_metrics(fusion_ts, phenocam_ts):
|
||||
"""Calculate all 6 temporal metrics."""
|
||||
"""Temporal metrics vs PhenoCam (nse_pc; nse is the same value)."""
|
||||
fusion_vals, phenocam_vals, dates = match_dates(fusion_ts, phenocam_ts)
|
||||
|
||||
if len(fusion_vals) < 2:
|
||||
return None
|
||||
|
||||
n_pc = nse(phenocam_vals, fusion_vals)
|
||||
metrics = {
|
||||
"pearson_r": pearson_correlation(phenocam_vals, fusion_vals),
|
||||
"r_squared": r_squared(phenocam_vals, fusion_vals),
|
||||
"rmse": rmse(phenocam_vals, fusion_vals),
|
||||
"mae": mae(phenocam_vals, fusion_vals),
|
||||
"nrmse": nrmse(phenocam_vals, fusion_vals),
|
||||
"nse": nse(phenocam_vals, fusion_vals),
|
||||
"nse_pc": n_pc,
|
||||
"nse": n_pc,
|
||||
"n_samples": len(fusion_vals),
|
||||
"date_range": {"start": dates[0], "end": dates[-1]} if dates else None,
|
||||
}
|
||||
|
|
@ -129,6 +152,59 @@ def calculate_phenocam_stats(phenocam_ts):
|
|||
}
|
||||
|
||||
|
||||
def _s2_kept_date_set(base: Path, strategy: str) -> set:
|
||||
path = base / "raw" / "preselection" / "s2_preselection.json"
|
||||
if not path.exists():
|
||||
return set()
|
||||
with open(path) as f:
|
||||
rows = json.load(f)
|
||||
key = f"excluded_{strategy}"
|
||||
out = set()
|
||||
for e in rows:
|
||||
if e.get(key):
|
||||
continue
|
||||
nk = _norm_date_key(e.get("date"))
|
||||
if nk:
|
||||
out.add(nk)
|
||||
return out
|
||||
|
||||
|
||||
def _whittaker_smooth_dict(obs_dates, obs_values, lam: float, n_min: int = 3):
|
||||
"""Daily Whittaker (weights 1 at obs); returns {YYYY-MM-DD: z}."""
|
||||
pairs = [
|
||||
(_norm_date_key(d), float(v))
|
||||
for d, v in zip(obs_dates, obs_values)
|
||||
if v is not None and _norm_date_key(d)
|
||||
]
|
||||
if len(pairs) < 2:
|
||||
return {}
|
||||
days = sorted({p[0] for p in pairs})
|
||||
t0 = datetime.strptime(days[0], "%Y-%m-%d").date()
|
||||
t1 = datetime.strptime(days[-1], "%Y-%m-%d").date()
|
||||
n = (t1 - t0).days + 1
|
||||
if n < n_min:
|
||||
return {}
|
||||
|
||||
w = np.zeros(n)
|
||||
y = np.zeros(n)
|
||||
for dk, val in pairs:
|
||||
i = (datetime.strptime(dk, "%Y-%m-%d").date() - t0).days
|
||||
if 0 <= i < n:
|
||||
w[i] = 1.0
|
||||
y[i] = val
|
||||
|
||||
D = sparse.diags(
|
||||
[1.0, -2.0, 1.0], [0, 1, 2], shape=(n - 2, n), format="csc", dtype=np.float64
|
||||
)
|
||||
H = D.T @ D
|
||||
Wm = sparse.diags(w.astype(np.float64), format="csc")
|
||||
z = spsolve(Wm + lam * H, w * y)
|
||||
out = {}
|
||||
for i in range(n):
|
||||
out[(t0 + timedelta(days=i)).isoformat()] = float(z[i])
|
||||
return out
|
||||
|
||||
|
||||
def _get_spatial_stats_from_raster(raster_file, site_position):
|
||||
"""Extract spatial statistics (mean, std, min, max) from GCC raster in 3x3 window."""
|
||||
try:
|
||||
|
|
@ -316,15 +392,52 @@ def calculate_all_metrics(season, site_name, site_position):
|
|||
if phenocam_stats:
|
||||
results["phenocam_stats"] = phenocam_stats
|
||||
|
||||
# Calculate S2 baseline metrics once (S2 data is identical across scenarios)
|
||||
s2_ts_path = (
|
||||
base / "processed_aggressive_sigma20" / "gcc" / "s2" / "timeseries.json"
|
||||
)
|
||||
s2_ts = load_timeseries(s2_ts_path)
|
||||
baseline = {}
|
||||
s2_ts = {}
|
||||
for sub in ("processed_aggressive_sigma20", "processed_nonaggressive_sigma20"):
|
||||
p = base / sub / "gcc" / "s2" / "timeseries.json"
|
||||
if p.exists():
|
||||
s2_ts = load_timeseries(p)
|
||||
if s2_ts:
|
||||
s2_metrics = calculate_temporal_metrics(s2_ts, phenocam_ts)
|
||||
if s2_metrics:
|
||||
results["baseline"] = {"s2": s2_metrics}
|
||||
break
|
||||
if s2_ts:
|
||||
m0 = calculate_temporal_metrics(s2_ts, phenocam_ts)
|
||||
if m0:
|
||||
baseline["s2"] = m0
|
||||
for strategy in ("aggressive", "nonaggressive"):
|
||||
kept = _s2_kept_date_set(base, strategy)
|
||||
filtered = [
|
||||
(k, v)
|
||||
for k, v in sorted(
|
||||
s2_ts.items(), key=lambda x: _norm_date_key(x[0]) or ""
|
||||
)
|
||||
if _norm_date_key(k) in kept and v is not None
|
||||
]
|
||||
if not filtered:
|
||||
continue
|
||||
sub_ts = dict(filtered)
|
||||
mcf = calculate_temporal_metrics(sub_ts, phenocam_ts)
|
||||
if mcf:
|
||||
baseline.setdefault("s2_cloudfree", {})[strategy] = mcf
|
||||
obs_d, obs_v = zip(*filtered)
|
||||
smooth = _whittaker_smooth_dict(obs_d, obs_v, WHITTAKER_LAMBDA_DAYS_SQ)
|
||||
if smooth:
|
||||
mw = calculate_temporal_metrics(smooth, phenocam_ts)
|
||||
if mw:
|
||||
baseline.setdefault("s2_whittaker_lambda400", {})[strategy] = mw
|
||||
|
||||
for strategy in ("aggressive", "nonaggressive"):
|
||||
p = base / f"processed_{strategy}_sigma20" / "gcc" / "s3" / "timeseries.json"
|
||||
if not p.exists():
|
||||
continue
|
||||
s3_ts = load_timeseries(p)
|
||||
if s3_ts:
|
||||
m3 = calculate_temporal_metrics(s3_ts, phenocam_ts)
|
||||
if m3:
|
||||
baseline.setdefault("s3", {})[strategy] = m3
|
||||
|
||||
if baseline:
|
||||
results["baseline"] = baseline
|
||||
|
||||
# Calculate fusion metrics for each scenario
|
||||
for strategy in ["aggressive", "nonaggressive"]:
|
||||
|
|
@ -378,15 +491,17 @@ def calculate_all_metrics(season, site_name, site_position):
|
|||
if spatial_metrics:
|
||||
results["spatial"][scenario_name] = spatial_metrics
|
||||
|
||||
# Add summary
|
||||
# Add summary (primary: NSE vs PhenoCam; R² kept for comparison)
|
||||
if results["temporal"]:
|
||||
best_temporal = max(
|
||||
results["temporal"].items(),
|
||||
key=lambda x: x[1].get("r_squared", -1)
|
||||
if x[1].get("r_squared") is not None
|
||||
else -1,
|
||||
)
|
||||
results["summary"] = {"best_temporal_scenario": best_temporal[0]}
|
||||
ti = list(results["temporal"].items())
|
||||
|
||||
def _score(k):
|
||||
return lambda x: x[1].get(k) if x[1].get(k) is not None else float("-inf")
|
||||
|
||||
results["summary"] = {
|
||||
"best_temporal_scenario": max(ti, key=_score("nse_pc"))[0],
|
||||
"best_temporal_scenario_by_r2": max(ti, key=_score("r_squared"))[0],
|
||||
}
|
||||
|
||||
if results["spatial"]:
|
||||
best_spatial = max(
|
||||
|
|
|
|||
|
|
@ -561,8 +561,8 @@
|
|||
|
||||
const scenarios = ["aggressive_sigma20", "aggressive_sigma30", "nonaggressive_sigma20", "nonaggressive_sigma30"];
|
||||
const scenarioNames = ["Aggressive σ20", "Aggressive σ30", "Non-aggressive σ20", "Non-aggressive σ30"];
|
||||
const metrics = ["pearson_r", "r_squared", "rmse", "mae", "nrmse", "nse"];
|
||||
const metricLabels = { pearson_r: "r", r_squared: "R²", rmse: "RMSE", mae: "MAE", nrmse: "nRMSE", nse: "NSE" };
|
||||
const metrics = ["pearson_r", "r_squared", "rmse", "mae", "nrmse", "nse_pc"];
|
||||
const metricLabels = { pearson_r: "r", r_squared: "R²", rmse: "RMSE", mae: "MAE", nrmse: "nRMSE", nse_pc: "NSE_PC" };
|
||||
|
||||
let html = "<table style='width:100%; border-collapse:collapse; font-size:11px;'>";
|
||||
html += "<thead><tr style='background:#f5f5f5; border-bottom:2px solid #ccc;'>";
|
||||
|
|
@ -576,8 +576,8 @@
|
|||
html += `<tr style='border-bottom:2px solid #ccc; background:#f9f9f9;'>`;
|
||||
html += `<td style='padding:6px 8px; font-weight:600;'>S2 (baseline)</td>`;
|
||||
metrics.forEach(m => {
|
||||
const val = data[m];
|
||||
const fmt = val !== null && val !== undefined ? (m === "pearson_r" || m === "r_squared" || m === "nse" ? val.toFixed(3) : val.toFixed(4)) : "—";
|
||||
const val = m === "nse_pc" ? (data.nse_pc ?? data.nse) : data[m];
|
||||
const fmt = val !== null && val !== undefined ? (m === "pearson_r" || m === "r_squared" || m === "nse_pc" ? val.toFixed(3) : val.toFixed(4)) : "—";
|
||||
html += `<td style='padding:6px 8px; text-align:right; font-family:monospace;'>${fmt}</td>`;
|
||||
});
|
||||
html += "</tr>";
|
||||
|
|
@ -590,8 +590,8 @@
|
|||
html += `<tr style='border-bottom:1px solid #eee;'>`;
|
||||
html += `<td style='padding:6px 8px; font-weight:500;'>${scenarioNames[i]}</td>`;
|
||||
metrics.forEach(m => {
|
||||
const val = data[m];
|
||||
const fmt = val !== null && val !== undefined ? (m === "pearson_r" || m === "r_squared" || m === "nse" ? val.toFixed(3) : val.toFixed(4)) : "—";
|
||||
const val = m === "nse_pc" ? (data.nse_pc ?? data.nse) : data[m];
|
||||
const fmt = val !== null && val !== undefined ? (m === "pearson_r" || m === "r_squared" || m === "nse_pc" ? val.toFixed(3) : val.toFixed(4)) : "—";
|
||||
html += `<td style='padding:6px 8px; text-align:right; font-family:monospace;'>${fmt}</td>`;
|
||||
});
|
||||
html += "</tr>";
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
th, td { border: 1px solid #ccc; padding: 6px 8px; text-align: left; }
|
||||
th { background: #f5f5f5; }
|
||||
td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.compare-note { font-size: 12px; color: #555; margin: 0 0 8px 0; max-width: 720px; }
|
||||
.empty { color: #666; font-style: italic; }
|
||||
.err { color: #a00; }
|
||||
</style>
|
||||
|
|
@ -42,7 +43,12 @@
|
|||
<div id="content"></div>
|
||||
</div>
|
||||
<script>
|
||||
const METRIC_COLS = ["pearson_r", "r_squared", "rmse", "mae", "nrmse", "nse", "n_samples"];
|
||||
const METRIC_COLS = ["pearson_r", "r_squared", "rmse", "mae", "nrmse", "nse_pc", "n_samples"];
|
||||
/** Spatial fusion metrics in metrics.json (no RMSE block at site level). */
|
||||
const SPATIAL_METRIC_COLS = ["pearson_r", "r_squared", "n_samples"];
|
||||
function mv(m, c) {
|
||||
return c === "nse_pc" ? (m.nse_pc ?? m.nse) : m[c];
|
||||
}
|
||||
let siteName = "innsbruck", season = "2024";
|
||||
let availableSiteSeasons = {};
|
||||
const urlParams = new URLSearchParams(location.search);
|
||||
|
|
@ -67,11 +73,75 @@
|
|||
let head = `<tr><th>Scenario</th>${METRIC_COLS.map((c) => `<th>${c}</th>`).join("")}</tr>`;
|
||||
const rows = keys.map((k) => {
|
||||
const m = obj[k] || {};
|
||||
return `<tr><td>${k}</td>${METRIC_COLS.map((c) => `<td class="num">${fmt(m[c])}</td>`).join("")}</tr>`;
|
||||
return `<tr><td>${k}</td>${METRIC_COLS.map((c) => `<td class="num">${fmt(mv(m, c))}</td>`).join("")}</tr>`;
|
||||
}).join("");
|
||||
return `<h2>${title}</h2><table>${head}${rows}</table>`;
|
||||
}
|
||||
|
||||
/** Pair BtI keys (`aggressive_sigma20`) with ItB (`aggressive_sigma20_itb`). */
|
||||
function btiItbPairs(obj) {
|
||||
if (!obj || typeof obj !== "object") return [];
|
||||
const pairs = [];
|
||||
for (const itbKey of Object.keys(obj)) {
|
||||
if (!itbKey.endsWith("_itb")) continue;
|
||||
const btiKey = itbKey.slice(0, -"_itb".length);
|
||||
const bti = obj[btiKey];
|
||||
const itb = obj[itbKey];
|
||||
if (!bti || !itb) continue;
|
||||
pairs.push({ label: btiKey, bti, itb });
|
||||
}
|
||||
pairs.sort((a, b) => a.label.localeCompare(b.label));
|
||||
return pairs;
|
||||
}
|
||||
|
||||
function fmtDelta(btiM, itbM, col) {
|
||||
const a = mv(btiM, col);
|
||||
const b = mv(itbM, col);
|
||||
if (a == null || b == null || typeof a !== "number" || typeof b !== "number") return "—";
|
||||
return fmt(b - a);
|
||||
}
|
||||
|
||||
function btiItbCompareSection(title, obj, blurb, metricCols = METRIC_COLS) {
|
||||
const pairs = btiItbPairs(obj);
|
||||
if (!pairs.length) return "";
|
||||
const subHead = metricCols.map(
|
||||
() => `<th class="num">BtI</th><th class="num">ItB</th><th class="num">Δ</th>`
|
||||
).join("");
|
||||
const head =
|
||||
`<tr><th rowspan="2">Scenario</th>${metricCols.map((c) => `<th colspan="3">${c}</th>`).join("")}</tr>` +
|
||||
`<tr>${subHead}</tr>`;
|
||||
const rows = pairs
|
||||
.map((p) => {
|
||||
const cells = metricCols.map((c) => {
|
||||
const vB = fmt(mv(p.bti, c));
|
||||
const vI = fmt(mv(p.itb, c));
|
||||
const d = fmtDelta(p.bti, p.itb, c);
|
||||
return `<td class="num">${vB}</td><td class="num">${vI}</td><td class="num">${d}</td>`;
|
||||
}).join("");
|
||||
return `<tr><td>${p.label}</td>${cells}</tr>`;
|
||||
})
|
||||
.join("");
|
||||
return `<h2>${title}</h2><p class="compare-note">${blurb}</p><table>${head}${rows}</table>`;
|
||||
}
|
||||
|
||||
function baselineSection(b) {
|
||||
if (!b || typeof b !== "object") return "";
|
||||
const rows = [];
|
||||
const pushRow = (label, m) => {
|
||||
if (!m || typeof m !== "object") return;
|
||||
rows.push(`<tr><td>${label}</td>${METRIC_COLS.map((c) => `<td class="num">${fmt(mv(m, c))}</td>`).join("")}</tr>`);
|
||||
};
|
||||
pushRow("S2 GCC (all acquisitions)", b.s2);
|
||||
for (const strat of ["aggressive", "nonaggressive"]) {
|
||||
pushRow(`S3 composite GCC (${strat})`, b.s3?.[strat]);
|
||||
pushRow(`S2 GCC cloud-screened (${strat})`, b.s2_cloudfree?.[strat]);
|
||||
pushRow(`S2 Whittaker λ=400 (${strat})`, b.s2_whittaker_lambda400?.[strat]);
|
||||
}
|
||||
if (!rows.length) return "";
|
||||
const head = `<tr><th>Baseline</th>${METRIC_COLS.map((c) => `<th>${c}</th>`).join("")}</tr>`;
|
||||
return `<h2>Baselines (temporal vs PhenoCam)</h2><table>${head}${rows.join("")}</table>`;
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
const el = document.getElementById("content");
|
||||
if (!data) {
|
||||
|
|
@ -84,12 +154,19 @@
|
|||
const p = data.phenocam_stats;
|
||||
html += `<td class="num">${fmt(p.mean)}</td><td class="num">${fmt(p.std)}</td><td class="num">${fmt(p.min)}</td><td class="num">${fmt(p.max)}</td><td class="num">${fmt(p.n_samples)}</td></tr></table>`;
|
||||
}
|
||||
if (data.baseline && data.baseline.s2) {
|
||||
html += `<h2>Baseline S2 (temporal)</h2><table><tr>${METRIC_COLS.map((c) => `<th>${c}</th>`).join("")}</tr><tr>`;
|
||||
const b = data.baseline.s2;
|
||||
html += METRIC_COLS.map((c) => `<td class="num">${fmt(b[c])}</td>`).join("") + "</tr></table>";
|
||||
}
|
||||
html += baselineSection(data.baseline);
|
||||
html += btiItbCompareSection(
|
||||
"Temporal: BtI vs ItB (paired)",
|
||||
data.temporal,
|
||||
"Δ = ItB − BtI. For Pearson r, R², and NSE (%), positive Δ means ItB is higher. For RMSE, MAE, and NRMSE, negative Δ means ItB is better (lower error)."
|
||||
);
|
||||
html += tableSection("Temporal (vs PhenoCam)", data.temporal);
|
||||
html += btiItbCompareSection(
|
||||
"Spatial: BtI vs ItB (paired)",
|
||||
data.spatial,
|
||||
"Δ = ItB − BtI. For Pearson r and R², positive Δ means ItB is higher.",
|
||||
SPATIAL_METRIC_COLS
|
||||
);
|
||||
html += tableSection("Spatial (3×3 fusion mean vs PhenoCam)", data.spatial);
|
||||
if (data.summary) {
|
||||
html += `<h2>Summary</h2><pre style="font-size:13px;background:#f9f9f9;padding:10px;">${JSON.stringify(data.summary, null, 2)}</pre>`;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue