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.
This commit is contained in:
221
studies/202602_AmazonConnect/notebooks/03_business_case.ipynb
Normal file
221
studies/202602_AmazonConnect/notebooks/03_business_case.ipynb
Normal file
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
Reference in New Issue
Block a user