Skip to content

Commit 2499a59

Browse files
committed
Merge branch 'feature/option-appraisal-costincome' into feature/option-appraisal-new-measure-base
2 parents 89fd015 + 2ee944b commit 2499a59

9 files changed

Lines changed: 1154 additions & 1565 deletions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
This module implements measure and measure set objects, as well as cost income
20+
objects which enable the definition of adapation measures and their associated
21+
effects on each part of risk/impacts (exposure, vulnerability and hazard).
22+
23+
"""
24+
25+
from .measure_config import MeasureConfig
26+
27+
__all__ = ["MeasureConfig"]

climada/entity/measures/cost_income.py

Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020
"""
2121

2222
from datetime import datetime
23-
from typing import Any, List, Optional, Tuple, cast
23+
from typing import Any, Optional, Tuple, cast
2424

2525
import matplotlib.pyplot as plt
2626
import numpy as np
2727
import pandas as pd
28+
import yaml
2829

29-
from climada.entity.disc_rates.base import DiscRates
3030
from climada.entity.measures.measure_config import CostIncomeConfig
3131

3232

@@ -101,11 +101,7 @@ def __init__(
101101
self.periodic_income = abs(periodic_income)
102102

103103
self.income_growth_rate = income_yearly_growth_rate
104-
105-
if custom_cash_flows is not None:
106-
self.custom_cash_flows = self._prepare_custom_flows(custom_cash_flows)
107-
else:
108-
self.custom_cash_flows = None
104+
self.custom_cash_flows = custom_cash_flows
109105

110106
def __repr__(self) -> str:
111107
lines = [
@@ -117,11 +113,34 @@ def __repr__(self) -> str:
117113
f" periodic_income = {self.periodic_income:,.2f}",
118114
f" cost_yearly_growth_rate = {self.cost_growth_rate:.2%}",
119115
f" income_yearly_growth_rate = {self.income_growth_rate:.2%}",
120-
f" custom_cash_flows = {None if self.custom_cash_flows is None else f'DataFrame({len(self.custom_cash_flows)} rows)'}",
116+
" custom_cash_flows = "
117+
f"{None if self.custom_cash_flows is None else f'DataFrame({len(self.custom_cash_flows)} rows)'}",
121118
")",
122119
]
123120
return "\n".join(lines)
124121

122+
@property
123+
def custom_cash_flows(self) -> pd.DataFrame | None:
124+
""":obj:`pd.DataFrame` : Get or set the optional user-defined cash
125+
flows.
126+
127+
Input cash flow have to contain a "date" column as well as at least one
128+
of "cost" and "income". The custom cash flow is coerced to the internal
129+
period frequency.
130+
"""
131+
return self._custom_cash_flows
132+
133+
@custom_cash_flows.setter
134+
def custom_cash_flows(self, value, /):
135+
if value is None:
136+
self._custom_cash_flows = None
137+
138+
else:
139+
if not isinstance(value, pd.DataFrame):
140+
raise ValueError("Custom cash flows only accept pandas DataFrame.")
141+
142+
self._custom_cash_flows = self._prepare_custom_flows(value)
143+
125144
def _prepare_custom_flows(self, df: pd.DataFrame) -> pd.DataFrame:
126145
"""Process and resample custom cash flow dataframe
127146
@@ -138,9 +157,18 @@ def _prepare_custom_flows(self, df: pd.DataFrame) -> pd.DataFrame:
138157
Processed custom cashflow
139158
"""
140159

160+
if "date" not in df.columns:
161+
raise ValueError("No 'date' column found in custom cash flow DataFrame.")
162+
163+
if "cost" not in df.columns and "income" not in df.columns:
164+
raise ValueError(
165+
"No 'cost' or 'income' column found in custom cash flow DataFrame."
166+
)
167+
141168
df = df.copy()
142169
if "cost" in df.columns:
143170
df["cost"] = -df["cost"].abs()
171+
144172
if "date" in df.columns:
145173
df["date"] = pd.to_datetime(df["date"])
146174
df = df.set_index("date")
@@ -231,8 +259,6 @@ def from_yaml(cls, path: str) -> "CostIncome":
231259
CostIncome
232260
"""
233261

234-
import yaml
235-
236262
with open(path) as f:
237263
return cls.from_dict(yaml.safe_load(f)["cost_income"])
238264

@@ -263,8 +289,8 @@ def _freq_to_days(cls, freq: str) -> str:
263289

264290
# Return the difference in days
265291
return f"{(end_date - base_date).days}d"
266-
except ValueError:
267-
raise ValueError(f"Invalid frequency string: {freq}")
292+
except ValueError as exc:
293+
raise ValueError(f"Invalid frequency string: {freq}") from exc
268294

269295
def _get_width_days(self) -> float:
270296
"""Return the number of days in the current frequency."""
@@ -274,7 +300,7 @@ def _get_width_days(self) -> float:
274300
offset = pd.tseries.frequencies.to_offset(freq)
275301
return float(((ref + offset) - ref).days)
276302

277-
def _calc_at_date(
303+
def calc_at_date(
278304
self, impl_date: pd.Timestamp, curr_date: pd.Timestamp
279305
) -> Tuple[float, float, float]:
280306
r"""Calculate cash flows for a single timestamp.
@@ -397,10 +423,12 @@ def calc_cash_flows(
397423
Total incomes for each period.
398424
"""
399425

400-
impl_ts = pd.Timestamp(impl_date)
426+
# 'Trick' to make sure that e.g., impl_date "2020-01-05" falls in
427+
# period "2020-01" if freq is "M"
428+
impl_ts = pd.Timestamp(str(impl_date)).to_period(self.freq).start_time
401429
periods = pd.period_range(start=start_date, end=end_date, freq=self.freq)
402430

403-
results = [self._calc_at_date(impl_ts, p.start_time) for p in periods]
431+
results = [self.calc_at_date(impl_ts, p.start_time) for p in periods]
404432
net, costs, incs = map(np.array, zip(*results))
405433
return net, costs, incs
406434

@@ -535,34 +563,59 @@ def comb_cost_income(cost_incomes: list["CostIncome"]) -> "CostIncome":
535563
first_ci = cost_incomes[0]
536564

537565
if not all(
538-
[
566+
(
539567
first_ci.mkt_price_year.year == c.mkt_price_year.year
540568
for c in cost_incomes
541-
]
569+
)
542570
):
543571
raise ValueError(
544-
"Measure cost incomes have different market price years, combination is not possible."
572+
"Measure cost incomes have different market price years,"
573+
" combination is not possible."
545574
)
546575

547576
if not all(
548-
[first_ci.cost_growth_rate == c.cost_growth_rate for c in cost_incomes]
577+
first_ci.cost_growth_rate == c.cost_growth_rate for c in cost_incomes
549578
):
550579
raise ValueError(
551-
"Measure cost incomes have different cost_growth_rate, combination is not possible."
580+
"Measure cost incomes have different cost_growth_rate,"
581+
" combination is not possible."
552582
)
553583

554584
if not all(
555-
[first_ci.income_growth_rate == c.income_growth_rate for c in cost_incomes]
585+
first_ci.income_growth_rate == c.income_growth_rate for c in cost_incomes
556586
):
557587
raise ValueError(
558-
"Measure cost incomes have different income_growth_rate, combination is not possible."
588+
"Measure cost incomes have different income_growth_rate,"
589+
" combination is not possible."
590+
)
591+
592+
if not all(first_ci.freq == c.freq for c in cost_incomes):
593+
raise ValueError(
594+
"Measure cost incomes have different period frequencies,"
595+
" combination is not possible."
596+
)
597+
598+
try:
599+
custom_cash_flows = cast(
600+
pd.DataFrame,
601+
pd.concat([c.custom_cash_flows for c in cost_incomes]) # type: ignore
602+
.groupby(level=0)
603+
.sum()
604+
.reset_index(),
559605
)
606+
except ValueError as err:
607+
if str(err) == "All objects passed were None":
608+
custom_cash_flows = None
609+
else:
610+
raise err
560611

561612
return CostIncome(
562613
mkt_price_year=first_ci.mkt_price_year.year,
563-
cost_yearly_growth_rate=first_ci.cost_growth_rate,
564614
init_cost=sum(c.init_cost for c in cost_incomes),
565615
periodic_cost=sum(c.periodic_cost for c in cost_incomes),
566616
periodic_income=sum(c.periodic_income for c in cost_incomes),
617+
cost_yearly_growth_rate=first_ci.cost_growth_rate,
567618
income_yearly_growth_rate=first_ci.income_growth_rate,
619+
freq=first_ci.freq,
620+
custom_cash_flows=custom_cash_flows,
568621
)

0 commit comments

Comments
 (0)