Skip to content

Commit 769125b

Browse files
committed
Merge branch 'feature/risk_trajectory' into feature/cb_refactoring
2 parents 8413f35 + 02244cb commit 769125b

File tree

4 files changed

+772
-48
lines changed

4 files changed

+772
-48
lines changed

climada/trajectories/risk_trajectory.py

Lines changed: 153 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
1717
---
1818
19+
This file implements risk trajectory objects, to allow a better evaluation
20+
of risk in between two points in time (snapshots).
21+
1922
"""
2023

2124
import datetime
@@ -106,6 +109,13 @@ def _reset_metrics(self):
106109

107110
@property
108111
def default_rp(self):
112+
"""The default return period values to use when computing risk period metrics.
113+
114+
Notes
115+
-----
116+
117+
Changing its value resets the corresponding metric.
118+
"""
109119
return self._default_rp
110120

111121
@default_rp.setter
@@ -120,7 +130,14 @@ def default_rp(self, value):
120130

121131
@property
122132
def risk_transf_cover(self):
123-
"""The risk transfer coverage."""
133+
"""The risk transfer coverage.
134+
135+
Notes
136+
-----
137+
138+
Changing its value resets the risk metrics.
139+
"""
140+
124141
return self._risk_transf_cover
125142

126143
@risk_transf_cover.setter
@@ -131,7 +148,14 @@ def risk_transf_cover(self, value):
131148

132149
@property
133150
def risk_transf_attach(self):
134-
"""The risk transfer attachment."""
151+
"""The risk transfer attachment.
152+
153+
Notes
154+
-----
155+
156+
Changing its value resets the risk metrics.
157+
"""
158+
135159
return self._risk_transf_attach
136160

137161
@risk_transf_attach.setter
@@ -190,11 +214,27 @@ def pairwise(container: list):
190214
]
191215

192216
@classmethod
193-
def npv_transform(cls, df: pd.DataFrame, risk_disc) -> pd.DataFrame:
217+
def npv_transform(cls, df: pd.DataFrame, risk_disc: DiscRates) -> pd.DataFrame:
218+
"""Apply discount rate to a metric `DataFrame`.
219+
220+
Parameters
221+
----------
222+
df : pd.DataFrame
223+
The `DataFrame` of the metric to discount.
224+
risk_disc : DiscRate
225+
The discount rate to apply.
226+
227+
Returns
228+
-------
229+
pd.DataFrame
230+
The discounted risk metric.
231+
232+
233+
"""
234+
194235
def _npv_group(group, disc):
195236
start_date = group.index.get_level_values("date").min()
196-
end_date = group.index.get_level_values("date").max()
197-
return calc_npv_cash_flows(group, start_date, end_date, disc)
237+
return calc_npv_cash_flows(group, start_date, disc)
198238

199239
df = df.set_index("date")
200240
grouper = cls._grouper
@@ -234,7 +274,11 @@ def _generic_metrics(
234274
tmp.append(getattr(calc_period, metric_meth)(**kwargs))
235275

236276
tmp = pd.concat(tmp)
237-
tmp.drop_duplicates(inplace=True)
277+
tmp = tmp.set_index(["date", "group", "measure", "metric"])
278+
tmp = tmp[
279+
~tmp.index.duplicated(keep="last")
280+
] # We want to avoid overlap when more than 2 snapshots
281+
tmp = tmp.reset_index()
238282
tmp["group"] = tmp["group"].fillna(self._all_groups_name)
239283
columns_to_front = ["group", "date", "measure", "metric"]
240284
tmp = tmp[
@@ -272,11 +316,39 @@ def _compute_metrics(
272316
return df
273317

274318
def eai_metrics(self, npv: bool = True, **kwargs):
319+
"""Return the estimatated annual impacts at each exposure point for each date.
320+
321+
This method computes and return a `GeoDataFrame` with eai metric
322+
(for each exposure point) for each date.
323+
324+
Parameters
325+
----------
326+
npv : bool
327+
Whether to apply the (risk) discount rate if it is defined.
328+
Defaults to `True`.
329+
330+
Notes
331+
-----
332+
333+
This computation may become quite expensive for big areas with high resolution.
334+
335+
"""
275336
return self._compute_metrics(
276337
npv=npv, metric_name="eai", metric_meth="calc_eai_gdf", **kwargs
277338
)
278339

279340
def aai_metrics(self, npv: bool = True, **kwargs):
341+
"""Return the average annual impacts for each date.
342+
343+
This method computes and return a `DataFrame` with aai metric for each date.
344+
345+
Parameters
346+
----------
347+
npv : bool
348+
Whether to apply the (risk) discount rate if it is defined.
349+
Defaults to `True`.
350+
"""
351+
280352
return self._compute_metrics(
281353
npv=npv, metric_name="aai", metric_meth="calc_aai_metric", **kwargs
282354
)
@@ -291,6 +363,18 @@ def return_periods_metrics(self, return_periods, npv: bool = True, **kwargs):
291363
)
292364

293365
def aai_per_group_metrics(self, npv: bool = True, **kwargs):
366+
"""Return the average annual impacts for each exposure group ID.
367+
368+
This method computes and return a `DataFrame` with aai metric for each
369+
of the exposure group defined by a group id, for each date.
370+
371+
Parameters
372+
----------
373+
npv : bool
374+
Whether to apply the (risk) discount rate if it is defined.
375+
Defaults to `True`.
376+
"""
377+
294378
return self._compute_metrics(
295379
npv=npv,
296380
metric_name="aai_per_group",
@@ -299,6 +383,24 @@ def aai_per_group_metrics(self, npv: bool = True, **kwargs):
299383
)
300384

301385
def risk_components_metrics(self, npv: bool = True, **kwargs):
386+
"""Return the "components" of change in future risk (Exposure and Hazard)
387+
388+
This method returns the components of the change in risk at each date:
389+
390+
- The base risk, i.e., the risk without change in hazard or exposure, compared to trajectory earliest date.
391+
- The "delta from exposure", i.e., the additional risks that come with change in exposure
392+
- The "delta from hazard", i.e., the additional risks that come with change in hazard
393+
394+
Due to how computations are being done the "delta from exposure" corresponds to the change of risk due to change in exposure while hazard remains constant to "baseline hazard", while "delta from hazard" corresponds to the change of risk due to change in hazard, while exposure remains constant to **future** exposure.
395+
396+
Parameters
397+
----------
398+
npv : bool
399+
Whether to apply the (risk) discount rate if it is defined.
400+
Defaults to `True`.
401+
402+
"""
403+
302404
return self._compute_metrics(
303405
npv=npv,
304406
metric_name="risk_components",
@@ -312,6 +414,30 @@ def per_date_risk_metrics(
312414
return_periods: list[int] | None = None,
313415
npv: bool = True,
314416
) -> pd.DataFrame | pd.Series:
417+
"""Returns a DataFrame of risk metrics for each dates
418+
419+
This methods collects (and if needed computes) the `metrics`
420+
(Defaulting to "aai", "return_periods" and "aai_per_group").
421+
422+
Parameters
423+
----------
424+
metrics : list[str], optional
425+
The list of metrics to return (defaults to
426+
["aai","return_periods","aai_per_group"])
427+
return_periods : list[int], optional
428+
The return periods to consider for the return periods metric
429+
(default to the value of the `.default_rp` attribute)
430+
npv : bool
431+
Whether to apply the (risk) discount rate if it was defined
432+
when instantiating the trajectory. Defaults to `True`.
433+
434+
Returns
435+
-------
436+
pd.DataFrame | pd.Series
437+
A tidy DataFrame with metrics value for all possible dates.
438+
439+
"""
440+
315441
metrics_df = []
316442
metrics = (
317443
["aai", "return_periods", "aai_per_group"] if metrics is None else metrics
@@ -578,20 +704,35 @@ def plot_waterfall(
578704
def calc_npv_cash_flows(
579705
cash_flows: pd.DataFrame,
580706
start_date: datetime.date,
581-
end_date: datetime.date | None = None,
582707
disc: DiscRates | None = None,
583708
):
584-
# If no discount rates are provided, return the cash flows as is
709+
"""Apply discount rate to cash flows
710+
711+
If it is defined, applies a discount rate `disc` to a given cash flow
712+
`cash_flows` assuming present year corresponds to `start_date`.
713+
714+
Parameters
715+
----------
716+
cash_flows : pd.DataFrame
717+
The cash flow to apply the discount rate to
718+
start_date : datetime.date
719+
The date representing the present
720+
end_date : datetime.date, optional
721+
disc : DiscRates, optional
722+
The discount rate to apply
723+
724+
Returns
725+
-------
726+
727+
A dataframe (copy) of `cash_flows` where values are discounted according to `disc`
728+
"""
729+
585730
if not disc:
586731
return cash_flows
587732

588733
if not isinstance(cash_flows.index, pd.DatetimeIndex):
589734
raise ValueError("cash_flows must be a pandas Series with a datetime index")
590735

591-
# Determine the end date if not provided
592-
if end_date is None:
593-
end_date = cash_flows.index[-1]
594-
595736
df = cash_flows.to_frame(name="cash_flow")
596737
df["year"] = df.index.year
597738

climada/trajectories/riskperiod.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@
1616
1717
---
1818
19-
This modules implements the Snapshot and SnapshotsCollection classes.
19+
This modules implements the CalcRiskPeriod class.
20+
21+
CalcRiskPeriod are used to compute risk metrics (and intermediate requirements)
22+
in between two snapshots.
23+
24+
As these computations are not always required and can become "heavy", a so called "lazy"
25+
approach is used: computation is only done when required, and then stored.
2026
2127
"""
2228

