Skip to content

Commit d84db1e

Browse files
Merge branch 'forecast-class' of github.com:CLIMADA-project/climada_python into forecast-class
2 parents 85b92ef + e66a558 commit d84db1e

File tree

6 files changed

+662
-25
lines changed

6 files changed

+662
-25
lines changed

climada/engine/impact.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,8 @@ def write_attribute(group, name, value):
14311431

14321432
def write_dataset(group, name, value):
14331433
"""Write a dataset"""
1434+
if name == "lead_time":
1435+
value = value.astype("timedelta64[ns]").astype("int64")
14341436
group.create_dataset(name, data=value, dtype=_str_type_helper(value))
14351437

14361438
def write_dict(group, name, value):
@@ -1618,7 +1620,9 @@ def read_excel(self, *args, **kwargs):
16181620
self.__dict__ = Impact.from_excel(*args, **kwargs).__dict__
16191621

16201622
@classmethod
1621-
def from_hdf5(cls, file_path: Union[str, Path]):
1623+
def from_hdf5(
1624+
cls, file_path: Union[str, Path], *, add_scalar_attrs=None, add_array_attrs=None
1625+
):
16221626
"""Create an impact object from an H5 file.
16231627
16241628
This assumes a specific layout of the file. If values are not found in the
@@ -1663,6 +1667,10 @@ def from_hdf5(cls, file_path: Union[str, Path]):
16631667
----------
16641668
file_path : str or Path
16651669
The file path of the file to read.
1670+
add_scalar_attrs : Iterable of str, optional
1671+
Scalar attributes to read from file. Defaults to None.
1672+
add_array_attrs : Iterable of str, optional
1673+
Array attributes to read from file. Defaults to None.
16661674
16671675
Returns
16681676
-------
@@ -1691,17 +1699,27 @@ def from_hdf5(cls, file_path: Union[str, Path]):
16911699
# Scalar attributes
16921700
scalar_attrs = set(
16931701
("crs", "tot_value", "unit", "aai_agg", "frequency_unit", "haz_type")
1694-
).intersection(file.attrs.keys())
1702+
)
1703+
if add_scalar_attrs is not None:
1704+
scalar_attrs = scalar_attrs.union(add_scalar_attrs)
1705+
scalar_attrs = scalar_attrs.intersection(file.attrs.keys())
16951706
kwargs.update({attr: file.attrs[attr] for attr in scalar_attrs})
16961707

16971708
# Array attributes
16981709
# NOTE: Need [:] to copy array data. Otherwise, it would be a view that is
16991710
# invalidated once we close the file.
17001711
array_attrs = set(
17011712
("event_id", "date", "coord_exp", "eai_exp", "at_event", "frequency")
1702-
).intersection(file.keys())
1713+
)
1714+
if add_array_attrs is not None:
1715+
array_attrs = array_attrs.union(add_array_attrs)
1716+
array_attrs = array_attrs.intersection(file.keys())
17031717
kwargs.update({attr: file[attr][:] for attr in array_attrs})
1704-
1718+
# correct lead_time attribut to timedelta
1719+
if "lead_time" in kwargs:
1720+
kwargs["lead_time"] = np.array(file["lead_time"][:]).astype(
1721+
"timedelta64[ns]"
1722+
)
17051723
# Special handling for 'event_name' because it should be a list of strings
17061724
if "event_name" in file:
17071725
# pylint: disable=no-member

climada/engine/impact_forecast.py

Lines changed: 229 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@
2020
"""
2121

2222
import logging
23+
from pathlib import Path
24+
from typing import Union
2325

2426
import numpy as np
27+
import scipy.sparse as sparse
2528

2629
from ..util import log_level
2730
from ..util.checker import size
@@ -172,6 +175,62 @@ def calc_freq_curve(self, return_per=None):
172175
LOGGER.error("calc_freq_curve is not defined for ImpactForecast")
173176
raise NotImplementedError("calc_freq_curve is not defined for ImpactForecast")
174177

