Files
Robert Helewka a2420ed692 refactor: restructure repo into core/app modules with per-study folders
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.
2026-05-20 22:28:12 -04:00

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)