diff --git a/include/evalhyd/evald.hpp b/include/evalhyd/evald.hpp
index 3e37e432927a005aec0288bcf71ea15d26fe5151..56926350ad3f9c87c7f8a69eaf0cdbcc20c4e481 100644
--- a/include/evalhyd/evald.hpp
+++ b/include/evalhyd/evald.hpp
@@ -28,17 +28,15 @@ namespace evalhyd
     ///       Streamflow observations. Time steps with missing observations
     ///       must be assigned `NAN` values. Those time steps will be ignored
     ///       both in the observations and the predictions before the
-    ///       *metrics* are computed. Observations and predictions must be
-    ///       broadcastable.
-    ///       shape: (1 or series, time)
+    ///       *metrics* are computed.
+    ///       shape: (1, time)
     ///
     ///    q_prd: ``xt::xtensor<double, 2>``
     ///       Streamflow predictions. Time steps with missing predictions
     ///       must be assigned `NAN` values. Those time steps will be ignored
     ///       both in the observations and the predictions before the
-    ///       *metrics* are computed. Observations and predictions must feature
-    ///       be broadcastable.
-    ///       shape: (1 or series, time)
+    ///       *metrics* are computed.
+    ///       shape: (series, time)
     ///
     ///    metrics: ``std::vector<std::string>``
     ///       The sequence of evaluation metrics to be computed.
@@ -85,11 +83,11 @@ namespace evalhyd
     ///       the subset). If provided, masks must feature the same number of
     ///       dimensions as observations and predictions, and it must
     ///       broadcastable with both of them.
-    ///       shape: (1 or series, time)
+    ///       shape: (subsets, time)
     ///
     ///       .. seealso:: :doc:`../../functionalities/temporal-masking`
     ///
-    ///    m_cdt: ``xt::xtensor<std::array<char, 32>, 2>``, optional
+    ///    m_cdt: ``xt::xtensor<std::array<char, 32>, 1>``, optional
     ///       Masking conditions to use to generate temporal subsets. Each
     ///       condition consists in a string and can be specified on observed
     ///       streamflow values/statistics (mean, median, quantile), or on time
@@ -97,7 +95,7 @@ namespace evalhyd
     ///       precedence. If not provided and neither is *t_msk*, no subset is
     ///       performed. If provided, there must be as many conditions as there
     ///       are time series of observations.
-    ///       shape: (1 or series, 1)
+    ///       shape: (subsets,)
     ///
     ///       .. seealso:: :doc:`../../functionalities/conditional-masking`
     ///
@@ -125,7 +123,7 @@ namespace evalhyd
     ///    ``std::vector<xt::xarray<double>>``
     ///       The sequence of evaluation metrics computed
     ///       in the same order as given in *metrics*.
-    ///       shape: (metrics,)<(1 or series, samples)>
+    ///       shape: (metrics,)<(series, subsets, samples)>
     ///
     /// :Examples:
     ///    
@@ -168,7 +166,7 @@ namespace evalhyd
             const double exponent = 1,
             double epsilon = -9,
             const xt::xtensor<bool, 2>& t_msk = {},
-            const xt::xtensor<std::array<char, 32>, 2>& m_cdt = {},
+            const xt::xtensor<std::array<char, 32>, 1>& m_cdt = {},
             const std::unordered_map<std::string, int>& bootstrap =
                     {{"n_samples", -9}, {"len_sample", -9}},
             const std::vector<std::string>& dts = {}
@@ -183,6 +181,30 @@ namespace evalhyd
         // check that optional parameters are valid
         eh::utils::check_bootstrap(bootstrap);
 
