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,71 @@
# 202602 — Amazon Connect TEI
Self-contained TEI study folder. All data, notebooks, and exports for the
Forrester *Total Economic Impact™ Of Amazon Connect* (February 2026,
commissioned by AWS) live here.
## Source
The full Forrester study is at [`docs/202602_TEI Report Amazon Connect.pdf`](docs/202602_TEI%20Report%20Amazon%20Connect.pdf).
Key composite numbers reproduced in `seed_data.py`:
| Metric | Value |
|---|---|
| ROI | **342%** |
| NPV | **$78.7M** |
| Benefits PV | $101.7M |
| Costs PV | $23.0M |
| Payback | <6 months |
| Discount rate | 10% |
| Analysis period | 3 years |
## Composite organization
* Global B2C, ~$10B revenue (Y1), 30% YoY growth
* 2,000 contact-center agents, 200 supervisors
* 20M annual contacts (75% calls, 25% chat)
* 10-min average handle time
## Layout
```
202602_AmazonConnect/
├── README.md ← this file
├── config.py ← TOOL_PUBLIC_ID, REPORT_PUBLIC_ID, study slug
├── seed_data.py ← BENEFITS, COSTS, ASSUMPTIONS as Python dicts
├── notebooks/
│ ├── 01_benefits.ipynb ← quantify the 5 benefits, push to Athena
│ ├── 02_costs.ipynb ← quantify the 3 costs
│ ├── 03_business_case.ipynb ← /calculate, charts, scenarios
│ └── 04_export.ipynb ← /export → exports/export.json
├── exports/ ← generated; .gitignored
└── docs/
└── 202602_TEI Report Amazon Connect.pdf
```
## Workflow
1. **Set up credentials** in the project root `.env` (see `.env.example`).
2. **Create / link the TEI tool** in Athena, then put its `public_id` in
[`config.py`](config.py).
3. **Open `notebooks/01_benefits.ipynb`** and run all — pushes the 5
benefit rows from `seed_data.py` into Athena.
4. **`02_costs.ipynb`** — pushes the 3 cost rows.
5. **`03_business_case.ipynb`** — calls `/calculate`, renders the cash
flow chart, runs scenario analysis. Should reproduce the PDF's
$78.7M NPV / 342% ROI.
6. **`04_export.ipynb`** — writes `exports/export.json` for the report
pipeline.
## Adding a new study
Copy this folder, rename to `YYYYMM_<Vendor><Solution>`, and:
1. Replace `seed_data.py` with your benefits/costs.
2. Update `config.py` with the new tool/report public IDs.
3. Tweak the notebooks' narrative; the helper imports are the same.
The only thing that changes between studies is the **data** and the
**narrative prose** in the notebooks. All math, charts, and API calls
come from `core/`.

View File

View File

@@ -0,0 +1,39 @@
"""
Study configuration for the Amazon Connect TEI (February 2026).
Set ``TOOL_PUBLIC_ID`` to the public_id of the live TEI tool instance in
Athena once it has been created. ``REPORT_PUBLIC_ID`` is the template
this tool was created from (Athena admin sets up Report templates).
Until both are filled in, the notebooks fall back to local-only mode:
they compute summaries from ``seed_data.py`` using ``core.calculations``
and skip the network round-trip.
"""
from __future__ import annotations
import os
#: Human-friendly study identifier — used in export metadata + filenames.
STUDY_SLUG = "202602_AmazonConnect"
#: TEI Report template public_id (12-char short UUID). Provisioned in
#: Athena admin → TEI → Reports.
REPORT_PUBLIC_ID: str = os.getenv("PALLADIUM_REPORT_PUBLIC_ID", "")
#: TEI Tool instance public_id. Created via the API
#: (``client.create_tool``) or the Streamlit app sidebar.
TOOL_PUBLIC_ID: str = os.getenv("PALLADIUM_TOOL_PUBLIC_ID", "")
#: Default discount rate used for local validation of the study numbers.
DISCOUNT_RATE = 0.10
#: Analysis horizon (years).
ANALYSIS_YEARS = 3
#: Optional Athena Proposal ID this tool is linked to (when known).
PROPOSAL_ID: int | None = (
int(os.environ["PALLADIUM_PROPOSAL_ID"])
if os.getenv("PALLADIUM_PROPOSAL_ID")
else None
)

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
}

View File

