diff --git a/include/evalhyd/detail/determinist/efficiencies.hpp b/include/evalhyd/detail/determinist/efficiencies.hpp index 034c84fdd30347cc390721aa57aaed48d430d029..93281ba80402a041cf10ae44dec048395a715dd9 100644 --- a/include/evalhyd/detail/determinist/efficiencies.hpp +++ b/include/evalhyd/detail/determinist/efficiencies.hpp @@ -83,6 +83,94 @@ namespace evalhyd return r_pearson; } + /// Compute the Spearman rank correlation coefficient. + /// + /// \param q_obs + /// Streamflow observations. + /// shape: (series, time) + /// \param q_prd + /// Streamflow predictions. + /// shape: (series, time) + /// \param t_msk + /// Temporal subsets of the whole record. + /// shape: (series, subsets, time) + /// \param b_exp + /// Boostrap samples. + /// shape: (samples, time slice) + /// \param n_srs + /// Number of prediction series. + /// \param n_msk + /// Number of temporal subsets. + /// \param n_exp + /// Number of bootstrap samples. + /// \return + /// Spearman rank correlation coefficients. + /// shape: (subsets, samples, series) + template <class XD2> + inline xt::xtensor<double, 3> calc_r_spearman( + const XD2& q_obs, + const XD2& q_prd, + const xt::xtensor<bool, 3>& t_msk, + const std::vector<xt::xkeep_slice<int>>& b_exp, + std::size_t n_srs, + std::size_t n_msk, + std::size_t n_exp + ) + { + // calculate error in timing and dynamics $r_{spearman}$ + // (Spearman's rank correlation coefficient) + xt::xtensor<double, 3> r_spearman = + xt::zeros<double>({n_msk, n_exp, n_srs}); + + for (std::size_t m = 0; m < n_msk; m++) + { + // apply the mask + // (using NaN workaround until reducers work on masked_view) + auto prd_masked = xt::where(xt::view(t_msk, xt::all(), m), + q_prd, NAN); + auto obs_masked = xt::where(xt::view(t_msk, xt::all(), m), + q_obs, NAN); + + // compute variable one sample at a time + for (std::size_t e = 0; e < n_exp; e++) + { + auto prd = xt::view(prd_masked, xt::all(), b_exp[e]); + auto obs = xt::view(obs_masked, xt::all(), b_exp[e]); + + auto prd_rank = xt::eval(xt::argsort( + xt::argsort(xt::eval(prd), {1}), + {1} + )); + auto obs_rank = xt::eval(xt::argsort( + xt::argsort(xt::eval(obs), {1}), + {1} + )); + + auto mean_prd_rank = + xt::eval(xt::nanmean(prd_rank, {1}, xt::keep_dims)); + auto mean_obs_rank = + xt::eval(xt::nanmean(obs_rank, {1}, xt::keep_dims)); + + auto prd_rank_err = xt::eval(prd_rank - mean_prd_rank); + auto obs_rank_err = xt::eval(obs_rank - mean_obs_rank); + + auto r_num = xt::nansum( + prd_rank_err * obs_rank_err, + {1} + ); + + auto r_den = xt::sqrt( + xt::nansum(xt::square(prd_rank_err), {1}) + * xt::nansum(xt::square(obs_rank_err), {1}) + ); + + xt::view(r_spearman, m, e) = r_num / r_den; + } + } + + return r_spearman; + } + /// Compute alpha. /// /// \param q_obs @@ -181,6 +269,85 @@ namespace evalhyd return gamma; } + /// Compute non-parametric alpha. + /// + /// \param q_obs + /// Streamflow observations. + /// shape: (series, time) + /// \param q_prd + /// Streamflow predictions. + /// shape: (series, time) + /// \param mean_obs + /// Mean observed streamflow. + /// shape: (subsets, samples, series, 1) + /// \param mean_prd + /// Mean predicted streamflow. + /// shape: (subsets, samples, series, 1) + /// \param t_msk + /// Temporal subsets of the whole record. + /// shape: (series, subsets, time) + /// \param b_exp + /// Boostrap samples. + /// shape: (samples, time slice) + /// \param n_srs + /// Number of prediction series. + /// \param n_msk + /// Number of temporal subsets. + /// \param n_exp + /// Number of bootstrap samples. + /// \return + /// Non-parametric alphas. + /// shape: (subsets, samples, series) + template <class XD2> + inline xt::xtensor<double, 3> calc_alpha_np( + const XD2& q_obs, + const XD2& q_prd, + const xt::xtensor<double, 4>& mean_obs, + const xt::xtensor<double, 4>& mean_prd, + const xt::xtensor<bool, 3>& t_msk, + const std::vector<xt::xkeep_slice<int>>& b_exp, + std::size_t n_srs, + std::size_t n_msk, + std::size_t n_exp, + std::size_t n_tim + ) + { + // calculate error in spread of flow $alpha$ + xt::xtensor<double, 3> alpha_np = + xt::zeros<double>({n_msk, n_exp, n_srs}); + + for (std::size_t m = 0; m < n_msk; m++) + { + // apply the mask + // (using NaN workaround until reducers work on masked_view) + auto prd_masked = xt::where(xt::view(t_msk, xt::all(), m), + q_prd, NAN); + auto obs_masked = xt::where(xt::view(t_msk, xt::all(), m), + q_obs, NAN); + + // compute variable one sample at a time + for (std::size_t e = 0; e < n_exp; e++) + { + auto prd = xt::view(prd_masked, xt::all(), b_exp[e]); + auto obs = xt::view(obs_masked, xt::all(), b_exp[e]); + + auto prd_fdc = xt::sort( + xt::eval(prd / (n_tim * xt::view(mean_prd, m, e))), + {1} + ); + auto obs_fdc = xt::sort( + xt::eval(obs / (n_tim * xt::view(mean_obs, m, e))), + {1} + ); + + xt::view(alpha_np, m, e) = + 1 - 0.5 * xt::nansum(xt::abs(prd_fdc - obs_fdc), {1}); + } + } + + return alpha_np; + } + /// Compute the bias. /// /// \param q_obs @@ -508,6 +675,106 @@ namespace evalhyd return KGEPRIME_D; } + + /// Compute the non-parametric Kling-Gupta Efficiency (KGENP). + /// + /// \param r_spearman + /// Spearman rank correlation coefficients. + /// shape: (subsets, samples, series) + /// \param alpha_np + /// Non-parametric alphas. + /// shape: (subsets, samples, series) + /// \param bias + /// Biases. + /// shape: (subsets, samples, series) + /// \param n_srs + /// Number of prediction series. + /// \param n_msk + /// Number of temporal subsets. + /// \param n_exp + /// Number of bootstrap samples. + /// \return + /// Modified Kling-Gupta efficiencies. + /// shape: (series, subsets, samples) + inline xt::xtensor<double, 3> calc_KGENP( + const xt::xtensor<double, 3>& r_spearman, + const xt::xtensor<double, 3>& alpha_np, + const xt::xtensor<double, 3>& bias, + std::size_t n_srs, + std::size_t n_msk, + std::size_t n_exp + ) + { + xt::xtensor<double, 3> KGENP = + xt::zeros<double>({n_srs, n_msk, n_exp}); + + for (std::size_t m = 0; m < n_msk; m++) + { + for (std::size_t e = 0; e < n_exp; e++) + { + // compute KGEPRIME + xt::view(KGENP, xt::all(), m, e) = 1 - xt::sqrt( + xt::square(xt::view(r_spearman, m, e) - 1) + + xt::square(xt::view(alpha_np, m, e) - 1) + + xt::square(xt::view(bias, m, e) - 1) + ); + } + } + + return KGENP; + } + + /// Compute the non-parametric Kling-Gupta Efficiency + /// Decomposed (KGENP) into its three components that are the rank + /// correlation (r), the variability (non-parametric alpha), and + /// the bias (beta), in this order. + /// + /// \param r_spearman + /// Spearman correlation coefficients. + /// shape: (subsets, samples, series) + /// \param alpha_np + /// Non-parametric alphas. + /// shape: (subsets, samples, series) + /// \param bias + /// Biases. + /// shape: (subsets, samples, series) + /// \param n_srs + /// Number of prediction series. + /// \param n_msk + /// Number of temporal subsets. + /// \param n_exp + /// Number of bootstrap samples. + /// \return + /// Modified Kling-Gupta efficiencies. + /// shape: (series, subsets, samples) + inline xt::xtensor<double, 4> calc_KGENP_D( + const xt::xtensor<double, 3>& r_spearman, + const xt::xtensor<double, 3>& alpha_np, + const xt::xtensor<double, 3>& bias, + std::size_t n_srs, + std::size_t n_msk, + std::size_t n_exp + ) + { + xt::xtensor<double, 4> KGENP_D = + xt::zeros<double>({n_srs, n_msk, n_exp, std::size_t {3}}); + + for (std::size_t m = 0; m < n_msk; m++) + { + for (std::size_t e = 0; e < n_exp; e++) + { + // put KGE components together + xt::view(KGENP_D, xt::all(), m, e, 0) = + xt::view(r_spearman, m, e); + xt::view(KGENP_D, xt::all(), m, e, 1) = + xt::view(alpha_np, m, e); + xt::view(KGENP_D, xt::all(), m, e, 2) = + xt::view(bias, m, e); + } + } + + return KGENP_D; + } } } } diff --git a/include/evalhyd/detail/determinist/evaluator.hpp b/include/evalhyd/detail/determinist/evaluator.hpp index 8276179cc3508b48ff73ce15cb86e88dadb07b2a..fb38f5bd6d4944289f385ecd28c071e6937427d0 100644 --- a/include/evalhyd/detail/determinist/evaluator.hpp +++ b/include/evalhyd/detail/determinist/evaluator.hpp @@ -47,8 +47,10 @@ namespace evalhyd xtl::xoptional<xt::xtensor<double, 4>, bool> err_prd; xtl::xoptional<xt::xtensor<double, 4>, bool> quad_err_prd; xtl::xoptional<xt::xtensor<double, 3>, bool> r_pearson; + xtl::xoptional<xt::xtensor<double, 3>, bool> r_spearman; xtl::xoptional<xt::xtensor<double, 3>, bool> alpha; xtl::xoptional<xt::xtensor<double, 3>, bool> gamma; + xtl::xoptional<xt::xtensor<double, 3>, bool> alpha_np; xtl::xoptional<xt::xtensor<double, 3>, bool> bias; // members for evaluation metrics @@ -61,6 +63,8 @@ namespace evalhyd xtl::xoptional<xt::xtensor<double, 4>, bool> KGE_D; xtl::xoptional<xt::xtensor<double, 3>, bool> KGEPRIME; xtl::xoptional<xt::xtensor<double, 4>, bool> KGEPRIME_D; + xtl::xoptional<xt::xtensor<double, 3>, bool> KGENP; + xtl::xoptional<xt::xtensor<double, 4>, bool> KGENP_D; // methods to compute elements xt::xtensor<double, 4> get_mean_obs() @@ -178,6 +182,18 @@ namespace evalhyd return r_pearson.value(); }; + xt::xtensor<double, 3> get_r_spearman() + { + if (!r_spearman.has_value()) + { + r_spearman = elements::calc_r_spearman( + q_obs, q_prd, t_msk, b_exp, + n_srs, n_msk, n_exp + ); + } + return r_spearman.value(); + }; + xt::xtensor<double, 3> get_alpha() { if (!alpha.has_value()) @@ -201,6 +217,18 @@ namespace evalhyd return gamma.value(); }; + xt::xtensor<double, 3> get_alpha_np() + { + if (!alpha_np.has_value()) + { + alpha_np = elements::calc_alpha_np( + q_obs, q_prd, get_mean_obs(), get_mean_prd(), + t_msk, b_exp, n_srs, n_msk, n_exp, n_tim + ); + } + return alpha_np.value(); + }; + xt::xtensor<double, 3> get_bias() { if (!bias.has_value()) @@ -351,6 +379,30 @@ namespace evalhyd } return KGEPRIME_D.value(); }; + + xt::xtensor<double, 3> get_KGENP() + { + if (!KGENP.has_value()) + { + KGENP = metrics::calc_KGENP( + get_r_spearman(), get_alpha_np(), get_bias(), + n_srs, n_msk, n_exp + ); + } + return KGENP.value(); + }; + + xt::xtensor<double, 4> get_KGENP_D() + { + if (!KGENP_D.has_value()) + { + KGENP_D = metrics::calc_KGENP_D( + get_r_spearman(), get_alpha_np(), get_bias(), + n_srs, n_msk, n_exp + ); + } + return KGENP_D.value(); + }; }; } } diff --git a/include/evalhyd/evald.hpp b/include/evalhyd/evald.hpp index ef858dfcfe97d058e7351155dd711f152a9f3d1b..76d2a8fcda8d8ca12e2f705fb6e9696cb7268123 100644 --- a/include/evalhyd/evald.hpp +++ b/include/evalhyd/evald.hpp @@ -209,7 +209,8 @@ namespace evalhyd utils::check_metrics( metrics, {"MAE", "MARE", "MSE", "RMSE", - "NSE", "KGE", "KGE_D", "KGEPRIME", "KGEPRIME_D"} + "NSE", "KGE", "KGE_D", "KGEPRIME", "KGEPRIME_D", + "KGENP", "KGENP_D"} ); // check that optional parameters are valid @@ -489,6 +490,18 @@ namespace evalhyd uncertainty::summarise_d(evaluator.get_KGEPRIME_D(), summary) ); } + else if ( metric == "KGENP" ) + { + r.emplace_back( + uncertainty::summarise_d(evaluator.get_KGENP(), summary) + ); + } + else if ( metric == "KGENP_D" ) + { + r.emplace_back( + uncertainty::summarise_d(evaluator.get_KGENP_D(), summary) + ); + } } return r;