diff --git a/include/evalhyd/determinist.hpp b/include/evalhyd/determinist.hpp
index 205c668e569c7aa90aaed63cdc55b2cf7d84988d..7bb8068c212b1c8ca47cfd2544e4ae22a2176493 100644
--- a/include/evalhyd/determinist.hpp
+++ b/include/evalhyd/determinist.hpp
@@ -6,7 +6,7 @@
 #include <xtensor/xexpression.hpp>
 
 #include "utils.hpp"
-#include "deterministic/nse.hpp"
+#include "deterministic/evaluator.hpp"
 
 namespace eh = evalhyd;
 
@@ -36,11 +36,11 @@ namespace evalhyd
             const A& obs = q_obs.derived_cast();
             const A& sim = q_sim.derived_cast();
 
+            eh::determinist::Evaluator<A> evaluator(obs, sim);
+
             // declare maps for memoisation purposes
             std::unordered_map<std::string, std::vector<std::string>> elt;
-            std::unordered_map<std::string, A> mem_elt;
             std::unordered_map<std::string, std::vector<std::string>> dep;
-            std::unordered_map<std::string, A> mem_dep;
 
             // register potentially recurring computation elt across metrics
             // TODO
@@ -71,15 +71,11 @@ namespace evalhyd
 
             for ( const auto& metric : metrics )
             {
-                // if exists, retrieve pre-computed metric
-                if ( mem_dep.find(metric) != mem_dep.end() )
-                {
-                    r.emplace_back(mem_dep[metric]);
-                }
-                // else, compute relevant metric
-                else if ( metric == "nse" )
+                if ( metric == "nse" )
                 {
-                    r.emplace_back(eh::nse<A>(obs, sim));
+                    if ( req_dep.find(metric) == req_dep.end() )
+                        evaluator.calc_nse();
+                    r.emplace_back(evaluator.nse);
                 }
             }
 
diff --git a/include/evalhyd/deterministic/evaluator.hpp b/include/evalhyd/deterministic/evaluator.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a0ae42dde690421a7991ad0da0ffb09d0c7b7ce0
--- /dev/null
+++ b/include/evalhyd/deterministic/evaluator.hpp
@@ -0,0 +1,72 @@
+#ifndef EVALHYD_DETERMINIST_EVALUATOR_HPP
+#define EVALHYD_DETERMINIST_EVALUATOR_HPP
+
+#include <xtensor/xtensor.hpp>
+
+namespace evalhyd
+{
+    namespace determinist
+    {
+        template <class A>
+        class Evaluator
+        {
+        private:
+            // members for input data
+            const A& q_obs;
+            const A& q_sim;
+            // members for computational elements
+            // TODO
+
+        public:
+            // constructor method
+            Evaluator(const A& obs, const A& sim) : q_obs{obs}, q_sim{sim} {
+                // check that data dimensions are compatible
+                if (q_sim.dimension() != q_obs.dimension())
+                {
+                    throw std::runtime_error(
+                            "number of dimensions of 'sim' and 'obs' "
+                            "must be identical"
+                    );
+                }
+            };
+            // members for evaluation metrics
+            A nse;
+            // methods to compute elements
+            // TODO
+            // methods to compute metrics
+            void calc_nse();
+        };
+
+        // Compute the Nash-Sutcliffe Efficiency (NSE).
+        //
+        // If multi-dimensional arrays are provided, the arrays of simulations
+        // and observations must feature the same number of dimensions, they must
+        // be broadcastable, and their temporal dimensions must be along their
+        // last axis.
+        //
+        // \require q_obs:
+        //     Array of streamflow observations.
+        //     shape: (1, time)
+        // \require q_sim:
+        //     Array of streamflow simulations.
+        //     shape: (..., time)
+        // \assign nse
+        //     Array of computed Nash-Sutcliffe efficiencies.
+        //     shape: (...)
+        template <class A>
+        void Evaluator<A>::calc_nse()
+        {
+            // compute average observed flow
+            A q_avg = xt::mean(q_obs, -1, xt::keep_dims);
+
+            // compute squared errors operands
+            A f_num = xt::sum(xt::square(q_obs - q_sim), -1, xt::keep_dims);
+            A f_den = xt::sum(xt::square(q_obs - q_avg), -1, xt::keep_dims);
+
+            // return computed NSE
+            nse = 1 - (f_num / f_den);
+        }
+    }
+}
+
+#endif //EVALHYD_DETERMINIST_EVALUATOR_HPP
diff --git a/include/evalhyd/deterministic/nse.hpp b/include/evalhyd/deterministic/nse.hpp
deleted file mode 100644
index d35d7fe5b4428065ea29c6bfad0bd9d089dde155..0000000000000000000000000000000000000000
--- a/include/evalhyd/deterministic/nse.hpp
+++ /dev/null
@@ -1,54 +0,0 @@
-#ifndef EVALHYD_NSE_HPP
-#define EVALHYD_NSE_HPP
-
-#include <vector>
-#include <xtensor/xexpression.hpp>
-#include <xtensor/xmath.hpp>
-
-namespace evalhyd
-{
-    /// Compute the Nash-Sutcliffe Efficiency (NSE).
-    ///
-    /// If multi-dimensional arrays are provided, the arrays of simulations
-    /// and observations must feature the same number of dimensions, they must
-    /// be broadcastable, and their temporal dimensions must be along their
-    /// last axis.
-    ///
-    /// \tparam A:
-    ///     The type of data structures (e.g. `xt::xarray`, `xt::xtensor`).
-    /// \param [in] obs:
-    ///     Array of streamflow observations.
-    ///     shape: (1, time)
-    /// \param [in] sim:
-    ///     Array of streamflow simulations.
-    ///     shape: (..., time)
-    /// \return
-    ///     Array of computed efficiencies.
-    ///     shape: (...)
-    template <class A>
-    inline A nse(
-            const A& obs,
-            const A& sim
-    )
-    {
-        // check that data dimensions are compatible
-        if (sim.dimension() != obs.dimension())
-        {
-            throw std::runtime_error(
-                    "number of dimensions of 'sim' and 'obs' must be identical"
-            );
-        }
-
-        // compute average observed flow
-        A avg = xt::mean(obs, -1, xt::keep_dims);
-
-        // compute squared errors operands
-        A f_num = xt::sum(xt::square(obs - sim), -1, xt::keep_dims);
-        A f_den = xt::sum(xt::square(obs - avg), -1, xt::keep_dims);
-
-        // return computed NSE
-        return 1 - (f_num / f_den);
-    }
-}
-
-#endif //EVALHYD_NSE_HPP
diff --git a/include/evalhyd/probabilist.hpp b/include/evalhyd/probabilist.hpp
index 2743b920b90cec9f2e371e326757d0899a28daec..00a305b69846b75272cef5af9687ff4ad5dc6905 100644
--- a/include/evalhyd/probabilist.hpp
+++ b/include/evalhyd/probabilist.hpp
@@ -1,19 +1,21 @@
 #ifndef EVALHYD_PROBABILIST_HPP
 #define EVALHYD_PROBABILIST_HPP
 
