""" 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