scoringrules 0.4.2__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {scoringrules-0.4.2 → scoringrules-0.5.0}/PKG-INFO +1 -1
  2. {scoringrules-0.4.2 → scoringrules-0.5.0}/pyproject.toml +7 -6
  3. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/__init__.py +2 -0
  4. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/_brier.py +5 -5
  5. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/_crps.py +52 -53
  6. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/_energy.py +36 -40
  7. scoringrules-0.5.0/scoringrules/_error_spread.py +46 -0
  8. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/_logs.py +6 -6
  9. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/_variogram.py +29 -34
  10. scoringrules-0.5.0/scoringrules/backend/__init__.py +10 -0
  11. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/backend/base.py +56 -0
  12. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/backend/jax.py +42 -0
  13. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/backend/numpy.py +63 -1
  14. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/backend/registry.py +27 -31
  15. scoringrules-0.5.0/scoringrules/backend/tensorflow.py +253 -0
  16. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/backend/torch.py +50 -3
  17. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/core/brier.py +5 -5
  18. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/core/crps/_approx.py +32 -32
  19. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/core/crps/_closed.py +15 -9
  20. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/core/crps/_gufuncs.py +63 -75
  21. scoringrules-0.5.0/scoringrules/core/energy/_gufuncs.py +91 -0
  22. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/core/energy/_score.py +20 -24
  23. scoringrules-0.5.0/scoringrules/core/error_spread/__init__.py +4 -0
  24. scoringrules-0.5.0/scoringrules/core/error_spread/_gufunc.py +39 -0
  25. scoringrules-0.5.0/scoringrules/core/error_spread/_score.py +19 -0
  26. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/core/logarithmic.py +3 -5
  27. scoringrules-0.5.0/scoringrules/core/stats.py +133 -0
  28. scoringrules-0.5.0/scoringrules/core/typing.py +13 -0
  29. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/core/utils.py +11 -11
  30. scoringrules-0.5.0/scoringrules/core/variogram/_gufuncs.py +93 -0
  31. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/core/variogram/_score.py +27 -27
  32. scoringrules-0.4.2/scoringrules/backend/__init__.py +0 -10
  33. scoringrules-0.4.2/scoringrules/core/energy/_gufuncs.py +0 -96
  34. scoringrules-0.4.2/scoringrules/core/stats.py +0 -24
  35. scoringrules-0.4.2/scoringrules/core/typing.py +0 -10
  36. scoringrules-0.4.2/scoringrules/core/variogram/_gufuncs.py +0 -96
  37. {scoringrules-0.4.2 → scoringrules-0.5.0}/LICENSE +0 -0
  38. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/core/__init__.py +0 -0
  39. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/core/crps/__init__.py +0 -0
  40. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/core/energy/__init__.py +0 -0
  41. {scoringrules-0.4.2 → scoringrules-0.5.0}/scoringrules/core/variogram/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: scoringrules
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: Scoring rules for probabilistic forecast evaluation.
5
5
  Home-page: https://github.com/frazane/scoringrules
6
6
  Keywords: probabilistic,forecasting,verification
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "scoringrules"
3
- version = "0.4.2"
3
+ version = "0.5.0"
4
4
  description = "Scoring rules for probabilistic forecast evaluation."
