From d37e69d289a13b8119e87cfe4f0f88da7ede47b0 Mon Sep 17 00:00:00 2001
From: Thibault Hallouin <thibault.hallouin@inrae.fr>
Date: Thu, 20 Oct 2022 16:00:17 +0200
Subject: [PATCH] pave the way for summary statistics on bootstrap samples

Ultimately, the objective is for the user to be able to get the raw
sampled metric values, or the mean and standard deviation of the sampled
metric values, or a series of quantiles of the sampled metric values.
There are still problems with the standard deviation on rtensor, and
the computation of the quantiles does not work on n-dim expressions yet.
So the second and third options are not possible yet, so only the raw
values can be returned. Nonetheless, the machinery and the choice of
where to introduce the summary functionality could be implemented,
which is the purpose of this commit. A new parameter of the bootstrap
experiment called "summary" is added: it can be given a value of 0 (to
get the raw values). In the future, it would also take a value of 1 for
mean+std, and 2 for quantiles.
---
 include/evalhyd/evald.hpp  | 30 +++++++++++++++------------
 include/evalhyd/evalp.hpp  | 34 ++++++++++++++++--------------
 src/uncertainty.hpp        | 42 ++++++++++++++++++++++++++++++++++++++
 src/utils.hpp              | 11 ++++++++++
 tests/test_determinist.cpp |  2 +-
 tests/test_probabilist.cpp |  2 +-
 6 files changed, 91 insertions(+), 30 deletions(-)

diff --git a/include/evalhyd/evald.hpp b/include/evalhyd/evald.hpp
index 5692635..71e6905 100644
--- a/include/evalhyd/evald.hpp
+++ b/include/evalhyd/evald.hpp
@@ -11,6 +11,7 @@
 
 #include "../../src/utils.hpp"
 #include "../../src/masks.hpp"
+#include "../../src/maths.hpp"
 #include "../../src/uncertainty.hpp"
 #include "../../src/determinist/evaluator.hpp"
 
@@ -103,10 +104,11 @@ namespace evalhyd
     ///       Parameters for the bootstrapping method used to estimate the
     ///       sampling uncertainty in the evaluation of the predictions.
     ///       Two parameters are mandatory ('n_samples' the number of random
-    ///       samples, and 'len_sample' the length of one sample in number
-    ///       of years), and one parameter is optional ('seed'). If not
-    ///       provided, no bootstrapping is performed. If provided, *dts* must
-    ///       also be provided.
+    ///       samples, 'len_sample' the length of one sample in number of years,
+    ///       and 'summary' the statistics to return to characterise the
+    ///       sampling distribution), and one parameter is optional ('seed').
+    ///       If not provided, no bootstrapping is performed. If provided,
+    ///       *dts* must also be provided.
     ///
     ///       .. seealso:: :doc:`../../functionalities/bootstrapping`
     ///
@@ -168,7 +170,7 @@ namespace evalhyd
             const xt::xtensor<bool, 2>& t_msk = {},
             const xt::xtensor<std::array<char, 32>, 1>& m_cdt = {},
             const std::unordered_map<std::string, int>& bootstrap =
-                    {{"n_samples", -9}, {"len_sample", -9}},
+                    {{"n_samples", -9}, {"len_sample", -9}, {"summary", 0}},
             const std::vector<std::string>& dts = {}
     )
     {
@@ -286,12 +288,12 @@ namespace evalhyd
 
         // generate bootstrap experiment if requested
         std::vector<xt::xkeep_slice<int>> exp;
-        if ((bootstrap.find("n_samples")->second != -9)
-            and (bootstrap.find("len_sample")->second != -9))
+        auto n_samples = bootstrap.find("n_samples")->second;
+        auto len_sample = bootstrap.find("len_sample")->second;
+        if ((n_samples != -9) and (len_sample != -9))
         {
             exp = eh::uncertainty::bootstrap(
-                    dts, bootstrap.find("n_samples")->second,
-                    bootstrap.find("len_sample")->second
+                    dts, n_samples, len_sample
             );
         }
         else
@@ -356,6 +358,8 @@ namespace evalhyd
         // retrieve or compute requested metrics
         std::vector<xt::xarray<double>> r;
 
+        auto summary = bootstrap.find("summary")->second;
+
         for ( const auto& metric : metrics )
         {
             if ( metric == "RMSE" )
@@ -363,28 +367,28 @@ namespace evalhyd
                 if (std::find(req_dep.begin(), req_dep.end(), metric)
                     == req_dep.end())
                     evaluator.calc_RMSE();
-                r.emplace_back(evaluator.RMSE);
+                r.emplace_back(eh::uncertainty::summarise(evaluator.RMSE, summary));
             }
             else if ( metric == "NSE" )
             {
                 if (std::find(req_dep.begin(), req_dep.end(), metric)
                         == req_dep.end())
                     evaluator.calc_NSE();
-                r.emplace_back(evaluator.NSE);
+                r.emplace_back(eh::uncertainty::summarise(evaluator.NSE, summary));
             }
             else if ( metric == "KGE" )
             {
                 if (std::find(req_dep.begin(), req_dep.end(), metric)
                     == req_dep.end())
                     evaluator.calc_KGE();
-                r.emplace_back(evaluator.KGE);
+                r.emplace_back(eh::uncertainty::summarise(evaluator.KGE, summary));
             }
             else if ( metric == "KGEPRIME" )
             {
                 if (std::find(req_dep.begin(), req_dep.end(), metric)
                     == req_dep.end())
                     evaluator.calc_KGEPRIME();
-                r.emplace_back(evaluator.KGEPRIME);
+                r.emplace_back(eh::uncertainty::summarise(evaluator.KGEPRIME, summary));
             }
         }
 
