Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds twcrps for ensembles #644

Merged
merged 23 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
.. autofunction:: scores.probability.crps_step_threshold_weight
.. autofunction:: scores.probability.crps_cdf_brier_decomposition
.. autofunction:: scores.probability.crps_for_ensemble
.. autofunction:: scores.probability.twcrps_for_ensemble
.. autofunction:: scores.probability.tail_twcrps_for_ensemble
.. autofunction:: scores.probability.tw_crps_for_ensemble
.. autofunction:: scores.probability.tail_tw_crps_for_ensemble
.. autofunction:: scores.probability.murphy_score
.. autofunction:: scores.probability.murphy_thetas
.. autofunction:: scores.probability.roc_curve_data
Expand Down
4 changes: 2 additions & 2 deletions docs/included.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,11 @@
- —
- —
* - Tail Threshold Weighted Continuous Ranked Probability Score (twCRPS) for Ensembles
- [API](api.md#scores.probability.tail_twcrps_for_ensemble)
- [API](api.md#scores.probability.tail_tw_crps_for_ensemble)
- —
- [Allen et al. (2023)](https://doi.org/10.1137/22M1532184)
* - Threshold Weighted Continuous Ranked Probability Score (twCRPS) for Ensembles
- [API](api.md#scores.probability.twcrps_for_ensemble)
- [API](api.md#scores.probability.tw_crps_for_ensemble)
- —
- [Allen et al. (2023)](https://doi.org/10.1137/22M1532184)
```
Expand Down
8 changes: 4 additions & 4 deletions src/scores/probability/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
crps_cdf_brier_decomposition,
crps_for_ensemble,
crps_step_threshold_weight,
tail_twcrps_for_ensemble,
twcrps_for_ensemble,
tail_tw_crps_for_ensemble,
tw_crps_for_ensemble,
)
from scores.probability.roc_impl import roc_curve_data
from scores.processing.isoreg_impl import isotonic_fit
Expand All @@ -27,6 +27,6 @@
"roc_curve_data",
"isotonic_fit",
"crps_step_threshold_weight",
"twcrps_for_ensemble",
"tail_twcrps_for_ensemble",
"tw_crps_for_ensemble",
"tail_tw_crps_for_ensemble",
]
39 changes: 21 additions & 18 deletions src/scores/probability/crps_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ def crps_for_ensemble(

The value of the constant K in this formula depends on the method:
- If `method="ecdf"` then :math:`K = M ^ 2`. In this case the CRPS value returned is \
the exact CRPS value for the emprical cumulation distribution function \
the exact CRPS value for the emprical cumulative distribution function \
nicholasloveday marked this conversation as resolved.
Show resolved Hide resolved
constructed using the ensemble values.
- If `method="fair"` then :math:`K = M(M - 1)`. In this case the CRPS value returned \
is the approximated CRPS where the ensemble values can be interpreted as a \
Expand Down Expand Up @@ -826,8 +826,8 @@ def crps_for_ensemble(

See also:
:py:func:`scores.probability.crps_cdf`
:py:func:`scores.probability.twcrps_for_ensemble`
:py:func:`scores.probability.tail_twcrps_for_ensemble`
:py:func:`scores.probability.tw_crps_for_ensemble`
:py:func:`scores.probability.tail_tw_crps_for_ensemble`

References:
- C. Ferro (2014), "Fair scores for ensemble forecasts", Quarterly Journal of the \
Expand Down Expand Up @@ -858,7 +858,7 @@ def crps_for_ensemble(
ensemble_member_dim1 = scores.utils.tmp_coord_name(fcst)

# calculate forecast spread contribution
fcst_copy = fcst.rename({ensemble_member_dim: ensemble_member_dim1}) # type: ignoreßß
fcst_copy = fcst.rename({ensemble_member_dim: ensemble_member_dim1}) # type: ignore
fcst_spread_term = abs(fcst - fcst_copy).sum(dim=[ensemble_member_dim, ensemble_member_dim1]) # type: ignore
ens_count = fcst.count(ensemble_member_dim)
if method == "ecdf":
Expand All @@ -876,7 +876,7 @@ def crps_for_ensemble(
return result # type: ignore


def twcrps_for_ensemble(
def tw_crps_for_ensemble(
fcst: XarrayLike,
obs: XarrayLike,
ensemble_member_dim: str,
Expand Down Expand Up @@ -910,7 +910,7 @@ def twcrps_for_ensemble(

There are currently two methods available for calculating the twCRPS: "ecdf" and "fair".
- If `method="ecdf"` then the twCRPS value returned is \
the exact twCRPS value for the emprical cumulation distribution function \
the exact twCRPS value for the emprical cumulative distribution function \
nicholasloveday marked this conversation as resolved.
Show resolved Hide resolved
constructed using the ensemble values.
- If `method="fair"` then the twCRPS value returned \
is the approximated twCRPS where the ensemble values can be interpreted as a \
Expand Down Expand Up @@ -940,7 +940,7 @@ def twcrps_for_ensemble(
ensemble_member_dim: the dimension that specifies the ensemble member or the sample
from the predictive distribution.
chaining_func: the chaining function.
method: Either "ecdf" or "fair".
method: Either "ecdf" for the emperical twCRPS or "fair" for the Fair twCRPS.
nicholasloveday marked this conversation as resolved.
Show resolved Hide resolved
reduce_dims: Dimensions to reduce. Can be "all" to reduce all dimensions.
preserve_dims: Dimensions to preserve. Can be "all" to preserve all dimensions.
weights: Weights for calculating a weighted mean of individual scores. Note that
Expand All @@ -962,9 +962,9 @@ def twcrps_for_ensemble(
events using transformed kernel scores. SIAM/ASA Journal on Uncertainty
Quantification, 11(3), 906-940. https://doi.org/10.1137/22M1532184

See also:ß
See also:
:py:func:`scores.probability.crps_for_ensemble`
:py:func:`scores.probability.tail_twcrps_for_ensemble`
:py:func:`scores.probability.tail_tw_crps_for_ensemble`
:py:func:`scores.probability.crps_cdf`


Expand All @@ -975,10 +975,10 @@ def twcrps_for_ensemble(

>>> import numpy as np
>>> import xarray as xr
>>> from scores.probability import twcrps_for_ensemble
>>> from scores.probability import tw_crps_for_ensemble
>>> fcst = xr.DataArray(np.random.rand(10, 10), dims=['time', 'ensemble'])
>>> obs = xr.DataArray(np.random.rand(10), dims=['time'])
>>> twcrps_for_ensemble(fcst, obs, 'ensemble', lambda x: np.maximum(x, 0.5))
>>> tw_crps_for_ensemble(fcst, obs, 'ensemble', lambda x: np.maximum(x, 0.5))

"""

Expand All @@ -996,7 +996,7 @@ def twcrps_for_ensemble(
return result


def tail_twcrps_for_ensemble(
def tail_tw_crps_for_ensemble(
fcst: XarrayLike,
obs: XarrayLike,
ensemble_member_dim: str,
Expand All @@ -1014,11 +1014,13 @@ def tail_twcrps_for_ensemble(

A threshold weight of 1 is assigned for values of the tail and a threshold weight of 0 otherwise.
The threshold value of where the tail begins is specified by the ``threshold`` argument.
The tail does not include the threshold value itself.
The ``tail`` argument specifies whether the tail is the upper or lower tail.
For example, if we only care about values above 40 degrees C, we can set ``threshold=40`` and ``tail="upper"``.
tennlee marked this conversation as resolved.
Show resolved Hide resolved


For more flexible weighting options and the relevant equations, see the
:py:func:`scores.probability.twcrps_for_ensemble` function.
:py:func:`scores.probability.tw_crps_for_ensemble` function.

Args:
fcst: Forecast data. Must have a dimension `ensemble_member_dim`.
Expand All @@ -1029,7 +1031,8 @@ def tail_twcrps_for_ensemble(
for a single threshold or an xarray object if the threshold varies across
dimensions (e.g., climatological values).
tail: the tail of the distribution to weight. Either "upper" or "lower".
method: Either "ecdf" or "fair".
method: Either "ecdf" or "fair". See :py:func:`scores.probability.tw_crps_for_ensemble`
for more details.
reduce_dims: Dimensions to reduce. Can be "all" to reduce all dimensions.
preserve_dims: Dimensions to preserve. Can be "all" to preserve all dimensions.
weights: Weights for calculating a weighted mean of individual scores. Note that
Expand All @@ -1049,7 +1052,7 @@ def tail_twcrps_for_ensemble(
Quantification, 11(3), 906-940. https://doi.org/10.1137/22M1532184

See also:
:py:func:`scores.probability.twcrps_for_ensemble`
:py:func:`scores.probability.tw_crps_for_ensemble`
:py:func:`scores.probability.crps_for_ensemble`
:py:func:`scores.probability.crps_cdf`

Expand All @@ -1059,10 +1062,10 @@ def tail_twcrps_for_ensemble(

>>> import numpy as np
>>> import xarray as xr
>>> from scores.probability import tail_twcrps_for_ensemble
>>> from scores.probability import tail_tw_crps_for_ensemble
>>> fcst = xr.DataArray(np.random.rand(10, 10), dims=['time', 'ensemble'])
>>> obs = xr.DataArray(np.random.rand(10), dims=['time'])
>>> tail_twcrps_for_ensemble(fcst, obs, 'ensemble', 0.5, tail='upper')
>>> tail_tw_crps_for_ensemble(fcst, obs, 'ensemble', 0.5, tail='upper')

"""
if tail not in ["upper", "lower"]:
Expand All @@ -1077,7 +1080,7 @@ def _vfunc(x):
def _vfunc(x):
return np.minimum(x, threshold)

result = twcrps_for_ensemble(
result = tw_crps_for_ensemble(
fcst,
obs,
ensemble_member_dim,
Expand Down
44 changes: 22 additions & 22 deletions tests/probabilty/test_crps.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
crps_cdf,
crps_cdf_brier_decomposition,
crps_for_ensemble,
tail_twcrps_for_ensemble,
twcrps_for_ensemble,
tail_tw_crps_for_ensemble,
tw_crps_for_ensemble,
)
from scores.probability.crps_impl import (
crps_cdf_exact,
Expand Down Expand Up @@ -802,9 +802,9 @@ def test_crps_for_ensemble_dask():
),
],
)
def test_tail_twcrps_for_ensemble(fcst, obs, method, tail, threshold, preserve_dims, reduce_dims, weights, expected):
"""Tests tail_twcrps_for_ensembles"""
result = tail_twcrps_for_ensemble(
def test_tail_tw_crps_for_ensemble(fcst, obs, method, tail, threshold, preserve_dims, reduce_dims, weights, expected):
"""Tests tail_tw_crps_for_ensembles"""
result = tail_tw_crps_for_ensemble(
fcst,
obs,
ensemble_member_dim="ens_member",
Expand All @@ -818,14 +818,14 @@ def test_tail_twcrps_for_ensemble(fcst, obs, method, tail, threshold, preserve_d
xr.testing.assert_allclose(result, expected)


def test_tail_twcrps_for_ensemble_dask():
"""Tests `tail_twcrps_for_ensemble` works with dask."""
def test_tail_tw_crps_for_ensemble_dask():
"""Tests `tail_tw_crps_for_ensemble` works with dask."""

if dask == "Unavailable": # pragma: no cover
pytest.skip("Dask unavailable, could not run test") # pragma: no cover

# Check that it works with xr.Datarrays
result = tail_twcrps_for_ensemble(
result = tail_tw_crps_for_ensemble(
fcst=crps_test_data.DA_FCST_CRPSENS.chunk(),
obs=crps_test_data.DA_OBS_CRPSENS.chunk(),
ensemble_member_dim="ens_member",
Expand All @@ -842,7 +842,7 @@ def test_tail_twcrps_for_ensemble_dask():
xr.testing.assert_allclose(result, crps_test_data.EXP_UPPER_TAIL_CRPSENS_ECDF_DA)

# Check that it works with xr.Datasets
result_ds = tail_twcrps_for_ensemble(
result_ds = tail_tw_crps_for_ensemble(
fcst=crps_test_data.DS_FCST_CRPSENS.chunk(),
obs=crps_test_data.DS_OBS_CRPSENS.chunk(),
ensemble_member_dim="ens_member",
Expand All @@ -859,9 +859,9 @@ def test_tail_twcrps_for_ensemble_dask():
xr.testing.assert_allclose(result_ds, crps_test_data.EXP_UPPER_TAIL_CRPSENS_ECDF_DS)


def test_tail_twcrps_for_ensemble_raises():
def test_tail_tw_crps_for_ensemble_raises():
with pytest.raises(ValueError, match="'middle' is not one of 'upper' or 'lower'"):
result = tail_twcrps_for_ensemble(
result = tail_tw_crps_for_ensemble(
fcst=crps_test_data.DA_FCST_CRPSENS,
obs=crps_test_data.DA_OBS_CRPSENS,
ensemble_member_dim="ens_member",
Expand All @@ -872,22 +872,22 @@ def test_tail_twcrps_for_ensemble_raises():


def v_func1(x):
"""For testing twcrps_for_ensembles. The equivalent of a tail weight for thresholds 1 and higher"""
"""For testing tw_crps_for_ensembles. The equivalent of a tail weight for thresholds 1 and higher"""
return np.maximum(x, 1)


def v_func2(x):
"""For testing twcrps_for_ensembles. The equivalent of the unweighted CRPS"""
"""For testing tw_crps_for_ensembles. The equivalent of the unweighted CRPS"""
return x


def v_func3(x):
"""For testing twcrps_for_ensembles. The equivalent of a tail weight for thresholds that vary across a dimension"""
"""For testing tw_crps_for_ensembles. The equivalent of a tail weight for thresholds that vary across a dimension"""
return np.maximum(x, crps_test_data.DA_T_TWCRPSENS)


def v_func4(x):
"""For testing twcrps_for_ensembles. The equivalent of a tail weight for thresholds that vary across a dimension with a xr.dataset"""
"""For testing tw_crps_for_ensembles. The equivalent of a tail weight for thresholds that vary across a dimension with a xr.dataset"""
return np.maximum(x, crps_test_data.DS_T_TWCRPSENS)


Expand Down Expand Up @@ -985,10 +985,10 @@ def v_func4(x):
),
],
)
def test_twcrps_for_ensemble(fcst, obs, method, v_func, preserve_dims, reduce_dims, weights, expected):
"""Tests twcrps_for_ensembles"""
def test_tw_crps_for_ensemble(fcst, obs, method, v_func, preserve_dims, reduce_dims, weights, expected):
"""Tests tw_crps_for_ensembles"""

result = twcrps_for_ensemble(
result = tw_crps_for_ensemble(
fcst,
obs,
ensemble_member_dim="ens_member",
Expand All @@ -1001,9 +1001,9 @@ def test_twcrps_for_ensemble(fcst, obs, method, v_func, preserve_dims, reduce_di
xr.testing.assert_allclose(result, expected)


def test_twcrps_for_ensemble_dask():
"""Tests `twcrps_for_ensemble` works with dask."""
result = twcrps_for_ensemble(
def test_tw_crps_for_ensemble_dask():
"""Tests `tw_crps_for_ensemble` works with dask."""
result = tw_crps_for_ensemble(
fcst=crps_test_data.DA_FCST_CRPSENS.chunk(),
obs=crps_test_data.DA_OBS_CRPSENS.chunk(),
ensemble_member_dim="ens_member",
Expand All @@ -1019,7 +1019,7 @@ def test_twcrps_for_ensemble_dask():
xr.testing.assert_allclose(result, crps_test_data.EXP_UPPER_TAIL_CRPSENS_ECDF_DA)

# Check that it works with xr.Datasets
result_ds = twcrps_for_ensemble(
result_ds = tw_crps_for_ensemble(
fcst=crps_test_data.DS_FCST_CRPSENS.chunk(),
obs=crps_test_data.DS_OBS_CRPSENS.chunk(),
ensemble_member_dim="ens_member",
Expand Down
Loading