+        // check that data dimensions are compatible
+        // > time
+        if (q_obs.shape(1) != q_prd.shape(1))
+            throw std::runtime_error(
+                    "observations and predictions feature different "
+                    "temporal lengths"
+            );
+        if (t_msk.size() > 0)
+            if (q_obs.shape(1) != t_msk.shape(1))
+                throw std::runtime_error(
+                        "observations and masks feature different "
+                        "temporal lengths"
+                );
+        // > series
+        if (q_obs.shape(0) != 1)
+            throw std::runtime_error(
+                    "observations contain more than one time series"
+            );
+
+        // retrieve dimensions
+        std::size_t n_tim = q_obs.shape(1);
+        std::size_t n_msk = t_msk.size() > 0 ? t_msk.shape(0) :
+                (m_cdt.size() > 0 ? m_cdt.shape(0) : 1);
+
         // initialise a mask if none provided
         // (corresponding to no temporal subset)
         auto gen_msk = [&]() {
@@ -192,95 +214,22 @@ namespace evalhyd
             // else if m_cdt provided, use them to generate t_msk
             else if (m_cdt.size() > 0)
             {
-                xt::xtensor<bool, 2> c_msk = xt::zeros<bool>(q_obs.shape());
-
-                // flatten arrays to bypass n-dim considerations
-                // (possible because shapes are constrained to be the same)
-                if (m_cdt.shape(m_cdt.dimension() - 1) != 1)
-                    throw std::runtime_error("length of last axis in masking "
-                                             "conditions must be equal to one");
-                for (int a = 0; a < m_cdt.dimension() - 1; a++)
-                    if (q_obs.shape(a) != m_cdt.shape(a))
-                        throw std::runtime_error("masking conditions and observations "
-                                                 "feature incompatible shapes");
-
-                auto f_cdt = xt::flatten(m_cdt);
-                auto f_msk = xt::flatten(c_msk);
-                auto f_obs = xt::flatten(q_obs);
-
-                // determine length of temporal axis
-                auto nt = q_obs.shape(q_obs.dimension() - 1);
+                xt::xtensor<bool, 2> c_msk = xt::zeros<bool>({n_msk, n_tim});
 
-                // generate mask gradually, one condition at a time
-                for (int i = 0; i < m_cdt.size(); i++)
-                {
-                    xt::view(f_msk, xt::range(i*nt, (i+1)*nt)) =
-                            evalhyd::masks::generate_mask_from_conditions(
-                                    xt::view(f_cdt, i),
-                                    xt::view(f_obs, xt::range(i*nt, (i+1)*nt))
+                for (int m = 0; m < n_msk; m++)
+                    xt::view(c_msk, m) =
+                            eh::masks::generate_mask_from_conditions(
+                                    m_cdt[0], xt::view(q_obs, 0), q_prd
                             );
-                }
 
                 return c_msk;
             }
             // if neither t_msk nor m_cdt provided, generate dummy mask
             else
-                return xt::xtensor<bool, 2>{xt::ones<bool>(q_obs.shape())};
+                return xt::xtensor<bool, 2>{xt::ones<bool>({std::size_t{1}, n_tim})};
         };
 
-        const xt::xtensor<bool, 2> msk = gen_msk();
-
-        // check that observations, predictions, and masks dimensions are compatible
-        if (q_obs.dimension() != q_prd.dimension())
-        {
-            throw std::runtime_error(
-                    "observations and predictions feature "
-                    "different numbers of dimensions"
-            );
-        }
-        if (q_obs.dimension() != msk.dimension())
-        {
-            throw std::runtime_error(
-                    "observations and masks feature "
-                    "different numbers of dimensions"
-            );
-        }
-        if (!dts.empty())
-            if (q_obs.shape(q_obs.dimension() - 1) != dts.size())
-                throw std::runtime_error(
-                        "observations and datetimes feature different "
-                        "temporal lengths"
-                );
-
-        auto obs_shp = q_obs.shape();
-        auto prd_shp = q_prd.shape();
-        auto msk_shp = msk.shape();
-
-        // check observations, predictions, and masks are broadcastable
-        if (obs_shp != prd_shp)
-            for (int a = 0; a < q_obs.dimension(); a++)
-                if (obs_shp[a] != prd_shp[a])
-                    if ((obs_shp[a] != 1) and (prd_shp[a] != 1))
-                        throw std::runtime_error(
-                                "observations and predictions are not "
-                                "broadcastable"
-                        );
-
-        if (obs_shp != msk_shp)
-            for (int a = 0; a < q_obs.dimension(); a++)
-                if (obs_shp[a] != msk_shp[a])
-                    if ((obs_shp[a] != 1) and (msk_shp[a] != 1))
-                        throw std::runtime_error(
-                                "observations and masks are not broadcastable"
-                        );
-
-        if (prd_shp != msk_shp)
-            for (int a = 0; a < q_prd.dimension(); a++)
-                if (prd_shp[a] != msk_shp[a])
-                    if ((prd_shp[a] != 1) and (msk_shp[a] != 1))
-                        throw std::runtime_error(
-                                "predictions and masks are not broadcastable"
-                        );
+        auto msk = gen_msk();
 
         // apply streamflow transformation if requested
         auto q_transform = [&](const xt::xtensor<double, 2>& q)
@@ -349,7 +298,7 @@ namespace evalhyd
         {
             // if no bootstrap requested, generate one sample
             // containing all the time indices once
-            xt::xtensor<int, 1> all = xt::arange(obs.shape(obs.dimension() - 1));
+            xt::xtensor<int, 1> all = xt::arange(n_tim);
             exp.push_back(xt::keep(all));
         }
 
diff --git a/src/determinist/evaluator.hpp b/src/determinist/evaluator.hpp
index 0f09078732323942465291f74f4e2ab69b7cdf62..bec074d92f4ddc70558382dff26241ff33006d35 100644
--- a/src/determinist/evaluator.hpp
+++ b/src/determinist/evaluator.hpp
@@ -27,36 +27,17 @@ namespace evalhyd
     {
         class Evaluator
         {
-        // define type for function to be used to "mask" (i.e. yield NaNs) the
-        // time steps for which the observations/predictions are missing data
-        typedef xt::xfunction<
-            xt::detail::conditional_ternary,
-            xt::xfunction<
-                xt::detail::bitwise_or,
-                xt::xfunction<
-                    xt::math::isnan_fun,
-                    const xt::xtensor<double, 2>&
-                >,
-                xt::xfunction<
-                    xt::detail::logical_not,
-                    const xt::xtensor<bool, 2>&
-                >
-            >,
-            xt::xscalar<float>,
-            const xt::xtensor<double, 2>&
-        > nan_function;
-
         private:
             // members for input data
-            const xt::xtensor<double, 2>& _q_obs;
-            const xt::xtensor<double, 2>& _q_prd;
-            nan_function q_obs;
-            nan_function q_prd;
+            const xt::xtensor<double, 2>& q_obs;
+            const xt::xtensor<double, 2>& q_prd;
+            xt::xtensor<bool, 3> t_msk;
             const std::vector<xt::xkeep_slice<int>>& b_exp;
 
             // members for dimensions
-            std::size_t n_srs;
             std::size_t n_tim;
+            std::size_t n_msk;
+            std::size_t n_srs;
             std::size_t n_exp;
             std::vector<std::size_t> inner_dims;
             std::vector<std::size_t> inter_dims;
@@ -64,14 +45,14 @@ namespace evalhyd
             std::vector<std::size_t> final_dims;
 
             // members for computational elements
-            xt::xtensor<double, 3> mean_obs;
-            xt::xtensor<double, 3> mean_prd;
+            xt::xtensor<double, 4> mean_obs;
+            xt::xtensor<double, 4> mean_prd;
             xt::xtensor<double, 2> quad_err;
-            xt::xtensor<double, 3> quad_obs;
-            xt::xtensor<double, 3> quad_prd;
-            xt::xtensor<double, 2> r_pearson;
-            xt::xtensor<double, 2> alpha;
-            xt::xtensor<double, 2> bias;
+            xt::xtensor<double, 4> quad_obs;
+            xt::xtensor<double, 4> quad_prd;
+            xt::xtensor<double, 3> r_pearson;
+            xt::xtensor<double, 3> alpha;
+            xt::xtensor<double, 3> bias;
 
         public:
             // constructor method
@@ -79,45 +60,44 @@ namespace evalhyd
                       const xt::xtensor<double, 2>& prd,
                       const xt::xtensor<bool, 2>& msk,
                       const std::vector<xt::xkeep_slice<int>>& exp) :
-                    _q_obs{obs},
-                    _q_prd{prd},
-                    // "mask" (i.e. use NaN) observations where predictions are
-                    // missing (i.e. NaN)
-                    q_obs{xt::where(xt::isnan(prd) | !msk, NAN, obs)},
-                    // "mask" (i.e. use NaN) predictions where observations are
-                    // missing (i.e. NaN)
-                    q_prd{xt::where(xt::isnan(obs) | !msk, NAN, prd)},
-                    b_exp{exp}
+                    q_obs{obs}, q_prd{prd}, b_exp{exp}
             {
-                if (q_obs.shape(0) > q_prd.shape(0))
-                    n_srs = q_obs.shape(0);
-                else
-                    n_srs = q_prd.shape(0);
-
-                if (q_obs.shape(1) > q_prd.shape(1))
-                    n_tim = q_obs.shape(1);
-                else
-                    n_tim = q_prd.shape(1);
-
+                // determine size for recurring dimensions
+                n_tim = q_prd.shape(1);
+                n_srs = q_prd.shape(0);
+                n_msk = msk.shape(0);
                 n_exp = b_exp.size();
 
                 // determine dimensions for inner components, intermediate
                 // elements, for time average elements, and for final metrics:
-                // -> inner components shape: (samples, series)
-                inner_dims = {n_exp, n_srs};
-                // -> intermediate elements shape: (samples, series, time)
-                inter_dims = {n_exp, n_srs, n_tim};
-                // -> time average elements shape: (samples, series, 1)
-                mean_dims = {n_exp, n_srs, 1};
-                // -> final metrics shape: (series, samples)
-                final_dims = {n_srs, n_exp};
+                // -> inner components shape: (samples, subsets, series)
+                inner_dims = {n_msk, n_exp, n_srs};
+                // -> intermediate elements shape: (samples, subsets, series, time)
+                inter_dims = {n_msk, n_exp, n_srs, n_tim};
+                // -> time average elements shape: (samples, subsets, series, 1)
+                mean_dims = {n_msk, n_exp, n_srs, 1};
+                // -> final metrics shape: (series, subsets, samples)
+                final_dims = {n_srs, n_msk, n_exp};
+
+                // drop time steps where observations or predictions are NaN
+                auto obs_nan = xt::isnan(q_obs);
+                auto prd_nan = xt::isnan(q_prd);
+
+                t_msk = xt::ones<bool>({n_msk, n_srs, n_tim});
+                xt::view(t_msk, xt::all()) =
+                        xt::view(msk, xt::all(), xt::newaxis(), xt::all());
+                for (int m = 0; m < n_msk; m++) {
+                    xt::view(t_msk, m) =
+                            xt::where(obs_nan | prd_nan,
+                                      false, xt::view(t_msk, m));
+                }
             };
 
             // members for evaluation metrics
-            xt::xtensor<double, 2> RMSE;
-            xt::xtensor<double, 2> NSE;
-            xt::xtensor<double, 2> KGE;
-            xt::xtensor<double, 2> KGEPRIME;
+            xt::xtensor<double, 3> RMSE;
+            xt::xtensor<double, 3> NSE;
+            xt::xtensor<double, 3> KGE;
+            xt::xtensor<double, 3> KGEPRIME;
 
             // methods to compute elements
             void calc_mean_obs();
@@ -143,13 +123,22 @@ namespace evalhyd
         //     shape: (series, time)
         // \assign mean_obs:
         //     Mean observed streamflow.
-        //     shape: (samples, series, 1)
+        //     shape: (subsets, samples, series, 1)
         void Evaluator::calc_mean_obs()
         {
             mean_obs = xt::zeros<double>(mean_dims);
-            for (int e = 0; e < n_exp; e++) {
-                auto obs = xt::view(q_obs, xt::all(), b_exp[e]);
-                xt::view(mean_obs, e) = xt::nanmean(obs, -1, xt::keep_dims);
+            for (int m = 0; m < n_msk; m++) {
+                // apply the mask
+                // (using NaN workaround until reducers work on masked_view)
+                auto obs_masked = xt::where(xt::view(t_msk, m), q_obs, NAN);
+
+                // compute variable one sample at a time
+                for (int e = 0; e < n_exp; e++) {
+                    // apply the bootstrap sampling
+                    auto obs = xt::view(obs_masked, xt::all(), b_exp[e]);
+                    xt::view(mean_obs, m, e) =
+                            xt::nanmean(obs, -1, xt::keep_dims);
+                }
             }
         }
 
@@ -160,13 +149,22 @@ namespace evalhyd
         //     shape: (series, time)
         // \assign mean_prd:
         //     Mean predicted streamflow.
-        //     shape: (samples, series, 1)
+        //     shape: (subsets, samples, series, 1)
         void Evaluator::calc_mean_prd()
         {
             mean_prd = xt::zeros<double>(mean_dims);
-            for (int e = 0; e < n_exp; e++) {
-                auto prd = xt::view(q_prd, xt::all(), b_exp[e]);
-                xt::view(mean_prd, e) = xt::nanmean(prd, -1, xt::keep_dims);
+            for (int 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, m), q_prd, NAN);
+
+                // compute variable one sample at a time
+                for (int e = 0; e < n_exp; e++) {
+                    // apply the bootstrap sampling
+                    auto prd = xt::view(prd_masked, xt::all(), b_exp[e]);
+                    xt::view(mean_prd, m, e) =
+                            xt::nanmean(prd, -1, xt::keep_dims);
+                }
             }
         }
 
@@ -197,14 +195,20 @@ namespace evalhyd
         //     shape: (samples, series, 1)
         // \assign quad_obs:
         //     Quadratic errors between observations and mean observation.
-        //     shape: (samples, series, time)
+        //     shape: (subsets, samples, series, time)
         void Evaluator::calc_quad_obs()
         {
             quad_obs = xt::zeros<double>(inter_dims);
-            for (int e = 0; e < n_exp; e++) {
-                xt::view(quad_obs, e) = xt::square(
-                        q_obs - xt::view(mean_obs, e)
-                );
+            for (int m = 0; m < n_msk; m++) {
+                // apply the mask
+                // (using NaN workaround until reducers work on masked_view)
+                auto obs_masked = xt::where(xt::view(t_msk, m), q_obs, NAN);
+
+                for (int e = 0; e < n_exp; e++) {
+                    xt::view(quad_obs, m, e) = xt::square(
+                            obs_masked - xt::view(mean_obs, m, e)
+                    );
+                }
             }
         }
 
@@ -218,14 +222,20 @@ namespace evalhyd
         //     shape: (samples, series, 1)
         // \assign quad_prd:
         //     Quadratic errors between predictions and mean prediction.
-        //     shape: (samples, series, time)
+        //     shape: (subsets, samples, series, time)
         void Evaluator::calc_quad_prd()
         {
             quad_prd = xt::zeros<double>(inter_dims);
-            for (int e = 0; e < n_exp; e++) {
-                xt::view(quad_prd, e) = xt::square(
-                        q_prd - xt::view(mean_prd, e)
-                );
+            for (int 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, m), q_prd, NAN);
+
+                for (int e = 0; e < n_exp; e++) {
+                    xt::view(quad_prd, m, e) = xt::square(
+                            prd_masked - xt::view(mean_prd, m, e)
+                    );
+                }
             }
         }
 
@@ -251,28 +261,36 @@ namespace evalhyd
         //     shape: (samples, series, time)
         // \assign r_pearson:
         //     Pearson correlation coefficients.
-        //     shape: (samples, series)
+        //     shape: (subsets, samples, series)
         void Evaluator::calc_r_pearson()
         {
             // calculate error in timing and dynamics $r_{pearson}$
             // (Pearson's correlation coefficient)
             r_pearson = xt::zeros<double>(inner_dims);
-            for (int e = 0; e < n_exp; e++) {
-                auto prd = xt::view(q_prd, xt::all(), b_exp[e]);
-                auto obs = xt::view(q_obs, xt::all(), b_exp[e]);
-                auto r_num = xt::nansum(
-                        (prd - xt::view(mean_prd, e))
-                        * (obs - xt::view(mean_obs, e)),
-                        -1
-                );
-
-                auto prd2 = xt::view(quad_prd, e, xt::all(), b_exp[e]);
-                auto obs2 = xt::view(quad_obs, e, xt::all(), b_exp[e]);
-                auto r_den = xt::sqrt(
-                        xt::nansum(prd2, -1) * xt::nansum(obs2, -1)
-                );
-
-                xt::view(r_pearson, e) = r_num / r_den;
+            for (int 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, m), q_prd, NAN);
+                auto obs_masked = xt::where(xt::view(t_msk, m), q_obs, NAN);
+
+                // compute variable one sample at a time
+                for (int 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 r_num = xt::nansum(
+                            (prd - xt::view(mean_prd, m, e))
+                            * (obs - xt::view(mean_obs, m, e)),
+                            -1
+                    );
+
+                    auto prd2 = xt::view(quad_prd, m, e, xt::all(), b_exp[e]);
+                    auto obs2 = xt::view(quad_obs, m, e, xt::all(), b_exp[e]);
+                    auto r_den = xt::sqrt(
+                            xt::nansum(prd2, -1) * xt::nansum(obs2, -1)
+                    );
+
+                    xt::view(r_pearson, m, e) = r_num / r_den;
+                }
             }
         }
 
@@ -292,17 +310,25 @@ namespace evalhyd
         //     shape: (samples, series, 1)
         // \assign alpha:
         //     Alphas, ratios of standard deviations.
-        //     shape: (samples, series)
+        //     shape: (subsets, samples, series)
         void Evaluator::calc_alpha()
         {
             // calculate error in spread of flow $alpha$
             alpha = xt::zeros<double>(inner_dims);
-            for (int e = 0; e < n_exp; e++) {
-                auto prd = xt::view(q_prd, xt::all(), b_exp[e]);
-                auto obs = xt::view(q_obs, xt::all(), b_exp[e]);
-                xt::view(alpha, e) =
-                        detail::std_dev(prd, xt::view(mean_prd, e))
-                        / detail::std_dev(obs, xt::view(mean_obs, e));
+            for (int 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, m), q_prd, NAN);
+                auto obs_masked = xt::where(xt::view(t_msk, m), q_obs, NAN);
+
+                // compute variable one sample at a time
+                for (int 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]);
+                    xt::view(alpha, m, e) =
+                            detail::std_dev(prd, xt::view(mean_prd, m, e))
+                            / detail::std_dev(obs, xt::view(mean_obs, m, e));
+                }
             }
         }
 
