Skip to content

Commit e46f85e

Browse files
ValentinGebhartpeanutfunluseverinEvelyn-MChahan Kropf
authored
Add forecast tutorial (#1193)
* Add impact forecast tutorial * Add 'dim' argument to reductions * Update tests --------- Co-authored-by: Lukas Riedel <[email protected]> Co-authored-by: luseverin <[email protected]> Co-authored-by: Evelyn Mühlhofer <[email protected]> Co-authored-by: Chahan Kropf <[email protected]>
1 parent e66a558 commit e46f85e

File tree

13 files changed

+2887
-347
lines changed

13 files changed

+2887
-347
lines changed

climada/engine/impact_calc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ def _return_impact(self, imp_mat_gen, save_mat):
234234
)
235235
if isinstance(self.hazard, HazardForecast):
236236
eai_exp = np.full_like(eai_exp, np.nan, dtype=eai_exp.dtype)
237-
aai_agg = np.full_like(aai_agg, np.nan, dtype=aai_agg.dtype)
237+
aai_agg = np.nan
238238
LOGGER.warning(
239239
"eai_exp and aai_agg are undefined with forecasts. "
240240
"Setting them to NaN arrays."

climada/engine/impact_forecast.py

Lines changed: 115 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@
2121

2222
import logging
2323
from pathlib import Path
24-
from typing import Union
24+
from typing import Literal, Union
2525

2626
import numpy as np
27-
import scipy.sparse as sparse
27+
from scipy import sparse
2828

2929
from ..util import log_level
3030
from ..util.checker import size
31-
from ..util.forecast import Forecast
31+
from ..util.forecast import Forecast, reduce_unique_selection
3232
from .impact import Impact
3333

3434
LOGGER = logging.getLogger(__name__)
@@ -244,48 +244,83 @@ def _check_sizes(self):
244244
size(exp_len=num_entries, var=self.member, var_name="Forecast.member")
245245
size(exp_len=num_entries, var=self.lead_time, var_name="Forecast.lead_time")
246246

247-
def _reduce_attrs(self, event_name: str):
248-
"""
249-
Reduce the attributes of an ImpactForecast to a single value.
250-
251-
Attributes are modified as follows:
252-
- lead_time: set to NaT
253-
- member: set to -1
254-
- event_id: set to 0
255-
- event_name: set to the name of the reduction method (default)
256-
- date: set to 0
257-
- frequency: set to 1
247+
def select(
248+
self,
249+
event_ids=None,
250+
event_names=None,
251+
dates=None,
252+
coord_exp=None,
253+
reset_frequency=False,
254+
member=None,
255+
lead_time=None,
256+
):
257+
"""Select entries based on the parameters and return a new instance.
258+
The selection will contain the intersection of all given parameters.
258259
259260
Parameters
260261
----------
261-
event_name : str
262-
The event name given to the reduced data.
262+
member : Sequence of ints
263+
Ensemble members to select
264+
lead_time : Sequence of numpy.timedelta64
265+
Lead times to select
266+
267+
See Also
268+
--------
269+
:py:meth:`~climada.engine.impact.Impact.select`
263270
"""
264-
reduced_attrs = {
265-
"lead_time": np.array([np.timedelta64("NaT")]),
266-
"member": np.array([-1]),
267-
"event_id": np.array([0]),
268-
"event_name": np.array([event_name]),
269-
"date": np.array([0]),
270-
"frequency": np.array([1]),
271-
}
272-
273-
return reduced_attrs
274-
275-
def min(self):
271+
if member is not None or lead_time is not None:
272+
mask_member = (
273+
self.idx_member(member)
274+
if member is not None
275+
else np.full_like(self.member, True, dtype=bool)
276+
)
277+
mask_lead_time = (
278+
self.idx_lead_time(lead_time)
279+
if lead_time is not None
280+
else np.full_like(self.lead_time, True, dtype=bool)
281+
)
282+
event_id_from_forecast_mask = np.asarray(self.event_id)[
283+
(mask_member & mask_lead_time)
284+
]
285+
event_ids = (
286+
np.intersect1d(event_ids, event_id_from_forecast_mask)
287+
if event_ids is not None
288+
else event_id_from_forecast_mask
289+
)
290+
291+
return super().select(
292+
event_ids=event_ids,
293+
event_names=event_names,
294+
dates=dates,
295+
coord_exp=coord_exp,
296+
reset_frequency=reset_frequency,
297+
)
298+
299+
def min(self, dim: Literal["member", "lead_time"] | None = None):
276300
"""
277301
Reduce the impact matrix and at_event of an ImpactForecast to the minimum
278302
value.
279303
280304
Parameters
281305
----------
282-
None
306+
dim : "member", "lead_time", or None
307+
Dimension to reduce over. If None, reduce over all data.
283308
284309
Returns
285310
-------
286311
ImpactForecast
287312
An ImpactForecast object with the min impact matrix and at_event.
288313
"""
314+
if dim is not None:
315+
rdim = self._reduce_iter_dim(dim)
316+
return reduce_unique_selection(
317+
self,
318+
values=getattr(self, rdim),
319+
select=rdim,
320+
reduce_attr="min",
321+
concat_kws={"reset_event_ids": True},
322+
)
323+
289324
red_imp_mat = self.imp_mat.min(axis=0).tocsr()
290325
red_at_event = np.array([red_imp_mat.sum()])
291326
return ImpactForecast(
@@ -302,20 +337,31 @@ def min(self):
302337
**self._reduce_attrs("min"),
303338
)
304339

305-
def max(self):
340+
def max(self, dim: Literal["member", "lead_time"] | None = None):
306341
"""
307342
Reduce the impact matrix and at_event of an ImpactForecast to the maximum
308343
value.
309344
310345
Parameters
311346
----------
312-
None
347+
dim : "member", "lead_time", or None
348+
Dimension to reduce over. If None, reduce over all data.
313349
314350
Returns
315351
-------
316352
ImpactForecast
317353
An ImpactForecast object with the max impact matrix and at_event.
318354
"""
355+
if dim is not None:
356+
rdim = self._reduce_iter_dim(dim)
357+
return reduce_unique_selection(
358+
self,
359+
values=getattr(self, rdim),
360+
select=rdim,
361+
reduce_attr="max",
362+
concat_kws={"reset_event_ids": True},
363+
)
364+
319365
red_imp_mat = self.imp_mat.max(axis=0).tocsr()
320366
red_at_event = np.array([red_imp_mat.sum()])
321367
return ImpactForecast(
@@ -332,19 +378,30 @@ def max(self):
332378
**self._reduce_attrs("max"),
333379
)
334380

335-
def mean(self):
381+
def mean(self, dim: Literal["member", "lead_time"] | None = None):
336382
"""
337383
Reduce the impact matrix and at_event of an ImpactForecast to the mean value.
338384
339385
Parameters
340386
----------
341-
None
387+
dim : "member", "lead_time", or None
388+
Dimension to reduce over. If None, reduce over all data.
342389
343390
Returns
344391
-------
345392
ImpactForecast
346393
An ImpactForecast object with the mean impact matrix and at_event.
347394
"""
395+
if dim is not None:
396+
rdim = self._reduce_iter_dim(dim)
397+
return reduce_unique_selection(
398+
self,
399+
values=getattr(self, rdim),
400+
select=rdim,
401+
reduce_attr="mean",
402+
concat_kws={"reset_event_ids": True},
403+
)
404+
348405
red_imp_mat = sparse.csr_matrix(self.imp_mat.mean(axis=0))
349406
red_at_event = np.array([red_imp_mat.sum()])
350407
return ImpactForecast(
@@ -361,62 +418,26 @@ def mean(self):
361418
**self._reduce_attrs("mean"),
362419
)
363420

364-
def select(
421+
def _quantile(
365422
self,
366-
event_ids=None,
367-
event_names=None,
368-
dates=None,
369-
coord_exp=None,
370-
reset_frequency=False,
371-
member=None,
372-
lead_time=None,
423+
q: float,
424+
dim: Literal["member", "lead_time"] | None = None,
425+
event_name: str | None = None,
373426
):
374-
"""Select entries based on the parameters and return a new instance.
375-
The selection will contain the intersection of all given parameters.
376-
377-
Parameters
378-
----------
379-
member : Sequence of ints
380-
Ensemble members to select
381-
lead_time : Sequence of numpy.timedelta64
382-
Lead times to select
383-
384-
See Also
385-
--------
386-
:py:meth:`~climada.engine.impact.Impact.select`
387-
"""
388-
if member is not None or lead_time is not None:
389-
mask_member = (
390-
self.idx_member(member)
391-
if member is not None
392-
else np.full_like(self.member, True, dtype=bool)
393-
)
394-
mask_lead_time = (
395-
self.idx_lead_time(lead_time)
396-
if lead_time is not None
397-
else np.full_like(self.lead_time, True, dtype=bool)
398-
)
399-
event_id_from_forecast_mask = np.asarray(self.event_id)[
400-
(mask_member & mask_lead_time)
401-
]
402-
event_ids = (
403-
np.intersect1d(event_ids, event_id_from_forecast_mask)
404-
if event_ids is not None
405-
else event_id_from_forecast_mask
406-
)
407-
408-
return super().select(
409-
event_ids=event_ids,
410-
event_names=event_names,
411-
dates=dates,
412-
coord_exp=coord_exp,
413-
reset_frequency=reset_frequency,
414-
)
415-
416-
def _quantile(self, q: float, event_name: str | None = None):
417427
"""
418428
Reduce the impact matrix and at_event of an ImpactForecast to the quantile value.
419429
"""
430+
if dim is not None:
431+
rdim = self._reduce_iter_dim(dim)
432+
return reduce_unique_selection(
433+
self,
434+
values=getattr(self, rdim),
435+
select=rdim,
436+
reduce_attr="quantile",
437+
q=q,
438+
concat_kws={"reset_event_ids": True},
439+
)
440+
420441
red_imp_mat = sparse.csr_matrix(np.quantile(self.imp_mat.toarray(), q, axis=0))
421442
red_at_event = np.array([red_imp_mat.sum()])
422443
if event_name is None:
@@ -435,33 +456,38 @@ def _quantile(self, q: float, event_name: str | None = None):
435456
**self._reduce_attrs(event_name),
436457
)
437458

438-
def quantile(self, q: float):
459+
def quantile(self, q: float, dim: Literal["member", "lead_time"] | None = None):
439460
"""
440461
Reduce the impact matrix and at_event of an ImpactForecast to the quantile value.
441462
442463
Parameters
443464
----------
444465
q : float
445466
The quantile to compute, which must be between 0 and 1.
467+
dim : "member", "lead_time", or None
468+
The dimension to reduce when computing the quantile. If ``None`` (default),
469+
this computes the centroid-wise quantile over the entire matrix.
446470
447471
Returns
448472
-------
449473
ImpactForecast
450474
An ImpactForecast object with the quantile impact matrix and at_event.
451475
"""
452-
return self._quantile(q=q)
476+
return self._quantile(q=q, dim=dim)
453477

454-
def median(self):
478+
def median(self, dim: Literal["member", "lead_time"] | None = None):
455479
"""
456480
Reduce the impact matrix and at_event of an ImpactForecast to the median value.
457481
458482
Parameters
459483
----------
460-
None
484+
dim : "member", "lead_time", or None
485+
The dimension to reduce when computing the median. If ``None`` (default),
486+
this computes the centroid-wise median over the entire matrix.
461487
462488
Returns
463489
-------
464490
ImpactForecast
465491
An ImpactForecast object with the median impact matrix and at_event.
466492
"""
467-
return self._quantile(q=0.5, event_name="median")
493+
return self._quantile(q=0.5, dim=dim, event_name="median")

0 commit comments

Comments
 (0)