Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
7da50f1
Return impactForecast object in _return_impact
luseverin Dec 8, 2025
bd3502f
Return impactForecast object in _return_empty
luseverin Dec 8, 2025
dfa0198
Add full impactcalc test for impactForecast
luseverin Dec 8, 2025
59c0e5b
Correct mistakes in _return_empty and _return_impact
luseverin Dec 8, 2025
4b5ae95
Raise value error when computing impact with impact forecast without …
luseverin Dec 8, 2025
9a516e1
Cosmetics: Improve error message, move test to own class
Dec 8, 2025
d571bb7
Merge branch 'forecast-class' into impactCalc_block_nonsense_attrs
luseverin Dec 9, 2025
6fe29f0
Merge branch 'impactCalc_return_impactForecast' into impactCalc_block…
luseverin Dec 9, 2025
899d8f0
add test to check that eai_exp and aai_agg are nan for forecasts
luseverin Dec 9, 2025
db32170
Write nans for eai_exp and aai_agg when forecast is used
luseverin Dec 9, 2025
d2f035f
update tests using pytest
luseverin Dec 9, 2025
c10a4b3
Fix error in test fixtures
luseverin Dec 9, 2025
d3a5642
Returns nans for eai_exp and aai_agg when exposures is empty
luseverin Dec 9, 2025
d43a46c
add warning when at_event is used with forecast
luseverin Dec 9, 2025
e197566
Update ImpactCalc tests for forecasts
peanutfun Dec 9, 2025
0a324b2
Merge branch 'impactCalc_return_impactForecast' of https://github.com…
peanutfun Dec 9, 2025
554cfc8
Review ImpactCalc forecast handling
peanutfun Dec 9, 2025
30ed14d
Block local_exceedance_impact
luseverin Dec 9, 2025
f3ab44a
Fix bug in test
peanutfun Dec 9, 2025
612a53c
Merge branch 'impactCalc_return_impactForecast' of https://github.com…
peanutfun Dec 9, 2025
9e4f2bb
Block return_period and exceedance_freq_curve
luseverin Dec 9, 2025
727357e
Log warning for at_event getter
luseverin Dec 9, 2025
0bc5846
Add mean max min methods to impact forecast and tests
luseverin Dec 9, 2025
a55ab3b
Merge branch 'forecast-class' into implement_mean_min_max
luseverin Dec 9, 2025
0aad2a5
Reduce lead time and member and update test
luseverin Dec 10, 2025
102f9be
Update docstrings
luseverin Dec 10, 2025
dfce16f
Remove useless comments
luseverin Dec 10, 2025
0b6e1e4
Add min max mean for hazard forecast
luseverin Dec 10, 2025
3fc25af
Correct some mistakes in mean min max on hazard forecast
luseverin Dec 10, 2025
a4fe3b0
Add tests for hazard forecast mean min max
luseverin Dec 10, 2025
e099868
Set reduced date to 0 and cosmetic changes
luseverin Dec 10, 2025
23660de
Set reduced date to 0 in impact forecast
luseverin Dec 10, 2025
64149df
Simplify code and tests
peanutfun Dec 10, 2025
2e028e3
Merge branch 'forecast-class' into implement_mean_min_max
peanutfun Dec 10, 2025
2c35791
Add reduce_unique_selection in forecast base class
luseverin Dec 10, 2025
c91a2cb
Add dim to mean min max in hazard forecast
luseverin Dec 10, 2025
64574ec
Draft test mean min max with dim hazard forecat
luseverin Dec 10, 2025
bc2e6bd
Merge branch 'forecast-class' into implement_unique_selection_reduction
luseverin Dec 10, 2025
1076719
Correct wrong use of self.dim in mean min max
luseverin Dec 10, 2025
bc6f576
Parametrize hazard fixtures with different members and lead times and…
luseverin Dec 10, 2025
001f340
Merge branch 'forecast-class' into implement_unique_selection_reduction
luseverin Dec 10, 2025
5b46f63
Adjust reduction logic with respect to members
peanutfun Dec 17, 2025
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
57 changes: 41 additions & 16 deletions climada/hazard/forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import scipy.sparse as sparse

from ..util.checker import size
from ..util.forecast import Forecast
from ..util.forecast import Forecast, reduce_unique_selection
from .base import Hazard

LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -106,7 +106,7 @@ def _check_sizes(self):
size(exp_len=num_entries, var=self.member, var_name="Forecast.member")
size(exp_len=num_entries, var=self.lead_time, var_name="Forecast.lead_time")

def _reduce_attrs(self, event_name: str):
def _reduce_attrs(self, event_name: str, **attrs):
"""
Reduce the attributes of a HazardForecast to a single value.

Expand All @@ -123,32 +123,45 @@ def _reduce_attrs(self, event_name: str):
event_name : str
The event_name given to the reduced data.
"""

