Skip to content

Commit 20f8485

Browse files
committed
Merge branch 'develop' into feature/option-appraisal-split-make-legacy
2 parents 7986917 + 2b19cf8 commit 20f8485

13 files changed

Lines changed: 756 additions & 64 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Code freeze date: YYYY-MM-DD
1313
### Added
1414

1515
- Better type hints and overloads signatures for ImpactFuncSet [#1250](https://github.com/CLIMADA-project/climada_python/pull/1250)
16+
- Add inter- and extrapolation options to `ImpactFreqCurve` with method `interpolate` [#1252](https://github.com/CLIMADA-project/climada_python/pull/1252)
1617

1718
### Changed
1819
- Updated Impact Calculation Tutorial (`doc.climada_engine_Impact.ipynb`) [#1095](https://github.com/CLIMADA-project/climada_python/pull/1095).
@@ -21,8 +22,10 @@ Code freeze date: YYYY-MM-DD
2122
### Fixed
2223

2324
- Fixed asset count in impact logging message [#1195](https://github.com/CLIMADA-project/climada_python/pull/1195).
25+
- `Hazard.from_raster_xarray` now returns a sparse matrix instead of a sparse array [#1261](https://github.com/CLIMADA-project/climada_python/pull/1261).
2426

2527
### Deprecated
28+
- `Impact.calc_freq_curve()` should not be given the parameter `return_per`. Use the parameter `return_periods` in `Impact.calc_freq_curve().interpolate()` instead.
2629

2730
### Removed
2831
- `climada.util.earth_engine.py` Google Earth Engine methods did not facilitate direct use of GEE data in CLIMADA. Code was relocated to [climada-snippets](https://github.com/CLIMADA-project/climada-snippets). [#1109](https://github.com/CLIMADA-project/climada_python/pull/1109)

climada/engine/impact.py

Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -576,15 +576,6 @@ def local_exceedance_impact(
576576
self.frequency_unit
577577
)
578578

579-
# check method
580-
if method not in [
581-
"interpolate",
582-
"extrapolate",
583-
"extrapolate_constant",
584-
"stepfunction",
585-
]:
586-
raise ValueError(f"Unknown method: {method}")
587-
588579
# calculate local exceedance impact
589580
test_frequency = 1 / np.array(return_periods)
590581

@@ -732,15 +723,6 @@ def local_return_period(
732723
self.frequency_unit
733724
)
734725

735-
# check method
736-
if method not in [
737-
"interpolate",
738-
"extrapolate",
739-
"extrapolate_constant",
740-
"stepfunction",
741-
]:
742-
raise ValueError(f"Unknown method: {method}")
743-
744726
return_periods = np.full((self.imp_mat.shape[1], len(threshold_impact)), np.nan)
745727

746728
nonzero_centroids = np.where(self.imp_mat.getnnz(axis=0) > 0)[0]
@@ -804,6 +786,12 @@ def calc_freq_curve(self, return_per=None):
804786
ifc_impact = self.at_event[sort_idxs][::-1]
805787

806788
if return_per is not None:
789+
warnings.warn(
790+
"Calculating the frequency curve on user-specified return periods is deprecated. "
791+
"Use ImpactFreqCurve.calc_freq_curve().interpolate() instead.",
792+
DeprecationWarning,
793+
stacklevel=2,
794+
)
807795
interp_imp = np.interp(return_per, ifc_return_per, ifc_impact)
808796
ifc_return_per = return_per
809797
ifc_impact = interp_imp
@@ -2299,15 +2287,108 @@ def plot(self, axis=None, log_frequency=False, **kwargs):
22992287
-------
23002288
matplotlib.axes.Axes
23012289
"""
2290+
# check frequency unit
2291+
return_period_unit = u_dt.convert_frequency_unit_to_time_unit(
2292+
self.frequency_unit
2293+
)
2294+
23022295
if not axis:
23032296
_, axis = plt.subplots(1, 1)
23042297
axis.set_title(self.label)
23052298
axis.set_ylabel("Impact (" + self.unit + ")")
2299+
23062300
if log_frequency:
23072301
axis.set_xlabel(f"Exceedance frequency ({self.frequency_unit})")
23082302
axis.set_xscale("log")
23092303
axis.plot(self.return_per**-1, self.impact, **kwargs)
2304+
23102305
else:
2311-
axis.set_xlabel("Return period (year)")
2306+
axis.set_xlabel(f"Return period ({return_period_unit})")
23122307
axis.plot(self.return_per, self.impact, **kwargs)
2308+
23132309
return axis
2310+
2311+
def interpolate(
2312+
self,
2313+
return_periods,
2314+
*,
2315+
method="interpolate",
2316+
log_frequency=True,
2317+
log_impact=True,
2318+
min_impact=0,
2319+
bin_decimals=None,
2320+
y_asymptotic=0.0,
2321+
):
2322+
"""Interpolate and extrapolate impact frequency curve using different methods.
2323+
2324+
Parameters
2325+
----------
2326+
return_periods : Iterable[float]
2327+
return periods for which to evaluate the impact frequency curve
2328+
2329+
method : str, optional
2330+
Method to interpolate to new return periods. Currently available are "interpolate",
2331+
"extrapolate", "extrapolate_constant" and "stepfunction". If set to "interpolate",
2332+
return periods outside the range of the Impact object's observed return periods
2333+
will be assigned NaN. If set to "extrapolate_constant" or "stepfunction",
2334+
return periods larger than the Impact object's observed return periods will be
2335+
assigned the largest impact, and return periods smaller than the Impact object's
2336+
observed return periods will be assigned 0. If set to "extrapolate",
2337+
exceedance impacts will be extrapolated (and interpolated). The extrapolation to
2338+
large return periods uses the two highest impacts of the centroid and their return
2339+
periods and extends the interpolation between these points to the given return period
2340+
(similar for small return periods). Defauls to "interpolate".
2341+
min_impact : float, optional
2342+
Minimum threshold to filter the impact. Defaults to 0.
2343+
log_frequency : bool, optional
2344+
If set to True, (cummulative) frequency values are converted to log scale before
2345+
inter- and extrapolation. Defaults to True.
2346+
log_impact : bool, optional
2347+
If set to True, impact values are converted to log scale before
2348+
inter- and extrapolation. Defaults to True.
2349+
bin_decimals : int, optional
2350+
Number of decimals to group and bin impact values. Binning results in smoother (and
2351+
coarser) interpolation and more stable extrapolation. For more details and sensible
2352+
values for bin_decimals, see Notes. If None, values are not binned. Defaults to None.
2353+
y_asymptotic : float, optional
2354+
Has no effect if method is "interpolate". Else, if data size < 2 or if method
2355+
is set to "extrapolate_constant" or "stepfunction", it provides return value for
2356+
exceeded impact for return periods smaller than the data range. Defaults to 0.
2357+
2358+
Returns
2359+
-------
2360+
ImpactFreqCurve
2361+
impact frequency curve with inter- and extrapolated values
2362+
2363+
See Also
2364+
--------
2365+
util.interpolation.preprocess_and_interpolate_ev :
2366+
inter- and extrapolation method
2367+
"""
2368+
exceedance_frequency = 1 / np.array(return_periods)
2369+
2370+
# sort return periods of ImpactFreqCurve
2371+
sorted_idxs = np.argsort(self.return_per)
2372+
impacts = np.squeeze(np.array(self.impact)[sorted_idxs])
2373+
rps = np.asarray(self.return_per)[sorted_idxs]
2374+
frequency = np.diff(1 / np.array(rps)[::-1], prepend=0)[::-1]
2375+
impact_interpolated = u_interp.preprocess_and_interpolate_ev(
2376+
exceedance_frequency,
2377+
None,
2378+
frequency,
2379+
impacts,
2380+
log_frequency=log_frequency,
2381+
log_values=log_impact,
2382+
value_threshold=min_impact,
2383+
method=method,
2384+
y_asymptotic=y_asymptotic,
2385+
bin_decimals=bin_decimals,
2386+
)
2387+
2388+
return ImpactFreqCurve(
2389+
return_per=return_periods,
2390+
impact=impact_interpolated,
2391+
unit=self.unit,
2392+
frequency_unit=self.frequency_unit,
2393+
label=self.label,
2394+
)

climada/engine/test/test_impact.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,61 @@ def test_ref_value_rp_pass(self):
321321
self.assertEqual("USD", ifc.unit)
322322
self.assertEqual("1/week", ifc.frequency_unit)
323323

324+
def test_interpolate_freq_curve(self):
325+
"""Test inter- and extrapolate method of freq curve"""
326+
imp = Impact()
327+
imp.frequency = np.array([0.2, 0.1, 0.1, 0.1])
328+
imp.at_event = np.array([0.0, 100.0, 50.0, 110.0])
329+
imp.unit = "USD"
330+
imp.frequency_unit = "1/year"
331+
332+
ifc = imp.calc_freq_curve()
333+
# the ifc has values
334+
# impacts [0, 50, 100, 110]
335+
# exceedance frequencies [.5, .3, .2, .1]
336+
# return periods [2, 3.3, 5, 10]
337+
338+
# stepfunction assigns zero return periods below data and max(impact) for those above
339+
npt.assert_array_almost_equal(
340+
ifc.interpolate([1, 5, 20], method="stepfunction").impact,
341+
[0.0, 100.0, 110.0],
342+
)
343+
344+
# interpolate assigns nan to return periods outside of data
345+
npt.assert_array_almost_equal(
346+
ifc.interpolate([1, 5, 20], method="interpolate").impact,
347+
[np.nan, 100.0, np.nan],
348+
)
349+
350+
# extrapolate_constant assigns zero return periods below data and max(impact) for those above
351+
npt.assert_array_almost_equal(
352+
ifc.interpolate([1, 5, 20], method="extrapolate_constant").impact,
353+
[0.0, 100.0, 110.0],
354+
)
355+
356+
# by binning the last two digits, 100 and 110 are rounded to 100
357+
npt.assert_array_almost_equal(
358+
ifc.interpolate(
359+
[1, 5, 20], method="extrapolate_constant", bin_decimals=-2
360+
).impact,
361+
[0.0, 100.0, 100.0],
362+
)
363+
364+
# extrapolation is done by neglecting 0 impacts (min_impact=0)
365+
# rp=1: extrapolate impacts [50, 100] and ex_freqs [.3, .2] to ex_freq=1 --> 0
366+
# rp=2.5: extrapolate impacts [50, 100] and ex_freqs [.3, .2] to ex_freq=0.4 --> 0
367+
# rp=4: extrapolate impacts [50, 100] and ex_freqs [.3, .2] to ex_freq=0.25 --> 75
368+
# rp=1: extrapolate impacts [100, 110] and ex_freqs [.2, .1] to ex_freq=0.05 --> 115
369+
npt.assert_array_almost_equal(
370+
ifc.interpolate(
371+
[1.0, 2.5, 4, 20],
372+
method="extrapolate",
373+
log_frequency=False,
374+
log_impact=False,
375+
).impact,
376+
[-300.0, 0.0, 75.0, 115.0],
377+
)
378+
324379

325380
class TestImpactPerYear(unittest.TestCase):
326381
"""Test calc_impact_year_set method"""

climada/entity/impact_funcs/test/test_tc.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@
2424
import numpy as np
2525
import pandas as pd
2626

27-
from climada.entity.impact_funcs.trop_cyclone import ImpfSetTropCyclone, ImpfTropCyclone
27+
from climada.entity.impact_funcs.trop_cyclone import (
28+
CountryCode,
29+
ImpfSetTropCyclone,
30+
ImpfTropCyclone,
31+
)
2832

2933

3034
class TestEmanuelFormula(unittest.TestCase):
@@ -168,6 +172,16 @@ def test_get_countries_per_region(self):
168172
self.assertListEqual(out[2], [124, 840])
169173
self.assertListEqual(out[3], ["CAN", "USA"])
170174

175+
def test_get_countries_per_region_all_or_none(self):
176+
ifs = ImpfSetTropCyclone()
177+
out = ifs.get_countries_per_region()
178+
out2 = ifs.get_countries_per_region("all")
179+
self.assertEqual(out, out2)
180+
for reg in CountryCode.REGION_NAME.value:
181+
out_reg = ifs.get_countries_per_region(reg)
182+
for i in range(4):
183+
self.assertEqual(out[i][reg], out_reg[i])
184+
171185
def test_get_imf_id_regions_per_countries(self):
172186
"""Test get_impf_id_regions_per_countries()"""
173187
ifs = ImpfSetTropCyclone()

climada/entity/impact_funcs/trop_cyclone.py

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import logging
2525
from enum import Enum
26+
from typing import Literal, overload
2627

2728
import numpy as np
2829
import pandas as pd
@@ -92,7 +93,7 @@ class CountryCode(Enum):
9293
"NAM", "NER", "NGA", "NLD", "NOR", "POL", "PRK", "PRT", "PSE", "REU", "ROU",
9394
"RUS", "RWA", "SDN", "SEN", "SGP", "SGS", "SJM", "SLE", "SMR", "SPM", "SRB",
9495
"SSD", "STP", "SVK", "SVN", "SWE", "SYC", "TCD", "TGO", "TUN", "TUR", "UKR",
95-
"UMI", "VAT", "XKX", "ZMB",
96+
"UMI", "VAT", "XKO", "ZMB",
9697
],
9798
}
9899

@@ -363,10 +364,47 @@ def calibrated_regional_vhalf(
363364
reg_v_half[regions_short[-1]] = np.round(df_reg["v_half"].values[0], 5)
364365
return reg_v_half
365366

367+
@overload
368+
@staticmethod
369+
def get_countries_per_region(
370+
region: Literal["all"] = "all",
371+
) -> tuple[
372+
dict[str, str], # region_name
373+
dict[str, int], # impf_id
374+
dict[str, list[int]], # numeric
375+
dict[str, list[str]], # alpha3
376+
]: ...
377+
378+
@overload
379+
@staticmethod
380+
def get_countries_per_region(
381+
region: None,
382+
) -> tuple[
383+
dict[str, str], dict[str, int], dict[str, list[int]], dict[str, list[str]]
384+
]: ...
385+
386+
@overload
387+
@staticmethod
388+
def get_countries_per_region(
389+
region: str,
390+
) -> tuple[
391+
str, int, list[int], list[str] # region_name # impf_id # numeric # alpha3
392+
]: ...
393+
366394
@staticmethod
367395
def get_countries_per_region(region=None):
368-
"""Returns dictionaries with numerical (numeric) and alphabetical (alpha3) ISO3 codes
369-
of all countries associated to a calibration region.
396+
"""Returns countries within a TC calibration region and associated impact functions.
397+
398+
This method returns a tuple with numerical (numeric) and alphabetical (alpha3)
399+
ISO3 codes of all countries associated to a calibration region.
400+
401+
If no region or "all" is provided as argument, the method return a tuple of
402+
dictionaries with short name of the tropical cyclone calibration regions as
403+
keys and the values for each of those.
404+
405+
Notes
406+
-----
407+
370408
Only contains countries that were affected by tropical cyclones
371409
between 1980 and 2017 according to EM-DAT.
372410
@@ -395,9 +433,12 @@ def get_countries_per_region(region=None):
395433
return (
396434
CountryCode.REGION_NAME.value,
397435
CountryCode.IMPF_ID.value,
398-
coordinates.country_to_iso(
399-
CountryCode.ALPHA3.value, representation="numeric"
400-
),
436+
{
437+
reg: coordinates.country_to_iso(
438+
CountryCode.ALPHA3.value[reg], representation="numeric"
439+
)
440+
for reg in CountryCode.REGION_NAME.value
441+
},
401442
CountryCode.ALPHA3.value,
402443
)
403444

climada/hazard/base.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -554,15 +554,6 @@ def local_exceedance_intensity(
554554
self.frequency_unit
555555
)
556556

557-
# check method
558-
if method not in [
559-
"interpolate",
560-
"extrapolate",
561-
"extrapolate_constant",
562-
"stepfunction",
563-
]:
564-
raise ValueError(f"Unknown method: {method}")
565-
566557
# calculate local exceedance intensity
567558
test_frequency = 1 / np.array(return_periods)
568559

@@ -707,15 +698,6 @@ def local_return_period(
707698
self.frequency_unit
708699
)
709700

710-
# check method
711-
if method not in [
712-
"interpolate",
713-
"extrapolate",
714-
"extrapolate_constant",
715-
"stepfunction",
716-
]:
717-
raise ValueError(f"Unknown method: {method}")
718-
719701
return_periods = np.full(
720702
(self.intensity.shape[1], len(threshold_intensities)), np.nan
721703
)

0 commit comments

Comments
 (0)