diff --git a/include/evalhyd/detail/uncertainty.hpp b/include/evalhyd/detail/uncertainty.hpp index de7d746621195ebb48f6439fed377b4003ce1173..d3b086b24cc40a35da2c3031f0472348ccc1226a 100644 --- a/include/evalhyd/detail/uncertainty.hpp +++ b/include/evalhyd/detail/uncertainty.hpp @@ -16,7 +16,6 @@ #include <xtensor/xtensor.hpp> #include <xtensor/xadapt.hpp> #include <xtensor/xrandom.hpp> -#include <xtensor/xio.hpp> #include "maths.hpp" @@ -154,41 +153,70 @@ namespace evalhyd return samples; } - - inline auto summarise(const xt::xarray<double>& values, int summary) + + inline auto summarise_d(const xt::xarray<double>& values, int summary) { - // TODO: wait for xt::quantile to be available - // or implement it internally for n-dim expressions + // define axis along which samples are + std::size_t axis = 2; + + // determine shape for output values + std::vector<std::size_t> shp; + std::size_t i = 0; + for (auto a : values.shape()) + { + if (i != axis) + { + shp.push_back(a); + } + else + { + if (summary == 1) + { + shp.push_back(2); + } + else if (summary == 2) + { + shp.push_back(7); + } + } + i++; + } + // summary 2: series of quantiles across samples if (summary == 2) { -// // adjust last axis size (from samples to number of statistics) -// auto s = values.shape(); -// s.pop_back(); -// s.push_back(7); -// xt::xarray<double> v = xt::zeros<double>(s); -// // quantiles -// xt::view(v, xt::all()) = -// xt::quantile(values, {0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95}, -1); -// return v; - return values; + xt::xarray<double> v = xt::zeros<double>(shp); + + // compute quantiles + auto quantiles = xt::quantile( + values, + {0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95}, + axis + ); + + // transfer quantiles into correct axis + // (since xt::quantile puts the quantiles on the first axis) + for (std::size_t q = 0; q < 7; q++) + { + xt::view(v, xt::all(), xt::all(), q) = + xt::view(quantiles, q); + } + + return v; } - // TODO: wait for xt::stddev to be fixed for rtensor - // or implement it internally for n-dim expressions // summary 1: mean and standard deviation across samples else if (summary == 1) { -// // adjust last axis size (from samples to number of statistics) -// auto s = values.shape(); -// s.pop_back(); -// s.push_back(2); -// xt::xarray<double> v = xt::zeros<double>(s); -// // mean -// xt::strided_view(s, xt::ellipsis(), 0) = xt::mean(values, -1); -// // standard deviation -// xt::strided_view(s, xt::ellipsis(), 1) = xt::stddev(values, -1); -// return v; - return values; + xt::xarray<double> v = xt::zeros<double>(shp); + + // compute mean + xt::view(v, xt::all(), xt::all(), 0) = + xt::mean(values, 2); + // compute standard deviation + xt::view(v, xt::all(), xt::all(), 1) = + xt::stddev(values, 2); + + return v; } // summary 0: raw (keep all samples) else @@ -197,6 +225,76 @@ namespace evalhyd } } + inline auto summarise_p(const xt::xarray<double>& values, int summary) + { + // define axis along which samples are + std::size_t axis = 3; + + // determine shape for output values + std::vector<std::size_t> shp; + std::size_t i = 0; + for (auto a : values.shape()) + { + if (i != axis) + { + shp.push_back(a); + } + else + { + if (summary == 1) + { + shp.push_back(2); + } + else if (summary == 2) + { + shp.push_back(7); + } + } + i++; + } + + // summary 2: series of quantiles across samples + if (summary == 2) + { + xt::xarray<double> v = xt::zeros<double>(shp); + + // compute quantiles + auto quantiles = xt::quantile( + values, + {0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95}, + axis + ); + + // transfer quantiles into correct axis + // (since xt::quantile puts the quantiles on the first axis) + for (std::size_t q = 0; q < 7; q++) + { + xt::view(v, xt::all(), xt::all(), xt::all(), q) = + xt::view(quantiles, q); + } + + return v; + } + // summary 1: mean and standard deviation across samples + else if (summary == 1) + { + xt::xarray<double> v = xt::zeros<double>(shp); + + // compute mean + xt::view(v, xt::all(), xt::all(), xt::all(), 0) = + xt::mean(values, axis); + // compute standard deviation + xt::view(v, xt::all(), xt::all(), xt::all(), 1) = + xt::stddev(values, axis); + + return v; + } + // summary 0: raw (keep all samples) + else + { + return values; + } + } } } diff --git a/include/evalhyd/detail/utils.hpp b/include/evalhyd/detail/utils.hpp index 839b49d53f7ccfafb17e8d29f0b8bdc156f1cf9d..94eae5387361ff4cbbd8513e3c4698048667f6f7 100644 --- a/include/evalhyd/detail/utils.hpp +++ b/include/evalhyd/detail/utils.hpp @@ -88,8 +88,7 @@ namespace evalhyd ); } auto summary = bootstrap.find("summary")->second; - // TODO: change upper bound when mean+stddev and quantiles implemented - if ((summary < 0) || (summary > 0)) + if ((summary < 0) || (summary > 2)) { throw std::runtime_error( "invalid value for bootstrap summary" diff --git a/include/evalhyd/evald.hpp b/include/evalhyd/evald.hpp index f6539c396cf9dd4566bea5ffa96c17061b4de768..682619161b4668990947dcf42e14a0d425ab5b87 100644 --- a/include/evalhyd/evald.hpp +++ b/include/evalhyd/evald.hpp @@ -437,25 +437,25 @@ namespace evalhyd if ( metric == "RMSE" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_RMSE(), summary) + uncertainty::summarise_d(evaluator.get_RMSE(), summary) ); } else if ( metric == "NSE" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_NSE(), summary) + uncertainty::summarise_d(evaluator.get_NSE(), summary) ); } else if ( metric == "KGE" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_KGE(), summary) + uncertainty::summarise_d(evaluator.get_KGE(), summary) ); } else if ( metric == "KGEPRIME" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_KGEPRIME(), summary) + uncertainty::summarise_d(evaluator.get_KGEPRIME(), summary) ); } } diff --git a/include/evalhyd/evalp.hpp b/include/evalhyd/evalp.hpp index 8cf1fa78a644aaad990a06c4e5ddeff923beeaca..80a14ce28a939120c16b1f72c3e50556fae688c0 100644 --- a/include/evalhyd/evalp.hpp +++ b/include/evalhyd/evalp.hpp @@ -400,121 +400,121 @@ namespace evalhyd if ( metric == "BS" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_BS(), summary) + uncertainty::summarise_p(evaluator.get_BS(), summary) ); } else if ( metric == "BSS" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_BSS(), summary) + uncertainty::summarise_p(evaluator.get_BSS(), summary) ); } else if ( metric == "BS_CRD" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_BS_CRD(), summary) + uncertainty::summarise_p(evaluator.get_BS_CRD(), summary) ); } else if ( metric == "BS_LBD" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_BS_LBD(), summary) + uncertainty::summarise_p(evaluator.get_BS_LBD(), summary) ); } else if ( metric == "QS" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_QS(), summary) + uncertainty::summarise_p(evaluator.get_QS(), summary) ); } else if ( metric == "CRPS" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_CRPS(), summary) + uncertainty::summarise_p(evaluator.get_CRPS(), summary) ); } else if ( metric == "POD" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_POD(), summary) + uncertainty::summarise_p(evaluator.get_POD(), summary) ); } else if ( metric == "POFD" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_POFD(), summary) + uncertainty::summarise_p(evaluator.get_POFD(), summary) ); } else if ( metric == "FAR" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_FAR(), summary) + uncertainty::summarise_p(evaluator.get_FAR(), summary) ); } else if ( metric == "CSI" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_CSI(), summary) + uncertainty::summarise_p(evaluator.get_CSI(), summary) ); } else if ( metric == "ROCSS" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_ROCSS(), summary) + uncertainty::summarise_p(evaluator.get_ROCSS(), summary) ); } else if ( metric == "RANK_HIST" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_RANK_HIST(), summary) + uncertainty::summarise_p(evaluator.get_RANK_HIST(), summary) ); } else if ( metric == "DS" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_DS(), summary) + uncertainty::summarise_p(evaluator.get_DS(), summary) ); } else if ( metric == "AS" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_AS(), summary) + uncertainty::summarise_p(evaluator.get_AS(), summary) ); } else if ( metric == "CR" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_CR(), summary) + uncertainty::summarise_p(evaluator.get_CR(), summary) ); } else if ( metric == "AW" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_AW(), summary) + uncertainty::summarise_p(evaluator.get_AW(), summary) ); } else if ( metric == "AWN" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_AWN(), summary) + uncertainty::summarise_p(evaluator.get_AWN(), summary) ); } else if ( metric == "AWI" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_AWI(), summary) + uncertainty::summarise_p(evaluator.get_AWI(), summary) ); } else if ( metric == "WS" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_WS(), summary) + uncertainty::summarise_p(evaluator.get_WS(), summary) ); } else if ( metric == "WSS" ) { r.emplace_back( - uncertainty::summarise(evaluator.get_WSS(), summary) + uncertainty::summarise_p(evaluator.get_WSS(), summary) ); } } diff --git a/tests/test_determinist.cpp b/tests/test_determinist.cpp index 0ddcd1860a22bd40bb23f8f44d44d6d548fcdf3c..c37e93f814565a267bc13981ce0fc50ae2a042f1 100644 --- a/tests/test_determinist.cpp +++ b/tests/test_determinist.cpp @@ -12,6 +12,8 @@ #include <xtensor/xtensor.hpp> #include <xtensor/xview.hpp> #include <xtensor/xmanipulation.hpp> +#include <xtensor/xmath.hpp> +#include <xtensor/xsort.hpp> #include <xtensor/xcsv.hpp> #include "evalhyd/evald.hpp" @@ -409,3 +411,114 @@ TEST(DeterministTests, TestBootstrap) ) << "Failure for (" << all_metrics_d[m] << ")"; } } + +TEST(DeterministTests, TestBootstrapSummary) +{ + // read in data + std::ifstream ifs; + + ifs.open(EVALHYD_DATA_DIR "/q_obs_1yr.csv"); + xt::xtensor<std::string, 1> x_dts = xt::squeeze(xt::load_csv<std::string>(ifs, ',', 0, 1)); + ifs.close(); + std::vector<std::string> datetimes (x_dts.begin(), x_dts.end()); + + ifs.open(EVALHYD_DATA_DIR "/q_obs_1yr.csv"); + xt::xtensor<double, 1> observed = xt::squeeze(xt::load_csv<double>(ifs, ',', 1)); + ifs.close(); + + ifs.open(EVALHYD_DATA_DIR "/q_prd_1yr.csv"); + xt::xtensor<double, 2> predicted = xt::load_csv<double>(ifs, ',', 1); + ifs.close(); + + // compute metrics via bootstrap with raw summary + std::unordered_map<std::string, int> bootstrap_0 = + {{"n_samples", 10}, {"len_sample", 3}, {"summary", 0}}; + + std::vector<xt::xarray<double>> metrics_raw = + evalhyd::evald( + xt::eval(xt::view(observed, xt::newaxis(), xt::all())), + predicted, + all_metrics_d, + {}, // transform + {}, // exponent + {}, // epsilon + xt::xtensor<bool, 3>({}), // t_msk + xt::xtensor<std::array<char, 32>, 2>({}), // m_cdt + bootstrap_0, + datetimes + ); + + // compute metrics via bootstrap with mean and standard deviation summary + std::unordered_map<std::string, int> bootstrap_1 = + {{"n_samples", 10}, {"len_sample", 3}, {"summary", 1}}; + + std::vector<xt::xarray<double>> metrics_mas = + evalhyd::evald( + xt::eval(xt::view(observed, xt::newaxis(), xt::all())), + predicted, + all_metrics_d, + {}, // transform + {}, // exponent + {}, // epsilon + xt::xtensor<bool, 3>({}), // t_msk + xt::xtensor<std::array<char, 32>, 2>({}), // m_cdt + bootstrap_1, + datetimes + ); + + // check results are identical + for (std::size_t m = 0; m < all_metrics_d.size(); m++) + { + // mean + EXPECT_TRUE( + xt::all(xt::isclose( + xt::mean(metrics_raw[m], 2), + xt::view(metrics_mas[m], xt::all(), xt::all(), 0) + )) + ) << "Failure for (" << all_metrics_d[m] << ") on mean"; + // standard deviation + EXPECT_TRUE( + xt::all(xt::isclose( + xt::stddev(metrics_raw[m], 2), + xt::view(metrics_mas[m], xt::all(), xt::all(), 1) + )) + ) << "Failure for (" << all_metrics_d[m] << ") on standard deviation"; + } + + // compute metrics via bootstrap with quantiles summary + std::unordered_map<std::string, int> bootstrap_2 = + {{"n_samples", 10}, {"len_sample", 3}, {"summary", 2}}; + + std::vector<xt::xarray<double>> metrics_qtl = + evalhyd::evald( + xt::eval(xt::view(observed, xt::newaxis(), xt::all())), + predicted, + all_metrics_d, + {}, // transform + {}, // exponent + {}, // epsilon + xt::xtensor<bool, 3>({}), // t_msk + xt::xtensor<std::array<char, 32>, 2>({}), // m_cdt + bootstrap_2, + datetimes + ); + + // check results are identical + for (std::size_t m = 0; m < all_metrics_d.size(); m++) + { + // quantiles + std::vector<double> quantiles = {0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95}; + std::size_t i = 0; + + for (auto q : quantiles) + { + EXPECT_TRUE( + xt::all(xt::isclose( + xt::quantile(metrics_raw[m], {q}, 2), + xt::view(metrics_qtl[m], xt::all(), xt::all(), i) + )) + ) << "Failure for (" << all_metrics_d[m] << ") on quantile " << q; + i++; + } + } +} diff --git a/tests/test_probabilist.cpp b/tests/test_probabilist.cpp index e0d8dd4ee856675594cd827548d729ebd502f029..31961005581e569e2ee897bfd9ba9577207b4b3f 100644 --- a/tests/test_probabilist.cpp +++ b/tests/test_probabilist.cpp @@ -12,6 +12,7 @@ #include <xtl/xoptional.hpp> #include <xtensor/xtensor.hpp> #include <xtensor/xview.hpp> +#include <xtensor/xmath.hpp> #include <xtensor/xsort.hpp> #include <xtensor/xmanipulation.hpp> #include <xtensor/xcsv.hpp> @@ -965,4 +966,146 @@ TEST(ProbabilistTests, TestBootstrap) )) ) << "Failure for (" << all_metrics_p[m] << ")"; } -} \ No newline at end of file +} + +TEST(ProbabilistTests, TestBootstrapSummary) +{ + xt::xtensor<double, 2> thresholds = {{ 33.87, 55.67 }}; + std::vector<double> confidence_levels = {30., 80.}; + + // read in data + std::ifstream ifs; + + ifs.open(EVALHYD_DATA_DIR "/q_obs_1yr.csv"); + xt::xtensor<std::string, 1> x_dts = xt::squeeze(xt::load_csv<std::string>(ifs, ',', 0, 1)); + ifs.close(); + std::vector<std::string> datetimes (x_dts.begin(), x_dts.end()); + + ifs.open(EVALHYD_DATA_DIR "/q_obs_1yr.csv"); + xt::xtensor<double, 1> observed = xt::squeeze(xt::load_csv<double>(ifs, ',', 1)); + ifs.close(); + + ifs.open(EVALHYD_DATA_DIR "/q_prd_1yr.csv"); + xt::xtensor<double, 2> predicted = xt::load_csv<double>(ifs, ',', 1); + ifs.close(); + + // compute metrics via bootstrap + std::unordered_map<std::string, int> bootstrap_0 = + {{"n_samples", 10}, {"len_sample", 3}, {"summary", 0}}; + + std::vector<xt::xarray<double>> metrics_raw = + evalhyd::evalp( + xt::eval(xt::view(observed, xt::newaxis(), xt::all())), + xt::eval(xt::view(predicted, xt::newaxis(), xt::newaxis(), xt::all(), xt::all())), + all_metrics_p, + thresholds, + "high", // events + confidence_levels, + xt::xtensor<bool, 4>({}), // t_msk + xt::xtensor<std::array<char, 32>, 2>({}), // m_cdt + bootstrap_0, + datetimes + ); + + // compute metrics via bootstrap with mean and standard deviation summary + std::unordered_map<std::string, int> bootstrap_1 = + {{"n_samples", 10}, {"len_sample", 3}, {"summary", 1}}; + + std::vector<xt::xarray<double>> metrics_mas = + evalhyd::evalp( + xt::eval(xt::view(observed, xt::newaxis(), xt::all())), + xt::eval(xt::view(predicted, xt::newaxis(), xt::newaxis(), xt::all(), xt::all())), + all_metrics_p, + thresholds, + "high", // events + confidence_levels, + xt::xtensor<bool, 4>({}), // t_msk + xt::xtensor<std::array<char, 32>, 2>({}), // m_cdt + bootstrap_1, + datetimes + ); + + // check results are identical + for (std::size_t m = 0; m < all_metrics_p.size(); m++) + { + // --------------------------------------------------------------------- + // /!\ skip ranks-based metrics because it contains a random process + // for which setting the seed will not work because the time series + // lengths are different between "bts" and "rep", which + // results in different tensor shapes, and hence in different + // random ranks for ties + if ((all_metrics_p[m] == "RANK_HIST") + || (all_metrics_p[m] == "DS") + || (all_metrics_p[m] == "AS")) + { + continue; + } + // --------------------------------------------------------------------- + + // mean + EXPECT_TRUE( + xt::all(xt::isclose( + xt::mean(metrics_raw[m], 3), + xt::view(metrics_mas[m], xt::all(), xt::all(), xt::all(), 0) + )) + ) << "Failure for (" << all_metrics_p[m] << ") on mean"; + // standard deviation + EXPECT_TRUE( + xt::all(xt::isclose( + xt::stddev(metrics_raw[m], 3), + xt::view(metrics_mas[m], xt::all(), xt::all(), xt::all(), 1) + )) + ) << "Failure for (" << all_metrics_p[m] << ") on standard deviation"; + } + + // compute metrics via bootstrap with quantiles summary + std::unordered_map<std::string, int> bootstrap_2 = + {{"n_samples", 10}, {"len_sample", 3}, {"summary", 2}}; + + std::vector<xt::xarray<double>> metrics_qtl = + evalhyd::evalp( + xt::eval(xt::view(observed, xt::newaxis(), xt::all())), + xt::eval(xt::view(predicted, xt::newaxis(), xt::newaxis(), xt::all(), xt::all())), + all_metrics_p, + thresholds, + "high", // events + confidence_levels, + xt::xtensor<bool, 4>({}), // t_msk + xt::xtensor<std::array<char, 32>, 2>({}), // m_cdt + bootstrap_2, + datetimes + ); + + // check results are identical + for (std::size_t m = 0; m < all_metrics_p.size(); m++) + { + // --------------------------------------------------------------------- + // /!\ skip ranks-based metrics because it contains a random process + // for which setting the seed will not work because the time series + // lengths are different between "bts" and "rep", which + // results in different tensor shapes, and hence in different + // random ranks for ties + if ((all_metrics_p[m] == "RANK_HIST") + || (all_metrics_p[m] == "DS") + || (all_metrics_p[m] == "AS")) + { + continue; + } + // --------------------------------------------------------------------- + + // quantiles + std::vector<double> quantiles = {0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95}; + std::size_t i = 0; + + for (auto q : quantiles) + { + EXPECT_TRUE( + xt::all(xt::isclose( + xt::quantile(metrics_raw[m], {q}, 3), + xt::view(metrics_qtl[m], xt::all(), xt::all(), xt::all(), i) + )) + ) << "Failure for (" << all_metrics_p[m] << ") on quantile " << q; + i++; + } + } +}