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:
71
studies/202602_AmazonConnect/README.md
Normal file
71
studies/202602_AmazonConnect/README.md
Normal 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/`.
|
||||
0
studies/202602_AmazonConnect/__init__.py
Normal file
0
studies/202602_AmazonConnect/__init__.py
Normal file
39
studies/202602_AmazonConnect/config.py
Normal file
39
studies/202602_AmazonConnect/config.py
Normal 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
|
||||
)
|
||||
Binary file not shown.
0
studies/202602_AmazonConnect/exports/.gitkeep
Normal file
0
studies/202602_AmazonConnect/exports/.gitkeep
Normal file
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
|
||||
}
|
||||
162
studies/202602_AmazonConnect/seed_data.py
Normal file
162
studies/202602_AmazonConnect/seed_data.py
Normal 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 1–3% 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
0
studies/__init__.py
Normal file
Reference in New Issue
Block a user