+#include <utility>
 #include <unordered_map>
 #include <unordered_set>
 #include <xtensor/xtensor.hpp>
+#include <xtensor/xview.hpp>
 
 #include "utils.hpp"
-#include "probabilistic/elements.hpp"
-#include "probabilistic/brier.hpp"
+#include "probabilistic/evaluator.h"
 
 namespace eh = evalhyd;
 
 namespace evalhyd
 {
-    namespace probabilist {
+    namespace probabilist
+    {
         /// Function allowing the evaluation of streamflow forecasts using a
         /// range of relevant metrics.
         ///
@@ -37,11 +39,11 @@ namespace evalhyd
                 const xt::xtensor<double, 1>& q_thr
         )
         {
+            eh::probabilist::Evaluator evaluator(q_obs, q_frc, q_thr);
+
             // declare maps for memoisation purposes
             std::unordered_map<std::string, std::vector<std::string>> elt;
-            std::unordered_map<std::string, xt::xtensor<double, 2>> mem_elt;
             std::unordered_map<std::string, std::vector<std::string>> dep;
-            std::unordered_map<std::string, xt::xtensor<double, 1>> mem_dep;
 
             // register potentially recurring computation elt across metrics
             elt["bs"] = {"o_k", "y_k"};
@@ -62,18 +64,16 @@ namespace evalhyd
             for (const auto& element : req_elt)
             {
                 if ( element == "o_k" )
-                    mem_elt["o_k"] =
-                            eh::elements::event_observed_outcome(q_obs, q_thr);
+                    evaluator.calc_o_k();
                 else if ( element == "y_k" )
-                    mem_elt["y_k"] =
-                            eh::elements::event_probability_forecast(q_frc, q_thr);
+                    evaluator.calc_y_k();
             }
 
             // pre-compute required dep
             for (const auto& dependency : req_dep)
             {
                 if ( dependency == "bs" )
-                    mem_dep["bs"] = eh::bs(mem_elt);
+                    evaluator.calc_bs();
             }
 
             // retrieve or compute requested metrics
@@ -81,33 +81,33 @@ namespace evalhyd
 
             for (const auto& metric : metrics)
             {
-                // if exists, retrieve pre-computed metric
-                if (mem_dep.find(metric) != mem_dep.end())
-                {
-                    r.emplace_back(mem_dep[metric]);
-                }
-                    // else, compute relevant metric
-                else if ( metric == "bs" )
+                if ( metric == "bs" )
                 {
-                    r.emplace_back(eh::bs(mem_elt));
+                    if (req_dep.find(metric) == req_dep.end())
+                        evaluator.calc_bs();
+                    r.emplace_back(evaluator.bs);
                 }
                 else if ( metric == "bss" )
                 {
-                    r.emplace_back(eh::bss(mem_elt, mem_dep));
+                    if (req_dep.find(metric) == req_dep.end())
+                        evaluator.calc_bss();
+                    r.emplace_back(evaluator.bss);
                 }
                 else if ( metric == "bs_crd" )
                 {
-                    auto c = eh::bs_crd(mem_elt, q_frc.shape(0));
-                    r.emplace_back(xt::row(c, 0));
-                    r.emplace_back(xt::row(c, 1));
-                    r.emplace_back(xt::row(c, 2));
+                    if (req_dep.find(metric) == req_dep.end())
+                        evaluator.calc_bs_crd();
+                    r.emplace_back(xt::row(evaluator.bs_crd, 0));
+                    r.emplace_back(xt::row(evaluator.bs_crd, 1));
+                    r.emplace_back(xt::row(evaluator.bs_crd, 2));
                 }
                 else if ( metric == "bs_lbd" )
                 {
-                    auto c = eh::bs_lbd(mem_elt);
-                    r.emplace_back(xt::row(c, 0));
-                    r.emplace_back(xt::row(c, 1));
-                    r.emplace_back(xt::row(c, 2));
+                    if (req_dep.find(metric) == req_dep.end())
+                        evaluator.calc_bs_lbd();
+                    r.emplace_back(xt::row(evaluator.bs_lbd, 0));
+                    r.emplace_back(xt::row(evaluator.bs_lbd, 1));
+                    r.emplace_back(xt::row(evaluator.bs_lbd, 2));
                 }
             }
 
diff --git a/include/evalhyd/probabilistic/brier.hpp b/include/evalhyd/probabilistic/brier.hpp
deleted file mode 100644
index 2164d1bdbb596380092b4510669977136624dd75..0000000000000000000000000000000000000000
--- a/include/evalhyd/probabilistic/brier.hpp
+++ /dev/null
@@ -1,289 +0,0 @@
-#ifndef EVALHYD_BRIER_HPP
-#define EVALHYD_BRIER_HPP
-
-#include <unordered_map>
-#include <xtensor/xtensor.hpp>
-#include <xtensor/xmath.hpp>
-#include <xtensor/xoperation.hpp>
-
-#include "elements.hpp"
-
-namespace eh = evalhyd;
-
-namespace evalhyd
-{
-    // NOTE --------------------------------------------------------------------
-    // All equations in metrics below are following notations from
-    // "Wilks, D. S. (2011). Statistical methods in the atmospheric sciences.
-    // Amsterdam; Boston: Elsevier Academic Press. ISBN: 9780123850225".
-    // In particular, pp. 302-303, 332-333.
-    // -------------------------------------------------------------------------
-
-    /// Compute the Brier score (BS).
-    ///
-    /// \param [in] elt:
-    ///     Map of pre-computed elements, including required elements:
-    ///         o_k:
-    ///             Observed event outcome.
-    ///             shape: (thresholds, time)
-    ///         y_k:
-    ///             Event probability forecast.
-    ///             shape: (thresholds, time)
-    /// \return
-    ///     1D array of Brier Score for each threshold.
-    ///     shape: (thresholds,)
-    xt::xtensor<double, 1> bs(
-            std::unordered_map<std::string, xt::xtensor<double, 2>>& elt
-    )
-    {
-        // return computed Brier score(s)
-        // $BS = \frac{1}{n} \sum_{k=1}^{n} (o_k - y_k)^2$
-        return xt::mean(xt::square(elt["o_k"] - elt["y_k"]), -1);
-    }
-
-    /// Compute the calibration-refinement decomposition of the Brier score
-    /// into reliability, resolution, and uncertainty.
-    ///
-    /// BS = reliability - resolution + uncertainty
-    ///
-    /// \param [in] elt:
-    ///     Map of pre-computed elements, including required elements:
-    ///         o_k:
-    ///             Observed event outcome.
-    ///             shape: (thresholds, time)
-    ///         y_k:
-    ///             Event probability forecast.
-    ///             shape: (thresholds, time)
-    /// \param [in] n_mbr:
-    ///     Number of forecast members.
-    /// \return
-    ///     2D array of Brier score components (reliability, resolution,
-    ///     uncertainty) for each threshold.
-    ///     shape: (components, thresholds)
-    xt::xtensor<double, 2> bs_crd(
-            std::unordered_map<std::string, xt::xtensor<double, 2>>& elt,
-            std::size_t n_mbr
-    )
-    {
-        // declare internal variables
-        // shape: (bins, thresholds, time)
-        xt::xtensor<double, 3> msk_bins;
-        // shape: (thresholds,)
-        xt::xtensor<double, 1> bar_o;
-        // shape: (bins, thresholds)
-        xt::xtensor<double, 2> N_i, bar_o_i;
-        // shape: (bins,)
-        xt::xtensor<double, 1> y_i;
-
-        // define some dimensions
-        std::size_t n = elt["o_k"].shape(1);
-        std::size_t n_thr = elt["o_k"].shape(0);
-        std::size_t n_cmp = 3;
-
-        // declare and initialise output variable
-        // shape: (components, thresholds)
-        xt::xtensor<double, 2> bs_components = xt::zeros<double>({n_cmp, n_thr});
-
-        // compute range of forecast values $y_i$
-        y_i = xt::arange<double>(double (n_mbr + 1)) / n_mbr;
-
-        // compute mask to subsample time steps belonging to same forecast bin
-        // (where bins are defined as the range of forecast values)
-        msk_bins = xt::equal(
-                elt["y_k"],
-                xt::view(y_i, xt::all(), xt::newaxis(), xt::newaxis())
-        );
-
-        // compute number of forecasts in each forecast bin $N_i$
-        N_i = xt::sum(msk_bins, -1);
-
-        // compute subsample relative frequency
-        // $\bar{o_i} = \frac{1}{N_i} \sum_{k \in N_i} o_k$
-        bar_o_i = xt::where(
-                N_i > 0,
-                xt::sum(
-                        xt::where(
-                                msk_bins,
-                                xt::view(elt["o_k"], xt::newaxis(), xt::all(), xt::all()),
-                                0.
-                        ),
-                        -1
-                ) / N_i,
-                0.
-        );
-
-        // compute mean "climatology" relative frequency of the event
-        // $\bar{o} = \frac{1}{n} \sum_{k=1}^{n} o_k$
-        bar_o = xt::mean(elt["o_k"], -1);
-
-        // calculate reliability =
-        // $\frac{1}{n} \sum_{i=1}^{I} N_i (y_i - \bar{o_i})^2$
-        xt::row(bs_components, 0) =
-                xt::sum(
-                        xt::square(
-                                xt::view(y_i, xt::all(), xt::newaxis())
-                                - bar_o_i
-                        ) * N_i,
-                        0
-                ) / n;
-
-        // calculate resolution =
-        // $\frac{1}{n} \sum_{i=1}^{I} N_i (\bar{o_i} - \bar{o})^2$
-        xt::row(bs_components, 1) =
-                xt::sum(
-                        xt::square(
-                                bar_o_i - bar_o
-                        ) * N_i,
-                        0
-                ) / n;
-
-        // calculate uncertainty = $\bar{o} (1 - \bar{o})$
-        xt::row(bs_components, 2) = bar_o * (1 - bar_o);
-
-        return bs_components;
-    }
-
-    /// Compute the likelihood-based decomposition of the Brier score
-    /// into type 2 bias, discrimination, and sharpness (a.k.a. refinement).
-    ///
-    /// BS = type 2 bias - discrimination + sharpness
-    ///
-    /// \param [in] elt:
-    ///     Map of pre-computed elements, including required elements:
-    ///         o_k:
-    ///             Observed event outcome.
-    ///             shape: (thresholds, time)
-    ///         y_k:
-    ///             Event probability forecast.
-    ///             shape: (thresholds, time)
-    /// \return
-    ///     2D array of Brier score components (type 2 bias, discrimination,
-    ///     sharpness) for each threshold.
-    ///     shape: (components, thresholds)
-    xt::xtensor<double, 2> bs_lbd(
-            std::unordered_map<std::string, xt::xtensor<double, 2>>& elt
-    )
-    {
-        // declare internal variables
-        // shape: (bins, thresholds, time)
-        xt::xtensor<double, 3> msk_bins;
-        // shape: (thresholds,)
-        xt::xtensor<double, 1> bar_y;
-        // shape: (bins, thresholds)
-        xt::xtensor<double, 2> M_j, bar_y_j;
-        // shape: (bins,)
-        xt::xtensor<double, 1> o_j;
-
-        // define some dimensions
-        std::size_t n = elt["o_k"].shape(1);
-        std::size_t n_thr = elt["o_k"].shape(0);
-        std::size_t n_cmp = 3;
-
-        // declare and initialise output variable
-        // shape: (components, thresholds)
-        xt::xtensor<double, 2> bs_components = xt::zeros<double>({n_cmp, n_thr});
-
-        // set the range of observed values $o_j$
-        o_j = {0., 1.};
-
-        // compute mask to subsample time steps belonging to same observation bin
-        // (where bins are defined as the range of forecast values)
-        msk_bins = xt::equal(
-                elt["o_k"],
-                xt::view(o_j, xt::all(), xt::newaxis(), xt::newaxis())
-        );
-
-        // compute number of observations in each observation bin $M_j$
-        M_j = xt::sum(msk_bins, -1);
-
-        // compute subsample relative frequency
-        // $\bar{y_j} = \frac{1}{M_j} \sum_{k \in M_j} y_k$
-        bar_y_j = xt::where(
-                M_j > 0,
-                xt::sum(
-                        xt::where(
-                                msk_bins,
-                                xt::view(elt["y_k"], xt::newaxis(), xt::all(), xt::all()),
-                                0.
-                        ),
-                        -1
-                ) / M_j,
-                0.
-        );
-
-        // compute mean "climatology" forecast probability
-        // $\bar{y} = \frac{1}{n} \sum_{k=1}^{n} y_k$
-        bar_y = xt::mean(elt["y_k"], -1);
-
-        // calculate type 2 bias =
-        // $\frac{1}{n} \sum_{j=1}^{J} M_j (o_j - \bar{y_j})^2$
-        xt::row(bs_components, 0) =
-                xt::sum(
-                        xt::square(
-                                xt::view(o_j, xt::all(), xt::newaxis())
-                                - bar_y_j
-                        ) * M_j,
-                        0
-                ) / n;
-
-        // calculate discrimination =
-        // $\frac{1}{n} \sum_{j=1}^{J} M_j (\bar{y_j} - \bar{y})^2$
-        xt::row(bs_components, 1) =
-                xt::sum(
-                        xt::square(
-                                bar_y_j - bar_y
-                        ) * M_j,
-                        0
-                ) / n;
-
-        // calculate sharpness =
-        // $\frac{1}{n} \sum_{k=1}^{n} (\bar{y_k} - \bar{y})^2$
-        xt::row(bs_components, 2) =
-                xt::sum(
-                        xt::square(
-                                elt["y_k"] -
-                                xt::view(bar_y, xt::all(), xt::newaxis())
-                        ),
-                        -1
-                ) / n;
-
-        return bs_components;
-    }
-
-    /// Compute the Brier skill score (BSS).
-    ///
-    /// \param [in] elt:
-    ///     Map of pre-computed elements, including required elements:
-    ///         o_k:
-    ///             Observed event outcome.
-    ///             shape: (thresholds, time)
-    /// \param [in] dep:
-    ///     Map of pre-computed metrics, including required metrics:
-    ///         bs:
-    ///             Brier score(s).
-    ///             shape: (thresholds,)
-    /// \return
-    ///     1D array of Brier skill score for each threshold.
-    ///     shape: (thresholds,)
-    xt::xtensor<double, 1> bss(
-            std::unordered_map<std::string, xt::xtensor<double, 2>>& elt,
-            std::unordered_map<std::string, xt::xtensor<double, 1>>& dep
-    )
-    {
-        // calculate mean observed event outcome
-        // $\bar{o_k} = \frac{1}{n} \sum_{k=1}^{n} o_k$
-        xt::xtensor<double, 2> bar_o_k = xt::mean(elt["o_k"], -1, xt::keep_dims);
-
-        // calculate reference Brier score(s)
-        // $BS_{ref} = \frac{1}{n} \sum_{k=1}^{n} (o_k - \bar{o_k})^2$
-        xt::xtensor<double, 1> bs_ref = xt::mean(
-                xt::square(elt["o_k"] - bar_o_k), -1
-        );
-
-        // return computed Brier skill score(s)
-        // $BSS = 1 - \frac{BS}{BS_{ref}}
-        return 1 - (dep["bs"] / bs_ref);
-    }
-}
-
-#endif //EVALHYD_BRIER_HPP
diff --git a/include/evalhyd/probabilistic/elements.hpp b/include/evalhyd/probabilistic/elements.hpp
deleted file mode 100644
index 9253f9018e51244f3319f402ec0294f99b5bb21c..0000000000000000000000000000000000000000
--- a/include/evalhyd/probabilistic/elements.hpp
+++ /dev/null
@@ -1,99 +0,0 @@
-#ifndef EVALHYD_ELEMENTS_HPP
-#define EVALHYD_ELEMENTS_HPP
-
-#include <xtensor/xtensor.hpp>
-#include "xtensor/xview.hpp"
-#include <xtensor/xmath.hpp>
-
-
-namespace eh = evalhyd;
-
-namespace evalhyd
-{
-    namespace elements
-    {
-        namespace detail
-        {
-            // determine whether flows `q` are greater than given threshold(s) `thr`
-            //     q shape: (1+, time)
-            //     thr shape: (thresholds,)
-            //     returned shape: (thresholds, 1+, time)
-            xt::xtensor<double, 3> is_above_threshold(
-                    const xt::xtensor<double, 2>& q,
-                    const xt::xtensor<double, 1>& thr
-            )
-            {
-                // return a boolean-like array
-                return q >= xt::view(thr, xt::all(), xt::newaxis(), xt::newaxis());
-            }
-
-            // determine whether flows `q` are strictly lower than given threshold(s) `thr`
-            //     q shape: (1+, time)
-            //     thr shape: (thresholds,)
-            //     returned shape: (thresholds, 1+, time)
-            xt::xtensor<double, 3> is_below_threshold(
-                    const xt::xtensor<double, 2>& q,
-                    const xt::xtensor<double, 1>& thr
-            )
-            {
-                // return a boolean-like array
-                return q < xt::view(thr, xt::all(), xt::newaxis(), xt::newaxis());
-            }
-        }
-
-        /// Determine whether exceedance event has occurred.
-        ///
-        /// \param [in] q_obs:
-        ///     2D array of streamflow observations.
-        ///     shape: (1, time)
-        /// \param [in] q_thr:
-        ///     1D array of streamflow exceedance threshold(s).
-        ///     shape: (thresholds,)
-        /// \return
-        ///     2D array of event observed outcome
-        ///     shape: (thresholds, time)
-        xt::xtensor<double, 2> event_observed_outcome(
-                const xt::xtensor<double, 2>& q_obs,
-                const xt::xtensor<double, 1>& q_thr
-        )
-        {
-            // determine observed realisation of threshold(s) exceedance
-            return xt::squeeze(detail::is_above_threshold(q_obs, q_thr));
-        }
-
-        /// Determine forecast probability of event to occur.
-        ///
-        /// \param [in] q_frc:
-        ///     2D array of streamflow forecasts.
-        ///     shape: (members, time)
-        /// \param [in] q_thr:
-        ///     1D array of streamflow exceedance threshold(s).
-        ///     shape: (thresholds,)
-        /// \return
-        ///     2D array of event probability forecast
-        ///     shape: (thresholds, time)
-        xt::xtensor<double, 2> event_probability_forecast(
-                const xt::xtensor<double, 2>& q_frc,
-                const xt::xtensor<double, 1>& q_thr
-        )
-        {
-            // determine if members have exceeded threshold(s)
-            xt::xtensor<double, 3> e_frc =
-                    detail::is_above_threshold(q_frc, q_thr);
-
-            // calculate how many members have exceeded threshold(s)
-            xt::xtensor<double, 2> n_frc = xt::sum(e_frc, 1);
-
-            // determine correspondence between number of exceeding members
-            // and forecast probabilities of exceedance
-            // /!\ probability calculation dividing by n (the number of
-            //     members), not n+1 (the number of ranks) like in other metrics
-            std::size_t n_mbr = q_frc.shape(0);
-            xt::xtensor<double, 2> p_frc = n_frc / n_mbr;
-
-            return p_frc;
-        }
-    }
-}
-
-#endif //EVALHYD_ELEMENTS_HPP
diff --git a/include/evalhyd/probabilistic/evaluator.h b/include/evalhyd/probabilistic/evaluator.h
new file mode 100644
index 0000000000000000000000000000000000000000..f2d8b72ec802a8003b7607a9a28cac6bb70cc622
--- /dev/null
+++ b/include/evalhyd/probabilistic/evaluator.h
@@ -0,0 +1,53 @@
+#ifndef EVALHYD_PROBABILIST_EVALUATOR_H
+#define EVALHYD_PROBABILIST_EVALUATOR_H
+
+#include <xtensor/xtensor.hpp>
+
+namespace evalhyd
+{
+    namespace probabilist
+    {
+        class Evaluator
+        {
+        private:
+            // members for input data
+            const xt::xtensor<double, 2>& q_obs;
+            const xt::xtensor<double, 2>& q_frc;
+            const xt::xtensor<double, 1>& q_thr;
+            // members for computational elements
+            xt::xtensor<double, 2> o_k;
+            xt::xtensor<double, 2> y_k;
+
+        public:
+            // constructor method
+            Evaluator(const xt::xtensor<double, 2>& obs,
+                      const xt::xtensor<double, 2>& frc,
+                      const xt::xtensor<double, 1>& thr) :
+                    q_obs{obs}, q_frc{frc}, q_thr{thr} {};
+            // members for evaluation metrics
+            xt::xtensor<double, 1> bs;
+            xt::xtensor<double, 2> bs_crd;
+            xt::xtensor<double, 2> bs_lbd;
+            xt::xtensor<double, 1> bss;
+            // methods to compute elements
+            void calc_o_k();
+            void calc_y_k();
+            // methods to compute metrics
+            void calc_bs();
+            void calc_bs_crd();
+            void calc_bs_lbd();
+            void calc_bss();
+        };
+
+        xt::xtensor<double, 3> is_above_threshold(
+                const xt::xtensor<double, 2>&, const xt::xtensor<double, 1>&
+        );
+
+        xt::xtensor<double, 3> is_below_threshold(
+                const xt::xtensor<double, 2>&, const xt::xtensor<double, 1>&
+        );
+
+    }
+}
+
+#endif //EVALHYD_PROBABILIST_EVALUATOR_H
diff --git a/include/evalhyd/probabilistic/evaluator_brier.cpp b/include/evalhyd/probabilistic/evaluator_brier.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..251cef469bd5994445ab4417c78c67b7c1e7228e
--- /dev/null
+++ b/include/evalhyd/probabilistic/evaluator_brier.cpp
@@ -0,0 +1,266 @@
+#include <unordered_map>
+#include <xtensor/xmath.hpp>
+#include <xtensor/xview.hpp>
+#include <xtensor/xoperation.hpp>
+
+#include "evaluator.h"
+
+namespace eh = evalhyd;
+
+// NOTE ------------------------------------------------------------------------
+// All equations in metrics below are following notations from
+// "Wilks, D. S. (2011). Statistical methods in the atmospheric sciences.
+// Amsterdam; Boston: Elsevier Academic Press. ISBN: 9780123850225".
+// In particular, pp. 302-303, 332-333.
+// -----------------------------------------------------------------------------
+
+namespace evalhyd
+{
+    namespace probabilist
+    {
+        // Compute the Brier score (BS).
+        //
+        // \require o_k:
+        //     Observed event outcome.
+        //     shape: (thresholds, time)
+        // \require y_k:
+        //     Event probability forecast.
+        //     shape: (thresholds, time)
+        // \assign bs:
+        //     1D array of Brier Score for each threshold.
+        //     shape: (thresholds,)
+        void Evaluator::calc_bs()
+        {
+            // return computed Brier score(s)
+            // $BS = \frac{1}{n} \sum_{k=1}^{n} (o_k - y_k)^2$
+            bs = xt::mean(xt::square(o_k - y_k), -1);
+        }
+
+        // Compute the calibration-refinement decomposition of the Brier score
+        // into reliability, resolution, and uncertainty.
+        //
+        // BS = reliability - resolution + uncertainty
+        //
+        // \require q_thr:
+        //     1D array of streamflow exceedance threshold(s).
+        //     shape: (thresholds,)
+        // \require o_k:
+        //     Observed event outcome.
+        //     shape: (thresholds, time)
+        // \require y_k:
+        //     Event probability forecast.
+        //     shape: (thresholds, time)
+        // \assign bs_crd:
+        //     2D array of Brier score components (reliability, resolution,
+        //     uncertainty) for each threshold.
+        //     shape: (components, thresholds)
+        void Evaluator::calc_bs_crd()
+        {
+            // declare internal variables
+            // shape: (bins, thresholds, time)
+            xt::xtensor<double, 3> msk_bins;
+            // shape: (thresholds,)
+            xt::xtensor<double, 1> bar_o;
+            // shape: (bins, thresholds)
+            xt::xtensor<double, 2> N_i, bar_o_i;
+            // shape: (bins,)
+            xt::xtensor<double, 1> y_i;
+
+            // define some dimensions
+            std::size_t n = o_k.shape(1);
+            std::size_t n_thr = o_k.shape(0);
+            std::size_t n_mbr = q_frc.shape(0);
+            std::size_t n_cmp = 3;
+
+            // initialise output variable
+            // shape: (components, thresholds)
+            bs_crd = xt::zeros<double>({n_cmp, n_thr});
+
+            // compute range of forecast values $y_i$
+            y_i = xt::arange<double>(double(n_mbr + 1)) / n_mbr;
+
+            // compute mask to subsample time steps belonging to same forecast bin
+            // (where bins are defined as the range of forecast values)
+            msk_bins = xt::equal(
+                    y_k, xt::view(y_i, xt::all(), xt::newaxis(), xt::newaxis())
+            );
+
+            // compute number of forecasts in each forecast bin $N_i$
+            N_i = xt::sum(msk_bins, -1);
+
+            // compute subsample relative frequency
+            // $\bar{o_i} = \frac{1}{N_i} \sum_{k \in N_i} o_k$
+            bar_o_i = xt::where(
+                    N_i > 0,
+                    xt::sum(
+                            xt::where(
+                                    msk_bins,
+                                    xt::view(o_k, xt::newaxis(),
+                                             xt::all(), xt::all()),
+                                    0.
+                            ),
+                            -1
+                    ) / N_i,
+                    0.
+            );
+
+            // compute mean "climatology" relative frequency of the event
+            // $\bar{o} = \frac{1}{n} \sum_{k=1}^{n} o_k$
+            bar_o = xt::mean(o_k, -1);
+
+            // calculate reliability =
+            // $\frac{1}{n} \sum_{i=1}^{I} N_i (y_i - \bar{o_i})^2$
+            xt::row(bs_crd, 0) =
+                    xt::sum(
+                            xt::square(
+                                    xt::view(y_i, xt::all(), xt::newaxis())
+                                    - bar_o_i
+                            ) * N_i,
+                            0
+                    ) / n;
+
+            // calculate resolution =
+            // $\frac{1}{n} \sum_{i=1}^{I} N_i (\bar{o_i} - \bar{o})^2$
+            xt::row(bs_crd, 1) =
+                    xt::sum(
+                            xt::square(
+                                    bar_o_i - bar_o
+                            ) * N_i,
+                            0
+                    ) / n;
+
+            // calculate uncertainty = $\bar{o} (1 - \bar{o})$
+            xt::row(bs_crd, 2) = bar_o * (1 - bar_o);
+        }
+
+        // Compute the likelihood-based decomposition of the Brier score
+        // into type 2 bias, discrimination, and sharpness (a.k.a. refinement).
+        //
+        // BS = type 2 bias - discrimination + sharpness
+        //
+        // \require o_k:
+        //     Observed event outcome.
+        //     shape: (thresholds, time)
+        // \require y_k:
+        //     Event probability forecast.
+        //     shape: (thresholds, time)
+        // \return bs_lbd:
+        //     2D array of Brier score components (type 2 bias, discrimination,
+        //     sharpness) for each threshold.
+        //     shape: (components, thresholds)
+        void Evaluator::calc_bs_lbd()
+        {
+            // declare internal variables
+            // shape: (bins, thresholds, time)
+            xt::xtensor<double, 3> msk_bins;
+            // shape: (thresholds,)
+            xt::xtensor<double, 1> bar_y;
+            // shape: (bins, thresholds)
+            xt::xtensor<double, 2> M_j, bar_y_j;
+            // shape: (bins,)
+            xt::xtensor<double, 1> o_j;
+
+            // define some dimensions
+            std::size_t n = o_k.shape(1);
+            std::size_t n_thr = o_k.shape(0);
+            std::size_t n_cmp = 3;
+
+            // declare and initialise output variable
+            // shape: (components, thresholds)
+            bs_lbd = xt::zeros<double>({n_cmp, n_thr});
+
+            // set the range of observed values $o_j$
+            o_j = {0., 1.};
+
+            // compute mask to subsample time steps belonging to same observation bin
+            // (where bins are defined as the range of forecast values)
+            msk_bins = xt::equal(
+                    o_k, xt::view(o_j, xt::all(), xt::newaxis(), xt::newaxis())
+            );
+
+            // compute number of observations in each observation bin $M_j$
+            M_j = xt::sum(msk_bins, -1);
+
+            // compute subsample relative frequency
+            // $\bar{y_j} = \frac{1}{M_j} \sum_{k \in M_j} y_k$
+            bar_y_j = xt::where(
+                    M_j > 0,
+                    xt::sum(
+                            xt::where(
+                                    msk_bins,
+                                    xt::view(y_k, xt::newaxis(),
+                                             xt::all(), xt::all()),
+                                    0.
+                            ),
+                            -1
+                    ) / M_j,
+                    0.
+            );
+
+            // compute mean "climatology" forecast probability
+            // $\bar{y} = \frac{1}{n} \sum_{k=1}^{n} y_k$
+            bar_y = xt::mean(y_k, -1);
+
+            // calculate type 2 bias =
+            // $\frac{1}{n} \sum_{j=1}^{J} M_j (o_j - \bar{y_j})^2$
+            xt::row(bs_lbd, 0) =
+                    xt::sum(
+                            xt::square(
+                                    xt::view(o_j, xt::all(), xt::newaxis())
+                                    - bar_y_j
+                            ) * M_j,
+                            0
+                    ) / n;
+
+            // calculate discrimination =
+            // $\frac{1}{n} \sum_{j=1}^{J} M_j (\bar{y_j} - \bar{y})^2$
+            xt::row(bs_lbd, 1) =
+                    xt::sum(
+                            xt::square(
+                                    bar_y_j - bar_y
+                            ) * M_j,
+                            0
+                    ) / n;
+
+            // calculate sharpness =
+            // $\frac{1}{n} \sum_{k=1}^{n} (\bar{y_k} - \bar{y})^2$
+            xt::row(bs_lbd, 2) =
+                    xt::sum(
+                            xt::square(
+                                    y_k -
+                                    xt::view(bar_y, xt::all(), xt::newaxis())
+                            ),
+                            -1
+                    ) / n;
+        }
+
+        // Compute the Brier skill score (BSS).
+        //
+        // \require o_k:
+        //     Observed event outcome.
+        //     shape: (thresholds, time)
+        // \require bs:
+        //     Brier score(s).
+        //     shape: (thresholds,)
+        // \assign bss:
+        //     1D array of Brier skill score for each threshold.
+        //     shape: (thresholds,)
+        void Evaluator::calc_bss()
+        {
+            // calculate mean observed event outcome
+            // $\bar{o_k} = \frac{1}{n} \sum_{k=1}^{n} o_k$
+            xt::xtensor<double, 2> bar_o_k = xt::mean(o_k, -1,
+                                                      xt::keep_dims);
+
+            // calculate reference Brier score(s)
+            // $BS_{ref} = \frac{1}{n} \sum_{k=1}^{n} (o_k - \bar{o_k})^2$
+            xt::xtensor<double, 1> bs_ref = xt::mean(
+                    xt::square(o_k - bar_o_k), -1
+            );
+
+            // return computed Brier skill score(s)
+            // $BSS = 1 - \frac{BS}{BS_{ref}}
+            bss = 1 - (bs / bs_ref);
+        }
+    }
+}
diff --git a/include/evalhyd/probabilistic/evaluator_elements.cpp b/include/evalhyd/probabilistic/evaluator_elements.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7d36c5d390675f58e438eb5b66360add17c6c959
--- /dev/null
+++ b/include/evalhyd/probabilistic/evaluator_elements.cpp
@@ -0,0 +1,56 @@
+#include <xtensor/xmath.hpp>
+
+#include "evaluator.h"
+
+namespace evalhyd
+{
+    namespace probabilist
+    {
+        // Determine observed realisation of threshold(s) exceedance.
+        //
+        // \require q_obs:
+        //     2D array of streamflow observations.
+        //     shape: (1, time)
+        // \require q_thr:
+        //     1D array of streamflow exceedance threshold(s).
+        //     shape: (thresholds,)
+        // \assign o_k:
+        //     2D array of event observed outcome.
+        //     shape: (thresholds, time)
+        void Evaluator::calc_o_k()
+        {
+            // determine observed realisation of threshold(s) exceedance
+            o_k = xt::squeeze(is_above_threshold(q_obs, q_thr));
+        }
+
+        // Determine forecast probability of threshold(s) exceedance to occur.
+        //
+        // \require q_frc:
+        //     2D array of streamflow forecasts.
+        //     shape: (members, time)
+        // \require q_thr:
+        //     1D array of streamflow exceedance threshold(s).
+        //     shape: (thresholds,)
+        // \assign y_k
+        //     2D array of event probability forecast.
+        //     shape: (thresholds, time)
+        void Evaluator::calc_y_k()
+        {
+            // determine if members have exceeded threshold(s)
+            xt::xtensor<double, 3> e_frc =
+                    is_above_threshold(q_frc, q_thr);
+
+            // calculate how many members have exceeded threshold(s)
+            xt::xtensor<double, 2> n_frc = xt::sum(e_frc, 1);
+
+            // determine correspondence between number of exceeding members
+            // and forecast probabilities of exceedance
+            // /!\ probability calculation dividing by n (the number of
+            //     members), not n+1 (the number of ranks) like in other metrics
+            std::size_t n_mbr = q_frc.shape(0);
+
+            // determine probability of threshold(s) exceedance
+            y_k = n_frc / n_mbr;
+        }
+    }
+}
diff --git a/include/evalhyd/probabilistic/evaluator_utils.cpp b/include/evalhyd/probabilistic/evaluator_utils.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5999e1fe6ad25a135c7d487fdaad687da1d11d72
--- /dev/null
+++ b/include/evalhyd/probabilistic/evaluator_utils.cpp
@@ -0,0 +1,44 @@
+#include <xtensor/xtensor.hpp>
+#include <xtensor/xview.hpp>
+
+namespace evalhyd
+{
+    namespace probabilist
+    {
+        // Determine whether flows are greater than given threshold(s).
+        //
+        // \param q:
+        //     Array of streamflow data.
+        //     shape: (1+, time)
+        // \param thr:
+        //     Array of streamflow thresholds.
+        //     shape: (thresholds,)
+        // \return
+        //     Array of boolean-like threshold(s) exceedance.
+        //     shape: (thresholds, 1+, time)
+        xt::xtensor<double, 3> is_above_threshold(
+                const xt::xtensor<double, 2> &q,
+                const xt::xtensor<double, 1> &thr
+        ) {
+            return q >= xt::view(thr, xt::all(), xt::newaxis(), xt::newaxis());
+        }
+
+        // Determine whether flows are strictly lower than given threshold(s)
+        //
+        // \param q:
+        //     Array of streamflow data.
+        //     shape: (1+, time)
+        // \param thr:
+        //     Array of streamflow thresholds.
+        //     shape: (thresholds,)
+        // \return
+        //     Array of boolean-like threshold(s) exceedance.
+        //     shape: (thresholds, 1+, time)
+        xt::xtensor<double, 3> is_below_threshold(
+                const xt::xtensor<double, 2> &q,
+                const xt::xtensor<double, 1> &thr
+        ) {
+            return q < xt::view(thr, xt::all(), xt::newaxis(), xt::newaxis());
+        }
+    }
+}