Files
palladium/core/notebook_helpers/tables.py
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

128 lines
4.2 KiB
Python

"""
Pandas dataframe builders for benefit / cost / summary tables.
Each builder accepts the value-row dicts produced by
``core.tei_client.TEIClient._normalize_value`` 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