2020"""
2121
2222from datetime import datetime
23- from typing import Any , List , Optional , Tuple , cast
23+ from typing import Any , Optional , Tuple , cast
2424
2525import matplotlib .pyplot as plt
2626import numpy as np
2727import pandas as pd
28+ import yaml
2829
29- from climada .entity .disc_rates .base import DiscRates
3030from 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