Commit 602b486d authored by Thibault Hallouin's avatar Thibault Hallouin
Browse files

add contingency table (CONT_TBL) as new deterministic metric

1 merge request!3release v0.1.0
Pipeline #44774 failed with stage
in 3 minutes and 35 seconds
Showing with 810 additions and 83 deletions
+810 -83
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
#include "diagnostics.hpp" #include "diagnostics.hpp"
#include "errors.hpp" #include "errors.hpp"
#include "efficiencies.hpp" #include "efficiencies.hpp"
#include "events.hpp"
namespace evalhyd namespace evalhyd
...@@ -28,6 +29,8 @@ namespace evalhyd ...@@ -28,6 +29,8 @@ namespace evalhyd
const XD2& q_obs; const XD2& q_obs;
const XD2& q_prd; const XD2& q_prd;
// members for optional input data // members for optional input data
const XD2& _q_thr;
xtl::xoptional<const std::string, bool> _events;
xt::xtensor<bool, 3> t_msk; xt::xtensor<bool, 3> t_msk;
const std::vector<xt::xkeep_slice<int>>& b_exp; const std::vector<xt::xkeep_slice<int>>& b_exp;
...@@ -35,6 +38,7 @@ namespace evalhyd ...@@ -35,6 +38,7 @@ namespace evalhyd
std::size_t n_tim; std::size_t n_tim;
std::size_t n_msk; std::size_t n_msk;
std::size_t n_srs; std::size_t n_srs;
std::size_t n_thr;
std::size_t n_exp; std::size_t n_exp;
// members for computational elements // members for computational elements
...@@ -54,6 +58,12 @@ namespace evalhyd ...@@ -54,6 +58,12 @@ namespace evalhyd
xtl::xoptional<xt::xtensor<double, 3>, bool> gamma; xtl::xoptional<xt::xtensor<double, 3>, bool> gamma;
xtl::xoptional<xt::xtensor<double, 3>, bool> alpha_np; xtl::xoptional<xt::xtensor<double, 3>, bool> alpha_np;
xtl::xoptional<xt::xtensor<double, 3>, bool> bias; xtl::xoptional<xt::xtensor<double, 3>, bool> bias;
xtl::xoptional<xt::xtensor<double, 3>, bool> obs_event;
xtl::xoptional<xt::xtensor<double, 3>, bool> prd_event;
xtl::xoptional<xt::xtensor<double, 3>, bool> ct_a;
xtl::xoptional<xt::xtensor<double, 3>, bool> ct_b;
xtl::xoptional<xt::xtensor<double, 3>, bool> ct_c;
xtl::xoptional<xt::xtensor<double, 3>, bool> ct_d;
// members for evaluation metrics // members for evaluation metrics
xtl::xoptional<xt::xtensor<double, 3>, bool> MAE; xtl::xoptional<xt::xtensor<double, 3>, bool> MAE;
...@@ -67,6 +77,50 @@ namespace evalhyd ...@@ -67,6 +77,50 @@ namespace evalhyd
xtl::xoptional<xt::xtensor<double, 4>, bool> KGEPRIME_D; xtl::xoptional<xt::xtensor<double, 4>, bool> KGEPRIME_D;
xtl::xoptional<xt::xtensor<double, 3>, bool> KGENP; xtl::xoptional<xt::xtensor<double, 3>, bool> KGENP;
xtl::xoptional<xt::xtensor<double, 4>, bool> KGENP_D; xtl::xoptional<xt::xtensor<double, 4>, bool> KGENP_D;
xtl::xoptional<xt::xtensor<double, 5>, bool> CONT_TBL;
// methods to get optional parameters
auto get_q_thr()
{
if (_q_thr.size() < 1)
{
throw std::runtime_error(
"threshold-based metric requested, "
"but *q_thr* not provided"
);
}
else{
return _q_thr;
}
}
bool is_high_flow_event()
{
if (_events.has_value())
{
if (_events.value() == "high")
{
return true;
}
else if (_events.value() == "low")
{
return false;
}
else
{
throw std::runtime_error(
"invalid value for *events*: " + _events.value()
);
}
}
else
{
throw std::runtime_error(
"threshold-based metric requested, "
"but *events* not provided"
);
}
}
// methods to compute elements // methods to compute elements
xt::xtensor<double, 3> get_t_counts() xt::xtensor<double, 3> get_t_counts()
...@@ -253,13 +307,83 @@ namespace evalhyd ...@@ -253,13 +307,83 @@ namespace evalhyd
return bias.value(); return bias.value();
}; };
xt::xtensor<double, 3> get_obs_event()
{
if (!obs_event.has_value())
{
obs_event = elements::calc_obs_event(
q_obs, get_q_thr(), is_high_flow_event()
);
}
return obs_event.value();
};
xt::xtensor<double, 3> get_prd_event()
{
if (!prd_event.has_value())
{
prd_event = elements::calc_prd_event(
q_prd, get_q_thr(), is_high_flow_event()
);
}
return prd_event.value();
};
xt::xtensor<double, 3> get_ct_a()
{
if (!ct_a.has_value())
{
ct_a = elements::calc_ct_a(
get_obs_event(), get_prd_event()
);
}
return ct_a.value();
};
xt::xtensor<double, 3> get_ct_b()
{
if (!ct_b.has_value())
{
ct_b = elements::calc_ct_b(
get_obs_event(), get_prd_event()
);
}
return ct_b.value();
};
xt::xtensor<double, 3> get_ct_c()
{
if (!ct_c.has_value())
{
ct_c = elements::calc_ct_c(
get_obs_event(), get_prd_event()
);
}
return ct_c.value();
};
xt::xtensor<double, 3> get_ct_d()
{
if (!ct_d.has_value())
{
ct_d = elements::calc_ct_d(
get_obs_event(), get_prd_event()
);
}
return ct_d.value();
};
public: public:
// constructor method // constructor method
Evaluator(const XD2& obs, Evaluator(const XD2& obs,
const XD2& prd, const XD2& prd,
const XD2& thr,
xtl::xoptional<const std::string&, bool> events,
const XB3& msk, const XB3& msk,
const std::vector<xt::xkeep_slice<int>>& exp) : const std::vector<xt::xkeep_slice<int>>& exp) :
q_obs{obs}, q_prd{prd}, t_msk{msk}, b_exp{exp} q_obs{obs}, q_prd{prd},
_q_thr{thr}, _events{events},
t_msk{msk}, b_exp{exp}
{ {
// initialise a mask if none provided // initialise a mask if none provided
// (corresponding to no temporal subset) // (corresponding to no temporal subset)
...@@ -274,6 +398,7 @@ namespace evalhyd ...@@ -274,6 +398,7 @@ namespace evalhyd
n_srs = q_prd.shape(0); n_srs = q_prd.shape(0);
n_tim = q_prd.shape(1); n_tim = q_prd.shape(1);
n_msk = t_msk.shape(1); n_msk = t_msk.shape(1);
n_thr = _q_thr.shape(1);
n_exp = b_exp.size(); n_exp = b_exp.size();
// drop time steps where observations or predictions are NaN // drop time steps where observations or predictions are NaN
...@@ -417,6 +542,19 @@ namespace evalhyd ...@@ -417,6 +542,19 @@ namespace evalhyd
return KGENP_D.value(); return KGENP_D.value();
}; };
xt::xtensor<double, 5> get_CONT_TBL()
{
if (!CONT_TBL.has_value())
{
CONT_TBL = metrics::calc_CONT_TBL(
get_q_thr(), get_ct_a(), get_ct_b(), get_ct_c(),
get_ct_d(), t_msk, b_exp,
n_srs, n_thr, n_msk, n_exp
);
}
return CONT_TBL.value();
};
// methods to compute diagnostics // methods to compute diagnostics
xt::xtensor<double, 3> get_completeness() xt::xtensor<double, 3> get_completeness()
{ {
......
// Copyright (c) 2023, INRAE.
// Distributed under the terms of the GPL-3 Licence.
// The full licence is in the file LICENCE, distributed with this software.
#ifndef EVALHYD_DETERMINIST_EVENTS_HPP
#define EVALHYD_DETERMINIST_EVENTS_HPP
#include <xtensor/xtensor.hpp>
#include <xtensor/xview.hpp>
#include <xtensor/xmasked_view.hpp>
#include <xtensor/xmath.hpp>
namespace evalhyd
{
namespace determinist
{
namespace elements
{
// Contingency table:
//
// OBS
// Y N
// +-----+-----+ a: hits
// Y | a | b | b: false alarms
// PRD +-----+-----+ c: misses
// N | c | d | d: correct rejections
// +-----+-----+
//
/// Determine observed realisation of threshold(s) exceedance.
///
/// \param q_obs
/// Streamflow observations.
/// shape: (1, time)
/// \param q_thr
/// Streamflow exceedance threshold(s).
/// shape: (series, thresholds)
/// \param is_high_flow_event
/// Whether events correspond to being above the threshold(s).
/// \return
/// Event observed outcome.
/// shape: (series, thresholds, time)
template<class XD2>
inline xt::xtensor<double, 3> calc_obs_event(
const XD2& q_obs,
const XD2& q_thr,
bool is_high_flow_event
)
{
if (is_high_flow_event)
{
// observations above threshold(s)
return xt::view(q_obs, xt::all(), xt::newaxis(), xt::all())
>= xt::view(q_thr, xt::all(), xt::all(), xt::newaxis());
}
else
{
// observations below threshold(s)
return xt::view(q_obs, xt::all(), xt::newaxis(), xt::all())
<= xt::view(q_thr, xt::all(), xt::all(), xt::newaxis());
}
}
/// Determine predicted realisation of threshold(s) exceedance.
///
/// \param q_prd
/// Streamflow predictions.
/// shape: (series, time)
/// \param q_thr
/// Streamflow exceedance threshold(s).
/// shape: (series, thresholds)
/// \param is_high_flow_event
/// Whether events correspond to being above the threshold(s).
/// \return
/// Event predicted outcome.
/// shape: (series, thresholds, time)
template<class XD2>
inline xt::xtensor<double, 3> calc_prd_event(
const XD2& q_prd,
const XD2& q_thr,
bool is_high_flow_event
)
{
if (is_high_flow_event)
{
// observations above threshold(s)
return xt::view(q_prd, xt::all(), xt::newaxis(), xt::all())
>= xt::view(q_thr, xt::all(), xt::all(), xt::newaxis());
}
else
{
// observations below threshold(s)
return xt::view(q_prd, xt::all(), xt::newaxis(), xt::all())
<= xt::view(q_thr, xt::all(), xt::all(), xt::newaxis());
}
}
/// Determine hits ('a' in contingency table).
///
/// \param obs_event
/// Observed event outcome.
/// shape: (sites, thresholds, time)
/// \param prd_event
/// Predicted event outcome.
/// shape: (sites, thresholds, time)
/// \return
/// Hits.
/// shape: (sites, thresholds, time)
inline xt::xtensor<double, 3> calc_ct_a(
const xt::xtensor<double, 3>& obs_event,
const xt::xtensor<double, 3>& prd_event
)
{
return xt::equal(obs_event, 1.) && xt::equal(prd_event, 1.);
}
/// Determine false alarms ('b' in contingency table).
///
/// \param obs_event
/// Observed event outcome.
/// shape: (sites, thresholds, time)
/// \param prd_event
/// Predicted event outcome.
/// shape: (sites, thresholds, time)
/// \return
/// False alarms.
/// shape: (sites, thresholds, time)
inline xt::xtensor<double, 3> calc_ct_b(
const xt::xtensor<double, 3>& obs_event,
const xt::xtensor<double, 3>& prd_event
)
{
return xt::equal(obs_event, 0.) && xt::equal(prd_event, 1.);
}
/// Determine misses ('c' in contingency table).
///
/// \param obs_event
/// Observed event outcome.
/// shape: (sites, thresholds, time)
/// \param prd_event
/// Predicted event outcome.
/// shape: (sites, thresholds, time)
/// \return
/// Misses.
/// shape: (sites, thresholds, time)
inline xt::xtensor<double, 3> calc_ct_c(
const xt::xtensor<double, 3>& obs_event,
const xt::xtensor<double, 3>& prd_event
)
{
return xt::equal(obs_event, 1.) && xt::equal(prd_event, 0.);
}
/// Determine correct rejections ('d' in contingency table).
///
/// \param obs_event
/// Observed event outcome.
/// shape: (sites, thresholds, time)
/// \param prd_event
/// Predicted event outcome.
/// shape: (sites, thresholds, time)
/// \return
/// Correct rejections.
/// shape: (sites, thresholds, time)
inline xt::xtensor<double, 3> calc_ct_d(
const xt::xtensor<double, 3>& obs_event,
const xt::xtensor<double, 3>& prd_event
)
{
return xt::equal(obs_event, 0.) && xt::equal(prd_event, 0.);
}
}
namespace metrics
{
/// Compute the cells of the contingency table (CONT_TBL),
/// i.e. 'hits', 'false alarms', 'misses', 'correct rejections',
/// in this order.
///
/// \param q_thr
/// Streamflow exceedance threshold(s).
/// shape: (series, thresholds)
/// \param ct_a
/// Hits for each time step.
/// shape: (series, thresholds, time)
/// \param ct_b
/// False alarms for each time step.
/// shape: (series, thresholds, time)
/// \param ct_c
/// Misses for each time step.
/// shape: (series, thresholds, time)
/// \param ct_d
/// Correct rejections for each time step.
/// shape: (series, thresholds, time)
/// \param t_msk
/// Temporal subsets of the whole record.
/// shape: (series, subsets, time)
/// \param b_exp
/// Boostrap samples.
/// shape: (samples, time slice)
/// \param n_sit
/// Number of sites.
/// \param n_ldt
/// Number of lead times.
/// \param n_thr
/// Number of thresholds.
/// \param n_mbr
/// Number of ensemble members.
/// \param n_msk
/// Number of temporal subsets.
/// \param n_exp
/// Number of bootstrap samples.
/// \return
/// Probabilities of detection.
/// shape: (series, subsets, samples, thresholds, cells)
template<class XD2>
inline xt::xtensor<double, 5> calc_CONT_TBL(
const XD2& q_thr,
const xt::xtensor<double, 3>& ct_a,
const xt::xtensor<double, 3>& ct_b,
const xt::xtensor<double, 3>& ct_c,
const xt::xtensor<double, 3>& ct_d,
const xt::xtensor<bool, 3>& t_msk,
const std::vector<xt::xkeep_slice<int>>& b_exp,
std::size_t n_srs,
std::size_t n_thr,
std::size_t n_msk,
std::size_t n_exp
)
{
// initialise output variable
xt::xtensor<double, 5> CONT_TBL =
xt::zeros<double>({n_srs, n_msk, n_exp,
n_thr, std::size_t {4}});
// compute variable one mask at a time to minimise memory imprint
for (std::size_t m = 0; m < n_msk; m++)
{
std::size_t i = 0;
for (auto cell: {ct_a, ct_b, ct_c, ct_d})
{
// apply the mask
// (using NaN workaround until reducers work on masked_view)
auto cell_masked = xt::where(
xt::view(t_msk, xt::all(), m, xt::newaxis(), xt::all()),
cell,
NAN
);
// compute variable one sample at a time
for (std::size_t e = 0; e < n_exp; e++)
{
// apply the bootstrap sampling
auto cell_masked_sampled =
xt::view(cell_masked, xt::all(), xt::all(),
b_exp[e]);
// calculate the mean over the time steps
xt::view(CONT_TBL, xt::all(), m, e, xt::all(), i) =
xt::nansum(cell_masked_sampled, -1);
}
i++;
}
}
// assign NaN where thresholds were not provided (i.e. set as NaN)
xt::masked_view(
CONT_TBL,
xt::isnan(xt::view(q_thr, xt::all(), xt::newaxis(),
xt::newaxis(), xt::all(),
xt::newaxis()))
) = NAN;
return CONT_TBL;
}
}
}
}
#endif //EVALHYD_DETERMINIST_EVENTS_HPP
...@@ -173,6 +173,9 @@ namespace evalhyd ...@@ -173,6 +173,9 @@ namespace evalhyd
const xt::xexpression<XD2>& q_obs, const xt::xexpression<XD2>& q_obs,
const xt::xexpression<XD2>& q_prd, const xt::xexpression<XD2>& q_prd,
const std::vector<std::string>& metrics, const std::vector<std::string>& metrics,
const xt::xexpression<XD2>& q_thr = XD2({}),
xtl::xoptional<const std::string, bool> events =
xtl::missing<const std::string>(),
xtl::xoptional<const std::string, bool> transform = xtl::xoptional<const std::string, bool> transform =
xtl::missing<const std::string>(), xtl::missing<const std::string>(),
xtl::xoptional<double, bool> exponent = xtl::xoptional<double, bool> exponent =
...@@ -194,7 +197,8 @@ namespace evalhyd ...@@ -194,7 +197,8 @@ namespace evalhyd
if (xt::get_rank<XD2>::value != 2) if (xt::get_rank<XD2>::value != 2)
{ {
throw std::runtime_error( throw std::runtime_error(
"observations and/or predictions are not two-dimensional" "observations and/or predictions and/or thresholds "
"are not two-dimensional"
); );
} }
if (xt::get_rank<XB3>::value != 3) if (xt::get_rank<XB3>::value != 3)
...@@ -207,6 +211,7 @@ namespace evalhyd ...@@ -207,6 +211,7 @@ namespace evalhyd
// retrieve real types of the expressions // retrieve real types of the expressions
const XD2& q_obs_ = q_obs.derived_cast(); const XD2& q_obs_ = q_obs.derived_cast();
const XD2& q_prd_ = q_prd.derived_cast(); const XD2& q_prd_ = q_prd.derived_cast();
const XD2& q_thr_ = q_thr.derived_cast();
const XB3& t_msk_ = t_msk.derived_cast(); const XB3& t_msk_ = t_msk.derived_cast();
const XS2& m_cdt_ = m_cdt.derived_cast(); const XS2& m_cdt_ = m_cdt.derived_cast();
...@@ -216,7 +221,8 @@ namespace evalhyd ...@@ -216,7 +221,8 @@ namespace evalhyd
metrics, metrics,
{"MAE", "MARE", "MSE", "RMSE", {"MAE", "MARE", "MSE", "RMSE",
"NSE", "KGE", "KGE_D", "KGEPRIME", "KGEPRIME_D", "NSE", "KGE", "KGE_D", "KGEPRIME", "KGEPRIME_D",
"KGENP", "KGENP_D"} "KGENP", "KGENP_D",
"CONT_TBL"}
); );
if ( diagnostics.has_value() ) if ( diagnostics.has_value() )
...@@ -274,6 +280,17 @@ namespace evalhyd ...@@ -274,6 +280,17 @@ namespace evalhyd
); );
} }
if (q_thr_.size() > 0)
{
if (q_prd_.shape(0) != q_thr_.shape(0))
{
throw std::runtime_error(
"predictions and thresholds feature different "
"numbers of series"
);
}
}
if (t_msk_.size() > 0) if (t_msk_.size() > 0)
{ {
if (q_prd_.shape(0) != t_msk_.shape(0)) if (q_prd_.shape(0) != t_msk_.shape(0))
...@@ -407,6 +424,7 @@ namespace evalhyd ...@@ -407,6 +424,7 @@ namespace evalhyd
const XD2& obs = q_transform(q_obs_); const XD2& obs = q_transform(q_obs_);
const XD2& prd = q_transform(q_prd_); const XD2& prd = q_transform(q_prd_);
const XD2& thr = q_transform(q_thr_);
// generate bootstrap experiment if requested // generate bootstrap experiment if requested
std::vector<xt::xkeep_slice<int>> exp; std::vector<xt::xkeep_slice<int>> exp;
...@@ -440,7 +458,7 @@ namespace evalhyd ...@@ -440,7 +458,7 @@ namespace evalhyd
// instantiate determinist evaluator // instantiate determinist evaluator
determinist::Evaluator<XD2, XB3> evaluator( determinist::Evaluator<XD2, XB3> evaluator(
obs, prd, obs, prd, thr, events,
t_msk_.size() > 0 ? t_msk_: (m_cdt_.size() > 0 ? c_msk : t_msk_), t_msk_.size() > 0 ? t_msk_: (m_cdt_.size() > 0 ? c_msk : t_msk_),
exp exp
); );
...@@ -516,6 +534,12 @@ namespace evalhyd ...@@ -516,6 +534,12 @@ namespace evalhyd
uncertainty::summarise_d(evaluator.get_KGENP_D(), summary) uncertainty::summarise_d(evaluator.get_KGENP_D(), summary)
); );
} }
else if ( metric == "CONT_TBL" )
{
r.emplace_back(
uncertainty::summarise_d(evaluator.get_CONT_TBL(), summary)
);
}
} }
if ( diagnostics.has_value() ) if ( diagnostics.has_value() )
......
157.,19.,15.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
157.,19.,15.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
157.,19.,15.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
157.,19.,15.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,19.,14.,120.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,21.,14.,118.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,21.,14.,118.
200.,15.,8.,88.
220.,21.,6.,64.
NAN,NAN,NAN,NAN
158.,21.,14.,118.
200.,16.,8.,87.
221.,21.,5.,64.
NAN,NAN,NAN,NAN
This diff is collapsed.
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment