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:
2026-05-20 22:28:12 -04:00
parent a6f3ee3676
commit a2420ed692
52 changed files with 35300 additions and 105 deletions

View 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
}

View 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
}

View 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
}

View 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
}