Skip to content

Commit d33721e

Browse files
committed
Makes a good basis for static appraiser
1 parent 064c056 commit d33721e

2 files changed

Lines changed: 311 additions & 0 deletions

File tree

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
"""
2+
This file is part of CLIMADA.
3+
4+
Copyright (C) 2017 ETH Zurich, CLIMADA contributors listed in AUTHORS.
5+
6+
CLIMADA is free software: you can redistribute it and/or modify it under the
7+
terms of the GNU General Public License as published by the Free
8+
Software Foundation, version 3.
9+
10+
CLIMADA is distributed in the hope that it will be useful, but WITHOUT ANY
11+
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
12+
PARTICULAR PURPOSE. See the GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License along
15+
with CLIMADA. If not, see <https://www.gnu.org/licenses/>.
16+
17+
---
18+
19+
"""
20+
21+
import copy
22+
import logging
23+
from typing import Iterable
24+
25+
import pandas as pd
26+
from tqdm import tqdm
27+
28+
from climada.engine.option_appraisal.constants import (
29+
AVERTED_RISK_NAME,
30+
MEASURE_IMPL_COST_NAME,
31+
REFERENCE_RISK_NAME,
32+
)
33+
from climada.entity.disc_rates.base import DiscRates
34+
from climada.entity.measures.measure_set import MeasureSet
35+
from climada.trajectories.calc_risk_metrics import CalcRiskMetricsPoints
36+
from climada.trajectories.constants import (
37+
COORD_ID_COL_NAME,
38+
DATE_COL_NAME,
39+
GROUP_COL_NAME,
40+
MEASURE_COL_NAME,
41+
METRIC_COL_NAME,
42+
NO_MEASURE_VALUE,
43+
RISK_COL_NAME,
44+
)
45+
from climada.trajectories.impact_calc_strat import ImpactComputationStrategy
46+
from climada.trajectories.snapshot import Snapshot
47+
from climada.trajectories.static_trajectory import StaticRiskTrajectory
48+
from climada.trajectories.trajectory import DEFAULT_DF_COLUMN_PRIORITY, DEFAULT_RP
49+
from climada.util import log_level
50+
from climada.util.config import CONFIG
51+
from climada.util.dataframe_handling import reorder_dataframe_columns
52+
53+
tqdm.pandas()
54+
55+
LOGGER = logging.getLogger(__name__)
56+
57+
58+
class StaticAppraiser(StaticRiskTrajectory):
59+
def __init__(
60+
self,
61+
snapshots_list: Iterable[Snapshot],
62+
*,
63+
measure_set: MeasureSet,
64+
return_periods: Iterable[int] = DEFAULT_RP,
65+
risk_disc_rates: DiscRates | None = None,
66+
cost_disc_rates: DiscRates | None = None,
67+
impact_computation_strategy: ImpactComputationStrategy | None = None,
68+
):
69+
"""Initialize a new `StaticAppraiser`.
70+
71+
Parameters
72+
----------
73+
snapshots_list : list[Snapshot]
74+
The list of `Snapshot` object to compute risk from.
75+
measure_set: MeasureSet
76+
The set of adaptation measures to appraise.
77+
return_periods: list[int], optional
78+
The return periods to use when computing the `return_periods_metric`.
79+
Defaults to `DEFAULT_RP` ([20, 50, 100]).
80+
all_groups_name: str, optional
81+
The string that should be used to define "all exposure points" subgroup.
82+
Defaults to `DEFAULT_ALLGROUP_NAME` ("All").
83+
risk_disc_rates: DiscRates, optional
84+
The discount rate to apply to future risk. Defaults to None.
85+
cost_disc_rates: DiscRates, optional
86+
The discount rate to apply to future costs (of adaptation measures).
87+
Defaults to None.
88+
impact_computation_strategy: ImpactComputationStrategy, optional
89+
The method used to calculate the impact from the (Haz,Exp,Vul)
90+
of the two snapshots. Defaults to :class:`ImpactCalcComputation`.
91+
92+
"""
93+
94+
self._cost_disc_rates = cost_disc_rates
95+
self.measure_set = copy.deepcopy(measure_set)
96+
super().__init__(
97+
snapshots_list,
98+
return_periods=return_periods,
99+
risk_disc_rates=risk_disc_rates,
100+
impact_computation_strategy=impact_computation_strategy,
101+
)
102+
self._risk_metrics_calculators = self._add_adaptation_metrics_calculators(
103+
self._risk_metrics_calculators, measure_set
104+
)
105+
106+
@staticmethod
107+
def _add_adaptation_metrics_calculators(
108+
risk_metrics_calculators, measure_set: MeasureSet
109+
) -> list[CalcRiskMetricsPoints]:
110+
"""Adds the risk metric calculators for the different adaptation options."""
111+
calculators = [risk_metrics_calculators] + [
112+
risk_metrics_calculators.apply_measure(meas)
113+
for _, meas in measure_set.measures().items()
114+
]
115+
return calculators
116+
117+
@property
118+
def cost_disc_rates(self) -> DiscRates | None:
119+
"""The discount rate applied to compute net present values of costs.
120+
None means no discount rate.
121+
122+
Notes
123+
-----
124+
125+
Changing its value resets the metrics.
126+
"""
127+
return self._cost_disc_rates
128+
129+
@cost_disc_rates.setter
130+
def cost_disc_rates(self, value, /):
131+
if value is not None and not isinstance(value, DiscRates):
132+
raise ValueError("Risk discount needs to be a `DiscRates` object.")
133+
134+
self._reset_metrics()
135+
self._cost_disc_rates = value
136+
137+
def _generic_metrics(
138+
self,
139+
metric_name: str | None = None,
140+
metric_meth: str | None = None,
141+
**kwargs,
142+
) -> pd.DataFrame:
143+
"""Generic method to compute metrics based on the provided metric name and method.
144+
145+
This method calls the appropriate method from each calculators (corresponding to
146+
each adaptation) to return the results for the given metric,
147+
in a tidy formatted dataframe.
148+
149+
It first checks whether the requested metric is a valid one.
150+
Then looks for a possible cached value and otherwised asks the
151+
calculators (`self._risk_metric_calculators`) to run the computation.
152+
The results are then regrouped in a nice and tidy DataFrame.
153+
If a `risk_disc_rates` was set, values are converted to net present values.
154+
Results are then cached within `self._<metric_name>_metrics` and returned.
155+
156+
Parameters
157+
----------
158+
metric_name : str, optional
159+
The name of the metric to return results for.
160+
metric_meth : str, optional
161+
The name of the specific method of the calculator to call.
162+
163+
Returns
164+
-------
165+
pd.DataFrame
166+
A tidy formatted dataframe of the risk metric computed for the
167+
different snapshots.
168+
169+
Raises
170+
------
171+
NotImplementedError
172+
If the requested metric is not part of `POSSIBLE_METRICS`.
173+
ValueError
174+
If either of the arguments are not provided.
175+
176+
"""
177+
178+
if metric_name is None or metric_meth is None:
179+
raise ValueError("Both metric_name and metric_meth must be provided.")
180+
181+
if metric_name not in self.POSSIBLE_METRICS:
182+
raise NotImplementedError(
183+
f"{metric_name} not implemented ({self.POSSIBLE_METRICS})."
184+
)
185+
186+
# Construct the attribute name for storing the metric results
187+
attr_name = f"_{metric_name}_metrics"
188+
189+
if getattr(self, attr_name) is not None:
190+
LOGGER.debug("Returning cached %s", attr_name)
191+
return getattr(self, attr_name)
192+
193+
LOGGER.debug("Computing %s", attr_name)
194+
with log_level(level="WARNING", name_prefix="climada"):
195+
tmp = [
196+
getattr(calc_period, metric_meth)(**kwargs)
197+
for calc_period in self._risk_metrics_calculators
198+
]
199+
200+
try:
201+
tmp = pd.concat(tmp)
202+
except ValueError as exc:
203+
if str(exc) == "All objects passed were None":
204+
return pd.DataFrame()
205+
raise exc
206+
207+
if len(tmp) == 0:
208+
return pd.DataFrame()
209+
210+
tmp = self._metric_post_treatment(tmp, metric_name)
211+
212+
if CONFIG.trajectory_caching.bool():
213+
LOGGER.debug("All computing done, caching value.")
214+
setattr(self, attr_name, tmp)
215+
return getattr(self, attr_name)
216+
217+
return tmp
218+
219+
def _metric_post_treatment(
220+
self, metric_df: pd.DataFrame, metric_name: str
221+
) -> pd.DataFrame:
222+
# Notably for per_group_aai being None:
223+
def meas_impl_cost(measure_name: str) -> float:
224+
if measure_name == NO_MEASURE_VALUE:
225+
return 0.0
226+
227+
return self.measure_set.measures()[measure_name].cost_income.init_cost
228+
229+
metric_df = self._handle_group_categories(metric_df)
230+
if self._risk_disc_rates:
231+
LOGGER.debug("Found risk discount rate. Computing NPV.")
232+
metric_df = self.npv_transform(metric_df, self._risk_disc_rates)
233+
234+
LOGGER.debug("Computing averted risk for: %s.", metric_name)
235+
metric_df = self._calc_averted(metric_df)
236+
metric_df[MEASURE_IMPL_COST_NAME] = metric_df[MEASURE_COL_NAME].map(
237+
meas_impl_cost
238+
)
239+
metric_df = reorder_dataframe_columns(metric_df, DEFAULT_DF_COLUMN_PRIORITY)
240+
return metric_df
241+
242+
def _handle_group_categories(self, metric_df: pd.DataFrame) -> pd.DataFrame:
243+
if self._all_groups_name not in metric_df[GROUP_COL_NAME].cat.categories:
244+
metric_df[GROUP_COL_NAME] = metric_df[GROUP_COL_NAME].cat.add_categories(
245+
[self._all_groups_name]
246+
)
247+
metric_df[GROUP_COL_NAME] = metric_df[GROUP_COL_NAME].fillna(
248+
self._all_groups_name
249+
)
250+
251+
return metric_df
252+
253+
@staticmethod
254+
def _calc_averted(base_metrics: pd.DataFrame) -> pd.DataFrame:
255+
def subtract_no_measure(group, no_measure, merger):
256+
# Merge with no_measure to get the corresponding NO_MEASURE_VALUE value
257+
merged = group.merge(
258+
no_measure, on=merger, suffixes=("", "_" + NO_MEASURE_VALUE)
259+
)
260+
# Subtract the NO_MEASURE_VALUE risk from the current risk
261+
merged[REFERENCE_RISK_NAME] = merged[RISK_COL_NAME + "_" + NO_MEASURE_VALUE]
262+
merged[AVERTED_RISK_NAME] = (
263+
merged[RISK_COL_NAME + "_" + NO_MEASURE_VALUE] - merged[RISK_COL_NAME]
264+
)
265+
return merged[
266+
list(group.columns) + [REFERENCE_RISK_NAME, AVERTED_RISK_NAME]
267+
]
268+
269+
no_measures_metrics = base_metrics[
270+
base_metrics[MEASURE_COL_NAME] == NO_MEASURE_VALUE
271+
].copy()
272+
merger = [GROUP_COL_NAME, METRIC_COL_NAME, DATE_COL_NAME]
273+
if COORD_ID_COL_NAME in base_metrics.columns:
274+
merger.append(COORD_ID_COL_NAME)
275+
276+
return base_metrics.groupby(
277+
[GROUP_COL_NAME, METRIC_COL_NAME, DATE_COL_NAME],
278+
group_keys=False,
279+
dropna=False,
280+
observed=False,
281+
).apply(subtract_no_measure, no_measure=no_measures_metrics, merger=merger)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
This file is part of CLIMADA.
3+
4+
Copyright (C) 2017 ETH Zurich, CLIMADA contributors listed in AUTHORS.
5+
6+
CLIMADA is free software: you can redistribute it and/or modify it under the
7+
terms of the GNU General Public License as published by the Free
8+
Software Foundation, version 3.
9+
10+
CLIMADA is distributed in the hope that it will be useful, but WITHOUT ANY
11+
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
12+
PARTICULAR PURPOSE. See the GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License along
15+
with CLIMADA. If not, see <https://www.gnu.org/licenses/>.
16+
17+
---
18+
19+
Define constants for option appraisal module.
20+
"""
21+
22+
REFERENCE_RISK_NAME = "reference risk"
23+
AVERTED_RISK_NAME = "averted risk"
24+
RESIDUAL_RISK_NAME = "residual risk"
25+
26+
MEASURE_IMPL_COST_NAME = "measure implementation cost"
27+
MEASURE_NET_COST_NAME = "measure net cost"
28+
CUMULATED_COST_NAME = "cumulated measure cost"
29+
CUMULATED_BENEFIT_NAME = "cumulated measure benefit"
30+
CB_RATIO_NAME = "cost/benefit ratio"

0 commit comments

Comments
 (0)