@@ -316,16 +342,24 @@ namespace evalhyd
         //     shape: (series, time)
         // \assign bias:
         //     Biases.
-        //     shape: (samples, series)
+        //     shape: (subsets, samples, series)
         void Evaluator::calc_bias()
         {
             // calculate $bias$
             bias = xt::zeros<double>(inner_dims);
-            for (int e = 0; e < n_exp; e++) {
-                auto prd = xt::view(q_prd, xt::all(), b_exp[e]);
-                auto obs = xt::view(q_obs, xt::all(), b_exp[e]);
-                xt::view(bias, e) =
-                        xt::nansum(prd, -1) / xt::nansum(obs, -1);
+            for (int 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, m), q_prd, NAN);
+                auto obs_masked = xt::where(xt::view(t_msk, m), q_obs, NAN);
+
+                // compute variable one sample at a time
+                for (int 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]);
+                    xt::view(bias, m, e) =
+                            xt::nansum(prd, -1) / xt::nansum(obs, -1);
+                }
             }
         }
 
@@ -336,14 +370,21 @@ namespace evalhyd
         //     shape: (series, time)
         // \assign RMSE:
         //     Root-mean-square errors.
-        //     shape: (series, samples)
+        //     shape: (series, subsets, samples)
         void Evaluator::calc_RMSE()
         {
             // compute RMSE
             RMSE = xt::zeros<double>(final_dims);
-            for (int e = 0; e < n_exp; e++) {
-                auto err2 = xt::view(quad_err, xt::all(), b_exp[e]);
-                xt::view(RMSE, xt::all(), e) = xt::sqrt(xt::nanmean(err2, -1));
+            for (int m = 0; m < n_msk; m++) {
+                // apply the mask
+                // (using NaN workaround until reducers work on masked_view)
+                auto quad_err_masked = xt::where(xt::view(t_msk, m), quad_err, NAN);
+
+                // compute variable one sample at a time
+                for (int e = 0; e < n_exp; e++) {
+                    auto err2 = xt::view(quad_err_masked, xt::all(), b_exp[e]);
+                    xt::view(RMSE, xt::all(), m, e) = xt::sqrt(xt::nanmean(err2, -1));
+                }
             }
         }
 
@@ -357,21 +398,28 @@ namespace evalhyd
         //     shape: (samples, series, time)
         // \assign NSE:
         //     Nash-Sutcliffe efficiencies.
-        //     shape: (series, samples)
+        //     shape: (series, subsets, samples)
         void Evaluator::calc_NSE()
         {
             NSE = xt::zeros<double>(final_dims);
-            for (int e = 0; e < n_exp; e++) {
-                // compute squared errors operands
-                auto err2 = xt::view(quad_err, xt::all(), b_exp[e]);
-                xt::xtensor<double, 1> f_num =
-                        xt::nansum(err2, -1);
-                auto obs2 = xt::view(quad_obs, e, xt::all(), b_exp[e]);
-                xt::xtensor<double, 1> f_den =
-                        xt::nansum(obs2, -1);
-
-                // compute NSE
-                xt::view(NSE, xt::all(), e) = 1 - (f_num / f_den);
+            for (int m = 0; m < n_msk; m++) {
+                // apply the mask
+                // (using NaN workaround until reducers work on masked_view)
+                auto quad_err_masked = xt::where(xt::view(t_msk, m), quad_err, NAN);
+
+                // compute variable one sample at a time
+                for (int e = 0; e < n_exp; e++) {
+                    // compute squared errors operands
+                    auto err2 = xt::view(quad_err_masked, xt::all(), b_exp[e]);
+                    xt::xtensor<double, 1> f_num =
+                            xt::nansum(err2, -1);
+                    auto obs2 = xt::view(quad_obs, m, e, xt::all(), b_exp[e]);
+                    xt::xtensor<double, 1> f_den =
+                            xt::nansum(obs2, -1);
+
+                    // compute NSE
+                    xt::view(NSE, xt::all(), m, e) = 1 - (f_num / f_den);
+                }
             }
         }
 
@@ -388,17 +436,19 @@ namespace evalhyd
         //     shape: (samples, series)
         // \assign KGE:
         //     Kling-Gupta efficiencies.
-        //     shape: (series, samples)
+        //     shape: (series, subsets, samples)
         void Evaluator::calc_KGE()
         {
             KGE = xt::zeros<double>(final_dims);
-            for (int e = 0; e < n_exp; e++) {
-                // compute KGE
-                xt::view(KGE, xt::all(), e) = 1 - xt::sqrt(
-                        xt::square(xt::view(r_pearson, e) - 1)
-                        + xt::square(xt::view(alpha, e) - 1)
-                        + xt::square(xt::view(bias, e) - 1)
-                );
+            for (int m = 0; m < n_msk; m++) {
+                for (int e = 0; e < n_exp; e++) {
+                    // compute KGE
+                    xt::view(KGE, xt::all(), m, e) = 1 - xt::sqrt(
+                            xt::square(xt::view(r_pearson, m, e) - 1)
+                            + xt::square(xt::view(alpha, m, e) - 1)
+                            + xt::square(xt::view(bias, m, e) - 1)
+                    );
+                }
             }
         }
 
@@ -421,22 +471,24 @@ namespace evalhyd
         //     shape: (samples, series)
         // \assign KGEPRIME:
         //     Modified Kling-Gupta efficiencies.
-        //     shape: (series, samples)
+        //     shape: (series, subsets, samples)
         void Evaluator::calc_KGEPRIME()
         {
             KGEPRIME = xt::zeros<double>(final_dims);
-            for (int e = 0; e < n_exp; e++) {
-                // calculate error in spread of flow $gamma$
-                auto gamma = xt::view(alpha, e)
-                        * (xt::view(mean_obs, e, xt::all(), 0)
-                           / xt::view(mean_prd, e, xt::all(), 0));
-
-                // compute KGEPRIME
-                xt::view(KGEPRIME, xt::all(), e) =  1 - xt::sqrt(
-                        xt::square(xt::view(r_pearson, e) - 1)
-                        + xt::square(gamma - 1)
-                        + xt::square(xt::view(bias, e) - 1)
-                );
+            for (int m = 0; m < n_msk; m++) {
+                for (int e = 0; e < n_exp; e++) {
+                    // calculate error in spread of flow $gamma$
+                    auto gamma = xt::view(alpha, m, e)
+                                 * (xt::view(mean_obs, m, e, xt::all(), 0)
+                                    / xt::view(mean_prd, m, e, xt::all(), 0));
+
+                    // compute KGEPRIME
+                    xt::view(KGEPRIME, xt::all(), m, e) =  1 - xt::sqrt(
+                            xt::square(xt::view(r_pearson, m, e) - 1)
+                            + xt::square(gamma - 1)
+                            + xt::square(xt::view(bias, m, e) - 1)
+                    );
+                }
             }
         }
     }
diff --git a/tests/test_determinist.cpp b/tests/test_determinist.cpp
index 76dbfdbca912c1e8378088415448bcfc2bacb23b..eb5ef2dfcdc99f0f341619720df1bcdd669225f7 100644
--- a/tests/test_determinist.cpp
+++ b/tests/test_determinist.cpp
@@ -43,36 +43,36 @@ TEST(DeterministTests, TestMetrics)
             );
 
     // check results on all metrics
-    xt::xtensor<double, 2> rmse =
-            {{777.034272},
-             {776.878479},
-             {777.800217},
-             {778.151082},
-             {778.61487}};
+    xt::xtensor<double, 3> rmse =
+            {{{777.034272}},
+             {{776.878479}},
+             {{777.800217}},
+             {{778.151082}},
+             {{778.61487 }}};
     EXPECT_TRUE(xt::allclose(metrics[0], rmse));
 
-    xt::xtensor<double, 2> nse =
-            {{0.718912},
-             {0.719025},
-             {0.718358},
-             {0.718104},
-             {0.717767}};
+    xt::xtensor<double, 3> nse =
+            {{{0.718912}},
+             {{0.719025}},
+             {{0.718358}},
+             {{0.718104}},
+             {{0.717767}}};
     EXPECT_TRUE(xt::allclose(metrics[1], nse));
 
-    xt::xtensor<double, 2> kge =
-            {{0.748088},
-             {0.746106},
-             {0.744111},
-             {0.743011},
-             {0.741768}};
+    xt::xtensor<double, 3> kge =
+            {{{0.748088}},
+             {{0.746106}},
+             {{0.744111}},
+             {{0.743011}},
+             {{0.741768}}};
     EXPECT_TRUE(xt::allclose(metrics[2], kge));
 
-    xt::xtensor<double, 2> kgeprime =
-            {{0.813141},
-             {0.812775},
-             {0.812032},
-             {0.811787},
-             {0.811387}};
+    xt::xtensor<double, 3> kgeprime =
+            {{{0.813141}},
+             {{0.812775}},
+             {{0.812032}},
+             {{0.811787}},
+             {{0.811387}}};
     EXPECT_TRUE(xt::allclose(metrics[3], kgeprime));
 }
 
@@ -87,45 +87,45 @@ TEST(DeterministTests, TestTransform)
     std::vector<xt::xarray<double>> metrics =
             evalhyd::evald(observed, predicted, {"NSE"}, "sqrt");
 
-    xt::xtensor<double, 2> nse_sqrt =
-            {{0.882817},
-             {0.883023},
-             {0.883019},
-             {0.883029},
-             {0.882972}};
+    xt::xtensor<double, 3> nse_sqrt =
+            {{{0.882817}},
+             {{0.883023}},
+             {{0.883019}},
+             {{0.883029}},
+             {{0.882972}}};
     EXPECT_TRUE(xt::allclose(metrics[0], nse_sqrt));
 
     // compute and check results on inverted streamflow series
     metrics = evalhyd::evald(observed, predicted, {"NSE"}, "inv");
 
-    xt::xtensor<double, 2> nse_inv =
-            {{0.737323},
-             {0.737404},
-             {0.737429},
-             {0.737546},
-             {0.737595}};
+    xt::xtensor<double, 3> nse_inv =
+            {{{0.737323}},
+             {{0.737404}},
+             {{0.737429}},
+             {{0.737546}},
+             {{0.737595}}};
     EXPECT_TRUE(xt::allclose(metrics[0], nse_inv));
 
     // compute and check results on square-rooted streamflow series
     metrics = evalhyd::evald(observed, predicted, {"NSE"}, "log");
 
-    xt::xtensor<double, 2> nse_log =
-            {{0.893344},
-             {0.893523},
-             {0.893585},
-             {0.893758},
-             {0.893793}};
+    xt::xtensor<double, 3> nse_log =
+            {{{0.893344}},
+             {{0.893523}},
+             {{0.893585}},
+             {{0.893758}},
+             {{0.893793}}};
     EXPECT_TRUE(xt::allclose(metrics[0], nse_log));
 
     // compute and check results on power-transformed streamflow series
     metrics = evalhyd::evald(observed, predicted, {"NSE"}, "pow", 0.2);
 
-    xt::xtensor<double, 2> nse_pow =
-            {{0.899207},
-             {0.899395},
-             {0.899451},
-             {0.899578},
-             {0.899588}};
+    xt::xtensor<double, 3> nse_pow =
+            {{{0.899207}},
+             {{0.899395}},
+             {{0.899451}},
+             {{0.899578}},
+             {{0.899588}}};
     EXPECT_TRUE(xt::allclose(metrics[0], nse_pow));
 
 }
