Skip to content

Commit ba6457c

Browse files
Merge pull request #1012 from CLIMADA-project/feature/improve_local_exceedance
Improve efficiency of local exceedance intensitry/impact and local return periods functions
2 parents 206e57b + 49fa82a commit ba6457c

File tree

9 files changed

+459
-331
lines changed

9 files changed

+459
-331
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ Code freeze date: YYYY-MM-DD
1313
### Added
1414

1515
### Changed
16-
16+
- `Hazard.local_exceedance_intensity`, `Hazard.local_return_period` and `Impact.local_exceedance_impact`, `Impact.local_return_period`, using the `climada.util.interpolation` module: New default (no binning), binning on decimals, and faster implementation [#1012](https://github.com/CLIMADA-project/climada_python/pull/1012)
1717
### Fixed
1818

1919
### Deprecated
2020

2121
### Removed
22+
- `climada.util.interpolation.round_to_sig_digits` [#1012](https://github.com/CLIMADA-project/climada_python/pull/1012)
2223

2324
## 6.0.1
2425

climada/engine/impact.py

Lines changed: 156 additions & 93 deletions
Large diffs are not rendered by default.

climada/engine/test/test_impact.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,7 @@ def test_local_exceedance_impact(self):
556556
method="extrapolate",
557557
log_frequency=False,
558558
log_impact=False,
559+
bin_decimals=2,
559560
)
560561
np.testing.assert_allclose(
561562
impact_stats.values[:, 1:].astype(float),
@@ -594,7 +595,7 @@ def test_local_exceedance_impact_methods(self):
594595

595596
# test log log extrapolation
596597
impact_stats, _, _ = impact.local_exceedance_impact(
597-
return_periods=(1000, 30, 0.1), method="extrapolate"
598+
return_periods=(1000, 30, 0.1), method="extrapolate", bin_decimals=-1
598599
)
599600
np.testing.assert_allclose(
600601
impact_stats.values[:, 1:].astype(float),
@@ -661,6 +662,7 @@ def test_local_return_period(self):
661662
method="extrapolate",
662663
log_frequency=False,
663664
log_impact=False,
665+
bin_decimals=2,
664666
)
665667

666668
np.testing.assert_allclose(
@@ -707,7 +709,7 @@ def test_local_return_period_methods(self):
707709

708710
# test log log extrapolation
709711
return_stats, _, _ = impact.local_return_period(
710-
threshold_impact=(0.1, 300, 1e5), method="extrapolate"
712+
threshold_impact=(0.1, 300, 1e5), method="extrapolate", bin_decimals=2
711713
)
712714
np.testing.assert_allclose(
713715
return_stats.values[:, 1:].astype(float),

climada/hazard/base.py

Lines changed: 116 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ def local_exceedance_intensity(
488488
min_intensity=None,
489489
log_frequency=True,
490490
log_intensity=True,
491+
bin_decimals=None,
491492
):
492493
"""Compute local exceedance intensity for given return periods. The default method
493494
is fitting the ordered intensitites per centroid to the corresponding cummulated
@@ -506,19 +507,23 @@ def local_exceedance_intensity(
506507
periods larger than the Hazard object's observed local return periods will be assigned
507508
the largest local intensity, and return periods smaller than the Hazard object's
508509
observed local return periods will be assigned 0. If set to "extrapolate", local
509-
exceedance intensities will be extrapolated (and interpolated).
510-
Defauls to "interpolate".
510+
exceedance intensities will be extrapolated (and interpolated). The extrapolation to
511+
large return periods uses the two highest intensites of the centroid and their return
512+
periods and extends the interpolation between these points to the given return period
513+
(similar for small return periods). Defauls to "interpolate".
511514
min_intensity : float, optional
512515
Minimum threshold to filter the hazard intensity. If set to None, self.intensity_thres
513516
will be used. Defaults to None.
514517
log_frequency : bool, optional
515-
This parameter is only used if method is set to "interpolate". If set to True,
516-
(cummulative) frequency values are converted to log scale before inter- and
517-
extrapolation. Defaults to True.
518+
If set to True, (cummulative) frequency values are converted to log scale before
519+
inter- and extrapolation. Defaults to True.
518520
log_intensity : bool, optional
519-
This parameter is only used if method is set to "interpolate". If set to True,
520-
intensity values are converted to log scale before inter- and extrapolation.
521-
Defaults to True.
521+
If set to True, intensity values are converted to log scale before
522+
inter- and extrapolation. Defaults to True.
523+
bin_decimals : int, optional
524+
Number of decimals to group and bin intensity values. Binning results in smoother (and
525+
coarser) interpolation and more stable extrapolation. For more details and sensible
526+
values for bin_decimals, see Notes. If None, values are not binned. Defaults to None.
522527
523528
Returns
524529
-------
@@ -531,6 +536,23 @@ def local_exceedance_intensity(
531536
GeoDataFrame label, for reporting and plotting
532537
column_label : function
533538
Column-label-generating function, for reporting and plotting
539+
540+
See Also
541+
--------
542+
util.interpolation.preprocess_and_interpolate_ev :
543+
inter- and extrapolation method
544+
545+
Notes
546+
-------
547+
If an integer bin_decimals is given, the intensity values are binned according to their
548+
bin_decimals decimals, and their corresponding frequencies are summed. This binning leads
549+
to a smoother (and coarser) interpolation, and a more stable extrapolation. For instance,
550+
if bin_decimals=1, the two values 12.01 and 11.97 with corresponding frequencies 0.1 and
551+
0.2 are combined to a value 12.0 with frequency 0.3. The default bin_decimals=None results
552+
in not binning the values.
553+
E.g., if your intensities range from 1 to 100, you could use bin_decimals=1, if your
554+
intensities range from 1e6 to 1e9, you could use bin_decimals=-5, if your intensities
555+
range from 0.0001 to .01, you could use bin_decimals=5.
534556
"""
535557
if not min_intensity and min_intensity != 0:
536558
min_intensity = self.intensity_thres
@@ -550,23 +572,32 @@ def local_exceedance_intensity(
550572

551573
# calculate local exceedance intensity
552574
test_frequency = 1 / np.array(return_periods)
553-
exceedance_intensity = np.array(
554-
[
555-
u_interp.preprocess_and_interpolate_ev(
556-
test_frequency,
557-
None,
558-
self.frequency,
559-
self.intensity.getcol(i_centroid).toarray().flatten(),
560-
log_frequency=log_frequency,
561-
log_values=log_intensity,
562-
value_threshold=min_intensity,
563-
method=method,
564-
y_asymptotic=0.0,
565-
)
566-
for i_centroid in range(self.intensity.shape[1])
567-
]
575+
576+
exceedance_intensity = np.full(
577+
(self.intensity.shape[1], len(test_frequency)),
578+
np.nan if method == "interpolate" else 0.0,
568579
)
569580

581+
nonzero_centroids = np.where(self.intensity.getnnz(axis=0) > 0)[0]
582+
if not len(nonzero_centroids) == 0:
583+
exceedance_intensity[nonzero_centroids, :] = np.array(
584+
[
585+
u_interp.preprocess_and_interpolate_ev(
586+
test_frequency,
587+
None,
588+
self.frequency,
589+
self.intensity.getcol(i_centroid).toarray().flatten(),
590+
log_frequency=log_frequency,
591+
log_values=log_intensity,
592+
value_threshold=min_intensity,
593+
method=method,
594+
y_asymptotic=0.0,
595+
bin_decimals=bin_decimals,
596+
)
597+
for i_centroid in nonzero_centroids
598+
]
599+
)
600+
570601
# create the output GeoDataFrame
571602
gdf = gpd.GeoDataFrame(
572603
geometry=self.centroids.gdf["geometry"], crs=self.centroids.gdf.crs
@@ -576,9 +607,11 @@ def local_exceedance_intensity(
576607

577608
# create label and column_label
578609
label = f"Intensity ({self.units})"
579-
column_label = lambda column_names: [
580-
f"Return Period: {col} {return_period_unit}" for col in column_names
581-
]
610+
611+
def column_label(column_names):
612+
return [
613+
f"Return Period: {col} {return_period_unit}" for col in column_names
614+
]
582615

583616
return gdf, label, column_label
584617

@@ -609,6 +642,7 @@ def local_return_period(
609642
min_intensity=None,
610643
log_frequency=True,
611644
log_intensity=True,
645+
bin_decimals=None,
612646
):
613647
"""Compute local return periods for given hazard intensities. The default method
614648
is fitting the ordered intensitites per centroid to the corresponding cummulated
@@ -628,18 +662,24 @@ def local_return_period(
628662
intensities will be assigned NaN, and threshold intensities smaller than the Hazard
629663
object's local intensities will be assigned the smallest observed local return period.
630664
If set to "extrapolate", local return periods will be extrapolated (and interpolated).
665+
The extrapolation to large threshold intensities uses the two highest intensites of
666+
the centroid and their return periods and extends the interpolation between these
667+
points to the given threshold intensity (similar for small threshold intensites).
631668
Defaults to "interpolate".
632669
min_intensity : float, optional
633670
Minimum threshold to filter the hazard intensity. If set to None, self.intensity_thres
634671
will be used. Defaults to None.
635672
log_frequency : bool, optional
636-
This parameter is only used if method is set to "interpolate". If set to True,
637-
(cummulative) frequency values are converted to log scale before inter- and
638-
extrapolation. Defaults to True.
673+
If set to True, (cummulative) frequency values are converted to log scale before
674+
inter- and extrapolation. Defaults to True.
639675
log_intensity : bool, optional
640-
This parameter is only used if method is set to "interpolate". If set to True,
641-
intensity values are converted to log scale before inter- and extrapolation.
642-
Defaults to True.
676+
If set to True, intensity values are converted to log scale before
677+
inter- and extrapolation. Defaults to True.
678+
bin_decimals : int, optional
679+
Number of decimals to group and bin intensity values. Binning results in smoother (and
680+
coarser) interpolation and more stable extrapolation. For more details and sensible
681+
values for bin_decimals, see Notes. If None, values are not binned. Defaults to None.
682+
643683
644684
Returns
645685
-------
@@ -652,6 +692,23 @@ def local_return_period(
652692
GeoDataFrame label, for reporting and plotting
653693
column_label : function
654694
Column-label-generating function, for reporting and plotting
695+
696+
See Also
697+
--------
698+
util.interpolation.preprocess_and_interpolate_ev :
699+
inter- and extrapolation method
700+
701+
Notes
702+
-------
703+
If an integer bin_decimals is given, the intensity values are binned according to their
704+
bin_decimals decimals, and their corresponding frequencies are summed. This binning leads
705+
to a smoother (and coarser) interpolation, and a more stable extrapolation. For instance,
706+
if bin_decimals=1, the two values 12.01 and 11.97 with corresponding frequencies 0.1 and
707+
0.2 are combined to a value 12.0 with frequency 0.3. The default bin_decimals=None results
708+
in not binning the values.
709+
E.g., if your intensities range from 1 to 100, you could use bin_decimals=1, if your
710+
intensities range from 1e6 to 1e9, you could use bin_decimals=-5, if your intensities
711+
range from 0.0001 to .01, you could use bin_decimals=5.
655712
"""
656713
if not min_intensity and min_intensity != 0:
657714
min_intensity = self.intensity_thres
@@ -669,23 +726,31 @@ def local_return_period(
669726
]:
670727
raise ValueError(f"Unknown method: {method}")
671728

672-
# calculate local return periods
673-
return_periods = np.array(
674-
[
675-
u_interp.preprocess_and_interpolate_ev(
676-
None,
677-
np.array(threshold_intensities),
678-
self.frequency,
679-
self.intensity.getcol(i_centroid).toarray().flatten(),
680-
log_frequency=log_frequency,
681-
log_values=log_intensity,
682-
value_threshold=min_intensity,
683-
method=method,
684-
y_asymptotic=np.nan,
685-
)
686-
for i_centroid in range(self.intensity.shape[1])
687-
]
729+
return_periods = np.full(
730+
(self.intensity.shape[1], len(threshold_intensities)), np.nan
688731
)
732+
733+
nonzero_centroids = np.where(self.intensity.getnnz(axis=0) > 0)[0]
734+
735+
if not len(nonzero_centroids) == 0:
736+
return_periods[nonzero_centroids, :] = np.array(
737+
[
738+
u_interp.preprocess_and_interpolate_ev(
739+
None,
740+
np.array(threshold_intensities),
741+
self.frequency,
742+
self.intensity.getcol(i_centroid).toarray().flatten(),
743+
log_frequency=log_frequency,
744+
log_values=log_intensity,
745+
value_threshold=min_intensity,
746+
method=method,
747+
y_asymptotic=np.nan,
748+
bin_decimals=bin_decimals,
749+
)
750+
for i_centroid in nonzero_centroids
751+
]
752+
)
753+
689754
return_periods = safe_divide(1.0, return_periods)
690755

691756
# create the output GeoDataFrame
@@ -697,9 +762,9 @@ def local_return_period(
697762

698763
# create label and column_label
699764
label = f"Return Periods ({return_period_unit})"
700-
column_label = lambda column_names: [
701-
f"Threshold Intensity: {col} {self.units}" for col in column_names
702-
]
765+
766+
def column_label(column_names):
767+
return [f"Threshold Intensity: {col} {self.units}" for col in column_names]
703768

704769
return gdf, label, column_label
705770

climada/hazard/plot.py

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -37,62 +37,62 @@ class HazardPlot:
3737
Contains all plotting methods of the Hazard class
3838
"""
3939

40-
@deprecated(
41-
details="The use of Hazard.plot_rp_intensity is deprecated."
42-
"Use Hazard.local_exceedance_intensity and util.plot.plot_from_gdf instead."
43-
)
4440
def plot_rp_intensity(
4541
self,
4642
return_periods=(25, 50, 100, 250),
47-
smooth=True,
4843
axis=None,
49-
figsize=(9, 13),
50-
adapt_fontsize=True,
44+
kwargs_local_exceedance_intensity=None,
5145
**kwargs,
5246
):
5347
"""
54-
This function is deprecated,
55-
use Impact.local_exceedance_impact and util.plot.plot_from_gdf instead.
56-
5748
Compute and plot hazard exceedance intensity maps for different
58-
return periods. Calls local_exceedance_inten.
49+
return periods. Calls local_exceedance_intensity. For handling large data sets and for
50+
further options, see Notes.
5951
6052
Parameters
6153
----------
6254
return_periods: tuple(int), optional
6355
return periods to consider
64-
smooth: bool, optional
65-
smooth plot to plot.RESOLUTIONxplot.RESOLUTION
6656
axis: matplotlib.axes._subplots.AxesSubplot, optional
6757
axis to use
68-
figsize: tuple, optional
69-
figure size for plt.subplots
58+
kwargs_local_exceedance_intensity: dict
59+
Dictionary of keyword arguments for the method hazard.local_exceedance_intensity.
7060
kwargs: optional
7161
arguments for pcolormesh matplotlib function used in event plots
7262
7363
Returns
7464
-------
7565
axis, inten_stats: matplotlib.axes._subplots.AxesSubplot, np.ndarray
7666
intenstats is return_periods.size x num_centroids
67+
68+
See Also
69+
---------
70+
hazard.local_exceedance_intensity: method to calculate local exceedance frequencies.
71+
72+
Notes
73+
-----
74+
For handling large data, and for more fleixble options in the exceedance
75+
intensity computation and in the plotting, we recommend to use
76+
gdf, title, labels = hazard.local_exceedance_intensity() and
77+
util.plot.plot_from_gdf(gdf, title, labels) instead.
7778
"""
78-
inten_stats = self.local_exceedance_intensity(return_periods)[0].values[:, 1:].T
79-
inten_stats = inten_stats.astype(float)
80-
colbar_name = "Intensity (" + self.units + ")"
81-
title = list()
82-
for ret in return_periods:
83-
title.append("Return period: " + str(ret) + " years")
84-
axis = u_plot.geo_im_from_array(
85-
inten_stats,
86-
self.centroids.coord,
87-
colbar_name,
88-
title,
89-
smooth=smooth,
90-
axes=axis,
91-
figsize=figsize,
92-
adapt_fontsize=adapt_fontsize,
93-
**kwargs,
79+
LOGGER.info(
80+
"Some errors in the previous calculation of local exceedance intensities have been corrected,"
81+
" see Hazard.local_exceedance_intensity. To reproduce data with the "
82+
"previous calculation, use CLIMADA v5.0.0 or less."
83+
)
84+
85+
if kwargs_local_exceedance_intensity is None:
86+
kwargs_local_exceedance_intensity = {}
87+
88+
inten_stats, title, column_labels = self.local_exceedance_intensity(
89+
return_periods, **kwargs_local_exceedance_intensity
90+
)
91+
92+
axis = u_plot.plot_from_gdf(
93+
inten_stats, title, column_labels, axis=axis, **kwargs
9494
)
95-
return axis, inten_stats
95+
return axis, inten_stats.values[:, 1:].T.astype(float)
9696

9797
def plot_intensity(
9898
self,

0 commit comments

Comments
 (0)