diff --git a/include/evalhyd/evalp.hpp b/include/evalhyd/evalp.hpp
index a24cd48..3168381 100644
--- a/include/evalhyd/evalp.hpp
+++ b/include/evalhyd/evalp.hpp
@@ -12,6 +12,7 @@
 
 #include "../../src/utils.hpp"
 #include "../../src/masks.hpp"
+#include "../../src/maths.hpp"
 #include "../../src/uncertainty.hpp"
 #include "../../src/probabilist/evaluator.h"
 
@@ -74,10 +75,11 @@ namespace evalhyd
     ///       Parameters for the bootstrapping method used to estimate the
     ///       sampling uncertainty in the evaluation of the predictions.
     ///       Two parameters are mandatory ('n_samples' the number of random
-    ///       samples, and 'len_sample' the length of one sample in number
-    ///       of years), and one parameter is optional ('seed'). If not
-    ///       provided, no bootstrapping is performed. If provided, *dts* must
-    ///       also be provided.
+    ///       samples, 'len_sample' the length of one sample in number of years,
+    ///       and 'summary' the statistics to return to characterise the
+    ///       sampling distribution), and one parameter is optional ('seed').
+    ///       If not provided, no bootstrapping is performed. If provided,
+    ///       *dts* must also be provided.
     ///
     ///       .. seealso:: :doc:`../../functionalities/bootstrapping`
     ///
@@ -131,7 +133,7 @@ namespace evalhyd
             const xt::xtensor<bool, 4>& t_msk = {},
             const xt::xtensor<std::array<char, 32>, 2>& m_cdt = {},
             const std::unordered_map<std::string, int>& bootstrap =
-                    {{"n_samples", -9}, {"len_sample", -9}},
+                    {{"n_samples", -9}, {"len_sample", -9}, {"summary", 0}},
             const std::vector<std::string>& dts = {}
     )
     {
@@ -253,12 +255,12 @@ namespace evalhyd
 
         // generate bootstrap experiment if requested
         std::vector<xt::xkeep_slice<int>> b_exp;
-        if ((bootstrap.find("n_samples")->second != -9)
-            and (bootstrap.find("len_sample")->second != -9))
+        auto n_samples = bootstrap.find("n_samples")->second;
+        auto len_sample = bootstrap.find("len_sample")->second;
+        if ((n_samples != -9) and (len_sample != -9))
         {
             b_exp = eh::uncertainty::bootstrap(
-                    dts, bootstrap.find("n_samples")->second,
-                    bootstrap.find("len_sample")->second
+                    dts, n_samples, len_sample
             );
         }
         else
@@ -274,6 +276,8 @@ namespace evalhyd
         for (const auto& metric : metrics)
             r.emplace_back(xt::zeros<double>(dim[metric]));
 
+        auto summary = bootstrap.find("summary")->second;
+
         // compute variables one site at a time to minimise memory imprint
         for (int s = 0; s < n_sit; s++)
             // compute variables one lead time at a time to minimise memory imprint
@@ -330,7 +334,7 @@ namespace evalhyd
                             evaluator.calc_BS();
                         // (sites, lead times, subsets, samples, thresholds)
                         xt::view(r[m], s, l, xt::all(), xt::all(), xt::all()) =
-                                evaluator.BS;
+                                eh::uncertainty::summarise(evaluator.BS, summary);
                     }
                     else if ( metric == "BSS" )
                     {
@@ -339,7 +343,7 @@ namespace evalhyd
                             evaluator.calc_BSS();
                         // (sites, lead times, subsets, samples, thresholds)
                         xt::view(r[m], s, l, xt::all(), xt::all(), xt::all()) =
-                                evaluator.BSS;
+                                eh::uncertainty::summarise(evaluator.BSS, summary);
                     }
                     else if ( metric == "BS_CRD" )
                     {
@@ -348,7 +352,7 @@ namespace evalhyd
                             evaluator.calc_BS_CRD();
                         // (sites, lead times, subsets, samples, thresholds, components)
                         xt::view(r[m], s, l, xt::all(), xt::all(), xt::all(), xt::all()) =
-                                evaluator.BS_CRD;
+                                eh::uncertainty::summarise(evaluator.BS_CRD, summary);
                     }
                     else if ( metric == "BS_LBD" )
                     {
@@ -357,7 +361,7 @@ namespace evalhyd
                             evaluator.calc_BS_LBD();
                         // (sites, lead times, subsets, samples, thresholds, components)
                         xt::view(r[m], s, l, xt::all(), xt::all(), xt::all(), xt::all()) =
-                                evaluator.BS_LBD;
+                                eh::uncertainty::summarise(evaluator.BS_LBD, summary);
                     }
                     else if ( metric == "QS" )
                     {
@@ -366,7 +370,7 @@ namespace evalhyd
                             evaluator.calc_QS();
                         // (sites, lead times, subsets, samples, quantiles)
                         xt::view(r[m], s, l, xt::all(), xt::all(), xt::all()) =
-                                evaluator.QS;
+                                eh::uncertainty::summarise(evaluator.QS, summary);
                     }
                     else if ( metric == "CRPS" )
                     {
@@ -375,7 +379,7 @@ namespace evalhyd
                             evaluator.calc_CRPS();
                         // (sites, lead times, subsets, samples)
                         xt::view(r[m], s, l, xt::all(), xt::all()) =
-                                evaluator.CRPS;
+                                eh::uncertainty::summarise(evaluator.CRPS, summary);
                     }
                 }
             }
