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:
269
studies/202602_AmazonConnect/notebooks/01_benefits.ipynb
Normal file
269
studies/202602_AmazonConnect/notebooks/01_benefits.ipynb
Normal file
@@ -0,0 +1,269 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "231c773a",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 01 — Benefits Analysis\n",
|
||||
"\n",
|
||||
"**Study:** Forrester *Total Economic Impact™ Of Amazon Connect* (Feb 2026)\n",
|
||||
"\n",
|
||||
"Quantify the five benefit categories Forrester identified for the\n",
|
||||
"composite organization, push them into Athena, and verify the totals\n",
|
||||
"match the published study (Benefits PV ≈ **$101.7M**)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "110d7e61",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"We add the project root to `sys.path` so the notebook can import `core` and\n",
|
||||
"the study's local modules without `pip install -e .`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "c83c2758",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Project root: /home/robert/notebook/git/palladium\n",
|
||||
"Study root: /home/robert/notebook/git/palladium/studies/202602_AmazonConnect\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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",
|
||||
"\n",
|
||||
"STUDY = ROOT / 'studies' / '202602_AmazonConnect'\n",
|
||||
"if str(STUDY) not in sys.path:\n",
|
||||
" sys.path.insert(0, str(STUDY))\n",
|
||||
"print(f'Project root: {ROOT}')\n",
|
||||
"print(f'Study root: {STUDY}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "c371ef85",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"ename": "ModuleNotFoundError",
|
||||
"evalue": "No module named 'pandas'",
|
||||
"output_type": "error",
|
||||
"traceback": [
|
||||
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
|
||||
"\u001b[31mModuleNotFoundError\u001b[39m Traceback (most recent call last)",
|
||||
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 4\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m config\n\u001b[32m 2\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m seed_data\n\u001b[32m 3\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m core.calculations \u001b[38;5;28;01mimport\u001b[39;00m npv, risk_adjust_benefit\n\u001b[32m----> \u001b[39m\u001b[32m4\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m core.notebook_helpers \u001b[38;5;28;01mimport\u001b[39;00m charts, display, tables\n\u001b[32m 5\u001b[39m \n\u001b[32m 6\u001b[39m display.alert(\n\u001b[32m 7\u001b[39m f'Study: <b>{config.STUDY_SLUG}</b> • discount rate {config.DISCOUNT_RATE:.0%} '\n",
|
||||
"\u001b[36mFile \u001b[39m\u001b[32m~/notebook/git/palladium/core/notebook_helpers/__init__.py:3\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[33;03m\"\"\"Notebook helpers — pandas tables, plotly charts, IPython display.\"\"\"\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mcore\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mnotebook_helpers\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m charts, display, tables\n\u001b[32m 5\u001b[39m __all__ = [\u001b[33m\"\u001b[39m\u001b[33mcharts\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mdisplay\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mtables\u001b[39m\u001b[33m\"\u001b[39m]\n",
|
||||
"\u001b[36mFile \u001b[39m\u001b[32m~/notebook/git/palladium/core/notebook_helpers/tables.py:13\u001b[39m\n\u001b[32m 9\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m__future__\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m annotations\n\u001b[32m 11\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mtyping\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m Any, Iterable\n\u001b[32m---> \u001b[39m\u001b[32m13\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mpandas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mpd\u001b[39;00m\n\u001b[32m 15\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mcore\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mcalculations\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m risk_adjust_benefit, risk_adjust_cost\n\u001b[32m 18\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_years_in_data\u001b[39m(items: Iterable[\u001b[38;5;28mdict\u001b[39m]) -> \u001b[38;5;28mlist\u001b[39m[\u001b[38;5;28mint\u001b[39m]:\n",
|
||||
"\u001b[31mModuleNotFoundError\u001b[39m: No module named 'pandas'"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import config\n",
|
||||
"import seed_data\n",
|
||||
"from core.calculations import npv, risk_adjust_benefit\n",
|
||||
"from core.notebook_helpers import charts, display, tables\n",
|
||||
"\n",
|
||||
"display.alert(\n",
|
||||
" f'Study: <b>{config.STUDY_SLUG}</b> • discount rate {config.DISCOUNT_RATE:.0%} '\n",
|
||||
" f'• {config.ANALYSIS_YEARS}-year horizon',\n",
|
||||
" 'info',\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "fd94503d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Benefits — nominal & risk-adjusted\n",
|
||||
"\n",
|
||||
"Forrester quantifies five benefit categories:\n",
|
||||
"\n",
|
||||
"| Ref | Benefit | Y1 | Y2 | Y3 | Risk Adj |\n",
|
||||
"|---|---|---|---|---|---|\n",
|
||||
"| At | AI-driven contact resolution efficiency | $13.9M | $23.9M | $37.8M | 15% |\n",
|
||||
"| Bt | AI-powered content & sentiment analysis | $4.6M | $5.4M | $6.3M | 15% |\n",
|
||||
"| Ct | AI-enabled forecasting & supervision | $6.7M | $9.1M | $12.4M | 15% |\n",
|
||||
"| Dt | Data-driven profit lift (conversion +20%) | $1.2M | $1.6M | $2.0M | 20% |\n",
|
||||
"| Et | Legacy solution cost savings | $6.2M | $8.0M | $10.4M | 20% |\n",
|
||||
"\n",
|
||||
"All five are seeded in `seed_data.BENEFITS` with full source notes."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "6177ea7c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"df = tables.benefits_table(seed_data.BENEFITS)\n",
|
||||
"df.style.format({col: '${:,.0f}' for col in df.columns if col not in ('field_key','label','category','risk_adjustment')})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "573f12d8",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Local validation against the PDF\n",
|
||||
"\n",
|
||||
"Re-derive the per-benefit risk-adjusted PV and confirm we land on Forrester's\n",
|
||||
"**$101,696,791** total within rounding."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "8cf32003",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"\n",
|
||||
"rows = []\n",
|
||||
"for b in seed_data.BENEFITS:\n",
|
||||
" rf = b['risk_adjustment']\n",
|
||||
" yr = [b['year_values'][str(y)] for y in (1, 2, 3)]\n",
|
||||
" yr_ra = [risk_adjust_benefit(v, rf) for v in yr]\n",
|
||||
" pv = npv(yr_ra, config.DISCOUNT_RATE)\n",
|
||||
" rows.append({\n",
|
||||
" 'Benefit': b['label'],\n",
|
||||
" 'Y1 (RA)': yr_ra[0],\n",
|
||||
" 'Y2 (RA)': yr_ra[1],\n",
|
||||
" 'Y3 (RA)': yr_ra[2],\n",
|
||||
" 'PV': pv,\n",
|
||||
" })\n",
|
||||
"df_check = pd.DataFrame(rows)\n",
|
||||
"df_check.loc[len(df_check)] = ['TOTAL', df_check['Y1 (RA)'].sum(), df_check['Y2 (RA)'].sum(), df_check['Y3 (RA)'].sum(), df_check['PV'].sum()]\n",
|
||||
"df_check.style.format({c: '${:,.0f}' for c in df_check.columns if c != 'Benefit'})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "3ded50c8",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"expected_pv = 101_696_791\n",
|
||||
"computed_pv = df_check.iloc[-1]['PV']\n",
|
||||
"delta = computed_pv - expected_pv\n",
|
||||
"kind = 'success' if abs(delta) < 1_000 else 'warning'\n",
|
||||
"display.alert(\n",
|
||||
" f'Computed Benefits PV: <b>${computed_pv:,.0f}</b><br>'\n",
|
||||
" f'Forrester target: <b>${expected_pv:,.0f}</b><br>'\n",
|
||||
" f'Δ = ${delta:,.0f} (rounding)',\n",
|
||||
" kind,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "a5ad453a",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Visualize\n",
|
||||
"\n",
|
||||
"Horizontal bar chart of risk-adjusted three-year totals — mirrors the PDF p.6\n",
|
||||
"*Benefits (Three-Year)* graphic."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "452b8408",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"charts.benefits_bar(seed_data.BENEFITS).show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1c4591f5",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Push to Athena\n",
|
||||
"\n",
|
||||
"When `config.TOOL_PUBLIC_ID` is set, persist the seed values to the live\n",
|
||||
"TEI tool. Otherwise this cell is a no-op so the notebook still runs\n",
|
||||
"offline."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "d10a54b6",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"if config.TOOL_PUBLIC_ID:\n",
|
||||
" from core.tei_client import TEIClient\n",
|
||||
"\n",
|
||||
" client = TEIClient()\n",
|
||||
" result = client.update_values(config.TOOL_PUBLIC_ID, seed_data.BENEFITS)\n",
|
||||
" display.alert(f'Pushed {len(seed_data.BENEFITS)} benefit rows to '\n",
|
||||
" f'tool <code>{config.TOOL_PUBLIC_ID}</code>.', 'success')\n",
|
||||
"else:\n",
|
||||
" display.alert(\n",
|
||||
" 'No TOOL_PUBLIC_ID set in config.py — skipped Athena push. '\n",
|
||||
" 'Set <code>PALLADIUM_TOOL_PUBLIC_ID</code> in your environment '\n",
|
||||
" 'or edit config.py to enable.',\n",
|
||||
" 'info',\n",
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "78693c14",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"Continue with [`02_costs.ipynb`](02_costs.ipynb) →"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.13.7"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
213
studies/202602_AmazonConnect/notebooks/02_costs.ipynb
Normal file
213
studies/202602_AmazonConnect/notebooks/02_costs.ipynb
Normal file
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1a76b7ed",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 02 — Costs Analysis\n",
|
||||
"\n",
|
||||
"**Study:** Forrester TEI™ Of Amazon Connect (Feb 2026)\n",
|
||||
"\n",
|
||||
"Three cost categories, three-year horizon, 10% discount rate.\n",
|
||||
"Target risk-adjusted PV = **$22,983,076**."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "46446223",
|
||||
"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,
|
||||
"id": "4ec64198",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import config\n",
|
||||
"import seed_data\n",
|
||||
"from core.calculations import npv, risk_adjust_cost\n",
|
||||
"from core.notebook_helpers import charts, display, tables"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "26f1d385",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Costs — nominal & risk-adjusted\n",
|
||||
"\n",
|
||||
"| Ref | Cost | Initial | Y1 | Y2 | Y3 | Risk Adj |\n",
|
||||
"|---|---|---|---|---|---|---|\n",
|
||||
"| Ft | Amazon Connect usage | — | $6.5M | $8.0M | $9.8M | ↑5% |\n",
|
||||
"| Gt | Implementation & migration | $1.09M | $188K | $188K | — | ↑10% |\n",
|
||||
"| Ht | Ongoing management | — | $256K | $187K | $187K | ↑15% |\n",
|
||||
"\n",
|
||||
"Note **costs are risk-adjusted *upward*** (higher risk → higher modelled cost)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "9635f334",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"df = tables.costs_table(seed_data.COSTS)\n",
|
||||
"df.style.format({c: '${:,.0f}' for c in df.columns if c not in ('field_key','label','category','risk_adjustment')})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "0667d1da",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Local validation\n",
|
||||
"\n",
|
||||
"Reproduce the **$22,983,076** Costs PV from the PDF Cash Flow Analysis."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "3e35a794",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"\n",
|
||||
"rows = []\n",
|
||||
"for c in seed_data.COSTS:\n",
|
||||
" rf = c['risk_adjustment']\n",
|
||||
" init_ra = risk_adjust_cost(c.get('initial') or 0, rf)\n",
|
||||
" yr = [c['year_values'][str(y)] for y in (1, 2, 3)]\n",
|
||||
" yr_ra = [risk_adjust_cost(v, rf) for v in yr]\n",
|
||||
" pv = npv(yr_ra, config.DISCOUNT_RATE, initial=init_ra)\n",
|
||||
" rows.append({\n",
|
||||
" 'Cost': c['label'],\n",
|
||||
" 'Initial (RA)': init_ra,\n",
|
||||
" 'Y1 (RA)': yr_ra[0],\n",
|
||||
" 'Y2 (RA)': yr_ra[1],\n",
|
||||
" 'Y3 (RA)': yr_ra[2],\n",
|
||||
" 'PV': pv,\n",
|
||||
" })\n",
|
||||
"df_check = pd.DataFrame(rows)\n",
|
||||
"totals = df_check.drop(columns='Cost').sum()\n",
|
||||
"df_check.loc[len(df_check)] = ['TOTAL'] + totals.tolist()\n",
|
||||
"df_check.style.format({c: '${:,.0f}' for c in df_check.columns if c != 'Cost'})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "4109784e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"expected_pv = 22_983_076\n",
|
||||
"computed_pv = df_check.iloc[-1]['PV']\n",
|
||||
"delta = computed_pv - expected_pv\n",
|
||||
"kind = 'success' if abs(delta) < 1_000 else 'warning'\n",
|
||||
"display.alert(\n",
|
||||
" f'Computed Costs PV: <b>${computed_pv:,.0f}</b><br>'\n",
|
||||
" f'Forrester target: <b>${expected_pv:,.0f}</b><br>'\n",
|
||||
" f'Δ = ${delta:,.0f}',\n",
|
||||
" kind,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "dd1b3c04",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Cost mix\n",
|
||||
"\n",
|
||||
"Most of the three-year cost (~90%) is Amazon Connect *usage* (Ft) —\n",
|
||||
"consistent with the PDF's framing that consumption-based pricing dominates,\n",
|
||||
"with implementation a one-time investment."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "90e9b5e2",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"charts.cost_breakdown_pie(seed_data.COSTS).show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "3d15ae10",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Push to Athena"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "03547040",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"if config.TOOL_PUBLIC_ID:\n",
|
||||
" from core.tei_client import TEIClient\n",
|
||||
"\n",
|
||||
" client = TEIClient()\n",
|
||||
" client.update_values(config.TOOL_PUBLIC_ID, seed_data.COSTS)\n",
|
||||
" display.alert(f'Pushed {len(seed_data.COSTS)} cost rows to '\n",
|
||||
" f'tool <code>{config.TOOL_PUBLIC_ID}</code>.', 'success')\n",
|
||||
"else:\n",
|
||||
" display.alert('No TOOL_PUBLIC_ID set — skipped Athena push.', 'info')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "6f5befbb",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Continue with [`03_business_case.ipynb`](03_business_case.ipynb) →"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.13.7"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
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
|
||||
}
|
||||
195
studies/202602_AmazonConnect/notebooks/04_export.ipynb
Normal file
195
studies/202602_AmazonConnect/notebooks/04_export.ipynb
Normal file
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "15a4163e",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 04 — Export for the report pipeline\n",
|
||||
"\n",
|
||||
"Build the structured JSON envelope consumed by the html2docx report\n",
|
||||
"generation pipeline (Peitho). Output goes to `exports/export.json`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "18f02ef8",
|
||||
"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,
|
||||
"id": "7d91c01d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import json\n",
|
||||
"from datetime import datetime, timezone\n",
|
||||
"\n",
|
||||
"import config\n",
|
||||
"import seed_data\n",
|
||||
"from core import __version__\n",
|
||||
"from core.calculations import apply_scenario\n",
|
||||
"from core.export.report_data import _compute_summary\n",
|
||||
"from core.notebook_helpers import display"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cff0b35b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Build the envelope\n",
|
||||
"\n",
|
||||
"Two paths:\n",
|
||||
"\n",
|
||||
"* **Live** — `core.export.build_report_data(client, public_id)` pulls\n",
|
||||
" authoritative values + summary from Athena and stamps it.\n",
|
||||
"* **Local** — when no `TOOL_PUBLIC_ID` is configured, build the envelope\n",
|
||||
" directly from `seed_data` so this notebook is always runnable."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "19416ff3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"if config.TOOL_PUBLIC_ID:\n",
|
||||
" from core.export import build_report_data\n",
|
||||
" from core.tei_client import TEIClient\n",
|
||||
"\n",
|
||||
" client = TEIClient()\n",
|
||||
" envelope = build_report_data(\n",
|
||||
" client,\n",
|
||||
" config.TOOL_PUBLIC_ID,\n",
|
||||
" include_scenarios=True,\n",
|
||||
" study_slug=config.STUDY_SLUG,\n",
|
||||
" )\n",
|
||||
" source = 'live (Athena)'\n",
|
||||
"else:\n",
|
||||
" summary = _compute_summary(\n",
|
||||
" seed_data.BENEFITS, seed_data.COSTS, config.DISCOUNT_RATE, config.ANALYSIS_YEARS\n",
|
||||
" )\n",
|
||||
" summary['roi'] = summary.get('roi_pct')\n",
|
||||
" scenarios = {}\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",
|
||||
" scenarios[name] = _compute_summary(sb, sc, config.DISCOUNT_RATE, config.ANALYSIS_YEARS)\n",
|
||||
" envelope = {\n",
|
||||
" 'metadata': {\n",
|
||||
" 'study_slug': config.STUDY_SLUG,\n",
|
||||
" 'tool_public_id': '',\n",
|
||||
" 'tool_name': 'Amazon Connect TEI (local seed)',\n",
|
||||
" 'report_name': 'Total Economic Impact™ Of Amazon Connect',\n",
|
||||
" 'report_vendor': 'AWS',\n",
|
||||
" 'report_version': '1.0',\n",
|
||||
" 'generated_at': datetime.now(timezone.utc).isoformat(),\n",
|
||||
" 'generator': f'palladium core {__version__} (offline)',\n",
|
||||
" },\n",
|
||||
" 'report': {\n",
|
||||
" 'name': 'Total Economic Impact™ Of Amazon Connect',\n",
|
||||
" 'vendor': 'AWS',\n",
|
||||
" 'version': '1.0',\n",
|
||||
" 'discount_rate': config.DISCOUNT_RATE,\n",
|
||||
" 'analysis_period_years': config.ANALYSIS_YEARS,\n",
|
||||
" },\n",
|
||||
" 'values': {'benefits': seed_data.BENEFITS, 'costs': seed_data.COSTS},\n",
|
||||
" 'summary': summary,\n",
|
||||
" 'scenarios': scenarios,\n",
|
||||
" 'assumptions': seed_data.ASSUMPTIONS,\n",
|
||||
" }\n",
|
||||
" source = 'offline seed data'\n",
|
||||
"\n",
|
||||
"display.alert(f'Envelope built from <b>{source}</b>.', 'info')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "98e94d07",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"out_path = STUDY / 'exports' / 'export.json'\n",
|
||||
"out_path.parent.mkdir(parents=True, exist_ok=True)\n",
|
||||
"out_path.write_text(json.dumps(envelope, indent=2, default=str))\n",
|
||||
"size_kb = out_path.stat().st_size / 1024\n",
|
||||
"display.alert(f'Wrote <code>{out_path.relative_to(ROOT)}</code> ({size_kb:.1f} KB).', 'success')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "d09cad64",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Envelope shape\n",
|
||||
"\n",
|
||||
"Top-level keys consumed by the report pipeline:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "841f12a1",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"for key in envelope:\n",
|
||||
" sub = envelope[key]\n",
|
||||
" if isinstance(sub, dict):\n",
|
||||
" print(f' {key}: dict with keys {list(sub.keys())}')\n",
|
||||
" elif isinstance(sub, list):\n",
|
||||
" print(f' {key}: list[{len(sub)}]')\n",
|
||||
" else:\n",
|
||||
" print(f' {key}: {type(sub).__name__}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "17d6d0ce",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Done. Hand off `exports/export.json` to **Peitho** / **html2docx** to produce the final Word report."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.13.7"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
Reference in New Issue
Block a user