@@ -0,0 +1,162 @@
"""
Seed dataset for the Amazon Connect TEI (Forrester, Feb 2026).
Each row matches the wire shape produced by
``core.tei_client.TEIClient._normalize_value`` so it can be passed
straight to ``client.update_values(public_id, BENEFITS + COSTS)``.
Numbers are the *nominal* (pre-risk-adjustment) values from the PDF —
risk adjustment is stored as a factor and applied by Athena's
calculator (or, locally, by ``core.calculations.risk_adjust_*``).
References for the totals (from the PDF):
Benefits (3-yr risk-adjusted PV @ 10%): $101,696,791
Costs (3-yr risk-adjusted PV @ 10%): $ 22,983,076
NPV $ 78,713,715
ROI 342%
Payback <6 months
"""
from __future__ import annotations
#: 3-year nominal benefit cashflows. Risk adjustment factor is stored
#: separately; calculator applies it.
BENEFITS: list[dict] = [
{
"field_key": "ai_contact_resolution",
"table": "benefits",
"label": "AI-driven contact resolution efficiency",
"category": "Productivity",
"year_values": {"1": 13_911_040, "2": 23_932_480, "3": 37_797_760},
"risk_adjustment": 0.15,
"notes": (
"PDF Section At/Atr. Composite: 20M annual contacts, 30% YoY "
"growth, 75% calls, 10-min AHT with legacy. Connect drops AHT "
"12% Y1 and shifts traffic to chat/self-service. 80% "
"productivity recapture. Risk adj 15% (legacy performance, "
"implementation depth, integration scope, growth)."
),
},
{
"field_key": "ai_content_sentiment",
"table": "benefits",
"label": "AI-powered content and sentiment analysis savings",
"category": "Productivity",
"year_values": {"1": 4_586_620, "2": 5_358_412, "3": 6_291_680},
"risk_adjustment": 0.15,
"notes": (
"PDF Section Bt/Btr. Auto post-contact summaries reclaim ~60s "
"per call; QA scaled from 13% to 100%; supervisors freed from "
"manual review. Risk adj 15%."
),
},
{
"field_key": "ai_forecasting_supervision",
"table": "benefits",
"label": "AI-enabled forecasting, agent scheduling, and supervision",
"category": "Productivity",
"year_values": {"1": 6_651_680, "2": 9_133_760, "3": 12_391_712},
"risk_adjustment": 0.15,
"notes": (
"PDF Section Ct/Ctr. ML-WFM yields 5% agent FTE optimization "
"and supervisors managing 20% more agents (10→12). 80% "
"productivity recapture. Risk adj 15%."
),
},
{
"field_key": "data_driven_profit_lift",
"table": "benefits",
"label": "Data-driven profit lift with increased conversion",
"category": "Revenue",
"year_values": {"1": 1_200_000, "2": 1_560_000, "3": 2_028_000},
"risk_adjustment": 0.20,
"notes": (
"PDF Section Dt/Dtr. Composite revenue $10B Y1 (+30% YoY); "
"5% from outbound contact-center marketing; conversion lifts "
"from 10% to 12% (+20% relative); 12% operating margin. "
"Risk adj 20%."
),
},
{
"field_key": "legacy_solution_savings",
"table": "benefits",
"label": "Legacy solution cost savings",
"category": "Cost Savings",
"year_values": {"1": 6_177_600, "2": 8_030_880, "3": 10_440_144},
"risk_adjustment": 0.20,
"notes": (
"PDF Section Et/Etr. Avg legacy license $180/agent-month × "
"(agents+supervisors) × 12, plus 30% overhead for infra & "
"third-party tools. Risk adj 20%."
),
},
]
#: Costs include an "initial" (year-0, undiscounted) component for
#: implementation. Cost risk adjustments are applied *upward*.
COSTS: list[dict] = [
{
"field_key": "amazon_connect_usage",
"table": "costs",
"label": "Amazon Connect usage cost",
"category": "Subscription",
"initial": 0,
"year_values": {"1": 6_456_448, "2": 7_951_164, "3": 9_832_961},
"risk_adjustment": 0.05,
"notes": (
"PDF Section Ft/Ftr. Telephony $0.0106/min + Unlimited AI "
"$0.0380/min on minutes that reach an agent, plus chat at "
"$0.0100/message (10 messages/chat). Risk adj 5%."
),
},
{
"field_key": "implementation_migration",
"table": "costs",
"label": "Implementation and migration cost",
"category": "Implementation",
"initial": 1_087_500,
"year_values": {"1": 188_333, "2": 188_333, "3": 0},
"risk_adjustment": 0.10,
"notes": (
"PDF Section Gt/Gtr. 6-month initial migration: 5 internal "
"FTE @ $115k + $800k pro-services. Y1/Y2 M&A integrations: 2 "
"months × 2 FTE + $150k pro-services. Risk adj 10%."
),
},
{
"field_key": "ongoing_management",
"table": "costs",
"label": "Ongoing management",
"category": "Operations",
"initial": 0,
"year_values": {"1": 256_200, "2": 187_200, "3": 187_200},
"risk_adjustment": 0.15,
"notes": (
"PDF Section Ht/Htr. Y1: 5 IT/PM @ 30% × $115k + 5 business "
"users @ 30% × $55,800. Y2/Y3: 3 IT/PM @ 30% + 5 business "
"users @ 30%. Risk adj 15%."
),
},
]
#: Top-line composite assumptions — for the 03_business_case narrative.
ASSUMPTIONS: dict = {
"agents_fte": 2_000,
"supervisors_fte": 200,
"annual_contacts_y1": 20_000_000,
"growth_rate": 0.30,
"call_share": 0.75,
"aht_legacy_minutes": 10,
"agent_salary": 45_760,
"supervisor_salary": 55_800,
"discount_rate": 0.10,
"analysis_years": 3,
}
def all_values() -> list[dict]:
"""Return BENEFITS + COSTS — handy single-call payload for update_values."""
return BENEFITS + COSTS

0
studies/__init__.py Normal file
View File