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.
64 lines
1.9 KiB
Python
64 lines
1.9 KiB
Python
"""
|
||
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
|