def unique_or_default(attr, default):
if len(unique := np.unique(getattr(self, attr))) == 1:
return unique.item(0)
return default

reduced_attrs = {
"lead_time": np.array([np.timedelta64("NaT")]),
"member": np.array([-1]),
"event_id": np.array([0]),
"event_name": np.array([event_name]),
"date": np.array([0]),
"frequency": np.array([1]),
"orig": np.array([True]),
}
"lead_time": unique_or_default("lead_time", np.timedelta64("NaT")),
"member": unique_or_default("member", -1),
"event_id": unique_or_default("event_id", 1),
"event_name": unique_or_default("event_name", event_name),
"date": unique_or_default("date", 0),
"frequency": 1,
"orig": unique_or_default("orig", True),
} | attrs
reduced_attrs = {key: np.array([value]) for key, value in reduced_attrs.items()}

return reduced_attrs

def min(self):
def min(self, dim=None):
"""
Reduce the intensity and fraction of a HazardForecast to the minimum
value.

Parameters
----------
None
dim : str | None
Dimension to reduce over. If None, reduce over all data.

Returns
-------
HazardForecast
A HazardForecast object with the min intensity and fraction.
"""
if dim is not None:
return reduce_unique_selection(
self, values=getattr(self, dim), select=dim, reduce_attr="min"
)

red_intensity = self.intensity.min(axis=0).tocsr()
red_fraction = self.fraction.min(axis=0).tocsr()
return HazardForecast(
Expand All @@ -162,20 +175,26 @@ def min(self):
**self._reduce_attrs("min"),
)

def max(self):
def max(self, dim=None):
"""
Reduce the intensity and fraction of a HazardForecast to the maximum
value.

Parameters
----------
None
dim : str | None
Dimension to reduce over. If None, reduce over all data.

Returns
-------
HazardForecast
A HazardForecast object with the min intensity and fraction.
"""
if dim is not None:
return reduce_unique_selection(
self, values=getattr(self, dim), select=dim, reduce_attr="max"
)

red_intensity = self.intensity.max(axis=0).tocsr()
red_fraction = self.fraction.max(axis=0).tocsr()
return HazardForecast(
Expand All @@ -189,19 +208,25 @@ def max(self):
**self._reduce_attrs("max"),
)

def mean(self):
def mean(self, dim=None):
"""
Reduce the intensity and fraction of a HazardForecast to the mean value.

Parameters
----------
None
dim : str | None
Dimension to reduce over. If None, reduce over all data.

Returns
-------
HazardForecast
A HazardForecast object with the min intensity and fraction.
"""
if dim is not None:
return reduce_unique_selection(
self, values=getattr(self, dim), select=dim, reduce_attr="mean"
)

