Skip to content

Commit d195226

Browse files
Merge pull request #918 from CLIMADA-project/feature/restructure_fitfuncs_exceedance
Combining several interpolation functions using a new util function
2 parents 242f1f3 + 4d29cbf commit d195226

File tree

13 files changed

+988
-436
lines changed

13 files changed

+988
-436
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Code freeze date: YYYY-MM-DD
1212

1313
### Added
1414

15+
- `Hazard.local_exceedance_intensity`, `Hazard.local_return_period` and `Impact.local_exceedance_impact`, that all use the `climada.util.interpolation` module [#918](https://github.com/CLIMADA-project/climada_python/pull/918)
1516
- `climada.util.interpolation` module for inter- and extrapolation util functions used in local exceedance intensity and return period functions [#930](https://github.com/CLIMADA-project/climada_python/pull/930)
1617
- `climada.exposures.exposures.Exposures.geometry` property
1718
- `climada.exposures.exposures.Exposures.latitude` property
@@ -28,6 +29,7 @@ Code freeze date: YYYY-MM-DD
2829
- Improved scaling factors implemented in `climada.hazard.trop_cyclone.apply_climate_scenario_knu` to model the impact of climate changes to tropical cyclones [#734](https://github.com/CLIMADA-project/climada_python/pull/734)
2930
- In `climada.util.plot.geo_im_from_array`, NaNs are plotted in gray while cells with no centroid are not plotted [#929](https://github.com/CLIMADA-project/climada_python/pull/929)
3031
- Renamed `climada.util.plot.subplots_from_gdf` to `climada.util.plot.plot_from_gdf` [#929](https://github.com/CLIMADA-project/climada_python/pull/929)
32+
- `Hazard.local_exceedance_inten`, `Hazard.local_return_period`, and `Impact.local_exceedance_imp` call the corresponding new functions and a deprecation warning is added [#918](https://github.com/CLIMADA-project/climada_python/pull/918). Some inconsistencies in the previous versions are removed and the default method is changed. To reconstruct results from the previous versions, use CLIMADA v5.0.0 or less.
3133
- Exposures complete overhaul. Notably
3234
- the _geometry_ column of the inherent `GeoDataFrame` is set up at initialization
3335
- latitude and longitude column are no longer present there (the according arrays can be retrieved as properties of the Exposures object: `exp.latitude` instead of `exp.gdf.latitude.values`).
@@ -44,6 +46,10 @@ Code freeze date: YYYY-MM-DD
4446
- `climada.entity.exposures.Exposures.meta` attribute
4547
- `climada.entity.exposures.Exposures.set_lat_lon` method
4648
- `climada.entity.exposures.Exposures.set_geometry_points` method
49+
- `climada.hazard.Hazard.local_exceedance_inten` method
50+
- `climada.hazard.Hazard.plot_rp_intensity` method
51+
- `climada.engine.impact.Impact.local_exceedance_imp` method
52+
- `climada.engine.impact.Impact.plot_rp_imp` method
4753

4854
### Removed
4955

climada/engine/impact.py

Lines changed: 119 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,22 @@
3333
from typing import Any, Iterable, Union
3434

3535
import contextily as ctx
36+
import geopandas as gpd
3637
import h5py
3738
import matplotlib.animation as animation
3839
import matplotlib.pyplot as plt
3940
import numpy as np
4041
import pandas as pd
4142
import xlsxwriter
43+
from deprecation import deprecated
4244
from pyproj import CRS as pyprojCRS
4345
from rasterio.crs import CRS as rasterioCRS # pylint: disable=no-name-in-module
4446
from scipy import sparse
4547
from tqdm import tqdm
4648

4749
import climada.util.coordinates as u_coord
4850
import climada.util.dates_times as u_dt
51+
import climada.util.interpolation as u_interp
4952
import climada.util.plot as u_plot
5053
from climada import CONFIG
5154
from climada.entity import Exposures
@@ -486,20 +489,54 @@ def calc_impact_year_set(self, all_years=True, year_range=None):
486489
)
487490
return self.impact_per_year(all_years=all_years, year_range=year_range)
488491

489-
# TODO: rewrite and deprecate method
490-
def local_exceedance_imp(self, return_periods=(25, 50, 100, 250)):
491-
"""Compute exceedance impact map for given return periods.
492-
Requires attribute imp_mat.
492+
def local_exceedance_impact(
493+
self,
494+
return_periods=(25, 50, 100, 250),
495+
method="interpolate",
496+
min_impact=0,
497+
log_frequency=True,
498+
log_impact=True,
499+
):
500+
"""Compute local exceedance impact for given return periods. The default method
501+
is fitting the ordered impacts per centroid to the corresponding cummulated
502+
frequency with by linear interpolation on log-log scale.
493503
494504
Parameters
495505
----------
496-
return_periods : Any, optional
497-
return periods to consider
498-
Dafault is (25, 50, 100, 250)
506+
return_periods : array_like
507+
User-specified return periods for which the exceedance intensity should be calculated
508+
locally (at each centroid). Defaults to (25, 50, 100, 250).
509+
method : str
510+
Method to interpolate to new return periods. Currently available are "interpolate",
511+
"extrapolate", "extrapolate_constant" and "stepfunction". If set to "interpolate",
512+
return periods outside the range of the Impact object's observed local return periods
513+
will be assigned NaN. If set to "extrapolate_constant" or "stepfunction",
514+
return periods larger than the Impact object's observed local return periods will be
515+
assigned the largest local impact, and return periods smaller than the Impact object's
516+
observed local return periods will be assigned 0. If set to "extrapolate", local
517+
exceedance impacts will be extrapolated (and interpolated). Defauls to "interpolate".
518+
min_impact : float, optional
519+
Minimum threshold to filter the impact. Defaults to 0.
520+
log_frequency : bool, optional
521+
This parameter is only used if method is set to "extrapolate" or "interpolate". If set
522+
to True, (cummulative) frequency values are converted to log scale before inter- and
523+
extrapolation. Defaults to True.
524+
log_impact : bool, optional
525+
This parameter is only used if method is set to "extrapolate" or "interpolate". If set
526+
to True, impact values are converted to log scale before inter- and extrapolation.
527+
Defaults to True.
499528
500529
Returns
501530
-------
502-
np.array
531+
gdf : gpd.GeoDataFrame
532+
GeoDataFrame containing exeedance impacts for given return periods. Each column
533+
corresponds to a return period, each row corresponds to a centroid. Values
534+
in the gdf correspond to the exceedance impact for the given centroid and
535+
return period
536+
label : str
537+
GeoDataFrame label, for reporting and plotting
538+
column_label : function
539+
Column-label-generating function, for reporting and plotting
503540
"""
504541
LOGGER.info(
505542
"Computing exceedance impact map for return periods: %s", return_periods
@@ -509,29 +546,69 @@ def local_exceedance_imp(self, return_periods=(25, 50, 100, 250)):
509546
"Attribute imp_mat is empty. Recalculate Impact"
510547
"instance with parameter save_mat=True"
511548
)
512-
num_cen = self.imp_mat.shape[1]
513-
imp_stats = np.zeros((len(return_periods), num_cen))
514-
cen_step = CONFIG.max_matrix_size.int() // self.imp_mat.shape[0]
515-
if not cen_step:
516-
raise ValueError(
517-
"Increase max_matrix_size configuration parameter to > "
518-
f"{self.imp_mat.shape[0]}"
519-
)
520-
# separte in chunks
521-
chk = -1
522-
for chk in range(int(num_cen / cen_step)):
523-
self._loc_return_imp(
524-
np.array(return_periods),
525-
self.imp_mat[:, chk * cen_step : (chk + 1) * cen_step].toarray(),
526-
imp_stats[:, chk * cen_step : (chk + 1) * cen_step],
527-
)
528-
self._loc_return_imp(
529-
np.array(return_periods),
530-
self.imp_mat[:, (chk + 1) * cen_step :].toarray(),
531-
imp_stats[:, (chk + 1) * cen_step :],
549+
550+
# check frequency unit
551+
return_period_unit = u_dt.convert_frequency_unit_to_time_unit(
552+
self.frequency_unit
532553
)
533554

534-
return imp_stats
555+
# check method
556+
if method not in [
557+
"interpolate",
558+
"extrapolate",
559+
"extrapolate_constant",
560+
"stepfunction",
561+
]:
562+
raise ValueError(f"Unknown method: {method}")
563+
564+
# calculate local exceedance impact
565+
test_frequency = 1 / np.array(return_periods)
566+
exceedance_impact = np.array(
567+
[
568+
u_interp.preprocess_and_interpolate_ev(
569+
test_frequency,
570+
None,
571+
self.frequency,
572+
self.imp_mat.getcol(i_centroid).toarray().flatten(),
573+
log_frequency=log_frequency,
574+
log_values=log_impact,
575+
value_threshold=min_impact,
576+
method=method,
577+
y_asymptotic=0.0,
578+
)
579+
for i_centroid in range(self.imp_mat.shape[1])
580+
]
581+
)
582+
583+
# create the output GeoDataFrame
584+
gdf = gpd.GeoDataFrame(
585+
geometry=gpd.points_from_xy(self.coord_exp[:, 1], self.coord_exp[:, 0]),
586+
crs=self.crs,
587+
)
588+
col_names = [f"{ret_per}" for ret_per in return_periods]
589+
gdf[col_names] = exceedance_impact
590+
# create label and column_label
591+
label = f"Impact ({self.unit})"
592+
column_label = lambda column_names: [
593+
f"Return Period: {col} {return_period_unit}" for col in column_names
594+
]
595+
596+
return gdf, label, column_label
597+
598+
@deprecated(
599+
details="The use of Impact.local_exceedance_imp is deprecated. Use "
600+
"Impact.local_exceedance_impact instead. Some errors in the previous calculation "
601+
"in Impact.local_exceedance_imp have been corrected. To reproduce data with the "
602+
"previous calculation, use CLIMADA v5.0.0 or less."
603+
)
604+
def local_exceedance_imp(self, return_periods=(25, 50, 100, 250)):
605+
"""This function is deprecated, use Impact.local_exceedance_impact instead."""
606+
607+
return (
608+
self.local_exceedance_impact(return_periods)[0]
609+
.values[:, 1:]
610+
.T.astype(float)
611+
)
535612

536613
def calc_freq_curve(self, return_per=None):
537614
"""Compute impact exceedance frequency curve.
@@ -924,6 +1001,10 @@ def plot_basemap_impact_exposure(
9241001

9251002
return axis
9261003

1004+
@deprecated(
1005+
details="The use of Impact.plot_rp_imp is deprecated."
1006+
"Use Impact.local_exceedance_impact and util.plot.plot_from_gdf instead."
1007+
)
9271008
def plot_rp_imp(
9281009
self,
9291010
return_periods=(25, 50, 100, 250),
@@ -932,7 +1013,11 @@ def plot_rp_imp(
9321013
axis=None,
9331014
**kwargs,
9341015
):
935-
"""Compute and plot exceedance impact maps for different return periods.
1016+
"""
1017+
This function is deprecated, use Impact.local_exceedance_impact and
1018+
util.plot.plot_from_gdf instead.
1019+
1020+
Compute and plot exceedance impact maps for different return periods.
9361021
Calls local_exceedance_imp.
9371022
9381023
Parameters
@@ -953,7 +1038,10 @@ def plot_rp_imp(
9531038
imp_stats : np.array
9541039
return_periods.size x num_centroids
9551040
"""
956-
imp_stats = self.local_exceedance_imp(np.array(return_periods))
1041+
imp_stats = (
1042+
self.local_exceedance_impact(np.array(return_periods))[0].values[:, 1:].T
1043+
)
1044+
imp_stats = imp_stats.astype(float)
9571045
if imp_stats.size == 0:
9581046
raise ValueError(
9591047
"Error: Attribute imp_mat is empty. Recalculate Impact"
@@ -1593,36 +1681,6 @@ def run(i_time):
15931681

15941682
return imp_list
15951683

1596-
# TODO: rewrite and deprecate method
1597-
def _loc_return_imp(self, return_periods, imp, exc_imp):
1598-
"""Compute local exceedence impact for given return period.
1599-
1600-
Parameters
1601-
----------
1602-
return_periods : np.array
1603-
return periods to consider
1604-
cen_pos :int
1605-
centroid position
1606-
1607-
Returns
1608-
-------
1609-
np.array
1610-
"""
1611-
# sorted impacts
1612-
sort_pos = np.argsort(imp, axis=0)[::-1, :]
1613-
columns = np.ones(imp.shape, int)
1614-
# pylint: disable=unsubscriptable-object # pylint/issues/3139
1615-
columns *= np.arange(columns.shape[1])
1616-
imp_sort = imp[sort_pos, columns]
1617-
# cummulative frequency at sorted intensity
1618-
freq_sort = self.frequency[sort_pos]
1619-
np.cumsum(freq_sort, axis=0, out=freq_sort)
1620-
1621-
for cen_idx in range(imp.shape[1]):
1622-
exc_imp[:, cen_idx] = self._cen_return_imp(
1623-
imp_sort[:, cen_idx], freq_sort[:, cen_idx], 0, return_periods
1624-
)
1625-
16261684
def _build_exp(self):
16271685
return Exposures(
16281686
data={
@@ -1657,43 +1715,6 @@ def _build_exp_event(self, event_id):
16571715
meta=None,
16581716
)
16591717

1660-
@staticmethod
1661-
def _cen_return_imp(imp, freq, imp_th, return_periods):
1662-
"""From ordered impact and cummulative frequency at centroid, get
1663-
exceedance impact at input return periods.
1664-
1665-
Parameters
1666-
----------
1667-
imp : np.array
1668-
sorted impact at centroid
1669-
freq : np.array
1670-
cummulative frequency at centroid
1671-
imp_th : float
1672-
impact threshold
1673-
return_periods : np.array
1674-
return periods
1675-
1676-
Returns
1677-
-------
1678-
np.array
1679-
"""
1680-
imp_th = np.asarray(imp > imp_th).squeeze()
1681-
imp_cen = imp[imp_th]
1682-
freq_cen = freq[imp_th]
1683-
if not imp_cen.size:
1684-
return np.zeros((return_periods.size,))
1685-
try:
1686-
with warnings.catch_warnings():
1687-
warnings.simplefilter("ignore")
1688-
pol_coef = np.polyfit(np.log(freq_cen), imp_cen, deg=1)
1689-
except ValueError:
1690-
pol_coef = np.polyfit(np.log(freq_cen), imp_cen, deg=0)
1691-
imp_fit = np.polyval(pol_coef, np.log(1 / return_periods))
1692-
wrong_inten = (return_periods > np.max(1 / freq_cen)) & np.isnan(imp_fit)
1693-
imp_fit[wrong_inten] = 0.0
1694-
1695-
return imp_fit
1696-
16971718
def select(
16981719
self,
16991720
event_ids=None,

0 commit comments

Comments
 (0)