From d55ee31e8d139514e25053f2b575b38f98656bb5 Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Wed, 17 Jun 2026 12:04:27 +0200 Subject: [PATCH] Added dropped sites. --- 6-statistics-fusion-order.py | 35 +++-- index.html | 292 +++++++++++++++++++++++++---------- 2 files changed, 229 insertions(+), 98 deletions(-) diff --git a/6-statistics-fusion-order.py b/6-statistics-fusion-order.py index 3a222cd..0ec0541 100644 --- a/6-statistics-fusion-order.py +++ b/6-statistics-fusion-order.py @@ -6,7 +6,8 @@ Inputs (``data/``, ``{year}`` = ``--evaluation-year``): Outputs (``data/statistics_fusion_order/``): -- ``{year}.json`` — paired Wilcoxon + t-test summary for NSE, RMSE, nRMSE, r +- ``{year}.json`` — paired Wilcoxon + t-test summary for NSE, RMSE, nRMSE, r; + includes ``dropped_sites`` (union) and per-metric ``dropped_sites`` lists CLI: @@ -50,48 +51,48 @@ def _r4(v: float | None) -> float | None: return round(v, 4) if v is not None else None -def _load_site_metrics(year: int) -> list[dict[str, Any]]: - """Return parsed ``metrics.json`` payloads for every site under ``{year}``.""" +def _load_site_metrics(year: int) -> list[tuple[str, dict[str, Any]]]: + """Return ``(sitename, metrics.json payload)`` for every site under ``{year}``.""" metrics_dir = DATA_DIR / "metrics" / str(year) if not metrics_dir.is_dir(): return [] - payloads: list[dict[str, Any]] = [] + payloads: list[tuple[str, dict[str, Any]]] = [] for site_dir in sorted(metrics_dir.iterdir()): if not site_dir.is_dir(): continue path = site_dir / "metrics.json" if not path.is_file(): continue - payloads.append(json.loads(path.read_text())) + payloads.append((site_dir.name, json.loads(path.read_text()))) return payloads def collect_pairs( - site_metrics: list[dict[str, Any]], metric: str -) -> tuple[list[float], list[float], int]: - """Return paired BtI / ItB values for ``metric`` and count of dropped sites.""" + site_metrics: list[tuple[str, dict[str, Any]]], metric: str +) -> tuple[list[float], list[float], list[str]]: + """Return paired BtI / ItB values for ``metric`` and dropped site names.""" bti_vals: list[float] = [] itb_vals: list[float] = [] - n_dropped = 0 + dropped_sites: list[str] = [] - for payload in site_metrics: + for site, payload in site_metrics: bti = payload.get("bti") itb = payload.get("itb") if not isinstance(bti, dict) or not isinstance(itb, dict): - n_dropped += 1 + dropped_sites.append(site) continue bti_v = bti.get(metric) itb_v = itb.get(metric) if bti_v is None or itb_v is None: - n_dropped += 1 + dropped_sites.append(site) continue bti_vals.append(float(bti_v)) itb_vals.append(float(itb_v)) - return bti_vals, itb_vals, n_dropped + return bti_vals, itb_vals, dropped_sites def _better_order( @@ -226,16 +227,20 @@ def main() -> None: ) metrics_out: dict[str, Any] = {} + all_dropped: set[str] = set() for metric in METRICS: - bti_vals, itb_vals, n_dropped = collect_pairs(site_metrics, metric) + bti_vals, itb_vals, dropped_sites = collect_pairs(site_metrics, metric) summary = paired_test(bti_vals, itb_vals, metric, alpha) - summary["n_dropped"] = n_dropped + summary["n_dropped"] = len(dropped_sites) + summary["dropped_sites"] = dropped_sites + all_dropped.update(dropped_sites) metrics_out[metric] = summary payload = { "year": year, "alpha": alpha, "n_sites_total": n_sites_total, + "dropped_sites": sorted(all_dropped), "metrics": metrics_out, } diff --git a/index.html b/index.html index 644cc54..47d6843 100644 --- a/index.html +++ b/index.html @@ -115,13 +115,13 @@ body { margin: 0; font: 13px/1.4 system-ui, sans-serif; background: #f5f5f5; col .inspector-table th, .inspector-table td { padding: 2px 8px; text-align: right; border-bottom: 1px solid #f0f0f0; } .inspector-table th:first-child, .inspector-table td:first-child { text-align: left; } -/* toolbar button */ -#worldMapBtn { +/* toolbar buttons */ +.toolbar-btn { font-size: 13px; padding: 4px 10px; border-radius: 4px; border: 1px solid #4a6fa5; background: #2a3f5f; color: #dceeff; cursor: pointer; } -#worldMapBtn:hover { background: #3a5278; } +.toolbar-btn:hover { background: #3a5278; } #repoLink { margin-left: auto; @@ -132,37 +132,44 @@ body { margin: 0; font: 13px/1.4 system-ui, sans-serif; background: #f5f5f5; col } #repoLink:hover { color: #9ecef8; text-decoration: underline; } -/* worldwide map overlay */ -#worldOverlay { +/* overlays (world map + statistics) */ +.overlay { display: none; position: fixed; inset: 0; z-index: 3000; background: rgba(0, 0, 0, 0.55); } -#worldOverlay.open { display: flex; align-items: stretch; justify-content: center; } -#worldPanel { +.overlay.open { display: flex; align-items: stretch; justify-content: center; } +.overlay-panel { flex: 1; margin: 12px; max-width: 1400px; display: flex; flex-direction: column; background: #fff; border-radius: 6px; overflow: hidden; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35); } -#worldHeader { +.overlay-header { display: flex; align-items: center; gap: 12px; padding: 10px 14px; - background: #1a1a2e; color: #eee; flex-shrink: 0; + background: #1a1a2e; color: #eee; flex-shrink: 0; flex-wrap: wrap; } -#overlayTabs { display: flex; gap: 4px; flex: 1; } -.otab { - padding: 4px 12px; border-radius: 4px; font-size: 13px; cursor: pointer; - border: 1px solid #555; background: transparent; color: #ccc; +.overlay-header h2 { + margin: 0; font-size: 14px; font-weight: 600; color: #7eb8f7; flex-shrink: 0; } -.otab.active { background: #2a3f5f; color: #dceeff; border-color: #4a6fa5; } -.otab:hover:not(.active) { background: rgba(255, 255, 255, 0.07); } -#worldHeader .world-meta { font-size: 12px; color: #aaa; flex-shrink: 0; } -#worldClose { +.overlay-header .overlay-meta { font-size: 12px; color: #aaa; flex-shrink: 0; margin-left: auto; } +.overlay-close { font-size: 13px; padding: 4px 12px; border-radius: 4px; border: 1px solid #666; background: transparent; color: #ddd; cursor: pointer; flex-shrink: 0; } -#worldClose:hover { background: rgba(255, 255, 255, 0.08); } +.overlay-close:hover { background: rgba(255, 255, 255, 0.08); } #worldMap { flex: 1; min-height: 0; } -#statsPanel { flex: 1; min-height: 0; overflow-y: auto; padding: 20px 24px; display: none; background: #f5f5f5; } + +/* statistics overlay tabs */ +#statsTabs { display: flex; gap: 4px; flex-wrap: wrap; } +.stab { + padding: 4px 12px; border-radius: 4px; font-size: 13px; cursor: pointer; + border: 1px solid #555; background: transparent; color: #ccc; +} +.stab.active { background: #2a3f5f; color: #dceeff; border-color: #4a6fa5; } +.stab:hover:not(.active) { background: rgba(255, 255, 255, 0.07); } +.stats-tab-panel { + flex: 1; min-height: 0; overflow-y: auto; padding: 20px 24px; background: #f5f5f5; +} .stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 14px; margin-top: 4px; } .stat-card { background: #fff; border: 1px solid #e0e0e0; border-radius: 6px; padding: 14px 16px; } .stat-card h3 { margin: 0 0 10px; font-size: 14px; color: #1a1a2e; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } @@ -183,6 +190,20 @@ body { margin: 0; font: 13px/1.4 system-ui, sans-serif; background: #f5f5f5; col .stat-divider { border: none; border-top: 1px solid #f0f0f0; margin: 8px 0; } .stat-summary { font-size: 12px; color: #666; margin-bottom: 14px; } .stat-nodata { color: #999; padding: 40px; text-align: center; font-size: 13px; } +.stat-placeholder { color: #999; padding: 60px 24px; text-align: center; font-size: 14px; } +.stat-placeholder p { margin: 0 0 8px; color: #666; } +.stat-dropped { + margin-top: 18px; background: #fff; border: 1px solid #e0e0e0; + border-radius: 6px; padding: 14px 16px; +} +.stat-dropped h4 { margin: 0 0 6px; font-size: 13px; color: #333; } +.stat-dropped-note { margin: 0 0 10px; font-size: 11px; color: #888; } +.stat-dropped-list { display: flex; flex-wrap: wrap; gap: 6px 10px; } +.stat-site-link { + font-size: 12px; color: #1565c0; background: none; border: none; padding: 0; + cursor: pointer; text-decoration: underline; font-family: inherit; +} +.stat-site-link:hover { color: #0d47a1; } .world-popup { font-size: 12px; line-height: 1.35; } .world-popup b { display: block; margin-bottom: 2px; } .world-popup .veg { color: #2e7d32; font-size: 11px; } @@ -194,22 +215,47 @@ body { margin: 0; font: 13px/1.4 system-ui, sans-serif; background: #f5f5f5; col

EFAST PhenoCam validation

- + + Source code -