Reorganize Palladium codebase into a modular architecture with `core/` shared logic and `app/` Streamlit UI, separating per-study assets into `studies/YYYYMM_<Vendor>/` folders containing notebooks, seed data, and configuration. Update README to reflect new structure, add `.gitignore` entries for `.env` and study exports, and refresh component documentation.
79 lines
2.3 KiB
Python
79 lines
2.3 KiB
Python
"""
|
|
Net Present Value and discounting.
|
|
|
|
Convention (matches the Forrester TEI methodology used in the Amazon Connect
|
|
study):
|
|
|
|
* The *Initial* investment (column "Initial" in TEI tables) is **not**
|
|
discounted — it occurs at time zero.
|
|
* Year-N cash flows are discounted at the end of the year:
|
|
``PV = CF_n / (1 + r) ** n`` for ``n = 1, 2, …``.
|
|
|
|
That matches the PDF note: *"The initial investment column contains costs
|
|
incurred at 'time 0' or at the beginning of Year 1 that are not discounted.
|
|
All other cash flows are discounted using the discount rate at the end of
|
|
the year."*
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterable
|
|
|
|
|
|
def discount_factor(year: int, discount_rate: float) -> float:
|
|
"""Return ``1 / (1 + r) ** year``. Year 0 → 1.0 (no discount)."""
|
|
if year < 0:
|
|
raise ValueError("year must be >= 0")
|
|
return 1.0 / ((1.0 + discount_rate) ** year)
|
|
|
|
|
|
def present_value(amount: float, year: int, discount_rate: float) -> float:
|
|
"""Discount ``amount`` from end-of-year ``year`` to present."""
|
|
return amount * discount_factor(year, discount_rate)
|
|
|
|
|
|
def present_value_series(
|
|
cashflows: Iterable[float],
|
|
discount_rate: float,
|
|
start_year: int = 1,
|
|
) -> float:
|
|
"""
|
|
Sum the present value of a stream of year-end cashflows.
|
|
|
|
Args:
|
|
cashflows: iterable of year-N amounts (Y1, Y2, …).
|
|
discount_rate: e.g. 0.10 for 10%.
|
|
start_year: year of the first element. Default 1 (skip year-0).
|
|
"""
|
|
total = 0.0
|
|
for offset, cf in enumerate(cashflows):
|
|
total += present_value(cf, start_year + offset, discount_rate)
|
|
return total
|
|
|
|
|
|
def npv(
|
|
cashflows: Iterable[float],
|
|
discount_rate: float,
|
|
initial: float = 0.0,
|
|
) -> float:
|
|
"""
|
|
Net Present Value.
|
|
|
|
Args:
|
|
cashflows: year-end cashflows for Year 1, Year 2, …
|
|
discount_rate: e.g. 0.10
|
|
initial: undiscounted year-0 cashflow (negative for a cost,
|
|
positive for a one-off benefit).
|
|
|
|
Returns:
|
|
``initial + Σ CF_n / (1 + r)^n`` for ``n = 1..len(cashflows)``.
|
|
|
|
Example::
|
|
|
|
>>> # Amazon Connect total benefits PV ≈ $101.7M
|
|
>>> benefits = [27_279_019, 40_333_658, 57_983_794]
|
|
>>> round(npv(benefits, 0.10) / 1_000_000, 1)
|
|
101.7
|
|
"""
|
|
return initial + present_value_series(cashflows, discount_rate, start_year=1)
|