178+
@classmethod
179+
def from_hdf5(cls, file_path: Union[str, Path]):
180+
"""Create an ImpactForecast object from an H5 file.
181+
182+
This assumes a specific layout of the file. If values are not found in the
183+
expected places, they will be set to the default values for an ``Impact`` object.
184+
185+
The following H5 file structure is assumed (H5 groups are terminated with ``/``,
186+
attributes are denoted by ``.attrs/``)::
187+
188+
file.h5
189+
├─ at_event
190+
├─ coord_exp
191+
├─ eai_exp
192+
├─ event_id
193+
├─ event_name
194+
├─ frequency
195+
├─ imp_mat
196+
├─ lead_time
197+
├─ member
198+
├─ .attrs/
199+
│ ├─ aai_agg
200+
│ ├─ crs
201+
│ ├─ frequency_unit
202+
│ ├─ haz_type
203+
│ ├─ tot_value
204+
│ ├─ unit
205+
206+
As per the :py:func:`climada.engine.impact.Impact.__init__`, any of these entries
207+
is optional. If it is not found, the default value will be used when constructing
208+
the Impact.
209+
210+
The impact matrix ``imp_mat`` can either be an H5 dataset, in which case it is
211+
interpreted as dense representation of the matrix, or an H5 group, in which case
212+
the group is expected to contain the following data for instantiating a
213+
`scipy.sparse.csr_matrix <https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csr_matrix.html>`_::
214+
215+
imp_mat/
216+
├─ data
217+
├─ indices
218+
├─ indptr
219+
├─ .attrs/
220+
│ ├─ shape
221+
222+
Parameters
223+
----------
224+
file_path : str or Path
225+
The file path of the file to read.
226+
227+
Returns
228+
-------
229+
imp : ImpactForecast
230+
ImpactForecast with data from the given file
231+
"""
232+
return super().from_hdf5(file_path, add_array_attrs={"member", "lead_time"})
233+
175234
def _check_sizes(self):
176235
"""Check sizes of forecast data vs. impact data.
177236
@@ -185,6 +244,123 @@ def _check_sizes(self):
185244
size(exp_len=num_entries, var=self.member, var_name="Forecast.member")
186245
size(exp_len=num_entries, var=self.lead_time, var_name="Forecast.lead_time")
187246

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
258+
259+
Parameters
260+
----------
261+
event_name : str
262+
The event name given to the reduced data.
263+
"""
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):
276+
"""
277+
Reduce the impact matrix and at_event of an ImpactForecast to the minimum
278+
value.
279+
280+
Parameters
281+
----------
282+
None
283+
284+
Returns
285+
-------
286+
ImpactForecast
287+
An ImpactForecast object with the min impact matrix and at_event.
288+
"""
289+
red_imp_mat = self.imp_mat.min(axis=0).tocsr()
290+
red_at_event = np.array([red_imp_mat.sum()])
291+
return ImpactForecast(
292+
frequency_unit=self.frequency_unit,
293+
coord_exp=self.coord_exp,
294+
crs=self.crs,
295+
eai_exp=self.eai_exp,
296+
at_event=red_at_event,
297+
tot_value=self.tot_value,
298+
aai_agg=self.aai_agg,
299+
unit=self.unit,
300+
imp_mat=red_imp_mat,
301+
haz_type=self.haz_type,
302+
**self._reduce_attrs("min"),
303+
)
304+
305+
def max(self):
306+
"""
307+
Reduce the impact matrix and at_event of an ImpactForecast to the maximum
308+
value.
309+
310+
Parameters
311+
----------
312+
None
313+
314+
Returns
315+
-------
316+
ImpactForecast
317+
An ImpactForecast object with the max impact matrix and at_event.
318+
"""
319+
red_imp_mat = self.imp_mat.max(axis=0).tocsr()
320+
red_at_event = np.array([red_imp_mat.sum()])
321+
return ImpactForecast(
322+
frequency_unit=self.frequency_unit,
323+
coord_exp=self.coord_exp,
324+
crs=self.crs,
325+
eai_exp=self.eai_exp,
326+
at_event=red_at_event,
327+
tot_value=self.tot_value,
328+
aai_agg=self.aai_agg,
329+
unit=self.unit,
330+
imp_mat=red_imp_mat,
331+
haz_type=self.haz_type,
332+
**self._reduce_attrs("max"),
333+
)
334+
335+
def mean(self):
336+
"""
337+
Reduce the impact matrix and at_event of an ImpactForecast to the mean value.
338+
339+
Parameters
340+
----------
341+
None
342+
343+
Returns
344+
-------
345+
ImpactForecast
346+
An ImpactForecast object with the mean impact matrix and at_event.
347+
"""
348+
red_imp_mat = sparse.csr_matrix(self.imp_mat.mean(axis=0))
349+
red_at_event = np.array([red_imp_mat.sum()])
350+
return ImpactForecast(
351+
frequency_unit=self.frequency_unit,
352+
coord_exp=self.coord_exp,
353+
crs=self.crs,
354+
eai_exp=self.eai_exp,
355+
at_event=red_at_event,
356+
tot_value=self.tot_value,
357+
aai_agg=self.aai_agg,
358+
unit=self.unit,
359+
imp_mat=red_imp_mat,
360+
haz_type=self.haz_type,
361+
**self._reduce_attrs("mean"),
362+
)
363+
188364
def select(
189365
self,
190366
event_ids=None,
@@ -205,10 +381,6 @@ def select(
205381
lead_time : Sequence of numpy.timedelta64
206382
Lead times to select
207383
208-
Returns
209-
-------
210-
ImpactForecast
211-
212384
See Also
213385
--------
214386
:py:meth:`~climada.engine.impact.Impact.select`
@@ -240,3 +412,56 @@ def select(
240412
coord_exp=coord_exp,
241413
reset_frequency=reset_frequency,
242414
)
415+
416+
def _quantile(self, q: float, event_name: str | None = None):
417+
"""
418+
Reduce the impact matrix and at_event of an ImpactForecast to the quantile value.
419+
"""
420+
red_imp_mat = sparse.csr_matrix(np.quantile(self.imp_mat.toarray(), q, axis=0))
421+
red_at_event = np.array([red_imp_mat.sum()])
422+
if event_name is None:
423+
event_name = f"quantile_{q}"
424+
return ImpactForecast(
425+
frequency_unit=self.frequency_unit,
426+
coord_exp=self.coord_exp,
427+
crs=self.crs,
428+
eai_exp=self.eai_exp,
429+
at_event=red_at_event,
430+
tot_value=self.tot_value,
431+
aai_agg=self.aai_agg,
432+
unit=self.unit,
433+
imp_mat=red_imp_mat,
434+
haz_type=self.haz_type,
435+
**self._reduce_attrs(event_name),
436+
)
437+
438+
def quantile(self, q: float):
439+
"""
440+
Reduce the impact matrix and at_event of an ImpactForecast to the quantile value.
441+
442+
Parameters
443+
----------
444+
q : float
445+
The quantile to compute, which must be between 0 and 1.
446+
447+
Returns
448+
-------
449+
ImpactForecast
450+
An ImpactForecast object with the quantile impact matrix and at_event.
451+
"""
452+
return self._quantile(q=q)
453+
454+
def median(self):
455+
"""
456+
Reduce the impact matrix and at_event of an ImpactForecast to the median value.
457+
458+
Parameters
459+
----------
460+
None
461+
462+
Returns
463+
-------
464+
ImpactForecast
465+
An ImpactForecast object with the median impact matrix and at_event.
466+
"""
467+
return self._quantile(q=0.5, event_name="median")

0 commit comments

Comments
 (0)