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:
63
core/calculations/payback.py
Normal file
63
core/calculations/payback.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Payback period.
|
||||
|
||||
Linear interpolation within the crossing year: returns the moment when the
|
||||
running net cashflow first turns non-negative.
|
||||
|
||||
Inputs are *risk-adjusted, undiscounted* cashflows by convention (TEI shows
|
||||
"<6 months" payback for the Amazon Connect composite using the risk-adjusted
|
||||
nominal cashflows from the Cash Flow Analysis table).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
||||
def payback_years(
|
||||
initial_cost: float,
|
||||
yearly_net_benefits: Iterable[float],
|
||||
) -> float | None:
|
||||
"""
|
||||
Years until cumulative net benefits cover the initial cost.
|
||||
|
||||
Args:
|
||||
initial_cost: positive number — undiscounted year-0 outlay.
|
||||
yearly_net_benefits: sequence of (benefits − costs) per year, Y1+.
|
||||
|
||||
Returns:
|
||||
Float number of years, or ``None`` if payback is never reached
|
||||
within the supplied horizon.
|
||||
|
||||
Example::
|
||||
|
||||
>>> # Amazon Connect: initial cost $1.196M, Y1 net $19.998M
|
||||
>>> # → ~0.06 years ≈ 0.7 months — well under 6 months.
|
||||
>>> round(payback_years(1_196_250, [19_997_953, 31_562_489, 47_443_905]), 3)
|
||||
0.06
|
||||
"""
|
||||
remaining = float(initial_cost)
|
||||
if remaining <= 0:
|
||||
return 0.0
|
||||
cumulative_year = 0
|
||||
for cf in yearly_net_benefits:
|
||||
cumulative_year += 1
|
||||
cf = float(cf)
|
||||
if cf <= 0:
|
||||
remaining += -cf # net loss this year increases the gap
|
||||
continue
|
||||
if cf >= remaining:
|
||||
# Crossing happens partway through this year.
|
||||
fraction = remaining / cf
|
||||
return (cumulative_year - 1) + fraction
|
||||
remaining -= cf
|
||||
return None
|
||||
|
||||
|
||||
def payback_months(
|
||||
initial_cost: float,
|
||||
yearly_net_benefits: Iterable[float],
|
||||
) -> float | None:
|
||||
"""Same as :func:`payback_years`, expressed in months."""
|
||||
yrs = payback_years(initial_cost, yearly_net_benefits)
|
||||
return yrs * 12.0 if yrs is not None else None
|
||||
Reference in New Issue
Block a user