diff --git a/src/uncertainty.hpp b/src/uncertainty.hpp
index 6e480f7..d7db1ea 100644
--- a/src/uncertainty.hpp
+++ b/src/uncertainty.hpp
@@ -14,6 +14,8 @@
 #include <xtensor/xrandom.hpp>
 #include <xtensor/xio.hpp>
 
+#include "../../src/maths.hpp"
+
 typedef std::chrono::time_point<
         std::chrono::system_clock, std::chrono::minutes
 > tp_minutes;
@@ -138,6 +140,46 @@ namespace evalhyd
 
             return samples;
         }
+
+        inline auto summarise(const xt::xarray<double>& values, int summary)
+        {
+            // TODO: wait for xt::quantile to be available
+            //       or implement it internally for n-dim expressions
+            // 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;
+            }
+            // 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;
+            }
+            // summary 0: raw (keep all samples)
+            else {
+                return values;
+            }
+        }
+
     }
 }
 
diff --git a/src/utils.hpp b/src/utils.hpp
index f67dea7..60761d2 100644
--- a/src/utils.hpp
+++ b/src/utils.hpp
@@ -112,6 +112,17 @@ namespace evalhyd
                 throw std::runtime_error(
                         "length of sample missing for bootstrap"
                 );
+            // check summary
+            if (bootstrap.find("summary") == bootstrap.end())
+                throw std::runtime_error(
+                        "summary missing for bootstrap"
+                );
+            auto s = bootstrap.find("summary")->second;
+            // TODO: change upper bound when mean+stddev and quantiles implemented
+            if ((s < 0) or (s > 0))
+                throw std::runtime_error(
+                        "invalid value for bootstrap summary"
+                );
             // set seed
             if (bootstrap.find("seed") == bootstrap.end())
                 xt::random::seed(time(nullptr));
diff --git a/tests/test_determinist.cpp b/tests/test_determinist.cpp
index eb5ef2d..0000cbb 100644
--- a/tests/test_determinist.cpp
+++ b/tests/test_determinist.cpp
@@ -344,7 +344,7 @@ TEST(DeterministTests, TestBootstrap)
 
     // compute metrics via bootstrap
     std::unordered_map<std::string, int> bootstrap =
-            {{"n_samples", 10}, {"len_sample", 3}};
+            {{"n_samples", 10}, {"len_sample", 3}, {"summary", 0}};
 
     std::vector<xt::xarray<double>> metrics_bts =
             eh::evald(
diff --git a/tests/test_probabilist.cpp b/tests/test_probabilist.cpp
index f53eaac..4583ade 100644
--- a/tests/test_probabilist.cpp
+++ b/tests/test_probabilist.cpp
@@ -407,7 +407,7 @@ TEST(ProbabilistTests, TestBootstrap)
 
     // compute metrics via bootstrap
     std::unordered_map<std::string, int> bootstrap =
-            {{"n_samples", 10}, {"len_sample", 3}};
+            {{"n_samples", 10}, {"len_sample", 3}, {"summary", 0}};
 
     std::vector<xt::xarray<double>> metrics_bts =
             eh::evalp(
-- 
GitLab