Skip to content

Commit bb08305

Browse files
committed
Fix discount rates
1 parent 5f96532 commit bb08305

File tree

2 files changed

+19
-31
lines changed

2 files changed

+19
-31
lines changed

climada/trajectories/test/test_static_risk_trajectory.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -221,18 +221,18 @@ def test_calc_npv_cash_flows_logic(mock_disc_rates):
221221
)
222222
start_date = datetime.date(2023, 1, 1)
223223

224-
# NPV Factor: (1 / (1 + rate)) ^ year_delta
224+
# NPV Factor: Product[ (1 / (1 + rate_i))]
225+
# For a constant rate or 0.01
225226
# 2023: (1/1.01)^0 = 1.0 -> 100
226-
# 2024: (1/1.02)^1 = 0.98039... -> 196.078...
227-
# 2025: (1/1.03)^2 = 0.94259... -> 282.778...
227+
# 2024: (1/1.01)^1 = 0.99099... -> 198.019...
228+
# 2025: (1/1.01)^2 = 0.98029... -> 294.088...
228229

229230
result = RiskTrajectory._calc_npv_cash_flows(
230231
cash_flows, start_date, mock_disc_rates
231232
)
232-
233233
assert result.iloc[0] == pytest.approx(100.0)
234-
assert result.iloc[1] == pytest.approx(200 / 1.02)
235-
assert result.iloc[2] == pytest.approx(300 / (1.03**2))
234+
assert result.iloc[1] == pytest.approx(200 / (1.02))
235+
assert result.iloc[2] == pytest.approx(300 * (1 / 1.02) * (1 / 1.03))
236236

237237

238238
def test_calc_npv_cash_flows_invalid_index(mock_disc_rates):

climada/trajectories/trajectory.py

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -218,56 +218,44 @@ def _npv_group(group, disc):
218218

219219
@staticmethod
220220
def _calc_npv_cash_flows(
221-
cash_flows: pd.DataFrame | pd.Series,
221+
cash_flows: pd.Series,
222222
start_date: datetime.date,
223223
disc_rates: DiscRates | None = None,
224-
):
224+
) -> pd.Series:
225225
"""Apply discount rate to cash flows.
226226
227227
If it is defined, applies a discount rate `disc` to a given cash flow
228-
`cash_flows` assuming present year corresponds to `start_date`.
228+
`cash_flows` using `start_date` as the reference year.
229229
230230
Parameters
231231
----------
232232
cash_flows : pd.DataFrame
233233
The cash flow to apply the discount rate to.
234234
start_date : datetime.date
235235
The date representing the present.
236-
end_date : datetime.date, optional
237236
disc : DiscRates, optional
238-
The discount rate to apply.
237+
The discount rates to apply.
239238
240239
Returns
241240
-------
242241
243-
A dataframe (copy) of `cash_flows` where values are discounted according to `disc`.
242+
A Series (copy) of `cash_flows` where values are discounted according to `disc`.
244243
245244
"""
246245

247-
if not disc_rates:
246+
if disc_rates is None:
248247
return cash_flows
249248

250249
if not isinstance(cash_flows.index, (pd.PeriodIndex, pd.DatetimeIndex)):
251250
raise ValueError(
252251
"cash_flows must be a pandas Series with a PeriodIndex or DatetimeIndex"
253252
)
254253

255-
metric_df = cash_flows.to_frame(name="cash_flow") # type: ignore
256-
metric_df["year"] = metric_df.index.year
257-
258-
# Merge with the discount rates based on the year
259-
tmp = pd.Series(index=disc_rates.years, data=disc_rates.rates, name="rate")
260-
tmp = tmp.loc[tmp.index >= start_date.year]
261-
tmp = 1 / tmp.shift(1, fill_value=0).add(1).cumprod()
262-
tmp = tmp.to_frame()
263-
tmp["year"] = tmp.index
264-
metric_df = metric_df.merge(
265-
tmp,
266-
on="year",
267-
how="left",
254+
growth_factors = (
255+
pd.Series(disc_rates.rates, index=disc_rates.years)
256+
.loc[lambda x: x.index > start_date.year]
257+
.add(1)
258+
.cumprod()
268259
)
269-
270-
# Apply the discount factors to the cash flows
271-
272-
metric_df["npv_cash_flow"] = metric_df["cash_flow"] * metric_df["rate"]
273-
return metric_df["npv_cash_flow"].values
260+
discount_factors = 1 / cash_flows.index.year.map(growth_factors).fillna(1.0)
261+
return cash_flows.multiply(discount_factors, axis=0)

0 commit comments

Comments
 (0)