@@ -180,8 +180,8 @@ TEST(DeterministTests, TestMaskingConditions)
     // conditions on streamflow values _________________________________________
 
     // compute scores using masking conditions on streamflow to subset whole record
-    xt::xtensor<std::array<char, 32>, 2> q_conditions ={
-            {{"q_obs{<2000,>3000}"}}
+    xt::xtensor<std::array<char, 32>, 1> q_conditions ={
+            {"q_obs{<2000,>3000}"}
     };
 
     std::vector<xt::xarray<double>> metrics_q_conditioned =
@@ -211,8 +211,8 @@ TEST(DeterministTests, TestMaskingConditions)
     // conditions on streamflow statistics _____________________________________
 
     // compute scores using masking conditions on streamflow to subset whole record
-    xt::xtensor<std::array<char, 32>, 2> q_conditions_ ={
-            {{"q_obs{>=mean}"}}
+    xt::xtensor<std::array<char, 32>, 1> q_conditions_ ={
+            {"q_obs{>=mean}"}
     };
 
     double mean = xt::mean(observed, {1})();
@@ -244,8 +244,8 @@ TEST(DeterministTests, TestMaskingConditions)
     // conditions on temporal indices __________________________________________
 
     // compute scores using masking conditions on time indices to subset whole record
-    xt::xtensor<std::array<char, 32>, 2> t_conditions = {
-            {{"t{0,1,2,3,4,5:97,97,98,99}"}}
+    xt::xtensor<std::array<char, 32>, 1> t_conditions = {
+            {"t{0,1,2,3,4,5:97,97,98,99}"}
     };
 
     std::vector<xt::xarray<double>> metrics_t_conditioned =