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.
This commit is contained in:
78
core/calculations/npv.py
Normal file
78
core/calculations/npv.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user