red_intensity = sparse.csr_matrix(self.intensity.mean(axis=0))
red_fraction = sparse.csr_matrix(self.fraction.mean(axis=0))
return HazardForecast(
Expand Down
79 changes: 72 additions & 7 deletions climada/hazard/test/test_forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from climada.hazard.test.test_base import hazard_kwargs


@pytest.fixture
@pytest.fixture(scope="module")
def haz_kwargs():
return hazard_kwargs()

Expand All @@ -40,14 +40,18 @@ def hazard(haz_kwargs):
return Hazard(**haz_kwargs)


# TODO: params should only apply to dimension reduction test
@pytest.fixture
def lead_time(haz_kwargs):
return pd.timedelta_range("1h", periods=len(haz_kwargs["event_id"])).to_numpy()
def lead_time(request, haz_kwargs):
return pd.timedelta_range("1h", periods=len(haz_kwargs["event_id"]))
return np.tile(base_range.to_numpy(), request.param)


# TODO: params should only apply to dimension reduction test
@pytest.fixture
def member(haz_kwargs):
def member(request, haz_kwargs):
return np.arange(len(haz_kwargs["event_id"]))
return np.tile(base_range, request.param)


@pytest.fixture
Expand Down Expand Up @@ -254,7 +258,7 @@ def test_hazard_forecast_mean_min_max(haz_fc, attr):
npt.assert_array_equal(np.isnat(haz_fcst_reduced.lead_time), [True])
npt.assert_array_equal(haz_fcst_reduced.member, [-1])
npt.assert_array_equal(haz_fcst_reduced.event_name, [attr])
npt.assert_array_equal(haz_fcst_reduced.event_id, [0])
npt.assert_array_equal(haz_fcst_reduced.event_id, [1])
npt.assert_array_equal(haz_fcst_reduced.frequency, [1])
npt.assert_array_equal(haz_fcst_reduced.date, [0])
npt.assert_array_equal(haz_fcst_reduced.orig, [True])
Expand Down Expand Up @@ -284,7 +288,7 @@ def test_hazard_forecast_quantile(haz_fc, quantile):
npt.assert_array_equal(
haz_fcst_quantile.event_name, np.array([f"quantile_{quantile}"])
)
npt.assert_array_equal(haz_fcst_quantile.event_id, np.array([0]))
npt.assert_array_equal(haz_fcst_quantile.event_id, np.array([1]))
npt.assert_array_equal(haz_fcst_quantile.frequency, np.array([1]))
npt.assert_array_equal(haz_fcst_quantile.date, np.array([0]))
npt.assert_array_equal(haz_fcst_quantile.orig, np.array([True]))
Expand All @@ -311,8 +315,69 @@ def test_median(haz_fc):
# check that attributes where reduced correctly
npt.assert_array_equal(haz_fcst_median.member, np.array([-1]))
npt.assert_array_equal(haz_fcst_median.lead_time, np.array([np.timedelta64("NaT")]))
npt.assert_array_equal(haz_fcst_median.event_id, np.array([0]))
npt.assert_array_equal(haz_fcst_median.event_id, np.array([1]))
npt.assert_array_equal(haz_fcst_median.event_name, np.array(["median"]))
npt.assert_array_equal(haz_fcst_median.frequency, np.array([1]))
npt.assert_array_equal(haz_fcst_median.date, np.array([0]))
npt.assert_array_equal(haz_fcst_median.orig, np.array([True]))


@pytest.mark.parametrize("attr", ["min", "mean", "max"])
def test_hazard_forecast_mean_min_max_member(haz_fc, attr):
"""Check mean, min, and max methods for HazardForecast with dim argument"""

for dim, unique_vals in zip(
["member", "lead_time"],
[np.unique(haz_fc.member), np.unique(haz_fc.lead_time)],
):
haz_fcst_reduced = getattr(haz_fc, attr)(dim=dim)
# Assert sparse matrices
expected_intensity = []
expected_fraction = []
for val in unique_vals:
mask = getattr(haz_fc, dim) == val
expected_intensity.append(
getattr(haz_fc.intensity.todense()[mask], attr)(axis=0)
)
expected_fraction.append(
getattr(haz_fc.fraction.todense()[mask], attr)(axis=0)
)
npt.assert_array_equal(
haz_fcst_reduced.intensity.todense(),
np.vstack(expected_intensity),
)
npt.assert_array_equal(
haz_fcst_reduced.fraction.todense(),
np.vstack(expected_fraction),
)
# Check that attributes where reduced correctly
if dim == "lead_time":
npt.assert_array_equal(haz_fcst_reduced.member, np.unique(haz_fc.member))
npt.assert_array_equal(
haz_fcst_reduced.lead_time,
np.array([np.timedelta64("NaT")] * len(unique_vals)),
)
else: # dim == "member"
npt.assert_array_equal(
haz_fcst_reduced.lead_time, np.unique(haz_fc.lead_time)
)
npt.assert_array_equal(
haz_fcst_reduced.member,
np.array([-1] * len(unique_vals)),
)
npt.assert_array_equal(
haz_fcst_reduced.event_name,
np.array([attr] * len(unique_vals)),
)
npt.assert_array_equal(haz_fcst_reduced.event_id, [0] * len(unique_vals))
npt.assert_array_equal(haz_fcst_reduced.frequency, [1] * len(unique_vals))
npt.assert_array_equal(haz_fcst_reduced.date, [0] * len(unique_vals))
npt.assert_array_equal(haz_fcst_reduced.orig, [True] * len(unique_vals))
# TODO add test in case no reduction happens (e.g., all values along dim are unique)


def test_hazard_forecast_mean_max_min_dim_error(haz_fc):
"""Check mean, min, and max methods for ImpactForecast with dim argument"""
for attr in ["min", "mean", "max"]:
with pytest.raises(AttributeError):
getattr(haz_fc, attr)(dim="invalid_dim")
30 changes: 30 additions & 0 deletions climada/util/forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,33 @@ def idx_lead_time(self, lead_time: np.ndarray) -> np.ndarray:
"""

return np.isin(self.lead_time, lead_time)


def reduce_unique_selection(forecast, values, select, reduce_attr):
"""
Reduce an attribute of a forecast object by selecting unique values
and performing an attribute reduction method.

Parameters
----------
forecast : HazardForecast | ImpactForecast
Forecast object to reduce.
values : np.ndarray
Array of values for which to select and reduce the attribute.
select : str
Name of the attribute to select on (e.g. 'lead_time', 'member').
reduce_attr : str
Name of the attribute reduction method to call (e.g. 'min', 'mean').

Returns
-------
Forecast
Forecast object with the attribute reduced by the reduction method
and selected by the unique values.
"""
return forecast.concat(
[
getattr(forecast.select(**{select: [val]}), reduce_attr)(dim=None)
for val in np.unique(values)
]
)
Loading