@@ -39,6 +45,10 @@
3945

4046
LOGGER = logging.getLogger(__name__)
4147

48+
logging.getLogger("climada.util.coordinates").setLevel(logging.WARNING)
49+
logging.getLogger("climada.entity.exposures.base").setLevel(logging.WARNING)
50+
logging.getLogger("climada.engine.impact_calc").setLevel(logging.WARNING)
51+
4252

4353
def lazy_property(method):
4454
# This function is used as a decorator for properties
@@ -105,7 +115,7 @@ def __init__(
105115
risk_transf_cover: float | None = None,
106116
calc_residual: bool = False,
107117
):
108-
LOGGER.info("Instantiating new CalcRiskPeriod.")
118+
LOGGER.debug("Instantiating new CalcRiskPeriod.")
109119
self._snapshot0 = snapshot0
110120
self._snapshot1 = snapshot1
111121
self.date_idx = CalcRiskPeriod._set_date_idx(
@@ -124,8 +134,16 @@ def __init__(
124134
self.calc_residual = calc_residual
125135
self.measure = None # Only possible to set with apply_measure to make sure snapshots are consistent
126136

127-
self._group_id_E0 = self.snapshot0.exposure.gdf["group_id"].values
128-
self._group_id_E1 = self.snapshot1.exposure.gdf["group_id"].values
137+
self._group_id_E0 = (
138+
self.snapshot0.exposure.gdf["group_id"].values
139+
if "group_id" in self.snapshot0.exposure.gdf.columns
140+
else np.array([])
141+
)
142+
self._group_id_E1 = (
143+
self.snapshot1.exposure.gdf["group_id"].values
144+
if "group_id" in self.snapshot1.exposure.gdf.columns
145+
else np.array([])
146+
)
129147

130148
def _reset_impact_data(self):
131149
self._impacts_arrays = None
@@ -212,7 +230,7 @@ def date_idx(self, value, /):
212230
if not isinstance(value, pd.DatetimeIndex):
213231
raise ValueError("Not a DatetimeIndex")
214232

215-
self._date_idx = value.normalize()
233+
self._date_idx = value.normalize() # Avoids weird hourly data
216234
self._time_points = len(self.date_idx)
217235
self._interval_freq = pd.infer_freq(self.date_idx)
218236
self._prop_H1 = np.linspace(0, 1, num=self.time_points)

climada/trajectories/snapshot.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
1717
---
1818
19-
This modules implements the Snapshot and SnapshotsCollection classes.
19+
This modules implements the Snapshot class.
20+
21+
Snapshot are used to store the a snapshot of Exposure, Hazard, Vulnerability
22+
at a specific date.
2023
2124
"""
2225

0 commit comments

Comments
 (0)