Skip to content

Commit 7a646c8

Browse files
chahankChahan Kropfpeanutfunemanuel-schmid
authored
Feature/change impact total value (#702)
* Add first try affected value with threshold * Remove test typos * Update test affected value * Fix affected value to correct results * Ignore centroids -1 and exposure values <=0 * Improve threshold affected value Co-authored-by: Lukas Riedel <[email protected]> * Deprecate Impact.tot_value attribute * Replace attribute with getter/setter and declare private attribute. * Adjust writers accordningly * Code line cosmetics * Add private function for legacy during deprecation period * Update climada/entity/exposures/base.py * Update climada/engine/impact.py * Update climada/entity/exposures/base.py * Format new code and improve docstrings * Update CHANGELOG.md * Update changelog for temporary method. * Clarify docstring. --------- Co-authored-by: Chahan Kropf <[email protected]> Co-authored-by: Lukas Riedel <[email protected]> Co-authored-by: emanuel-schmid <[email protected]>
1 parent 0548d95 commit 7a646c8

File tree

4 files changed

+100
-29
lines changed

4 files changed

+100
-29
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Removed:
2222
- `Impact.match_centroids` convenience method for matching (hazard) centroids to impact objects [#602](https://github.com/CLIMADA-project/climada_python/pull/602)
2323
- `climada.util.coordinates.match_centroids` method for matching (hazard) centroids to GeoDataFrames [#602](https://github.com/CLIMADA-project/climada_python/pull/602)
2424
- 'Extra' requirements `doc`, `test`, and `dev` for Python package [#712](https://github.com/CLIMADA-project/climada_python/pull/712)
25+
- Added method `Exposures.centroids_total_value` to replace the functionality of `Exposures.affected_total_value`. This method is temporary and deprecated. [#702](https://github.com/CLIMADA-project/climada_python/pull/702)
26+
2527

2628
### Changed
2729

@@ -36,6 +38,7 @@ Removed:
3638
- Tests with long runtime were moved to integration tests in `climada/test` [#709](https://github.com/CLIMADA-project/climada_python/pull/709)
3739
- Use `myst-nb` for parsing Jupyter Notebooks for the documentation instead of `nbsphinx` [#712](https://github.com/CLIMADA-project/climada_python/pull/712)
3840
- Installation guide now recommends installing CLIMADA directly via `conda install` [#714](https://github.com/CLIMADA-project/climada_python/pull/714)
41+
- `Exposures.affected_total_value` now takes a hazard intensity threshold as argument. Affected values are only those for which at least one event exceeds the threshold. (previously, all exposures points with an assigned centroid were considered affected) [#702](https://github.com/CLIMADA-project/climada_python/pull/702)
3942

4043
### Fixed
4144

@@ -44,6 +47,7 @@ Removed:
4447
### Deprecated
4548

4649
- `Centroids.from_geodataframe` and `Centroids.from_pix_bounds` [#721](https://github.com/CLIMADA-project/climada_python/pull/721)
50+
- `Impact.tot_value`: Use `Exposures.affected_total_value` to compute the total value affected by a hazard intensity above a custom threshold [#702](https://github.com/CLIMADA-project/climada_python/pull/702)
4751

4852
### Removed
4953

climada/engine/impact.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,6 @@ class Impact():
8080
frequency of event
8181
frequency_unit : str
8282
frequency unit used (given by hazard), default is '1/year'
83-
tot_value : float
84-
total exposure value affected
8583
aai_agg : float
8684
average impact within a period of 1/frequency_unit (aggregated)
8785
unit : str
@@ -154,7 +152,7 @@ def __init__(self,
154152
self.at_event = np.array([], float) if at_event is None else at_event
155153
self.frequency = np.array([],float) if frequency is None else frequency
156154
self.frequency_unit = frequency_unit
157-
self.tot_value = tot_value
155+
self._tot_value = tot_value
158156
self.aai_agg = aai_agg
159157
self.unit = unit
160158

@@ -246,7 +244,7 @@ def from_eih(cls, exposures, impfset, hazard,
246244
axis=1),
247245
crs = exposures.crs,
248246
unit = exposures.value_unit,
249-
tot_value = exposures.affected_total_value(hazard),
247+
tot_value = exposures.centroids_total_value(hazard),
250248
eai_exp = eai_exp,
251249
at_event = at_event,
252250
aai_agg = aai_agg,
@@ -257,6 +255,29 @@ def from_eih(cls, exposures, impfset, hazard,
257255
}
258256
)
259257

258+
@property
259+
def tot_value(self):
260+
"""Return the total exposure value close to a hazard
261+
262+
.. deprecated:: 3.3
263+
Use :py:meth:`climada.entity.exposures.base.Exposures.affected_total_value`
264+
instead.
265+
"""
266+
LOGGER.warning("The Impact.tot_value attribute is deprecated."
267+
"Use Exposures.affected_total_value to calculate the affected "
268+
"total exposure value based on a specific hazard intensity "
269+
"threshold")
270+
return self._tot_value
271+
272+
@tot_value.setter
273+
def tot_value(self, value):
274+
"""Set the total exposure value close to a hazard"""
275+
LOGGER.warning("The Impact.tot_value attribute is deprecated."
276+
"Use Exposures.affected_total_value to calculate the affected "
277+
"total exposure value based on a specific hazard intensity "
278+
"threshold")
279+
self._tot_value = value
280+
260281
def transfer_risk(self, attachment, cover):
261282
"""Compute the risk transfer for the full portfolio. This is the risk
262283
of the full portfolio summed over all events. For each
@@ -860,7 +881,7 @@ def write_csv(self, file_name):
860881
[self.tag['haz'].description]],
861882
[[self.tag['exp'].file_name], [self.tag['exp'].description]],
862883
[[self.tag['impf_set'].file_name], [self.tag['impf_set'].description]],
863-
[self.unit], [self.tot_value], [self.aai_agg],
884+
[self.unit], [self._tot_value], [self.aai_agg],
864885
self.event_id, self.event_name, self.date,
865886
self.frequency, [self.frequency_unit], self.at_event,
866887
self.eai_exp, self.coord_exp[:, 0], self.coord_exp[:, 1],
@@ -901,7 +922,7 @@ def write_col(i_col, imp_ws, xls_data):
901922
data = [str(self.tag['impf_set'].file_name), str(self.tag['impf_set'].description)]
902923
write_col(2, imp_ws, data)
903924
write_col(3, imp_ws, [self.unit])
904-
write_col(4, imp_ws, [self.tot_value])
925+
write_col(4, imp_ws, [self._tot_value])
905926
write_col(5, imp_ws, [self.aai_agg])
906927
write_col(6, imp_ws, self.event_id)
907928
write_col(7, imp_ws, self.event_name)
@@ -1030,8 +1051,9 @@ def write_csr(group, name, value):
10301051
with h5py.File(file_path, "w") as file:
10311052

10321053
# Now write all attributes
1054+
# NOTE: Remove leading underscore to write '_tot_value' as regular attribute
10331055
for name, value in self.__dict__.items():
1034-
write(file, name, value)
1056+
write(file, name.lstrip("_"), value)
10351057

10361058
def write_sparse_csr(self, file_name):
10371059
"""Write imp_mat matrix in numpy's npz format."""

climada/entity/exposures/base.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import contextily as ctx
3737
import cartopy.crs as ccrs
3838

39+
from climada.hazard import Hazard
3940
from climada.entity.tag import Tag
4041
import climada.util.hdf5_handler as u_hdf5
4142
from climada.util.constants import ONE_LAT_KM, DEF_CRS, CMAP_RASTER
@@ -1019,11 +1020,15 @@ def concat(exposures_list):
10191020

10201021
return exp
10211022

1022-
def affected_total_value(self, hazard):
1023-
"""
1024-
Total value of the exposures that are close enough to be affected
1025-
by the hazard (sum of value of all exposures points for which
1026-
a centroids is assigned)
1023+
def centroids_total_value(self, hazard):
1024+
"""Compute value of exposures close enough to be affected by hazard
1025+
1026+
.. deprecated:: 3.3
1027+
This method will be removed in a future version. Use
1028+
:py:meth:`affected_total_value` instead.
1029+
1030+
This method computes the sum of the value of all exposures points for which a
1031+
Hazard centroid is assigned.
10271032
10281033
Parameters
10291034
----------
@@ -1043,6 +1048,39 @@ def affected_total_value(self, hazard):
10431048
)
10441049
return np.sum(self.gdf.value.values[nz_mask])
10451050

1051+
def affected_total_value(self, hazard: Hazard, threshold_affected: float = 0):
1052+
"""
1053+
Total value of the exposures that are affected by at least
1054+
one hazard event (sum of value of all exposures points for which
1055+
at least one event has intensity larger than the threshold).
1056+
1057+
Parameters
1058+
----------
1059+
hazard : Hazard
1060+
Hazard affecting Exposures
1061+
threshold_affected : int or float
1062+
Hazard intensity threshold above which an exposures is
1063+
considere affected.
1064+
1065+
Returns
1066+
-------
1067+
float
1068+
Sum of value of all exposures points for which
1069+
a centroids is assigned and that have at least one
1070+
event intensity above threshold.
1071+
1072+
"""
1073+
assigned_centroids = self.gdf[hazard.centr_exp_col]
1074+
nz_mask = (self.gdf.value.values > 0) & (assigned_centroids.values >= 0)
1075+
cents = np.unique(assigned_centroids[nz_mask])
1076+
cent_with_inten_above_thres = (
1077+
hazard.intensity[:, cents].max(axis=0) > threshold_affected
1078+
).nonzero()[1]
1079+
above_thres_mask = np.isin(
1080+
self.gdf[hazard.centr_exp_col].values, cents[cent_with_inten_above_thres]
1081+
)
1082+
return np.sum(self.gdf.value.values[above_thres_mask])
1083+
10461084

10471085
def add_sea(exposures, sea_res, scheduler=None):
10481086
"""Add sea to geometry's surroundings with given resolution. region_id

climada/entity/exposures/test/test_base.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from sklearn.metrics import DistanceMetric
2626
import rasterio
2727
from rasterio.windows import Window
28+
import scipy as sp
2829

2930
from climada import CONFIG
3031
from climada.entity.exposures.base import Exposures, INDICATOR_IMPF, \
@@ -185,23 +186,29 @@ def test_assign_large_hazard_subset_pass(self):
185186
np.testing.assert_array_equal(assigned_centroids.lon, exp.gdf.longitude)
186187

187188
def test_affected_total_value(self):
188-
exp = Exposures.from_raster(HAZ_DEMO_FL, window=Window(25, 90, 10, 5))
189-
haz = Hazard.from_raster([HAZ_DEMO_FL], haz_type='FL', window=Window(25, 90, 10, 5))
190-
exp.assign_centroids(haz)
191-
tot_val = exp.affected_total_value(haz)
192-
self.assertEqual(tot_val, np.sum(exp.gdf.value))
193-
new_centr = exp.gdf.centr_FL
194-
new_centr[6] = -1
195-
exp.gdf.centr_FL = new_centr
196-
tot_val = exp.affected_total_value(haz)
197-
self.assertAlmostEqual(tot_val, np.sum(exp.gdf.value) - exp.gdf.value[6], places=4)
198-
new_vals = exp.gdf.value
199-
new_vals[7] = 0
200-
exp.gdf.value = new_vals
201-
tot_val = exp.affected_total_value(haz)
202-
self.assertAlmostEqual(tot_val, np.sum(exp.gdf.value) - exp.gdf.value[6], places=4)
203-
exp.gdf.centr_FL = -1
204-
tot_val = exp.affected_total_value(haz)
189+
haz_type = "RF"
190+
gdf = gpd.GeoDataFrame(
191+
{
192+
"value": [1, 2, 3, 4, 5, 6],
193+
"latitude": [1, 2, 3, 4, 5, 6],
194+
"longitude": [-1, -2, -3, -4, -5, -6],
195+
"centr_" + haz_type: [0, 2, 2, 3, -1, 4],
196+
}
197+
)
198+
exp = Exposures(gdf, crs=4326)
199+
intensity = sp.sparse.csr_matrix(np.array([[0, 0, 1, 10, 2], [-1, 0, 0, 1, 2]]))
200+
cent = Centroids(lat=np.array([1, 2, 3, 4]), lon=np.array([1, 2, 3, 4]))
201+
haz = Hazard(
202+
haz_type=haz_type, centroids=cent, intensity=intensity, event_id=[1, 2]
203+
)
204+
205+
tot_val = exp.affected_total_value(haz, threshold_affected=0)
206+
self.assertEqual(tot_val, np.sum(exp.gdf.value[[1, 2, 3, 5]]))
207+
tot_val = exp.affected_total_value(haz, threshold_affected=3)
208+
self.assertEqual(tot_val, np.sum(exp.gdf.value[[3]]))
209+
tot_val = exp.affected_total_value(haz, threshold_affected=-2)
210+
self.assertEqual(tot_val, np.sum(exp.gdf.value[[0, 1, 2, 3, 5]]))
211+
tot_val = exp.affected_total_value(haz, threshold_affected=11)
205212
self.assertEqual(tot_val, 0)
206213

207214
class TestChecker(unittest.TestCase):

0 commit comments

Comments
 (0)