Skip to content

Commit 4410174

Browse files
* compute metrics on residuals
* compute median, rather than mean * add pearson-R
1 parent 1f3eca1 commit 4410174

3 files changed

Lines changed: 70 additions & 26 deletions

File tree

cybench/evaluation/eval.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import numpy as np
22
from sklearn.metrics import mean_squared_error, mean_absolute_percentage_error, r2_score
3+
from scipy.stats import pearsonr
34

45
from cybench.models.model import BaseModel
56
from cybench.datasets.dataset import Dataset
@@ -13,9 +14,14 @@ def metric(func):
1314
implemented_metrics[func.__name__] = func
1415
return func
1516

17+
def get_default_metrics(residual: bool = False):
18+
if residual:
19+
# Only metrics that make sense on residuals
20+
return ("r", "r2")
21+
else:
22+
# Full set
23+
return ("mape", "normalized_rmse", "r", "r2")
1624

17-
def get_default_metrics():
18-
return ("normalized_rmse", "mape", "r2")
1925

2026

2127
def evaluate_model(model: BaseModel, dataset: Dataset, metrics=get_default_metrics()):
@@ -58,7 +64,7 @@ def evaluate_predictions(
5864
metric_function = implemented_metrics.get(metric_name)
5965
if metric_function:
6066
result = metric_function(y_true, y_pred)
61-
results[metric_name] = result
67+
results[metric_name] = np.round(result, 4)
6268
else:
6369
raise ValueError(f"Metric function '{metric_name}' not implemented.")
6470

@@ -114,3 +120,21 @@ def r2(y_true: np.ndarray, y_pred: np.ndarray):
114120
"""
115121

116122
return r2_score(y_true, y_pred)
123+
124+
125+
@metric
126+
def r(y_true: np.ndarray, y_pred: np.ndarray):
127+
"""
128+
Calculate Pearson correlation coefficient.
129+
130+
Args:
131+
- y_true (numpy.ndarray): True values.
132+
- y_pred (numpy.ndarray): Predicted values.
133+
134+
Returns:
135+
- float: Pearson's R.
136+
"""
137+
try:
138+
return pearsonr(y_true, y_pred)[0]
139+
except Exception:
140+
return float("nan")

cybench/runs/process_results.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ def write_results_to_table(output_file: str):
103103
df_metrics = results_to_metrics()
104104
default_metrics = get_default_metrics()
105105
metrics = [m for m in default_metrics if m in df_metrics.columns]
106-
df_metrics = df_metrics.groupby(["crop", KEY_COUNTRY, "model"], observed=True).agg(
107-
{m: "mean" for m in metrics}
108-
)
106+
df_metrics = df_metrics.groupby(
107+
["crop", KEY_COUNTRY, "model"], observed=True
108+
).agg({m: "median" for m in metrics})
109109
crops = df_metrics.index.get_level_values("crop").unique()
110110
metrics = df_metrics.columns.unique()
111111
tables = {}
@@ -116,8 +116,7 @@ def write_results_to_table(output_file: str):
116116
tables[crop][metric] = crop_df.reset_index().pivot_table(
117117
index=["crop", KEY_COUNTRY], columns="model", values=metric
118118
)
119-
120-
# Open a file to write Markdown content
119+
print(f"write to {os.path.join(PATH_OUTPUT_DIR, output_file)}")
121120
with open(os.path.join(PATH_OUTPUT_DIR, output_file), "w") as file:
122121
for crop, metrics in tables.items():
123122
for metric, values in metrics.items():
@@ -132,12 +131,12 @@ def write_results_to_table(output_file: str):
132131

133132
if __name__ == "__main__":
134133
parser = argparse.ArgumentParser(
135-
prog="process_results.py", description="Output markdown tables with summary of metrics"
134+
prog="process_results.py",
135+
description="Output markdown tables with summary of metrics",
136136
)
137137
parser.add_argument("-o", "--output_file")
138138
args = parser.parse_args()
139139
output_file = "output_tables.md"
140-
if (args.output_file is not None):
140+
if args.output_file is not None:
141141
output_file = args.output_file
142-
143142
write_results_to_table(output_file=output_file)

cybench/runs/run_benchmark.py

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717

1818
from cybench.datasets.dataset import Dataset
19-
from cybench.evaluation.eval import evaluate_predictions
19+
from cybench.evaluation.eval import evaluate_predictions, get_default_metrics
2020
from cybench.models.naive_models import AverageYieldModel
2121
from cybench.models.trend_models import TrendModel
2222
from cybench.models.sklearn_models import SklearnRidge, SklearnRandomForest
@@ -252,55 +252,76 @@ def get_prediction_residuals(run_name: str, model_names: dict) -> pd.DataFrame:
252252
return df_all
253253

254254

255+
def _prepare_targets_preds(df_yr, model_name, y_loc_mean=None, residual=False):
256+
"""Prepare y_true and y_pred, optionally using residuals."""
257+
y_true = df_yr[KEY_TARGET].values
258+
y_pred = df_yr[model_name].values
259+
260+
if residual and y_loc_mean is not None:
261+
y_true = y_true - df_yr[KEY_LOC].map(y_loc_mean)
262+
y_pred = y_pred - df_yr[KEY_LOC].map(y_loc_mean)
263+
264+
return y_true, y_pred
265+
266+
255267
def compute_metrics(
256268
run_name: str,
257269
model_names: list = None,
270+
residual: bool = False,
258271
) -> pd.DataFrame:
259272
"""
260273
Compute evaluation metrics on saved predictions.
274+
261275
Args:
262-
run_name (str): The name of the run. Will be used to store log files and model results
263-
model_names (list) : names of models
276+
run_name (str): The name of the run. Will be used to store log files and model results.
277+
model_names (list): Names of models to evaluate. If None, all model columns are used.
278+
residual (bool): If True, compute metrics on residuals (values adjusted per location).
264279
265280
Returns:
266-
a pd.DataFrame containing evaluation metrics
281+
pd.DataFrame containing evaluation metrics
267282
"""
268283
df_all = load_results(run_name)
269284
if df_all.empty:
270285
return pd.DataFrame(columns=[KEY_COUNTRY, "model", KEY_YEAR])
271286

272287
rows = []
273288
country_codes = df_all[KEY_COUNTRY].unique()
289+
274290
for cn in country_codes:
275291
df_cn = df_all[df_all[KEY_COUNTRY] == cn]
276292
all_years = sorted(df_cn[KEY_YEAR].unique())
293+
294+
# Precompute location means for residuals
295+
y_loc_mean = df_cn.groupby(KEY_LOC)[KEY_TARGET].mean() if residual else None
296+
277297
for yr in all_years:
278298
df_yr = df_cn[df_cn[KEY_YEAR] == yr]
279-
y_true = df_yr[KEY_TARGET].values
299+
280300
if model_names is None:
281301
model_names = [
282-
c
283-
for c in df_yr.columns
302+
c for c in df_yr.columns
284303
if c not in [KEY_COUNTRY, KEY_LOC, KEY_YEAR, KEY_TARGET]
285304
]
286305

287306
for model_name in model_names:
288-
metrics = evaluate_predictions(y_true, df_yr[model_name].values)
307+
y_true, y_pred = _prepare_targets_preds(df_yr, model_name, y_loc_mean, residual)
308+
309+
# Select metrics based on residual mode
310+
metrics_to_use = get_default_metrics(residual=residual)
311+
metrics = evaluate_predictions(y_true, y_pred, metrics=metrics_to_use)
312+
289313
metrics_row = {
290314
KEY_COUNTRY: cn,
291315
"model": model_name,
292316
KEY_YEAR: yr,
317+
**metrics
293318
}
294-
295-
for metric_name, value in metrics.items():
296-
metrics_row[metric_name] = value
297-
298319
rows.append(metrics_row)
299320

300-
df_all = pd.DataFrame(rows)
301-
df_all.set_index([KEY_COUNTRY, "model", KEY_YEAR], inplace=True)
321+
df_out = pd.DataFrame(rows)
322+
df_out.set_index([KEY_COUNTRY, "model", KEY_YEAR], inplace=True)
302323

303-
return df_all
324+
return df_out
304325

305326

306327
def run_benchmark_on_all_data():

0 commit comments

Comments
 (0)