@@ -48,11 +48,15 @@ def calc_median_yearly_r2(
4848 target_col : str ,
4949 model_col : str ,
5050 * ,
51+ loc_col : str = KEY_LOC ,
5152 year_col : str = KEY_YEAR ,
53+ min_regions : int = 3 ,
5254) -> float :
5355 """Median of per-year R², where each year's R² is computed across regions."""
5456 yearly_r2 : list [float ] = []
5557 for _ , year_df in df .groupby (year_col ):
58+ if year_df [loc_col ].nunique () < min_regions :
59+ continue
5660 _ , r2 = calc_r_r2 (
5761 year_df [target_col ].values ,
5862 year_df [model_col ].values ,
@@ -63,6 +67,55 @@ def calc_median_yearly_r2(
6367 return float (np .nanmedian (yearly_r2 ))
6468
6569
70+ def calc_median_regional_r2 (
71+ df : pd .DataFrame ,
72+ target_col : str ,
73+ model_col : str ,
74+ * ,
75+ loc_col : str = KEY_LOC ,
76+ year_col : str = KEY_YEAR ,
77+ min_years : int = 3 ,
78+ ) -> float :
79+ """Median of per-region R², where each region's R² is computed across years."""
80+ del year_col # API symmetry with yearly helper; years come from row groups.
81+ regional_r2 : list [float ] = []
82+ for _ , loc_df in df .groupby (loc_col ):
83+ if len (loc_df ) < min_years :
84+ continue
85+ _ , r2 = calc_r_r2 (
86+ loc_df [target_col ].values ,
87+ loc_df [model_col ].values ,
88+ )
89+ regional_r2 .append (r2 )
90+ if not regional_r2 :
91+ return float ("nan" )
92+ return float (np .nanmedian (regional_r2 ))
93+
94+
95+ def calc_median_regional_r2_res (
96+ df : pd .DataFrame ,
97+ target_col : str ,
98+ model_col : str ,
99+ * ,
100+ loc_col : str = KEY_LOC ,
101+ min_years : int = 3 ,
102+ ) -> float :
103+ """Median of per-region R² on location-de-meaned yields (anomaly view)."""
104+ loc_means = df .groupby (loc_col )[target_col ].mean ()
105+ work = df .copy ()
106+ work ["_true_res" ] = work [target_col ] - work [loc_col ].map (loc_means )
107+ work ["_pred_res" ] = work [model_col ] - work [loc_col ].map (loc_means )
108+ regional_r2 : list [float ] = []
109+ for _ , loc_df in work .groupby (loc_col ):
110+ if len (loc_df ) < min_years :
111+ continue
112+ _ , r2 = calc_r_r2 (loc_df ["_true_res" ].values , loc_df ["_pred_res" ].values )
113+ regional_r2 .append (r2 )
114+ if not regional_r2 :
115+ return float ("nan" )
116+ return float (np .nanmedian (regional_r2 ))
117+
118+
66119def get_metrics_dict (
67120 df : pd .DataFrame ,
68121 target_col : str ,
@@ -102,36 +155,53 @@ def compute_report_metrics(
102155 Metrics used by visualize_results_aggregated / build_results_dashboard.
103156
104157 Views:
105- - region_year: pooled region-year rows (r, R², NRMSE, med per-year R², anomaly r/R²)
106- - spatial: per-location means across years
107- - temporal: per-year means across locations (r, R²)
158+ - region_year: pooled region-year rows (r, R², NRMSE, pooled anomaly r/R²)
159+ - spatial: typical-year cross-region R² (median over years); climatology map r/R²
160+ - temporal: typical-region cross-year R² (median over regions); aggregate series r/R²
161+ - anomaly: typical-region R² on residuals; pooled anomaly r/R²
108162 """
109163 complete = df [target_col ].notna () & df [model_col ].notna ()
110164 df = df .loc [complete ].copy ()
111165 region_year = get_metrics_dict (df , target_col , model_col , loc_col = loc_col )
112- region_year ["median_r2" ] = calc_median_yearly_r2 (
113- df , target_col , model_col , year_col = year_col
114- )
115166
116- spatial = df .groupby (loc_col )[[target_col , model_col ]].mean ()
117- r_spatial , r2_spatial = calc_r_r2 (
118- spatial [target_col ].values ,
119- spatial [model_col ].values ,
167+ spatial_clim = df .groupby (loc_col )[[target_col , model_col ]].mean ()
168+ r_spatial_clim , r2_spatial_clim = calc_r_r2 (
169+ spatial_clim [target_col ].values ,
170+ spatial_clim [model_col ].values ,
120171 )
121172
122- temporal = df .groupby (year_col )[[target_col , model_col ]].mean ().sort_index ()
123- r_time , r2_time = calc_r_r2 (
124- temporal [target_col ].values ,
125- temporal [model_col ].values ,
173+ temporal_agg = df .groupby (year_col )[[target_col , model_col ]].mean ().sort_index ()
174+ r_temporal_agg , r2_temporal_agg = calc_r_r2 (
175+ temporal_agg [target_col ].values ,
176+ temporal_agg [model_col ].values ,
126177 )
127178
128179 return {
129180 "n_regions" : int (df [loc_col ].nunique ()),
130181 "n_years" : int (df [year_col ].nunique ()),
131182 "n_samples" : int (len (df )),
132183 "region_year" : region_year ,
133- "spatial" : {"r" : r_spatial , "r2" : r2_spatial },
134- "temporal" : {"r" : r_time , "r2" : r2_time },
184+ "spatial" : {
185+ "r2_typical_year" : calc_median_yearly_r2 (
186+ df , target_col , model_col , loc_col = loc_col , year_col = year_col
187+ ),
188+ "r_climatology" : r_spatial_clim ,
189+ "r2_climatology" : r2_spatial_clim ,
190+ },
191+ "temporal" : {
192+ "r2_typical_region" : calc_median_regional_r2 (
193+ df , target_col , model_col , loc_col = loc_col , year_col = year_col
194+ ),
195+ "r_aggregate" : r_temporal_agg ,
196+ "r2_aggregate" : r2_temporal_agg ,
197+ },
198+ "anomaly" : {
199+ "r2_typical_region" : calc_median_regional_r2_res (
200+ df , target_col , model_col , loc_col = loc_col
201+ ),
202+ "r_pooled" : region_year ["r_res" ],
203+ "r2_pooled" : region_year ["r2_res" ],
204+ },
135205 }
136206
137207
@@ -140,10 +210,13 @@ def format_report_metrics(metrics: dict[str, Any]) -> str:
140210 ry = metrics ["region_year" ]
141211 sp = metrics ["spatial" ]
142212 tm = metrics ["temporal" ]
213+ an = metrics ["anomaly" ]
143214 return (
144- f"region-year r={ ry ['r' ]:.2f} R²={ ry ['r2' ]:.2f} NRMSE={ ry ['nrmse' ]:.2f} "
145- f"med-R²/yr={ ry ['median_r2' ]:.2f} | "
146- f"spatial r={ sp ['r' ]:.2f} R²={ sp ['r2' ]:.2f} | "
147- f"temporal r={ tm ['r' ]:.2f} R²={ tm ['r2' ]:.2f} | "
148- f"anomaly r={ ry ['r_res' ]:.2f} R²={ ry ['r2_res' ]:.2f} "
215+ f"region-year r={ ry ['r' ]:.2f} R²={ ry ['r2' ]:.2f} NRMSE={ ry ['nrmse' ]:.2f} | "
216+ f"spatial R²(med/yr)={ sp ['r2_typical_year' ]:.2f} "
217+ f"clim R²={ sp ['r2_climatology' ]:.2f} | "
218+ f"temporal R²(med/reg)={ tm ['r2_typical_region' ]:.2f} "
219+ f"agg R²={ tm ['r2_aggregate' ]:.2f} | "
220+ f"anomaly R²(med/reg)={ an ['r2_typical_region' ]:.2f} "
221+ f"pooled R²={ an ['r2_pooled' ]:.2f} "
149222 )
0 commit comments