Source code for mhm_tools.common.metrics.metrics_handler

"""
Dispatch metric calculations and write metric CSV files.

Authors
-------
- Simon Lüdke
- Carla Peter
"""

import logging

import numpy as np
import pandas as pd

from mhm_tools.common.metrics.esp import ESP
from mhm_tools.common.metrics.mspaef import MSPAEF
from mhm_tools.common.metrics.spaef import SPAEF
from mhm_tools.common.metrics.tsm import calculate_tsm_for_gridded_data
from mhm_tools.common.metrics.waspaef import WASPAEF
from mhm_tools.common.utils import pretty_print_df

logger = logging.getLogger(__name__)
RESULT_METRIC_TSM = "TSM"
RESULT_METRIC_SPAEF = "SPAEF"
RESULT_METRIC_ESP = "ESP"
RESULT_METRIC_WASPAEF = "WASPAEF"
RESULT_METRIC_MSPAEF = "MSPAEF"
ACCEPTED_RESULT_METRICS = (
    RESULT_METRIC_TSM,
    RESULT_METRIC_SPAEF,
    RESULT_METRIC_ESP,
    RESULT_METRIC_WASPAEF,
    RESULT_METRIC_MSPAEF,
)


[docs] def normalize_results_metric(metric): """Normalize and validate a result CSV metric name.""" if metric is None: return RESULT_METRIC_TSM normalized = str(metric).strip().upper() if normalized == "ALL": return ACCEPTED_RESULT_METRICS if normalized not in ACCEPTED_RESULT_METRICS: accepted = ", ".join(ACCEPTED_RESULT_METRICS) msg = f"Unsupported result metric {metric!r}. Use one of: {accepted}." raise ValueError(msg) return normalized
[docs] def calculate_metric_per_timestep_and_average(map1, map2, func, *args, **kwargs): """Apply `func` to matching 2D timesteps of two 3D arrays,then average the per-timestep results. Parameters ---------- func: callable Function applied to matching 2D timestep slices from both arrays. map1: numpy.ndarray observed data 3D array with shape ``(time, lat, lon)``. map2: numpy.ndarray simulated data 3D array with shape ``(time, lat, lon)``. *args Additional positional arguments passed to ``func``. **kwargs Additional keyword arguments passed to ``func``. Returns ------- numpy.ndarray Mean metric values over all timesteps. """ results_per_timestep = np.asarray( [ func(simulated, observed, *args, **kwargs) for simulated, observed in zip(map1, map2) ] ) return np.nanmean(results_per_timestep, axis=0)
[docs] def calculate_spaef_for_gridded_data(map1, map2, ds1_name, ds2_name): """Calculate SPAEF metrics for two gridded datasets.""" spaef, alpha, beta, gamma = calculate_metric_per_timestep_and_average( map1, map2, SPAEF ) return { "name": ds1_name + "-" + ds2_name, "avg_spaef": spaef, "avg_alpha": alpha, "avg_beta": beta, "avg_gamma": gamma, }
[docs] def calculate_esp_for_gridded_data(map1, map2, ds1_name, ds2_name): """Calculate ESP metrics for two gridded datasets.""" esp, rs, gamma, alpha = calculate_metric_per_timestep_and_average(map1, map2, ESP) return { "name": ds1_name + "-" + ds2_name, "avg_esp": esp, "avg_rs": rs, "avg_gamma": gamma, "avg_alpha": alpha, }
[docs] def calculate_waspaef_for_gridded_data(map1, map2, ds1_name, ds2_name): """Calculate WASPAEF metrics for two gridded datasets.""" waspaef, rho, sigma, wd = calculate_metric_per_timestep_and_average( map1, map2, WASPAEF ) return { "name": ds1_name + "-" + ds2_name, "avg_waspaef": waspaef, "avg_rho": rho, "avg_sigma": sigma, "avg_wd": wd, }
[docs] def calculate_mspaef_for_gridded_data(map1, map2, ds1_name, ds2_name): """Calculate MSPAEF metrics for two gridded datasets.""" mspaef, nrmse, sigma, sigma_error, mean_bias, rho = ( calculate_metric_per_timestep_and_average(map1, map2, MSPAEF) ) return { "name": ds1_name + "-" + ds2_name, "avg_mspaef": mspaef, "avg_nrmse": nrmse, "avg_sigma": sigma, "avg_sigma_error": sigma_error, "avg_mean_bias": mean_bias, "avg_rho": rho, }
[docs] def calculate_results_metric(map1, map2, ds1_name, ds2_name, metric=RESULT_METRIC_TSM): """Calculate the requested gridded result metric.""" metric = normalize_results_metric(metric) if metric == RESULT_METRIC_TSM: return calculate_tsm_for_gridded_data( map1=map1, map2=map2, ds1_name=ds1_name, ds2_name=ds2_name ) if metric == RESULT_METRIC_SPAEF: return calculate_spaef_for_gridded_data( map1=map1, map2=map2, ds1_name=ds1_name, ds2_name=ds2_name ) if metric == RESULT_METRIC_ESP: return calculate_esp_for_gridded_data( map1=map1, map2=map2, ds1_name=ds1_name, ds2_name=ds2_name ) if metric == RESULT_METRIC_WASPAEF: return calculate_waspaef_for_gridded_data( map1=map1, map2=map2, ds1_name=ds1_name, ds2_name=ds2_name ) if metric == RESULT_METRIC_MSPAEF: return calculate_mspaef_for_gridded_data( map1=map1, map2=map2, ds1_name=ds1_name, ds2_name=ds2_name ) msg = f"Unsupported result metric {metric!r}." raise ValueError(msg)
[docs] def create_csv_from_dict(results_dict: dict, out_path): """Create a CSV file from the provided dictionary.""" df = pd.DataFrame(results_dict, index=[0]) logger.info(f"Written metrics to {out_path}") df.to_csv(out_path)
[docs] def create_results_csv( map1, map2, ds1_name, ds2_name, out_dir, out_name="", metric="all", ): """Calculate the selected metric and create a CSV file.""" norm_metric = normalize_results_metric(metric) if isinstance(norm_metric, tuple): logger.info("Create csv for all metrics") for nm in norm_metric: create_results_csv( map1=map1, map2=map2, ds1_name=ds1_name, ds2_name=ds2_name, out_dir=out_dir, out_name=out_name, metric=nm, ) return logger.info(f"Calculating metrics for {metric}") results_dict = calculate_results_metric( map1=map1, map2=map2, ds1_name=ds1_name, ds2_name=ds2_name, metric=norm_metric, ) logger.info(f"Spatial metrics: {results_dict}") metric_name = norm_metric.lower() file_name = f"{out_name}_{metric_name}.csv" if out_name else f"{metric_name}.csv" create_csv_from_dict(results_dict=results_dict, out_path=out_dir / file_name) df = pd.DataFrame(results_dict, index=[0]) pretty_print_df(df, title=norm_metric)