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.
222 lines
6.1 KiB
Plaintext
222 lines
6.1 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"# 03 — Business Case\n",
|
|
"\n",
|
|
"Combine the benefits and costs into the consolidated TEI summary,\n",
|
|
"render the Cash Flow chart, and run scenario analysis. This notebook\n",
|
|
"should reproduce the headline numbers from the PDF Financial Summary:\n",
|
|
"\n",
|
|
"* **NPV $78.7M • ROI 342% • Payback <6 months**"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import sys\n",
|
|
"from pathlib import Path\n",
|
|
"\n",
|
|
"ROOT = Path.cwd().resolve()\n",
|
|
"while ROOT != ROOT.parent and not (ROOT / 'core').is_dir():\n",
|
|
" ROOT = ROOT.parent\n",
|
|
"if str(ROOT) not in sys.path:\n",
|
|
" sys.path.insert(0, str(ROOT))\n",
|
|
"STUDY = ROOT / 'studies' / '202602_AmazonConnect'\n",
|
|
"if str(STUDY) not in sys.path:\n",
|
|
" sys.path.insert(0, str(STUDY))"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import config\n",
|
|
"import seed_data\n",
|
|
"from core.export import build_report_data\n",
|
|
"from core.export.report_data import _compute_summary\n",
|
|
"from core.notebook_helpers import charts, display, tables"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Local summary (no Athena round-trip)\n",
|
|
"\n",
|
|
"Compute the moderate-case TEI summary directly from `seed_data` so the\n",
|
|
"notebook produces results even before the Athena tool is provisioned."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"summary = _compute_summary(\n",
|
|
" seed_data.BENEFITS,\n",
|
|
" seed_data.COSTS,\n",
|
|
" config.DISCOUNT_RATE,\n",
|
|
" config.ANALYSIS_YEARS,\n",
|
|
")\n",
|
|
"# `_compute_summary` returns roi_pct; expose it as `roi` for kpi_cards.\n",
|
|
"summary['roi'] = summary.get('roi_pct')\n",
|
|
"display.kpi_cards(summary, title='Forrester composite — moderate case')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"df_cash = tables.cashflow_table(summary)\n",
|
|
"df_cash.style.format({c: '${:,.0f}' for c in df_cash.columns if c != 'Year'})"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Cash flow chart\n",
|
|
"\n",
|
|
"Mirrors the chart on PDF page 25: stacked benefits/costs by year +\n",
|
|
"cumulative-net line."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"charts.cashflow_chart(\n",
|
|
" summary['yearly_breakdown'],\n",
|
|
" initial_cost=summary.get('initial_costs', 0),\n",
|
|
").show()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Waterfall: Benefits PV → Costs PV → NPV"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"charts.waterfall([\n",
|
|
" ('Benefits PV', summary['total_benefits_pv']),\n",
|
|
" ('Costs PV', -summary['total_costs_pv']),\n",
|
|
" ('NPV', summary['npv']),\n",
|
|
"]).show()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Scenario analysis\n",
|
|
"\n",
|
|
"Apply the default Palladium multipliers (see `core.calculations.SCENARIOS`):\n",
|
|
"\n",
|
|
"* **Conservative** — 80% adoption, +10pp risk on benefits / -10pp on costs\n",
|
|
"* **Moderate** — base case (= the published Forrester study)\n",
|
|
"* **Aggressive** — 115% adoption, -5pp risk on benefits / +5pp on costs"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from core.calculations import apply_scenario\n",
|
|
"import pandas as pd\n",
|
|
"\n",
|
|
"scenario_summaries = {}\n",
|
|
"for name in ('conservative', 'moderate', 'aggressive'):\n",
|
|
" sb = apply_scenario(seed_data.BENEFITS, name, table='benefits')\n",
|
|
" sc = apply_scenario(seed_data.COSTS, name, table='costs')\n",
|
|
" scenario_summaries[name] = _compute_summary(sb, sc, config.DISCOUNT_RATE, config.ANALYSIS_YEARS)\n",
|
|
"\n",
|
|
"scen_df = pd.DataFrame([\n",
|
|
" {\n",
|
|
" 'Scenario': k,\n",
|
|
" 'Benefits PV': v['total_benefits_pv'],\n",
|
|
" 'Costs PV': v['total_costs_pv'],\n",
|
|
" 'NPV': v['npv'],\n",
|
|
" 'ROI %': v['roi_pct'],\n",
|
|
" 'Payback (mo)': round(v['payback_months'], 1) if v['payback_months'] is not None else None,\n",
|
|
" }\n",
|
|
" for k, v in scenario_summaries.items()\n",
|
|
"])\n",
|
|
"scen_df.style.format({\n",
|
|
" 'Benefits PV': '${:,.0f}', 'Costs PV': '${:,.0f}', 'NPV': '${:,.0f}', 'ROI %': '{:,.0f}%'\n",
|
|
"})"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"charts.scenario_comparison(scenario_summaries).show()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Cross-check vs Athena (optional)\n",
|
|
"\n",
|
|
"When `TOOL_PUBLIC_ID` is set, ask Athena to recalculate the summary on\n",
|
|
"the server side and confirm it matches our local computation."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"if config.TOOL_PUBLIC_ID:\n",
|
|
" from core.tei_client import TEIClient\n",
|
|
"\n",
|
|
" client = TEIClient()\n",
|
|
" client.calculate(config.TOOL_PUBLIC_ID)\n",
|
|
" server_summary = client.get_summary(config.TOOL_PUBLIC_ID)\n",
|
|
" display.kpi_cards(server_summary, title='Athena server-side summary')\n",
|
|
"else:\n",
|
|
" display.alert('Set TOOL_PUBLIC_ID to compare Athena vs local.', 'info')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Continue with [`04_export.ipynb`](04_export.ipynb) →"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
|
|
"language_info": {"name": "python", "version": "3.11"}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5
|
|
}
|