5
5
  authors = [
6
6
  "Francesco Zanetta <zanetta.francesco@gmail.com>",
@@ -19,16 +19,16 @@ numpy = "^1.23.4"
19
19
  numba = "^0.57.0"
20
20
 
21
21
  [tool.poetry.group.docs.dependencies]
22
- mkdocs = "^1.4.3"
23
- mkdocs-material = "^9.1.11"
24
- mkdocstrings = { extras = ["python"], version = "^0.21.2" }
22
+ mkdocs = "^1.5.3"
23
+ mkdocs-material = "^9.5.3"
24
+ mkdocstrings = { extras = ["python"], version = "^0.24.0" }
25
25
  jupyter = "^1.0.0"
26
26
  nbconvert = "7.3.1"
27
27
  ipykernel = "6.22.0"
28
28
  properscoring = "^0.1"
29
29
  matplotlib = "^3.7.1"
30
- mkdocs-bibtex = "^2.8.16"
31
- mkdocs-section-index = "^0.3.5"
30
+ mkdocs-bibtex = "^2.11.0"
31
+ mkdocs-section-index = "^0.3.8"
32
32
 
33
33
 
34
34
  [tool.poetry.group.dev.dependencies]
@@ -41,6 +41,7 @@ jax = { extras = ["cpu"], version = "^0.4.10" }
41
41
  dask = "^2023.7.1"
42
42
  xarray = "^2023.10.1"
43
43
  torch = { version = ">=2.0.0, !=2.0.1, !=2.1.0" }
44
+ tensorflow = "^2.15.0"
44
45
 
45
46
  [build-system]
46
47
  requires = ["poetry-core>=1.0.0"]
@@ -16,6 +16,7 @@ from scoringrules._energy import (
16
16
  twenergy_score,
17
17
  vrenergy_score,
18
18
  )
19
+ from scoringrules._error_spread import error_spread_score
19
20
  from scoringrules._logs import logs_normal
20
21
  from scoringrules._variogram import (
21
22
  owvariogram_score,
@@ -40,6 +41,7 @@ __all__ = [
40
41
  "vrcrps_ensemble",
41
42
  "logs_normal",
42
43
  "brier_score",
44
+ "error_spread_score",
43
45
  "energy_score",
44
46
  "owenergy_score",
45
47
  "twenergy_score",
@@ -3,15 +3,15 @@ import typing as tp
3
3
  from scoringrules.core import brier
4
4
 
5
5
  if tp.TYPE_CHECKING:
6
- from scoringrules.core.typing import Array, ArrayLike
6
+ from scoringrules.core.typing import Array, ArrayLike, Backend
7
7
 
8
8
 
9
9
  def brier_score(
10
- forecasts: "ArrayLike",
11
10
  observations: "ArrayLike",
11
+ forecasts: "ArrayLike",
12
12
  /,
13
13
  *,
14
- backend: tp.Literal["numpy", "jax", "torch"] | None = None,
14
+ backend: "Backend" = None,
15
15
  ) -> "Array":
16
16
  r"""
17
17
  Compute the Brier Score (BS).
@@ -24,10 +24,10 @@ def brier_score(
24
24
 
25
25
  Parameters
26
26
  ----------
27
- forecasts : NDArray
28
- Forecasted probabilities between 0 and 1.
29
27
  observations: NDArray
30
28
  Observed outcome, either 0 or 1.
29
+ forecasts : NDArray
30
+ Forecasted probabilities between 0 and 1.
31
31
  backend: str
32
32
  The name of the backend used for computations. Defaults to 'numpy'.
33
33
 
@@ -4,28 +4,28 @@ from scoringrules.backend import backends
4
4
  from scoringrules.core import crps
5
5
 
6
6
  if tp.TYPE_CHECKING:
7
- from scoringrules.core.typing import Array, ArrayLike
7
+ from scoringrules.core.typing import Array, ArrayLike, Backend
8
8
 
9
9
 
10
10
  def crps_ensemble(
11
- forecasts: "Array",
12
11
  observations: "ArrayLike",
12
+ forecasts: "Array",
13
13
  /,
14
14
  axis: int = -1,
15
15
  *,
16
16
  sorted_ensemble: bool = False,
17
17
  estimator: str = "pwm",
18
- backend: tp.Literal["numba", "numpy", "jax", "torch"] | None = None,
18
+ backend: "Backend" = None,
19
19
  ) -> "Array":
20
20
  r"""Estimate the Continuous Ranked Probability Score (CRPS) for a finite ensemble.
21
21
 
22
22
  Parameters
23
23
  ----------
24
+ observations: ArrayLike
25
+ The observed values.
24
26
  forecasts: ArrayLike
25
27
  The predicted forecast ensemble, where the ensemble dimension is by default
26
28
  represented by the last axis.
27
- observations: ArrayLike
28
- The observed values.
29
29
  axis: int
30
30
  The axis corresponding to the ensemble. Default is the last axis.
31
31
  sorted_ensemble: bool
@@ -47,7 +47,7 @@ def crps_ensemble(
47
47
  >>> crps.ensemble(pred, obs)
48
48
  """
49
49
  B = backends.active if backend is None else backends[backend]
50
- forecasts, observations = map(B.asarray, (forecasts, observations))
50
+ observations, forecasts = map(B.asarray, (observations, forecasts))
51
51
 
52
52
  if estimator not in crps.estimator_gufuncs:
53
53
  raise ValueError(
@@ -62,21 +62,21 @@ def crps_ensemble(
62
62
  forecasts = B.sort(forecasts, axis=-1)
63
63
 
64
64
  if backend == "numba":
65
- return crps.estimator_gufuncs[estimator](forecasts, observations)
65
+ return crps.estimator_gufuncs[estimator](observations, forecasts)
66
66
 
67
- return crps.ensemble(forecasts, observations, estimator, backend=backend)
67
+ return crps.ensemble(observations, forecasts, estimator, backend=backend)
68
68
 
69
69
 
70
70
  def twcrps_ensemble(
71
- forecasts: "Array",
72
71
  observations: "ArrayLike",
72
+ forecasts: "Array",
73
73
  v_func: tp.Callable[["ArrayLike"], "ArrayLike"],
74
74
  /,
75
75
  axis: int = -1,
76
76
  *,
77
77
  estimator: str = "pwm",
78
78
  sorted_ensemble: bool = False,
79
- backend: tp.Literal["numba", "numpy", "jax", "torch"] | None = None,
79
+ backend: "Backend" = None,
80
80
  ) -> "Array":
81
81
  r"""Estimate the Threshold-Weighted Continuous Ranked Probability Score (twCRPS) for a finite ensemble.
82
82
 
@@ -91,17 +91,15 @@ def twcrps_ensemble(
91
91
 
92
92
  Parameters
93
93
  ----------
94
+ observations: ArrayLike
95
+ The observed values.
94
96
  forecasts: ArrayLike
95
97
  The predicted forecast ensemble, where the ensemble dimension is by default
96
98
  represented by the last axis.
97
- observations: ArrayLike
98
- The observed values.
99
99
  v_func: tp.Callable
100
100
  Chaining function used to emphasise particular outcomes. For example, a function that
101
101
  only considers values above a certain threshold $t$ by projecting forecasts and observations
102
- to $\[t, \inf)$.
103
- v_funcargs: tuple
104
- Additional arguments to the chaining function.
102
+ to $[t, \inf)$.
105
103
  axis: int
106
104
  The axis corresponding to the ensemble. Default is the last axis.
107
105
  backend: str
@@ -117,10 +115,10 @@ def twcrps_ensemble(
117
115
  >>> from scoringrules import crps
118
116
  >>> twcrps.ensemble(pred, obs)
119
117
  """
120
- forecasts, observations = map(v_func, (forecasts, observations))
118
+ observations, forecasts = map(v_func, (observations, forecasts))
121
119
  return crps_ensemble(
122
- forecasts,
123
120
  observations,
121
+ forecasts,
124
122
  axis=axis,
125
123
  sorted_ensemble=sorted_ensemble,
126
124
  estimator=estimator,
@@ -129,14 +127,14 @@ def twcrps_ensemble(
129
127
 
130
128
 
131
129
  def owcrps_ensemble(
132
- forecasts: "Array",
133
130
  observations: "ArrayLike",
131
+ forecasts: "Array",
134
132
  w_func: tp.Callable[["ArrayLike"], "ArrayLike"],
135
133
  /,
136
134
  axis: int = -1,
137
135
  *,
138
136
  estimator: tp.Literal["nrg"] = "nrg",
139
- backend: tp.Literal["numba", "numpy", "jax", "torch"] | None = None,
137
+ backend: "Backend" = None,
140
138
  ) -> "Array":
141
139
  r"""Estimate the Outcome-Weighted Continuous Ranked Probability Score (owCRPS) for a finite ensemble.
142
140
 
@@ -151,15 +149,13 @@ def owcrps_ensemble(
151
149
 
152
150
  Parameters
153
151
  ----------
152
+ observations: ArrayLike
153
+ The observed values.
154
154
  forecasts: ArrayLike
155
155
  The predicted forecast ensemble, where the ensemble dimension is by default
156
156
  represented by the last axis.
157
- observations: ArrayLike
158
- The observed values.
159
157
  w_func: tp.Callable
160
158
  Weight function used to emphasise particular outcomes.
161
- w_funcargs: tuple
162
- Additional arguments to the weight function.
163
159
  axis: int
164
160
  The axis corresponding to the ensemble. Default is the last axis.
165
161
  backend: str
@@ -185,27 +181,30 @@ def owcrps_ensemble(
185
181
  if axis != -1:
186
182
  forecasts = B.moveaxis(forecasts, axis, -1)
187
183
 
188
- fcts_weights, obs_weights = map(w_func, (forecasts, observations))
184
+ obs_weights, fct_weights = map(w_func, (observations, forecasts))
189
185
 
190
186
  if backend == "numba":
191
187
  return crps.estimator_gufuncs["ow" + estimator](
192
- forecasts, observations, fcts_weights, obs_weights
188
+ observations, forecasts, obs_weights, fct_weights
193
189
  )
194
190
 
191
+ observations, forecasts, obs_weights, fct_weights = map(
192
+ B.asarray, (observations, forecasts, obs_weights, fct_weights)
193
+ )
195
194
  return crps.ow_ensemble(
196
- forecasts, observations, fcts_weights, obs_weights, backend=backend
195
+ observations, forecasts, obs_weights, fct_weights, backend=backend
197
196
  )
198
197
 
199
198
 
200
199
  def vrcrps_ensemble(
201
- forecasts: "Array",
202
200
  observations: "ArrayLike",
201
+ forecasts: "Array",
203
202
  w_func: tp.Callable[["ArrayLike"], "ArrayLike"],
204
203
  /,
205
204
  axis: int = -1,
206
205
  *,
207
206
  estimator: tp.Literal["nrg"] = "nrg",
208
- backend: tp.Literal["numba", "numpy", "jax", "torch"] | None = None,
207
+ backend: "Backend" = None,
209
208
  ) -> "Array":
210
209
  r"""Estimate the Vertically Re-scaled Continuous Ranked Probability Score (vrCRPS) for a finite ensemble.
211
210
 
@@ -225,15 +224,13 @@ def vrcrps_ensemble(
225
224
 
226
225
  Parameters
227
226
  ----------
227
+ observations: ArrayLike
228
+ The observed values.
228
229
  forecasts: ArrayLike
229
230
  The predicted forecast ensemble, where the ensemble dimension is by default
230
231
  represented by the last axis.
231
- observations: ArrayLike
232
- The observed values.
233
232
  w_func: tp.Callable
234
233
  Weight function used to emphasise particular outcomes.
235
- w_funcargs: tuple
236
- Additional arguments to the weight function.
237
234
  axis: int
238
235
  The axis corresponding to the ensemble. Default is the last axis.
239
236
  backend: str
@@ -259,25 +256,28 @@ def vrcrps_ensemble(
259
256
  if axis != -1:
260
257
  forecasts = B.moveaxis(forecasts, axis, -1)
261
258
 
262
- fcts_weights, obs_weights = map(w_func, (forecasts, observations))
259
+ obs_weights, fct_weights = map(w_func, (observations, forecasts))
263
260
 
264
261
  if backend == "numba":
265
262
  return crps.estimator_gufuncs["vr" + estimator](
266
- forecasts, observations, fcts_weights, obs_weights
263
+ observations, forecasts, obs_weights, fct_weights
267
264
  )
268
265
 
266
+ observations, forecasts, obs_weights, fct_weights = map(
267
+ B.asarray, (observations, forecasts, obs_weights, fct_weights)
268
+ )
269
269
  return crps.vr_ensemble(
270
- forecasts, observations, fcts_weights, obs_weights, backend=backend
270
+ observations, forecasts, obs_weights, fct_weights, backend=backend
271
271
  )
272
272
 
273
273
 
274
274
  def crps_normal(
275
+ observation: "ArrayLike",
275
276
  mu: "ArrayLike",
276
277
  sigma: "ArrayLike",
277
- observation: "ArrayLike",
278
278
  /,
279
279
  *,
280
- backend: tp.Literal["numpy", "jax", "torch"] | None = None,
280
+ backend: "Backend" = None,
281
281
  ) -> "ArrayLike":
282
282
  r"""Compute the closed form of the CRPS for the normal distribution.
283
283
 
@@ -291,12 +291,12 @@ def crps_normal(
291
291
 
292
292
  Parameters
293
293
  ----------
294
+ observations: ArrayLike
295
+ The observed values.
294
296
  mu: ArrayLike
295
297
  Mean of the forecast normal distribution.
296
298
  sigma: ArrayLike
297
299
  Standard deviation of the forecast normal distribution.
298
- observation: ArrayLike
299
- The observed values.
300
300
 
301
301
  Returns
302
302
  -------
@@ -308,14 +308,14 @@ def crps_normal(
308
308
  >>> from scoringrules import crps
309
309
  >>> crps.normal(0.1, 0.4, 0.0)
310
310
  """
311
- return crps.normal(mu, sigma, observation, backend=backend)
311
+ return crps.normal(observation, mu, sigma, backend=backend)
312
312
 
313
313
 
314
314
  def crps_lognormal(
315
+ observation: "ArrayLike",
315
316
  mulog: "ArrayLike",
316
317
  sigmalog: "ArrayLike",
317
- observation: "ArrayLike",
318
- backend: tp.Literal["numpy", "jax", "torch"] | None = None,
318
+ backend: "Backend" = None,
319
319
  ) -> "ArrayLike":
320
320
  r"""Compute the closed form of the CRPS for the lognormal distribution.
321
321
 
@@ -329,9 +329,13 @@ def crps_lognormal(
329
329
  where $\Phi$ is the CDF of the standard normal distribution and
330
330
  $\omega = \frac{\mathrm{log}y - \mu}{\sigma}$.
331
331
 
332
+ Note that mean and standard deviation are not the values for the distribution itself,
333
+ but of the underlying normal distribution it is derived from.
332
334
 
333
335
  Parameters
334
336
  ----------
337
+ observations: ArrayLike
338
+ The observed values.
335
339
  mulog: ArrayLike
336
340
  Mean of the normal underlying distribution.
337
341
  sigmalog: ArrayLike
@@ -342,26 +346,21 @@ def crps_lognormal(
342
346
  crps: ArrayLike
343
347
  The CRPS between Lognormal(mu, sigma) and obs.
344
348
 
345
- Notes
346
- -----
347
- The mean and standard deviation are not the values for the distribution itself,
348
- but of the underlying normal distribution it is derived from.
349
-
350
349
  Examples
351
350
  --------
352
351
  >>> from scoringrules import crps
353
352
  >>> crps.lognormal(0.1, 0.4, 0.0)
354
353
  """
355
- return crps.lognormal(mulog, sigmalog, observation, backend=backend)
354
+ return crps.lognormal(observation, mulog, sigmalog, backend=backend)
356
355
 
357
356
 
358
357
  def crps_logistic(
358
+ observation: "ArrayLike",
359
359
  mu: "ArrayLike",
360
360
  sigma: "ArrayLike",
361
- observation: "ArrayLike",
362
361
  /,
363
362
  *,
364
- backend: tp.Literal["numpy", "jax", "torch"] | None = None,
363
+ backend: "Backend" = None,
365
364
  ) -> "ArrayLike":
366
365
  r"""Compute the closed form of the CRPS for the logistic distribution.
367
366
 
@@ -375,12 +374,12 @@ def crps_logistic(
375
374
 
376
375
  Parameters
377
376
  ----------
377
+ observations: ArrayLike
378
+ Observed values.
378
379
  mu: ArrayLike
379
380
  Location parameter of the forecast logistic distribution.
380
381
  sigma: ArrayLike
381
382
  Scale parameter of the forecast logistic distribution.
382
- observation: ArrayLike
383
- Observed values.
384
383
 
385
384
  Returns
386
385
  -------
@@ -392,7 +391,7 @@ def crps_logistic(
392
391
  >>> from scoringrules import crps
393
392
  >>> crps.logistic(0.1, 0.4, 0.0)
394
393
  """
395
- return crps.logistic(mu, sigma, observation, backend=backend)
394
+ return crps.logistic(observation, mu, sigma, backend=backend)
396
395
 
397
396
 
398
397
  __all__ = [
@@ -5,24 +5,24 @@ from scoringrules.core import energy
5
5
  from scoringrules.core.utils import multivariate_array_check
6
6
 
7
7
  if tp.TYPE_CHECKING:
8
- from scoringrules.core.typing import Array, ArrayLike
8
+ from scoringrules.core.typing import Array, ArrayLike, Backend
9
9
 
10
10
 
11
11
  def energy_score(
12
- forecasts: "Array",
13
12
  observations: "Array",
13
+ forecasts: "Array",
14
14
  /,
15
15
  m_axis: int = -2,
16
16
  v_axis: int = -1,
17
17
  *,
18
- backend: tp.Literal["numba", "numpy", "jax", "torch"] | None = None,
18
+ backend: "Backend" = None,
19
19
  ) -> "Array":
20
20
  r"""Compute the Energy Score for a finite multivariate ensemble.
21
21
 
22
22
  The Energy Score is a multivariate scoring rule expressed as
23
23
 
24
- $$ES(F, \mathbf{y}) = E_{F} ||\mathbf{X} - \mathbf{y}||
25
- - \frac{1}{2}E_{F} ||\mathbf{X} - \mathbf{X'}||,$$
24
+ $$\text{ES}(F_{ens}, \mathbf{y})= \frac{1}{M} \sum_{m=1}^{M} \| \mathbf{x}_{m} -
25
+ \mathbf{y} \| - \frac{1}{2 M^{2}} \sum_{m=1}^{M} \sum_{j=1}^{M} \| \mathbf{x}_{m} - \mathbf{x}_{j} \| $$
26
26
 
27
27
  where $\mathbf{X}$ and $\mathbf{X'}$ are independent samples from $F$
28
28
  and $||\cdot||$ is the euclidean norm over the input dimensions (the variables).
@@ -30,11 +30,11 @@ def energy_score(
30
30
 
31
31
  Parameters
32
32
  ----------
33
+ observations: Array
34
+ The observed values, where the variables dimension is by default the last axis.
33
35
  forecasts: Array
34
36
  The predicted forecast ensemble, where the ensemble dimension is by default
35
37
  represented by the second last axis and the variables dimension by the last axis.
36
- observations: Array
37
- The observed values, where the variables dimension is by default the last axis.
38
38
  m_axis: int
39
39
  The axis corresponding to the ensemble dimension on the forecasts array. Defaults to -2.
40
40
  v_axis: int
@@ -49,25 +49,25 @@ def energy_score(
49
49
  The computed Energy Score.
50
50
  """
51
51
  backend = backend if backend is not None else backends._active
52
- forecasts, observations = multivariate_array_check(
53
- forecasts, observations, m_axis, v_axis, backend=backend
52
+ observations, forecasts = multivariate_array_check(
53
+ observations, forecasts, m_axis, v_axis, backend=backend
54
54
  )
55
55
 
56
56
  if backend == "numba":
57
- return energy._energy_score_gufunc(forecasts, observations)
57
+ return energy._energy_score_gufunc(observations, forecasts)
58
58
 
59
- return energy.nrg(forecasts, observations, backend=backend)
59
+ return energy.nrg(observations, forecasts, backend=backend)
60
60
 
61
61
 
62
62
  def twenergy_score(
63
- forecasts: "Array",
64
63
  observations: "Array",
64
+ forecasts: "Array",
65
65
  v_func: tp.Callable[["ArrayLike"], "ArrayLike"],
66
66
  /,
67
67
  m_axis: int = -2,
68
68
  v_axis: int = -1,
69
69
  *,
70
- backend: tp.Literal["numba", "numpy", "jax", "torch"] | None = None,
70
+ backend: "Backend" = None,
71
71
  ) -> "Array":
72
72
  r"""Compute the Threshold-Weighted Energy Score (twES) for a finite multivariate ensemble.
73
73
 
@@ -85,11 +85,11 @@ def twenergy_score(
85
85
 
86
86
  Parameters
87
87
  ----------
88
+ observations: ArrayLike of shape (...,D)
89
+ The observed values, where the variables dimension is by default the last axis.
88
90
  forecasts: ArrayLike of shape (..., M, D)
89
91
  The predicted forecast ensemble, where the ensemble dimension is by default
90
92
  represented by the second last axis and the variables dimension by the last axis.
91
- observations: ArrayLike of shape (...,D)
92
- The observed values, where the variables dimension is by default the last axis.
93
93
  v_func: tp.Callable
94
94
  Chaining function used to emphasise particular outcomes.
95
95
  m_axis: int
@@ -104,21 +104,21 @@ def twenergy_score(
104
104
  twenergy_score: ArrayLike of shape (...)
105
105
  The computed Threshold-Weighted Energy Score.
106
106
  """
107
- forecasts, observations = map(v_func, (forecasts, observations))
107
+ observations, forecasts = map(v_func, (observations, forecasts))
108
108
  return energy_score(
109
- forecasts, observations, m_axis=m_axis, v_axis=v_axis, backend=backend
109
+ observations, forecasts, m_axis=m_axis, v_axis=v_axis, backend=backend
110
110
  )
111
111
 
112
112
 
113
113
  def owenergy_score(
114
- forecasts: "Array",
115
114
  observations: "Array",
115
+ forecasts: "Array",
116
116
  w_func: tp.Callable[["ArrayLike"], "ArrayLike"],
117
117
  /,
118
118
  m_axis: int = -2,
119
119
  v_axis: int = -1,
120
120
  *,
121
- backend: tp.Literal["numba", "numpy", "jax", "torch"] | None = None,
121
+ backend: "Backend" = None,
122
122
  ) -> "Array":
123
123
  r"""Compute the Outcome-Weighted Energy Score (owES) for a finite multivariate ensemble.
124
124
 
@@ -136,15 +136,13 @@ def owenergy_score(
136
136
 
137
137
  Parameters
138
138
  ----------
139
+ observations: ArrayLike of shape (...,D)
140
+ The observed values, where the variables dimension is by default the last axis.
139
141
  forecasts: ArrayLike of shape (..., M, D)
140
142
  The predicted forecast ensemble, where the ensemble dimension is by default
141
143
  represented by the second last axis and the variables dimension by the last axis.
142
- observations: ArrayLike of shape (...,D)
143
- The observed values, where the variables dimension is by default the last axis.
144
144
  w_func: tp.Callable
145
145
  Weight function used to emphasise particular outcomes.
146
- w_funcargs: tuple
147
- Additional arguments to the weight function.
148
146
  m_axis: int
149
147
  The axis corresponding to the ensemble dimension. Defaults to -2.
150
148
  v_axis: int or tuple(int)
@@ -159,32 +157,32 @@ def owenergy_score(
159
157
  """
160
158
  B = backends.active if backend is None else backends[backend]
161
159
 
162
- forecasts, observations = multivariate_array_check(
163
- forecasts, observations, m_axis, v_axis, backend=backend
160
+ observations, forecasts = multivariate_array_check(
161
+ observations, forecasts, m_axis, v_axis, backend=backend
164
162
  )
165
163
 
166
- fcts_weights = B.apply_along_axis(w_func, forecasts, -1)
164
+ fct_weights = B.apply_along_axis(w_func, forecasts, -1)
167
165
  obs_weights = B.apply_along_axis(w_func, observations, -1)
168
166
 
169
- if backend == "numba":
167
+ if B.name == "numba":
170
168
  return energy._owenergy_score_gufunc(
171
- forecasts, observations, fcts_weights, obs_weights
169
+ observations, forecasts, obs_weights, fct_weights
172
170
  )
173
171
 
174
172
  return energy.ownrg(
175
- forecasts, observations, fcts_weights, obs_weights, backend=backend
173
+ observations, forecasts, obs_weights, fct_weights, backend=backend
176
174
  )
177
175
 
178
176
 
179
177
  def vrenergy_score(
180
- forecasts: "Array",
181
178
  observations: "Array",
179
+ forecasts: "Array",
182
180
  w_func: tp.Callable[["ArrayLike"], "ArrayLike"],
183
181
  /,
184
182
  *,
185
183
  m_axis: int = -2,
186
184
  v_axis: int = -1,
187
- backend: tp.Literal["numba", "numpy", "jax", "torch"] | None = None,
185
+ backend: "Backend" = None,
188
186
  ) -> "Array":
189
187
  r"""Compute the Vertically Re-scaled Energy Score (vrES) for a finite multivariate ensemble.
190
188
 
@@ -204,15 +202,13 @@ def vrenergy_score(
204
202
 
205
203
  Parameters
206
204
  ----------
205
+ observations: ArrayLike of shape (...,D)
206
+ The observed values, where the variables dimension is by default the last axis.
207
207
  forecasts: ArrayLike of shape (..., M, D)
208
208
  The predicted forecast ensemble, where the ensemble dimension is by default
209
209
  represented by the second last axis and the variables dimension by the last axis.
210
- observations: ArrayLike of shape (...,D)
211
- The observed values, where the variables dimension is by default the last axis.
212
210
  w_func: tp.Callable
213
211
  Weight function used to emphasise particular outcomes.
214
- w_funcargs: tuple
215
- Additional arguments to the weight function.
216
212
  m_axis: int
217
213
  The axis corresponding to the ensemble dimension. Defaults to -2.
218
214
  v_axis: int or tuple(int)
@@ -227,18 +223,18 @@ def vrenergy_score(
227
223
  """
228
224
  B = backends.active if backend is None else backends[backend]
229
225
 
230
- forecasts, observations = multivariate_array_check(
231
- forecasts, observations, m_axis, v_axis, backend=backend
226
+ observations, forecasts = multivariate_array_check(
227
+ observations, forecasts, m_axis, v_axis, backend=backend
232
228
  )
233
229
 
234
- fcts_weights = B.apply_along_axis(w_func, forecasts, -1)
230
+ fct_weights = B.apply_along_axis(w_func, forecasts, -1)
235
231
  obs_weights = B.apply_along_axis(w_func, observations, -1)
236
232
 
237
233
  if backend == "numba":
238
234
  return energy._vrenergy_score_gufunc(
239
- forecasts, observations, fcts_weights, obs_weights
235
+ observations, forecasts, obs_weights, fct_weights
240
236
  )
241
237
 
242
238
  return energy.vrnrg(
243
- forecasts, observations, fcts_weights, obs_weights, backend=backend
239
+ observations, forecasts, obs_weights, fct_weights, backend=backend
244
240
  )
@@ -0,0 +1,46 @@
1
+ import typing as tp
2
+
3
+ from scoringrules.backend import backends
4
+ from scoringrules.core import error_spread
5
+
6
+ if tp.TYPE_CHECKING:
7
+ from scoringrules.core.typing import Array, ArrayLike, Backend
8
+
9
+
10
+ def error_spread_score(
11
+ observations: "ArrayLike",
12
+ forecasts: "Array",
13
+ /,
14
+ axis: int = -1,
15
+ *,
16
+ backend: "Backend" = None,
17
+ ) -> "Array":
18
+ r"""Compute the error-spread score [(Christensen et al., 2015)](https://doi.org/10.1002/qj.2375) for a finite ensemble.
19
+
20
+ Parameters
21
+ ----------
22
+ observations: ArrayLike
23
+ The observed values.
24
+ forecasts: Array
25
+ The predicted forecast ensemble, where the ensemble dimension is by default
26
+ represented by the last axis.
27
+ axis: int
28
+ The axis corresponding to the ensemble. Default is the last axis.
29
+ backend: str
30
+ The name of the backend used for computations. Defaults to 'numba' if available, else 'numpy'.
31
+
32
+ Returns
33
+ -------
34
+ - Array
35
+ An array of error spread scores for each ensemble forecast, which should be averaged to get meaningful values.
36
+ """
37
+ B = backends.active if backend is None else backends[backend]
38
+ observations, forecasts = map(B.asarray, (observations, forecasts))
39
+
40
+ if axis != -1:
41
+ forecasts = B.moveaxis(forecasts, axis, -1)
42
+
43
+ if B.name == "numba":
44
+ return error_spread._ess_gufunc(observations, forecasts)
45
+
46
+ return error_spread.ess(observations, forecasts, backend=backend)