128 lines
4.2 KiB
Python
128 lines
4.2 KiB
Python
"""
|
|
Pandas dataframe builders for benefit / cost / summary tables.
|
|
|
|
Each builder accepts the friendly value-row dicts returned by
|
|
``core.tei_client.TEIClient.get_values`` and returns a
|
|
nicely-formatted DataFrame for display in notebooks.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterable
|
|
from typing import Any
|
|
|
|
import pandas as pd
|
|
|
|
from core.calculations import risk_adjust_benefit, risk_adjust_cost
|
|
|
|
|
|
def _years_in_data(items: Iterable[dict]) -> list[int]:
|
|
years: set[int] = set()
|
|
for it in items:
|
|
for k in (it.get("year_values") or {}):
|
|
try:
|
|
years.add(int(k))
|
|
except (TypeError, ValueError):
|
|
continue
|
|
return sorted(years)
|
|
|
|
|
|
def benefits_table(items: list[dict]) -> pd.DataFrame:
|
|
"""Tidy benefits dataframe with one row per benefit, year columns, totals."""
|
|
if not items:
|
|
return pd.DataFrame(
|
|
columns=["field_key", "label", "category", "risk_adjustment"]
|
|
)
|
|
years = _years_in_data(items)
|
|
rows: list[dict[str, Any]] = []
|
|
for it in items:
|
|
rf = float(it.get("risk_adjustment") or 0.0)
|
|
yv = it.get("year_values") or {}
|
|
row = {
|
|
"field_key": it.get("field_key", ""),
|
|
"label": it.get("label", "") or it.get("field_key", ""),
|
|
"category": it.get("category", ""),
|
|
"risk_adjustment": rf,
|
|
}
|
|
nominal_total = 0.0
|
|
ra_total = 0.0
|
|
for y in years:
|
|
v = float(yv.get(str(y)) or 0.0)
|
|
ra = risk_adjust_benefit(v, rf)
|
|
row[f"Year {y}"] = v
|
|
row[f"Year {y} (RA)"] = ra
|
|
nominal_total += v
|
|
ra_total += ra
|
|
row["Total"] = nominal_total
|
|
row["Total (RA)"] = ra_total
|
|
rows.append(row)
|
|
return pd.DataFrame(rows)
|
|
|
|
|
|
def costs_table(items: list[dict]) -> pd.DataFrame:
|
|
"""Tidy costs dataframe — adds an Initial column when present."""
|
|
if not items:
|
|
return pd.DataFrame(
|
|
columns=["field_key", "label", "category", "risk_adjustment", "Initial"]
|
|
)
|
|
years = _years_in_data(items)
|
|
rows: list[dict[str, Any]] = []
|
|
for it in items:
|
|
rf = float(it.get("risk_adjustment") or 0.0)
|
|
yv = it.get("year_values") or {}
|
|
initial = float(it.get("initial") or 0.0)
|
|
row = {
|
|
"field_key": it.get("field_key", ""),
|
|
"label": it.get("label", "") or it.get("field_key", ""),
|
|
"category": it.get("category", ""),
|
|
"risk_adjustment": rf,
|
|
"Initial": initial,
|
|
"Initial (RA)": risk_adjust_cost(initial, rf),
|
|
}
|
|
nominal_total = initial
|
|
ra_total = risk_adjust_cost(initial, rf)
|
|
for y in years:
|
|
v = float(yv.get(str(y)) or 0.0)
|
|
ra = risk_adjust_cost(v, rf)
|
|
row[f"Year {y}"] = v
|
|
row[f"Year {y} (RA)"] = ra
|
|
nominal_total += v
|
|
ra_total += ra
|
|
row["Total"] = nominal_total
|
|
row["Total (RA)"] = ra_total
|
|
rows.append(row)
|
|
return pd.DataFrame(rows)
|
|
|
|
|
|
def summary_table(summary: dict) -> pd.DataFrame:
|
|
"""Single-row summary dataframe of headline KPIs."""
|
|
pb = summary.get("payback_months")
|
|
pb_str = f"{float(pb):.1f} months" if pb not in (None, "") else "N/A"
|
|
data = {
|
|
"NPV": [float(summary.get("npv") or 0)],
|
|
"ROI %": [float(summary.get("roi") or summary.get("roi_pct") or 0)],
|
|
"Payback": [pb_str],
|
|
"Benefits PV": [float(summary.get("total_benefits_pv") or 0)],
|
|
"Costs PV": [float(summary.get("total_costs_pv") or 0)],
|
|
"Discount rate": [float(summary.get("discount_rate") or 0)],
|
|
"Analysis years": [int(summary.get("analysis_years") or 0)],
|
|
}
|
|
return pd.DataFrame(data)
|
|
|
|
|
|
def cashflow_table(summary: dict) -> pd.DataFrame:
|
|
"""Per-year cashflow dataframe from a summary's ``yearly_breakdown``."""
|
|
yb = summary.get("yearly_breakdown") or []
|
|
if not yb:
|
|
return pd.DataFrame(columns=["Year", "Benefits", "Costs", "Net", "Cumulative"])
|
|
df = pd.DataFrame(yb)
|
|
rename = {
|
|
"year": "Year",
|
|
"benefits": "Benefits",
|
|
"costs": "Costs",
|
|
"net": "Net",
|
|
"cumulative_net": "Cumulative",
|
|
}
|
|
df = df.rename(columns=rename)
|
|
return df
|