Files
palladium/studies/202512_TEI_Genesys_CX_Cloud/notebooks/business_case.ipynb
Robert Helewka e88449d15a Migrate Genesys CX Cloud TEI study to the pattern; retire Streamlit app
studies/202512_GenesysCX -> studies/202512_TEI_Genesys_CX_Cloud,
rebuilt as pattern Variant 4 (TEI composite reproduction):

- teicalc/ self-contained engine: Forrester's tables as the never-edited
  verbatim anchor (incl. the p.14 typo note and the $0 AI-token line),
  generic model/scenarios/staging carried over from the Amazon Connect
  study, ClientDrivers overlay (agents / weekly interactions / revenue,
  flat composite so no growth re-base) with ai_tokens_annual as a direct
  input for the token line the published study models at $0
- one deliverable notebook (business_case.ipynb): widget-pair sidebar
  drivers incl. the AI-token price, published-vs-overlay KPI columns,
  cash-flow/waterfall/scenario charts, verification gate, backstage JSON
  data appendix
- gate + tests reproduce the published totals within $2: NPV $10.8M /
  ROI 266% (engine $10,783,466 / 265.79%; payback 3.3 months, not
  headlined in the PDF); 29 study tests, headless nbconvert green,
  stage simulation leak-free, exports carry the appendix
- old Athena workflow (00_provision..04_export, config.py, seed_data.py,
  PALLADIUM_GENESYSCX_* keys, ATHENA_EXPECTED reconciliation) deleted;
  git history preserves it

With the last legacy study migrated, the retirement lands too:
- app/ (Streamlit UI) and core/notebook_helpers deleted; nothing else
  imported them
- streamlit stripped from pyproject extras, requirements.txt, Makefile;
  .env.example reduced to the Athena keys; 00_setup.ipynb and
  core/bootstrap.py repointed at the pattern studies
- root README reworked: self-contained studies + slim core/ Athena
  toolkit (tei_client, calculations, export, cli)

All suites green: Genesys 29, Amazon Connect 27, CTM 55, template 7,
root 58.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:38:51 -04:00

7134 lines
268 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"cells": [
{
"cell_type": "markdown",
"id": "cell-0",
"metadata": {},
"source": [
"# Genesys CX Cloud TEI — Business Case\n",
"\n",
"Reproduction of Forrester's *The Total Economic Impact™ Of CX Cloud — Cost\n",
"Savings And Business Benefits Enabled By Genesys And Salesforce* (December\n",
"2025, commissioned by Genesys and Salesforce) — and a live personalization\n",
"of it. The published composite organization is the **verbatim anchor**\n",
"(never edited); the verification gate proves this notebook reproduces the\n",
"published **$10.8M NPV · 266% ROI**; the client drivers then rescale the\n",
"composite to your organization.\n",
"\n",
"One line deserves special attention: the published study models **$0 of\n",
"Genesys AI Experience token consumption**, even though three of its four\n",
"benefits depend on AI capabilities that Genesys bills via tokens. That line\n",
"is anchored at $0 (so the reproduction matches the PDF) and exposed as a\n",
"direct 🔴 input — price it from the Genesys quote and watch the case move.\n",
"\n",
"**This notebook is the deliverable** — served interactively with Mercury,\n",
"exported via nbconvert as the report source (Mercury Notebook Pattern,\n",
"Variant 4).\n",
"\n",
"| Layer | What it is | Confidence |\n",
"|---|---|---|\n",
"| Verbatim anchor | Forrester's composite tables, unedited | 🟢 published |\n",
"| Client overlay | first-order linear rescale by your drivers | 🟡 estimated |\n",
"| AI-token line | negotiated annual quote figure, direct input | 🔴 unknown until quoted |\n",
"| Scenario | adoption × risk stress | 🟡 estimated |\n",
"\n",
"Confidence legend: 🟢 confirmed/published · 🟡 estimated (stated assumption) · 🔴 unknown (flagged)\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "cell-1",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:18.686288Z",
"iopub.status.busy": "2026-07-09T19:15:18.686041Z",
"iopub.status.idle": "2026-07-09T19:15:19.083373Z",
"shell.execute_reply": "2026-07-09T19:15:19.082724Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"teicalc loaded — window 20262028 · published NPV $10.8M · ROI 266% · AI-token line anchored at $0\n"
]
}
],
"source": [
"# ── Setup ──────────────────────────────────────────────────────────\n",
"import sys, pathlib\n",
"_ROOT = pathlib.Path.cwd()\n",
"if not (_ROOT / \"teicalc\").exists(): # notebook lives in notebooks/\n",
" _ROOT = _ROOT.parent\n",
"sys.path.insert(0, str(_ROOT))\n",
"\n",
"import pandas as pd\n",
"import plotly.graph_objects as go\n",
"\n",
"import mercury as mr\n",
"\n",
"# Single source of truth — all math lives in the study package; only\n",
"# presentation (and Mercury input widgets) lives here.\n",
"from teicalc import (\n",
" ASSUMPTIONS, BENEFITS_VERBATIM, COSTS_VERBATIM, PUBLISHED,\n",
" YEARS, X_LABELS,\n",
" BENEFIT_DRIVERS, COST_DRIVERS, COMPOSITE, ClientDrivers,\n",
" SCENARIOS, apply_scenario, compute_summary,\n",
" money, html_money, overlay_rows,\n",
")\n",
"from teicalc.staging import backstage\n",
"\n",
"pd.options.display.float_format = \"{:,.0f}\".format\n",
"\n",
"# ── Chart chrome (dataviz reference palette, light surface) ─────────\n",
"INK, INK2, MUTED = \"#0b0b0b\", \"#52514e\", \"#898781\"\n",
"SURFACE, GRID, BASELINE = \"#fcfcfb\", \"#e1e0d9\", \"#c3c2b7\"\n",
"CUMULATIVE, CONTEXT = \"#52514e\", \"#c3c2b7\" # neutral line; de-emphasized context series\n",
"FONT_STACK = 'system-ui, -apple-system, \"Segoe UI\", sans-serif'\n",
"\n",
"# Fixed row colors — color follows the entity across every figure.\n",
"BENEFIT_COLOR = {\n",
" \"legacy_retirement\": \"#2a78d6\", # blue\n",
" \"self_service_savings\": \"#1baf7a\", # aqua\n",
" \"agent_efficiency\": \"#4a3aa7\", # violet\n",
" \"agent_assist_sales\": \"#eda100\", # yellow\n",
"}\n",
"COST_COLOR = {\n",
" \"cx_cloud_licenses\": \"#2a78d6\", # blue\n",
" \"implementation\": \"#eda100\", # yellow\n",
" \"ongoing_management\": \"#4a3aa7\", # violet\n",
" \"genesys_ai_tokens\": \"#e34948\", # red — the flagged $0 line\n",
"}\n",
"BEN_TOTAL, COST_TOTAL, NPV_COLOR = \"#1baf7a\", \"#e34948\", \"#2a78d6\"\n",
"\n",
"\n",
"def tei_layout(fig, title, subtitle=None, height=460):\n",
" t = f\"<b>{title}</b>\"\n",
" if subtitle:\n",
" t += f\"<br><span style='font-size:12px;color:{MUTED}'>{subtitle}</span>\"\n",
" fig.update_layout(\n",
" title=dict(text=t, font=dict(size=16, color=INK), x=0.02, xanchor=\"left\"),\n",
" paper_bgcolor=SURFACE, plot_bgcolor=SURFACE,\n",
" font=dict(family=FONT_STACK, size=12, color=INK2),\n",
" legend=dict(orientation=\"h\", yanchor=\"top\", y=-0.10, x=0,\n",
" font=dict(size=11, color=INK2)),\n",
" xaxis=dict(type=\"category\", showgrid=False, linecolor=BASELINE,\n",
" tickfont=dict(color=MUTED)),\n",
" yaxis=dict(gridcolor=GRID, zerolinecolor=BASELINE, zerolinewidth=1.5,\n",
" tickformat=\"$~s\", tickfont=dict(color=MUTED)),\n",
" hovermode=\"x unified\", bargap=0.45, height=height,\n",
" margin=dict(t=70, r=30, b=80, l=70),\n",
" )\n",
" return fig\n",
"\n",
"\n",
"def bar(x, y, name, color):\n",
" return go.Bar(x=x, y=y, name=name,\n",
" marker=dict(color=color, line=dict(width=2, color=SURFACE)),\n",
" hovertemplate=\"%{fullData.name}: %{y:$,.0f}<extra></extra>\")\n",
"\n",
"\n",
"def cum_line(x, y, name, color=CUMULATIVE, dash=None):\n",
" return go.Scatter(x=x, y=y, name=name, mode=\"lines+markers\",\n",
" line=dict(color=color, width=2, dash=dash),\n",
" marker=dict(size=8, line=dict(width=2, color=SURFACE)),\n",
" hovertemplate=\"%{fullData.name}: %{y:$,.0f}<extra></extra>\")\n",
"\n",
"\n",
"backstage(f\"teicalc loaded — window {YEARS[0]}{YEARS[-1]} · published \"\n",
" f\"NPV {money(PUBLISHED['npv'])} · ROI {PUBLISHED['roi_pct']}% · \"\n",
" f\"AI-token line anchored at $0\")\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "cell-2",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:19.085680Z",
"iopub.status.busy": "2026-07-09T19:15:19.085417Z",
"iopub.status.idle": "2026-07-09T19:15:19.097643Z",
"shell.execute_reply": "2026-07-09T19:15:19.096879Z"
}
},
"outputs": [
{
"data": {
"application/mercury+json": {
"model_id": "c7bf126b79ee49b2bb7140ec4fb10b96",
"position": "sidebar",
"widget": "MarkdownWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "c7bf126b79ee49b2bb7140ec4fb10b96",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"MarkdownWidget(value='<div style=\"font-family: ui-sans-serif, system-ui, -apple-system, \\'Segoe UI\\', Roboto, …"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# ── Jump-to-section ToC (Mercury sidebar — widgets only, no output) ──\n",
"# Fragment links don't scroll inside Mercury's app shell (SPA base URL),\n",
"# so each entry scrolls via JS: anchor id first, heading-text fallback.\n",
"_TOC = [\n",
" (1, \"Composite organization\"),\n",
" (2, \"Client inputs\"),\n",
" (3, \"Benefits\"),\n",
" (4, \"Costs & the $0 AI line\"),\n",
" (5, \"Business case\"),\n",
" (6, \"Scenarios\"),\n",
" (7, \"Verification & assertions\"),\n",
" (8, \"Data appendix\"),\n",
"]\n",
"# NB: no raw \"<\" allowed inside the handler — python-markdown escapes the\n",
"# whole tag if the attribute text looks like malformed HTML.\n",
"_JS = (\"var el=document.getElementById('section-{n}');\"\n",
" \"if(!el){{document.querySelectorAll('h1,h2').forEach(function(h){{\"\n",
" \"if(!el&&h.textContent.trim().indexOf('{n} ')===0){{el=h;}}}});}}\"\n",
" \"if(el)el.scrollIntoView({{behavior:'smooth',block:'start'}});\")\n",
"_items = \"\".join(\n",
" f'<li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline\"'\n",
" f' onclick=\"{_JS.format(n=n)}\">{label}</a></li>'\n",
" for n, label in _TOC)\n",
"_toc = mr.Markdown(\n",
" text=(f'<b>Jump to section</b>'\n",
" f'<ol style=\"padding-left:1.2em;margin:6px 0\">{_items}</ol>'),\n",
" position=\"sidebar\")\n"
]
},
{
"cell_type": "markdown",
"id": "cell-3",
"metadata": {},
"source": [
"<a id=\"section-1\"></a>\n",
"## 1 · The Forrester composite (verbatim anchor 🟢)\n",
"\n",
"Forrester's composite organization: a **global supply company with $2.5B\n",
"revenue and 10,000 employees**, running **600 CX agents** (400 concurrent\n",
"licenses) who handle **80,000 weekly interactions** averaging 12 minutes.\n",
"\n",
"TEI methodology, carried verbatim into the engine: benefits are\n",
"risk-adjusted **down** (×(1rf)), costs **up** (×(1+rf)); the initial\n",
"investment sits at time 0 undiscounted; year flows discount at end-of-year\n",
"(10%, 3 years). Two source footnotes preserved in the anchor: the p.14\n",
"Total Costs table's $1,304,600 implementation figure is a typo (the detail\n",
"table and cash-flow analysis give $1,309,000), and the study models **$0\n",
"AI-token consumption** (§4).\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "cell-4",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:19.100446Z",
"iopub.status.busy": "2026-07-09T19:15:19.100167Z",
"iopub.status.idle": "2026-07-09T19:15:19.119649Z",
"shell.execute_reply": "2026-07-09T19:15:19.118932Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Value 🟢</th>\n",
" </tr>\n",
" <tr>\n",
" <th>Assumption</th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>annual revenue</th>\n",
" <td>$2,500,000,000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>employees</th>\n",
" <td>10,000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>agents fte</th>\n",
" <td>600</td>\n",
" </tr>\n",
" <tr>\n",
" <th>concurrent licenses</th>\n",
" <td>400</td>\n",
" </tr>\n",
" <tr>\n",
" <th>weekly interactions</th>\n",
" <td>80,000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>interaction minutes</th>\n",
" <td>12 min</td>\n",
" </tr>\n",
" <tr>\n",
" <th>self service rate before</th>\n",
" <td>15%</td>\n",
" </tr>\n",
" <tr>\n",
" <th>self service rate after</th>\n",
" <td>25%</td>\n",
" </tr>\n",
" <tr>\n",
" <th>mttr saved minutes</th>\n",
" <td>2 min</td>\n",
" </tr>\n",
" <tr>\n",
" <th>agent hourly rate</th>\n",
" <td>$28/hr</td>\n",
" </tr>\n",
" <tr>\n",
" <th>agent annual salary</th>\n",
" <td>$58,240</td>\n",
" </tr>\n",
" <tr>\n",
" <th>revenue impacted</th>\n",
" <td>$500,000,000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>revenue lift</th>\n",
" <td>1.5%</td>\n",
" </tr>\n",
" <tr>\n",
" <th>gross margin</th>\n",
" <td>8%</td>\n",
" </tr>\n",
" <tr>\n",
" <th>discount rate</th>\n",
" <td>10%</td>\n",
" </tr>\n",
" <tr>\n",
" <th>analysis years</th>\n",
" <td>3 years</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Value 🟢\n",
"Assumption \n",
"annual revenue $2,500,000,000\n",
"employees 10,000\n",
"agents fte 600\n",
"concurrent licenses 400\n",
"weekly interactions 80,000\n",
"interaction minutes 12 min\n",
"self service rate before 15%\n",
"self service rate after 25%\n",
"mttr saved minutes 2 min\n",
"agent hourly rate $28/hr\n",
"agent annual salary $58,240\n",
"revenue impacted $500,000,000\n",
"revenue lift 1.5%\n",
"gross margin 8%\n",
"discount rate 10%\n",
"analysis years 3 years"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Published 🟢</th>\n",
" </tr>\n",
" <tr>\n",
" <th>Metric</th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>Benefits PV (risk-adjusted)</th>\n",
" <td>$14,840,638</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Costs PV (risk-adjusted)</th>\n",
" <td>$4,057,170</td>\n",
" </tr>\n",
" <tr>\n",
" <th>NPV</th>\n",
" <td>$10,783,468</td>\n",
" </tr>\n",
" <tr>\n",
" <th>ROI</th>\n",
" <td>266%</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Payback</th>\n",
" <td>not headlined (engine: 3.3 months)</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Published 🟢\n",
"Metric \n",
"Benefits PV (risk-adjusted) $14,840,638\n",
"Costs PV (risk-adjusted) $4,057,170\n",
"NPV $10,783,468\n",
"ROI 266%\n",
"Payback not headlined (engine: 3.3 months)"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"engine reproduction Δ vs PDF: benefits -1.19 · costs +0.40 · npv -1.58\n"
]
}
],
"source": [
"# ── Composite assumptions & published financial summary (🟢) ─────────\n",
"composite = compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM,\n",
" PUBLISHED[\"discount_rate\"])\n",
"\n",
"_fmt = {\n",
" \"annual_revenue\": \"${:,}\", \"employees\": \"{:,}\", \"agents_fte\": \"{:,}\",\n",
" \"concurrent_licenses\": \"{:,}\", \"weekly_interactions\": \"{:,}\",\n",
" \"interaction_minutes\": \"{} min\", \"self_service_rate_before\": \"{:.0%}\",\n",
" \"self_service_rate_after\": \"{:.0%}\", \"mttr_saved_minutes\": \"{} min\",\n",
" \"agent_hourly_rate\": \"${}/hr\", \"agent_annual_salary\": \"${:,}\",\n",
" \"revenue_impacted\": \"${:,}\", \"revenue_lift\": \"{:.1%}\",\n",
" \"gross_margin\": \"{:.0%}\", \"discount_rate\": \"{:.0%}\",\n",
" \"analysis_years\": \"{} years\",\n",
"}\n",
"assumptions_df = pd.DataFrame(\n",
" [{\"Assumption\": k.replace(\"_\", \" \"), \"Value 🟢\": _fmt[k].format(v)}\n",
" for k, v in ASSUMPTIONS.items()])\n",
"display(assumptions_df.set_index(\"Assumption\"))\n",
"\n",
"published_df = pd.DataFrame([\n",
" {\"Metric\": \"Benefits PV (risk-adjusted)\", \"Published 🟢\": f\"${PUBLISHED['benefits_pv']:,}\"},\n",
" {\"Metric\": \"Costs PV (risk-adjusted)\", \"Published 🟢\": f\"${PUBLISHED['costs_pv']:,}\"},\n",
" {\"Metric\": \"NPV\", \"Published 🟢\": f\"${PUBLISHED['npv']:,}\"},\n",
" {\"Metric\": \"ROI\", \"Published 🟢\": f\"{PUBLISHED['roi_pct']}%\"},\n",
" {\"Metric\": \"Payback\", \"Published 🟢\": \"not headlined (engine: 3.3 months)\"},\n",
"])\n",
"display(published_df.set_index(\"Metric\"))\n",
"\n",
"backstage(f\"engine reproduction Δ vs PDF: \"\n",
" f\"benefits {composite['benefits_pv'] - PUBLISHED['benefits_pv']:+,.2f} · \"\n",
" f\"costs {composite['costs_pv'] - PUBLISHED['costs_pv']:+,.2f} · \"\n",
" f\"npv {composite['npv'] - PUBLISHED['npv']:+,.2f}\")\n"
]
},
{
"cell_type": "markdown",
"id": "cell-5",
"metadata": {},
"source": [
"<a id=\"section-2\"></a>\n",
"## 2 · Client inputs (overlay 🟡)\n",
"\n",
"The overlay is a **first-order linear rescale** of Forrester's composite —\n",
"it answers *\"what does the composite look like at your size?\"*, not *\"what\n",
"is your TEI?\"*. Each published row scales with the driver that dominates\n",
"its derivation in the PDF; project-based costs stay fixed. This composite's\n",
"trajectory is flat (Y2 = Y3), so there is no growth re-base.\n",
"\n",
"| Published row | Scales with | Confidence |\n",
"|---|---|---|\n",
"| Retirement of legacy systems | agents | 🟡 seat-scoped platform costs |\n",
"| Self-service savings | interactions | 🟡 |\n",
"| CX agent efficiency gains | interactions | 🟡 |\n",
"| Incremental sales from agent assist | revenue | 🟡 |\n",
"| CX Cloud licenses | agents | 🟡 |\n",
"| Implementation & deployment | fixed | 🟡 project-based |\n",
"| Ongoing management | fixed | 🟡 |\n",
"| **Genesys AI Experience tokens** | **direct $/yr input** | 🔴 $0 until quoted |\n",
"\n",
"*Change any input in the sidebar — every table, figure and KPI below\n",
"recomputes. The assertions in §7 hold at any setting.*\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "cell-6",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:19.121610Z",
"iopub.status.busy": "2026-07-09T19:15:19.121442Z",
"iopub.status.idle": "2026-07-09T19:15:19.139328Z",
"shell.execute_reply": "2026-07-09T19:15:19.138672Z"
}
},
"outputs": [
{
"data": {
"application/mercury+json": {
"model_id": "b2751bf42368472ebf73f08317e9cde9",
"position": "sidebar",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "b2751bf42368472ebf73f08317e9cde9",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x7888c41e12b0>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "2247afa5bfb04ad8905333e3f64caf67",
"position": "sidebar",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "2247afa5bfb04ad8905333e3f64caf67",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x7888c41d9310>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "55da27f443ca4021895933ad5d96f23b",
"position": "sidebar",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "55da27f443ca4021895933ad5d96f23b",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x7888c41d8f50>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "9173bebf5a34432e8e4819f906db4add",
"position": "sidebar",
"widget": "NumberInputWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "9173bebf5a34432e8e4819f906db4add",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.number.NumberInputWidget object at 0x78886eafd940>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "76c10bd18e9c466daec54721bc92b435",
"position": "sidebar",
"widget": "SelectWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "76c10bd18e9c466daec54721bc92b435",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.select.SelectWidget object at 0x7888c41e16a0>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/mercury+json": {
"model_id": "ce038c3719d240a1a635401ac4a6208e",
"position": "sidebar",
"widget": "SelectWidget"
},
"application/vnd.jupyter.widget-view+json": {
"model_id": "ce038c3719d240a1a635401ac4a6208e",
"version_major": 2,
"version_minor": 1
},
"text/plain": [
"<mercury.select.SelectWidget object at 0x7888c41d9950>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# ── Client drivers (Mercury sidebar — widgets only, NO other output) ─\n",
"# NB: Mercury re-executes only cells BELOW a changed widget's cell, so\n",
"# this cell constructs widgets ONLY — .value is read downstream.\n",
"_agents_w = mr.NumberInput(label=\"CX agents (FTE) — composite 600\",\n",
" value=600, min=25, max=20_000, step=25)\n",
"_interactions_w = mr.NumberInput(label=\"Weekly interactions — composite 80,000\",\n",
" value=80_000, min=1_000, max=2_000_000,\n",
" step=5_000)\n",
"_revenue_w = mr.NumberInput(label=\"Annual revenue ($) — composite 2.5B\",\n",
" value=2_500_000_000, min=100_000_000,\n",
" max=100_000_000_000, step=100_000_000)\n",
"_tokens_w = mr.NumberInput(label=\"Genesys AI tokens ($/yr) — study modeled $0\",\n",
" value=0, min=0, max=5_000_000, step=25_000)\n",
"_discount_w = mr.Select(label=\"Discount rate\", value=\"10% (Forrester)\",\n",
" choices=[\"8%\", \"10% (Forrester)\", \"12%\"])\n",
"_scenario_w = mr.Select(label=\"Scenario\", value=\"moderate\",\n",
" choices=[\"conservative\", \"moderate\", \"aggressive\"])\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "cell-7",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:19.141205Z",
"iopub.status.busy": "2026-07-09T19:15:19.141026Z",
"iopub.status.idle": "2026-07-09T19:15:19.147273Z",
"shell.execute_reply": "2026-07-09T19:15:19.146674Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Client frame: 600 agents · 80,000 weekly interactions · moderate scenario → NPV $10.8M · ROI 266% · payback 3.3 months (~Apr 2026)\n",
"scale factors — agents 1.00× · interactions 1.00× · revenue 1.00×\n"
]
}
],
"source": [
"# ── Client overlay state (re-runs on any change to the widgets above) ─\n",
"AGENTS_FTE = int(_agents_w.value)\n",
"WEEKLY_INTERACTIONS = int(_interactions_w.value)\n",
"ANNUAL_REVENUE = float(_revenue_w.value)\n",
"AI_TOKENS_ANNUAL = float(_tokens_w.value)\n",
"DISCOUNT_RATE = {\"8%\": 0.08, \"10% (Forrester)\": 0.10,\n",
" \"12%\": 0.12}[str(_discount_w.value)]\n",
"SCENARIO = str(_scenario_w.value)\n",
"\n",
"DRIVERS = ClientDrivers(agents_fte=AGENTS_FTE,\n",
" weekly_interactions=WEEKLY_INTERACTIONS,\n",
" annual_revenue=ANNUAL_REVENUE,\n",
" ai_tokens_annual=AI_TOKENS_ANNUAL,\n",
" discount_rate=DISCOUNT_RATE)\n",
"overlay_benefits, overlay_costs = overlay_rows(DRIVERS)\n",
"client_benefits = apply_scenario(overlay_benefits, SCENARIO)\n",
"client_costs = apply_scenario(overlay_costs, SCENARIO)\n",
"client = compute_summary(client_benefits, client_costs, DISCOUNT_RATE)\n",
"\n",
"_at_default = (DRIVERS == COMPOSITE and SCENARIO == \"moderate\")\n",
"\n",
"_tok = f\" · AI tokens {money(AI_TOKENS_ANNUAL)}/yr\" if AI_TOKENS_ANNUAL else \"\"\n",
"print(f\"Client frame: {AGENTS_FTE:,} agents · {WEEKLY_INTERACTIONS:,} weekly \"\n",
" f\"interactions{_tok} · {SCENARIO} scenario → NPV {money(client['npv'])} · \"\n",
" f\"ROI {client['roi_pct']:.0f}% · payback {client['payback_label']}\")\n",
"backstage(f\"scale factors — agents {AGENTS_FTE / ASSUMPTIONS['agents_fte']:.2f}× · \"\n",
" f\"interactions {WEEKLY_INTERACTIONS / ASSUMPTIONS['weekly_interactions']:.2f}× · \"\n",
" f\"revenue {ANNUAL_REVENUE / ASSUMPTIONS['annual_revenue']:.2f}×\")\n"
]
},
{
"cell_type": "markdown",
"id": "cell-8",
"metadata": {},
"source": [
"<a id=\"section-3\"></a>\n",
"## 3 · Benefits\n",
"\n",
"Four benefit streams (Forrester refs AD), risk-adjusted down 515%.\n",
"Agent efficiency and self-service deflection carry ~three-quarters of the\n",
"total; legacy retirement ramps as the old telephony sunsets mid-year-1;\n",
"the profile is otherwise flat.\n"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "cell-9",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:19.149355Z",
"iopub.status.busy": "2026-07-09T19:15:19.149198Z",
"iopub.status.idle": "2026-07-09T19:15:19.161479Z",
"shell.execute_reply": "2026-07-09T19:15:19.160698Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Driver</th>\n",
" <th>Risk adj</th>\n",
" <th>2026</th>\n",
" <th>2027</th>\n",
" <th>2028</th>\n",
" <th>3-yr RA</th>\n",
" <th>PV</th>\n",
" </tr>\n",
" <tr>\n",
" <th>Benefit</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>Retirement of legacy systems with CX Cloud adoption</th>\n",
" <td>agents</td>\n",
" <td>-5%</td>\n",
" <td>646,000</td>\n",
" <td>883,500</td>\n",
" <td>883,500</td>\n",
" <td>2,413,000</td>\n",
" <td>1,981,225</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Cost savings from reallocated workers and avoided seasonal hires with increased customer self-service</th>\n",
" <td>interactions</td>\n",
" <td>-15%</td>\n",
" <td>1,980,160</td>\n",
" <td>1,980,160</td>\n",
" <td>1,980,160</td>\n",
" <td>5,940,480</td>\n",
" <td>4,924,365</td>\n",
" </tr>\n",
" <tr>\n",
" <th>CX agent efficiency gains</th>\n",
" <td>interactions</td>\n",
" <td>-10%</td>\n",
" <td>2,620,800</td>\n",
" <td>2,620,800</td>\n",
" <td>2,620,800</td>\n",
" <td>7,862,400</td>\n",
" <td>6,517,542</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Incremental sales from agent assist capabilities</th>\n",
" <td>revenue</td>\n",
" <td>-5%</td>\n",
" <td>570,000</td>\n",
" <td>570,000</td>\n",
" <td>570,000</td>\n",
" <td>1,710,000</td>\n",
" <td>1,417,506</td>\n",
" </tr>\n",
" <tr>\n",
" <th>TOTAL</th>\n",
" <td></td>\n",
" <td></td>\n",
" <td>5,816,960</td>\n",
" <td>6,054,460</td>\n",
" <td>6,054,460</td>\n",
" <td>17,925,880</td>\n",
" <td>14,840,637</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Driver Risk adj \\\n",
"Benefit \n",
"Retirement of legacy systems with CX Cloud adop... agents -5% \n",
"Cost savings from reallocated workers and avoid... interactions -15% \n",
"CX agent efficiency gains interactions -10% \n",
"Incremental sales from agent assist capabilities revenue -5% \n",
"TOTAL \n",
"\n",
" 2026 2027 \\\n",
"Benefit \n",
"Retirement of legacy systems with CX Cloud adop... 646,000 883,500 \n",
"Cost savings from reallocated workers and avoid... 1,980,160 1,980,160 \n",
"CX agent efficiency gains 2,620,800 2,620,800 \n",
"Incremental sales from agent assist capabilities 570,000 570,000 \n",
"TOTAL 5,816,960 6,054,460 \n",
"\n",
" 2028 3-yr RA \\\n",
"Benefit \n",
"Retirement of legacy systems with CX Cloud adop... 883,500 2,413,000 \n",
"Cost savings from reallocated workers and avoid... 1,980,160 5,940,480 \n",
"CX agent efficiency gains 2,620,800 7,862,400 \n",
"Incremental sales from agent assist capabilities 570,000 1,710,000 \n",
"TOTAL 6,054,460 17,925,880 \n",
"\n",
" PV \n",
"Benefit \n",
"Retirement of legacy systems with CX Cloud adop... 1,981,225 \n",
"Cost savings from reallocated workers and avoid... 4,924,365 \n",
"CX agent efficiency gains 6,517,542 \n",
"Incremental sales from agent assist capabilities 1,417,506 \n",
"TOTAL 14,840,637 "
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# ── Benefits table — client overlay, risk-adjusted ───────────────────\n",
"_ben_rows = client[\"rows\"][\"benefits\"]\n",
"benefits_df = pd.DataFrame([{\n",
" \"Benefit\": r[\"label\"],\n",
" \"Driver\": BENEFIT_DRIVERS[r[\"field_key\"]],\n",
" \"Risk adj\": f\"-{r['risk_adjustment']:.0%}\",\n",
" **{str(y): r[\"ra_by_year\"][y] for y in YEARS},\n",
" \"3-yr RA\": r[\"three_yr_ra\"],\n",
" \"PV\": r[\"pv\"],\n",
"} for r in _ben_rows]).set_index(\"Benefit\")\n",
"benefits_df.loc[\"TOTAL\"] = [\"\", \"\"] + [client[\"benefits_by_year\"][y] for y in YEARS] \\\n",
" + [sum(r[\"three_yr_ra\"] for r in _ben_rows), client[\"benefits_pv\"]]\n",
"if not _at_default: # published composite beside the overlay for reference\n",
" benefits_df[\"Composite PV 🟢\"] = \\\n",
" [r[\"pv\"] for r in composite[\"rows\"][\"benefits\"]] + [composite[\"benefits_pv\"]]\n",
"display(benefits_df)\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "cell-10",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:19.163518Z",
"iopub.status.busy": "2026-07-09T19:15:19.163257Z",
"iopub.status.idle": "2026-07-09T19:15:20.430977Z",
"shell.execute_reply": "2026-07-09T19:15:20.429934Z"
}
},
"outputs": [
{
"data": {
"application/vnd.plotly.v1+json": {
"config": {
"plotlyServerURL": "https://plot.ly"
},
"data": [
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#2a78d6",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "Retirement of legacy systems with CX Cloud adoption",
"type": "bar",
"x": [
"2026",
"2027",
"2028"
],
"y": [
646000.0,
883500.0,
883500.0
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#1baf7a",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "Cost savings from reallocated workers and avoided seasonal hires with increased customer self-service",
"type": "bar",
"x": [
"2026",
"2027",
"2028"
],
"y": [
1980160.0,
1980160.0,
1980160.0
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#4a3aa7",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "CX agent efficiency gains",
"type": "bar",
"x": [
"2026",
"2027",
"2028"
],
"y": [
2620800.0,
2620800.0,
2620800.0
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#eda100",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "Incremental sales from agent assist capabilities",
"type": "bar",
"x": [
"2026",
"2027",
"2028"
],
"y": [
570000.0,
570000.0,
570000.0
]
}
],
"layout": {
"bargap": 0.45,
"barmode": "stack",
"font": {
"color": "#52514e",
"family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
"size": 12
},
"height": 480,
"hovermode": "x unified",
"legend": {
"font": {
"color": "#52514e",
"size": 11
},
"orientation": "h",
"x": 0,
"y": -0.1,
"yanchor": "top"
},
"margin": {
"b": 80,
"l": 70,
"r": 30,
"t": 70
},
"paper_bgcolor": "#fcfcfb",
"plot_bgcolor": "#fcfcfb",
"template": {
"data": {
"bar": [
{
"error_x": {
"color": "#2a3f5f"
},
"error_y": {
"color": "#2a3f5f"
},
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "bar"
}
],
"barpolar": [
{
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "barpolar"
}
],
"carpet": [
{
"aaxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"baxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"type": "carpet"
}
],
"choropleth": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "choropleth"
}
],
"contour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "contour"
}
],
"contourcarpet": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "contourcarpet"
}
],
"heatmap": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "heatmap"
}
],
"histogram": [
{
"marker": {
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "histogram"
}
],
"histogram2d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2d"
}
],
"histogram2dcontour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2dcontour"
}
],
"mesh3d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "mesh3d"
}
],
"parcoords": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "parcoords"
}
],
"pie": [
{
"automargin": true,
"type": "pie"
}
],
"scatter": [
{
"fillpattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
},
"type": "scatter"
}
],
"scatter3d": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatter3d"
}
],
"scattercarpet": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattercarpet"
}
],
"scattergeo": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergeo"
}
],
"scattergl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergl"
}
],
"scattermap": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermap"
}
],
"scattermapbox": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermapbox"
}
],
"scatterpolar": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolar"
}
],
"scatterpolargl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolargl"
}
],
"scatterternary": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterternary"
}
],
"surface": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "surface"
}
],
"table": [
{
"cells": {
"fill": {
"color": "#EBF0F8"
},
"line": {
"color": "white"
}
},
"header": {
"fill": {
"color": "#C8D4E3"
},
"line": {
"color": "white"
}
},
"type": "table"
}
]
},
"layout": {
"annotationdefaults": {
"arrowcolor": "#2a3f5f",
"arrowhead": 0,
"arrowwidth": 1
},
"autotypenumbers": "strict",
"coloraxis": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"colorscale": {
"diverging": [
[
0,
"#8e0152"
],
[
0.1,
"#c51b7d"
],
[
0.2,
"#de77ae"
],
[
0.3,
"#f1b6da"
],
[
0.4,
"#fde0ef"
],
[
0.5,
"#f7f7f7"
],
[
0.6,
"#e6f5d0"
],
[
0.7,
"#b8e186"
],
[
0.8,
"#7fbc41"
],
[
0.9,
"#4d9221"
],
[
1,
"#276419"
]
],
"sequential": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"sequentialminus": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
]
},
"colorway": [
"#636efa",
"#EF553B",
"#00cc96",
"#ab63fa",
"#FFA15A",
"#19d3f3",
"#FF6692",
"#B6E880",
"#FF97FF",
"#FECB52"
],
"font": {
"color": "#2a3f5f"
},
"geo": {
"bgcolor": "white",
"lakecolor": "white",
"landcolor": "#E5ECF6",
"showlakes": true,
"showland": true,
"subunitcolor": "white"
},
"hoverlabel": {
"align": "left"
},
"hovermode": "closest",
"mapbox": {
"style": "light"
},
"paper_bgcolor": "white",
"plot_bgcolor": "#E5ECF6",
"polar": {
"angularaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"radialaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"scene": {
"xaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"yaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"zaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
}
},
"shapedefaults": {
"line": {
"color": "#2a3f5f"
}
},
"ternary": {
"aaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"baxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"caxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"title": {
"x": 0.05
},
"xaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
},
"yaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
}
}
},
"title": {
"font": {
"color": "#0b0b0b",
"size": 16
},
"text": "<b>Benefits by year (risk-adjusted)</b><br><span style='font-size:12px;color:#898781'>stacked by benefit stream — client overlay</span>",
"x": 0.02,
"xanchor": "left"
},
"xaxis": {
"linecolor": "#c3c2b7",
"showgrid": false,
"tickfont": {
"color": "#898781"
},
"type": "category"
},
"yaxis": {
"gridcolor": "#e1e0d9",
"tickfont": {
"color": "#898781"
},
"tickformat": "$~s",
"zerolinecolor": "#c3c2b7",
"zerolinewidth": 1.5
}
}
}
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig = go.Figure()\n",
"_x = [str(y) for y in YEARS]\n",
"for r in client[\"rows\"][\"benefits\"]:\n",
" fig.add_trace(bar(_x, [r[\"ra_by_year\"][y] for y in YEARS],\n",
" r[\"label\"], BENEFIT_COLOR[r[\"field_key\"]]))\n",
"if not _at_default: # composite yearly total as de-emphasized context\n",
" fig.add_trace(cum_line(_x, [composite[\"benefits_by_year\"][y] for y in YEARS],\n",
" \"Forrester composite total\", CONTEXT, dash=\"dot\"))\n",
"fig.update_layout(barmode=\"stack\")\n",
"tei_layout(fig, \"Benefits by year (risk-adjusted)\",\n",
" subtitle=\"stacked by benefit stream — client overlay\", height=480)\n",
"fig.show()\n"
]
},
{
"cell_type": "markdown",
"id": "cell-11",
"metadata": {},
"source": [
"<a id=\"section-4\"></a>\n",
"## 4 · Costs — and the $0 AI line (🔴)\n",
"\n",
"Three published cost lines, risk-adjusted **up** 510%: seat licenses,\n",
"a 10-week implementation (the only time-0 outlay: $1.19M nominal → $1.31M\n",
"risk-adjusted), and a small ops team.\n",
"\n",
"The fourth line is the one the study *doesn't* price: **Genesys AI\n",
"Experience token consumption**. Forrester models it at $0, yet the\n",
"self-service uplift (B), agent efficiency (C), and agent assist (D)\n",
"benefits all run on AI capabilities that Genesys bills via tokens. The\n",
"anchor keeps the $0 verbatim so the reproduction matches the PDF; the\n",
"sidebar's token input prices it for a real client from the Genesys quote.\n"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "cell-12",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:20.435367Z",
"iopub.status.busy": "2026-07-09T19:15:20.435032Z",
"iopub.status.idle": "2026-07-09T19:15:20.449985Z",
"shell.execute_reply": "2026-07-09T19:15:20.449281Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Driver</th>\n",
" <th>Risk adj</th>\n",
" <th>Initial</th>\n",
" <th>2026</th>\n",
" <th>2027</th>\n",
" <th>2028</th>\n",
" <th>PV</th>\n",
" </tr>\n",
" <tr>\n",
" <th>Cost</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>CX Cloud solution costs (licenses)</th>\n",
" <td>agents</td>\n",
" <td>+5%</td>\n",
" <td>0</td>\n",
" <td>882,000</td>\n",
" <td>882,000</td>\n",
" <td>882,000</td>\n",
" <td>2,193,403</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Implementation and deployment cost</th>\n",
" <td>fixed</td>\n",
" <td>+10%</td>\n",
" <td>1,309,000</td>\n",
" <td>0</td>\n",
" <td>0</td>\n",
" <td>0</td>\n",
" <td>1,309,000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Ongoing management costs</th>\n",
" <td>fixed</td>\n",
" <td>+10%</td>\n",
" <td>0</td>\n",
" <td>223,080</td>\n",
" <td>223,080</td>\n",
" <td>223,080</td>\n",
" <td>554,767</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Genesys AI Experience token consumption</th>\n",
" <td>ai_tokens</td>\n",
" <td>+0%</td>\n",
" <td>0</td>\n",
" <td>0</td>\n",
" <td>0</td>\n",
" <td>0</td>\n",
" <td>0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>TOTAL</th>\n",
" <td></td>\n",
" <td></td>\n",
" <td>1,309,000</td>\n",
" <td>1,105,080</td>\n",
" <td>1,105,080</td>\n",
" <td>1,105,080</td>\n",
" <td>4,057,170</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Driver Risk adj Initial \\\n",
"Cost \n",
"CX Cloud solution costs (licenses) agents +5% 0 \n",
"Implementation and deployment cost fixed +10% 1,309,000 \n",
"Ongoing management costs fixed +10% 0 \n",
"Genesys AI Experience token consumption ai_tokens +0% 0 \n",
"TOTAL 1,309,000 \n",
"\n",
" 2026 2027 2028 \\\n",
"Cost \n",
"CX Cloud solution costs (licenses) 882,000 882,000 882,000 \n",
"Implementation and deployment cost 0 0 0 \n",
"Ongoing management costs 223,080 223,080 223,080 \n",
"Genesys AI Experience token consumption 0 0 0 \n",
"TOTAL 1,105,080 1,105,080 1,105,080 \n",
"\n",
" PV \n",
"Cost \n",
"CX Cloud solution costs (licenses) 2,193,403 \n",
"Implementation and deployment cost 1,309,000 \n",
"Ongoing management costs 554,767 \n",
"Genesys AI Experience token consumption 0 \n",
"TOTAL 4,057,170 "
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# ── Costs table — client overlay, risk-adjusted ──────────────────────\n",
"_cost_rows = client[\"rows\"][\"costs\"]\n",
"costs_df = pd.DataFrame([{\n",
" \"Cost\": r[\"label\"],\n",
" \"Driver\": COST_DRIVERS[r[\"field_key\"]],\n",
" \"Risk adj\": f\"+{r['risk_adjustment']:.0%}\",\n",
" \"Initial\": r[\"initial_ra\"],\n",
" **{str(y): r[\"ra_by_year\"][y] for y in YEARS},\n",
" \"PV\": r[\"pv\"],\n",
"} for r in _cost_rows]).set_index(\"Cost\")\n",
"costs_df.loc[\"TOTAL\"] = [\"\", \"\", client[\"initial_costs\"]] \\\n",
" + [client[\"costs_by_year\"][y] for y in YEARS] + [client[\"costs_pv\"]]\n",
"if not _at_default:\n",
" costs_df[\"Composite PV 🟢\"] = \\\n",
" [r[\"pv\"] for r in composite[\"rows\"][\"costs\"]] + [composite[\"costs_pv\"]]\n",
"display(costs_df)\n"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "cell-13",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:20.458332Z",
"iopub.status.busy": "2026-07-09T19:15:20.458028Z",
"iopub.status.idle": "2026-07-09T19:15:20.482508Z",
"shell.execute_reply": "2026-07-09T19:15:20.481680Z"
}
},
"outputs": [
{
"data": {
"application/vnd.plotly.v1+json": {
"config": {
"plotlyServerURL": "https://plot.ly"
},
"data": [
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#2a78d6",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "CX Cloud solution costs (licenses)",
"type": "bar",
"x": [
"Initial",
"2026",
"2027",
"2028"
],
"y": [
0.0,
882000.0,
882000.0,
882000.0
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#eda100",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "Implementation and deployment cost",
"type": "bar",
"x": [
"Initial",
"2026",
"2027",
"2028"
],
"y": [
1309000.0,
0.0,
0.0,
0.0
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#4a3aa7",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "Ongoing management costs",
"type": "bar",
"x": [
"Initial",
"2026",
"2027",
"2028"
],
"y": [
0.0,
223080.00000000003,
223080.00000000003,
223080.00000000003
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#e34948",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "Genesys AI Experience token consumption",
"type": "bar",
"x": [
"Initial",
"2026",
"2027",
"2028"
],
"y": [
0.0,
0.0,
0.0,
0.0
]
}
],
"layout": {
"bargap": 0.45,
"barmode": "stack",
"font": {
"color": "#52514e",
"family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
"size": 12
},
"height": 460,
"hovermode": "x unified",
"legend": {
"font": {
"color": "#52514e",
"size": 11
},
"orientation": "h",
"x": 0,
"y": -0.1,
"yanchor": "top"
},
"margin": {
"b": 80,
"l": 70,
"r": 30,
"t": 70
},
"paper_bgcolor": "#fcfcfb",
"plot_bgcolor": "#fcfcfb",
"template": {
"data": {
"bar": [
{
"error_x": {
"color": "#2a3f5f"
},
"error_y": {
"color": "#2a3f5f"
},
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "bar"
}
],
"barpolar": [
{
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "barpolar"
}
],
"carpet": [
{
"aaxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"baxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"type": "carpet"
}
],
"choropleth": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "choropleth"
}
],
"contour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "contour"
}
],
"contourcarpet": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "contourcarpet"
}
],
"heatmap": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "heatmap"
}
],
"histogram": [
{
"marker": {
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "histogram"
}
],
"histogram2d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2d"
}
],
"histogram2dcontour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2dcontour"
}
],
"mesh3d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "mesh3d"
}
],
"parcoords": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "parcoords"
}
],
"pie": [
{
"automargin": true,
"type": "pie"
}
],
"scatter": [
{
"fillpattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
},
"type": "scatter"
}
],
"scatter3d": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatter3d"
}
],
"scattercarpet": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattercarpet"
}
],
"scattergeo": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergeo"
}
],
"scattergl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergl"
}
],
"scattermap": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermap"
}
],
"scattermapbox": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermapbox"
}
],
"scatterpolar": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolar"
}
],
"scatterpolargl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolargl"
}
],
"scatterternary": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterternary"
}
],
"surface": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "surface"
}
],
"table": [
{
"cells": {
"fill": {
"color": "#EBF0F8"
},
"line": {
"color": "white"
}
},
"header": {
"fill": {
"color": "#C8D4E3"
},
"line": {
"color": "white"
}
},
"type": "table"
}
]
},
"layout": {
"annotationdefaults": {
"arrowcolor": "#2a3f5f",
"arrowhead": 0,
"arrowwidth": 1
},
"autotypenumbers": "strict",
"coloraxis": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"colorscale": {
"diverging": [
[
0,
"#8e0152"
],
[
0.1,
"#c51b7d"
],
[
0.2,
"#de77ae"
],
[
0.3,
"#f1b6da"
],
[
0.4,
"#fde0ef"
],
[
0.5,
"#f7f7f7"
],
[
0.6,
"#e6f5d0"
],
[
0.7,
"#b8e186"
],
[
0.8,
"#7fbc41"
],
[
0.9,
"#4d9221"
],
[
1,
"#276419"
]
],
"sequential": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"sequentialminus": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
]
},
"colorway": [
"#636efa",
"#EF553B",
"#00cc96",
"#ab63fa",
"#FFA15A",
"#19d3f3",
"#FF6692",
"#B6E880",
"#FF97FF",
"#FECB52"
],
"font": {
"color": "#2a3f5f"
},
"geo": {
"bgcolor": "white",
"lakecolor": "white",
"landcolor": "#E5ECF6",
"showlakes": true,
"showland": true,
"subunitcolor": "white"
},
"hoverlabel": {
"align": "left"
},
"hovermode": "closest",
"mapbox": {
"style": "light"
},
"paper_bgcolor": "white",
"plot_bgcolor": "#E5ECF6",
"polar": {
"angularaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"radialaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"scene": {
"xaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"yaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"zaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
}
},
"shapedefaults": {
"line": {
"color": "#2a3f5f"
}
},
"ternary": {
"aaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"baxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"caxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"title": {
"x": 0.05
},
"xaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
},
"yaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
}
}
},
"title": {
"font": {
"color": "#0b0b0b",
"size": 16
},
"text": "<b>Costs by year (risk-adjusted)</b><br><span style='font-size:12px;color:#898781'>Initial = undiscounted time-0 outlay; the red AI-token segment is $0 until priced</span>",
"x": 0.02,
"xanchor": "left"
},
"xaxis": {
"linecolor": "#c3c2b7",
"showgrid": false,
"tickfont": {
"color": "#898781"
},
"type": "category"
},
"yaxis": {
"gridcolor": "#e1e0d9",
"tickfont": {
"color": "#898781"
},
"tickformat": "$~s",
"zerolinecolor": "#c3c2b7",
"zerolinewidth": 1.5
}
}
}
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig = go.Figure()\n",
"for r in client[\"rows\"][\"costs\"]:\n",
" fig.add_trace(bar(X_LABELS,\n",
" [r[\"initial_ra\"]] + [r[\"ra_by_year\"][y] for y in YEARS],\n",
" r[\"label\"], COST_COLOR[r[\"field_key\"]]))\n",
"fig.update_layout(barmode=\"stack\")\n",
"tei_layout(fig, \"Costs by year (risk-adjusted)\",\n",
" subtitle=\"Initial = undiscounted time-0 outlay; the red AI-token \"\n",
" \"segment is $0 until priced\")\n",
"fig.show()\n"
]
},
{
"cell_type": "markdown",
"id": "cell-14",
"metadata": {},
"source": [
"<a id=\"section-5\"></a>\n",
"## 5 · Business case\n",
"\n",
"The left column is Forrester's published Financial Summary, verbatim. The\n",
"right column is the client overlay at the sidebar's drivers. At the\n",
"defaults the engine reproduces the published totals to within $2 — the\n",
"gate in §7 enforces it. Forrester does not headline a payback for this\n",
"study; the engine computes **3.3 months** from the cash-flow table\n",
"($1.31M initial vs ~$4.7M year-1 net).\n"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "cell-15",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:20.484624Z",
"iopub.status.busy": "2026-07-09T19:15:20.484408Z",
"iopub.status.idle": "2026-07-09T19:15:20.493816Z",
"shell.execute_reply": "2026-07-09T19:15:20.493019Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Forrester composite (published 🟢)</th>\n",
" <th>Client overlay (🟡)</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>Benefits PV</th>\n",
" <td>$14,840,638</td>\n",
" <td>$14,840,637</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Costs PV</th>\n",
" <td>$4,057,170</td>\n",
" <td>$4,057,170</td>\n",
" </tr>\n",
" <tr>\n",
" <th>NPV</th>\n",
" <td>$10,783,468</td>\n",
" <td>$10,783,466</td>\n",
" </tr>\n",
" <tr>\n",
" <th>ROI</th>\n",
" <td>266%</td>\n",
" <td>266%</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Payback</th>\n",
" <td>not headlined</td>\n",
" <td>3.3 months (~Apr 2026)</td>\n",
" </tr>\n",
" <tr>\n",
" <th>Discount rate</th>\n",
" <td>10%</td>\n",
" <td>10%</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Forrester composite (published 🟢) Client overlay (🟡)\n",
"Benefits PV $14,840,638 $14,840,637\n",
"Costs PV $4,057,170 $4,057,170\n",
"NPV $10,783,468 $10,783,466\n",
"ROI 266% 266%\n",
"Payback not headlined 3.3 months (~Apr 2026)\n",
"Discount rate 10% 10%"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"engine composite NPV $10,783,466.42 vs published $10,783,468 (Δ -1.58)\n"
]
}
],
"source": [
"# ── KPIs — published composite beside the client overlay ─────────────\n",
"kpis_fmt = pd.DataFrame({\n",
" \"Forrester composite (published 🟢)\": {\n",
" \"Benefits PV\": f\"${PUBLISHED['benefits_pv']:,}\",\n",
" \"Costs PV\": f\"${PUBLISHED['costs_pv']:,}\",\n",
" \"NPV\": f\"${PUBLISHED['npv']:,}\",\n",
" \"ROI\": f\"{PUBLISHED['roi_pct']}%\",\n",
" \"Payback\": \"not headlined\",\n",
" \"Discount rate\": f\"{PUBLISHED['discount_rate']:.0%}\",\n",
" },\n",
" \"Client overlay (🟡)\": {\n",
" \"Benefits PV\": f\"${client['benefits_pv']:,.0f}\",\n",
" \"Costs PV\": f\"${client['costs_pv']:,.0f}\",\n",
" \"NPV\": f\"${client['npv']:,.0f}\",\n",
" \"ROI\": f\"{client['roi_pct']:.0f}%\",\n",
" \"Payback\": client[\"payback_label\"],\n",
" \"Discount rate\": f\"{DISCOUNT_RATE:.0%}\",\n",
" },\n",
"})\n",
"display(kpis_fmt)\n",
"backstage(f\"engine composite NPV ${composite['npv']:,.2f} vs published \"\n",
" f\"${PUBLISHED['npv']:,} (Δ {composite['npv'] - PUBLISHED['npv']:+,.2f})\")\n"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "cell-16",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:20.496223Z",
"iopub.status.busy": "2026-07-09T19:15:20.495912Z",
"iopub.status.idle": "2026-07-09T19:15:20.525205Z",
"shell.execute_reply": "2026-07-09T19:15:20.524534Z"
}
},
"outputs": [
{
"data": {
"application/vnd.plotly.v1+json": {
"config": {
"plotlyServerURL": "https://plot.ly"
},
"data": [
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#1baf7a",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "Benefits (risk-adjusted)",
"type": "bar",
"x": [
"Initial",
"2026",
"2027",
"2028"
],
"y": [
0,
5816960.0,
6054460.0,
6054460.0
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#e34948",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "Costs (risk-adjusted)",
"type": "bar",
"x": [
"Initial",
"2026",
"2027",
"2028"
],
"y": [
-1309000.0,
-1105080.0,
-1105080.0,
-1105080.0
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"line": {
"color": "#52514e",
"width": 2
},
"marker": {
"line": {
"color": "#fcfcfb",
"width": 2
},
"size": 8
},
"mode": "lines+markers",
"name": "Cumulative net",
"type": "scatter",
"x": [
"Initial",
"2026",
"2027",
"2028"
],
"y": [
-1309000.0,
3402880.0,
8352260.0,
13301640.0
]
}
],
"layout": {
"annotations": [
{
"align": "left",
"bgcolor": "#fcfcfb",
"bordercolor": "#e1e0d9",
"borderwidth": 1,
"font": {
"color": "#0b0b0b",
"size": 12
},
"showarrow": false,
"text": "NPV &#36;10.8M · ROI 266% · payback 3.3 months (~Apr 2026)",
"x": 0.02,
"xref": "paper",
"y": 0.98,
"yref": "paper"
}
],
"bargap": 0.45,
"barmode": "relative",
"font": {
"color": "#52514e",
"family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
"size": 12
},
"height": 460,
"hovermode": "x unified",
"legend": {
"font": {
"color": "#52514e",
"size": 11
},
"orientation": "h",
"x": 0,
"y": -0.1,
"yanchor": "top"
},
"margin": {
"b": 80,
"l": 70,
"r": 30,
"t": 70
},
"paper_bgcolor": "#fcfcfb",
"plot_bgcolor": "#fcfcfb",
"template": {
"data": {
"bar": [
{
"error_x": {
"color": "#2a3f5f"
},
"error_y": {
"color": "#2a3f5f"
},
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "bar"
}
],
"barpolar": [
{
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "barpolar"
}
],
"carpet": [
{
"aaxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"baxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"type": "carpet"
}
],
"choropleth": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "choropleth"
}
],
"contour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "contour"
}
],
"contourcarpet": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "contourcarpet"
}
],
"heatmap": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "heatmap"
}
],
"histogram": [
{
"marker": {
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "histogram"
}
],
"histogram2d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2d"
}
],
"histogram2dcontour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2dcontour"
}
],
"mesh3d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "mesh3d"
}
],
"parcoords": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "parcoords"
}
],
"pie": [
{
"automargin": true,
"type": "pie"
}
],
"scatter": [
{
"fillpattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
},
"type": "scatter"
}
],
"scatter3d": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatter3d"
}
],
"scattercarpet": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattercarpet"
}
],
"scattergeo": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergeo"
}
],
"scattergl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergl"
}
],
"scattermap": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermap"
}
],
"scattermapbox": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermapbox"
}
],
"scatterpolar": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolar"
}
],
"scatterpolargl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolargl"
}
],
"scatterternary": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterternary"
}
],
"surface": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "surface"
}
],
"table": [
{
"cells": {
"fill": {
"color": "#EBF0F8"
},
"line": {
"color": "white"
}
},
"header": {
"fill": {
"color": "#C8D4E3"
},
"line": {
"color": "white"
}
},
"type": "table"
}
]
},
"layout": {
"annotationdefaults": {
"arrowcolor": "#2a3f5f",
"arrowhead": 0,
"arrowwidth": 1
},
"autotypenumbers": "strict",
"coloraxis": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"colorscale": {
"diverging": [
[
0,
"#8e0152"
],
[
0.1,
"#c51b7d"
],
[
0.2,
"#de77ae"
],
[
0.3,
"#f1b6da"
],
[
0.4,
"#fde0ef"
],
[
0.5,
"#f7f7f7"
],
[
0.6,
"#e6f5d0"
],
[
0.7,
"#b8e186"
],
[
0.8,
"#7fbc41"
],
[
0.9,
"#4d9221"
],
[
1,
"#276419"
]
],
"sequential": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"sequentialminus": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
]
},
"colorway": [
"#636efa",
"#EF553B",
"#00cc96",
"#ab63fa",
"#FFA15A",
"#19d3f3",
"#FF6692",
"#B6E880",
"#FF97FF",
"#FECB52"
],
"font": {
"color": "#2a3f5f"
},
"geo": {
"bgcolor": "white",
"lakecolor": "white",
"landcolor": "#E5ECF6",
"showlakes": true,
"showland": true,
"subunitcolor": "white"
},
"hoverlabel": {
"align": "left"
},
"hovermode": "closest",
"mapbox": {
"style": "light"
},
"paper_bgcolor": "white",
"plot_bgcolor": "#E5ECF6",
"polar": {
"angularaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"radialaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"scene": {
"xaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"yaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"zaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
}
},
"shapedefaults": {
"line": {
"color": "#2a3f5f"
}
},
"ternary": {
"aaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"baxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"caxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"title": {
"x": 0.05
},
"xaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
},
"yaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
}
}
},
"title": {
"font": {
"color": "#0b0b0b",
"size": 16
},
"text": "<b>Cash flow — risk-adjusted</b><br><span style='font-size:12px;color:#898781'>benefits up, costs down; initial outlay undiscounted at time 0</span>",
"x": 0.02,
"xanchor": "left"
},
"xaxis": {
"linecolor": "#c3c2b7",
"showgrid": false,
"tickfont": {
"color": "#898781"
},
"type": "category"
},
"yaxis": {
"gridcolor": "#e1e0d9",
"tickfont": {
"color": "#898781"
},
"tickformat": "$~s",
"zerolinecolor": "#c3c2b7",
"zerolinewidth": 1.5
}
}
}
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# ── Cash flow — mirrors the PDF's Cash Flow Chart ────────────────────\n",
"fig = go.Figure()\n",
"fig.add_trace(bar(X_LABELS,\n",
" [0] + [client[\"benefits_by_year\"][y] for y in YEARS],\n",
" \"Benefits (risk-adjusted)\", BEN_TOTAL))\n",
"fig.add_trace(bar(X_LABELS,\n",
" [-client[\"initial_costs\"]] + [-client[\"costs_by_year\"][y] for y in YEARS],\n",
" \"Costs (risk-adjusted)\", COST_TOTAL))\n",
"fig.add_trace(cum_line(X_LABELS,\n",
" [-client[\"initial_costs\"]] + [client[\"cumulative_net_by_year\"][y] for y in YEARS],\n",
" \"Cumulative net\"))\n",
"fig.update_layout(barmode=\"relative\")\n",
"fig.add_annotation(\n",
" x=0.02, y=0.98, xref=\"paper\", yref=\"paper\", showarrow=False, align=\"left\",\n",
" text=(f\"NPV {html_money(client['npv'])} · ROI {client['roi_pct']:.0f}% · \"\n",
" f\"payback {client['payback_label']}\"),\n",
" font=dict(size=12, color=INK), bgcolor=SURFACE,\n",
" bordercolor=GRID, borderwidth=1)\n",
"tei_layout(fig, \"Cash flow — risk-adjusted\",\n",
" subtitle=\"benefits up, costs down; initial outlay undiscounted at time 0\")\n",
"fig.show()\n"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "cell-17",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:20.527870Z",
"iopub.status.busy": "2026-07-09T19:15:20.527655Z",
"iopub.status.idle": "2026-07-09T19:15:20.551901Z",
"shell.execute_reply": "2026-07-09T19:15:20.551086Z"
}
},
"outputs": [
{
"data": {
"application/vnd.plotly.v1+json": {
"config": {
"plotlyServerURL": "https://plot.ly"
},
"data": [
{
"connector": {
"line": {
"color": "#e1e0d9"
}
},
"decreasing": {
"marker": {
"color": "#e34948"
}
},
"increasing": {
"marker": {
"color": "#1baf7a"
}
},
"measure": [
"relative",
"relative",
"total"
],
"text": [
"&#36;14.8M",
"-&#36;4.1M",
"&#36;10.8M"
],
"textposition": "outside",
"totals": {
"marker": {
"color": "#2a78d6"
}
},
"type": "waterfall",
"x": [
"Benefits PV",
"Costs PV",
"NPV"
],
"y": [
14840636.814425241,
-4057170.398196844,
0
]
}
],
"layout": {
"bargap": 0.45,
"font": {
"color": "#52514e",
"family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
"size": 12
},
"height": 420,
"hovermode": "x unified",
"legend": {
"font": {
"color": "#52514e",
"size": 11
},
"orientation": "h",
"x": 0,
"y": -0.1,
"yanchor": "top"
},
"margin": {
"b": 80,
"l": 70,
"r": 30,
"t": 70
},
"paper_bgcolor": "#fcfcfb",
"plot_bgcolor": "#fcfcfb",
"showlegend": false,
"template": {
"data": {
"bar": [
{
"error_x": {
"color": "#2a3f5f"
},
"error_y": {
"color": "#2a3f5f"
},
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "bar"
}
],
"barpolar": [
{
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "barpolar"
}
],
"carpet": [
{
"aaxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"baxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"type": "carpet"
}
],
"choropleth": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "choropleth"
}
],
"contour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "contour"
}
],
"contourcarpet": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "contourcarpet"
}
],
"heatmap": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "heatmap"
}
],
"histogram": [
{
"marker": {
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "histogram"
}
],
"histogram2d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2d"
}
],
"histogram2dcontour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2dcontour"
}
],
"mesh3d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "mesh3d"
}
],
"parcoords": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "parcoords"
}
],
"pie": [
{
"automargin": true,
"type": "pie"
}
],
"scatter": [
{
"fillpattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
},
"type": "scatter"
}
],
"scatter3d": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatter3d"
}
],
"scattercarpet": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattercarpet"
}
],
"scattergeo": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergeo"
}
],
"scattergl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergl"
}
],
"scattermap": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermap"
}
],
"scattermapbox": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermapbox"
}
],
"scatterpolar": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolar"
}
],
"scatterpolargl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolargl"
}
],
"scatterternary": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterternary"
}
],
"surface": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "surface"
}
],
"table": [
{
"cells": {
"fill": {
"color": "#EBF0F8"
},
"line": {
"color": "white"
}
},
"header": {
"fill": {
"color": "#C8D4E3"
},
"line": {
"color": "white"
}
},
"type": "table"
}
]
},
"layout": {
"annotationdefaults": {
"arrowcolor": "#2a3f5f",
"arrowhead": 0,
"arrowwidth": 1
},
"autotypenumbers": "strict",
"coloraxis": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"colorscale": {
"diverging": [
[
0,
"#8e0152"
],
[
0.1,
"#c51b7d"
],
[
0.2,
"#de77ae"
],
[
0.3,
"#f1b6da"
],
[
0.4,
"#fde0ef"
],
[
0.5,
"#f7f7f7"
],
[
0.6,
"#e6f5d0"
],
[
0.7,
"#b8e186"
],
[
0.8,
"#7fbc41"
],
[
0.9,
"#4d9221"
],
[
1,
"#276419"
]
],
"sequential": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"sequentialminus": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
]
},
"colorway": [
"#636efa",
"#EF553B",
"#00cc96",
"#ab63fa",
"#FFA15A",
"#19d3f3",
"#FF6692",
"#B6E880",
"#FF97FF",
"#FECB52"
],
"font": {
"color": "#2a3f5f"
},
"geo": {
"bgcolor": "white",
"lakecolor": "white",
"landcolor": "#E5ECF6",
"showlakes": true,
"showland": true,
"subunitcolor": "white"
},
"hoverlabel": {
"align": "left"
},
"hovermode": "closest",
"mapbox": {
"style": "light"
},
"paper_bgcolor": "white",
"plot_bgcolor": "#E5ECF6",
"polar": {
"angularaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"radialaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"scene": {
"xaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"yaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"zaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
}
},
"shapedefaults": {
"line": {
"color": "#2a3f5f"
}
},
"ternary": {
"aaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"baxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"caxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"title": {
"x": 0.05
},
"xaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
},
"yaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
}
}
},
"title": {
"font": {
"color": "#0b0b0b",
"size": 16
},
"text": "<b>Present value walk — client overlay</b><br><span style='font-size:12px;color:#898781'>discounted at 10%, 3 years</span>",
"x": 0.02,
"xanchor": "left"
},
"xaxis": {
"linecolor": "#c3c2b7",
"showgrid": false,
"tickfont": {
"color": "#898781"
},
"type": "category"
},
"yaxis": {
"gridcolor": "#e1e0d9",
"tickfont": {
"color": "#898781"
},
"tickformat": "$~s",
"zerolinecolor": "#c3c2b7",
"zerolinewidth": 1.5
}
}
}
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig = go.Figure(go.Waterfall(\n",
" x=[\"Benefits PV\", \"Costs PV\", \"NPV\"],\n",
" measure=[\"relative\", \"relative\", \"total\"],\n",
" y=[client[\"benefits_pv\"], -client[\"costs_pv\"], 0],\n",
" text=[html_money(client[\"benefits_pv\"]), html_money(-client[\"costs_pv\"]),\n",
" html_money(client[\"npv\"])],\n",
" textposition=\"outside\",\n",
" connector=dict(line=dict(color=GRID)),\n",
" increasing=dict(marker=dict(color=BEN_TOTAL)),\n",
" decreasing=dict(marker=dict(color=COST_TOTAL)),\n",
" totals=dict(marker=dict(color=NPV_COLOR)),\n",
"))\n",
"fig.update_layout(showlegend=False)\n",
"tei_layout(fig, \"Present value walk — client overlay\",\n",
" subtitle=f\"discounted at {'{:.0%}'.format(DISCOUNT_RATE)}, 3 years\",\n",
" height=420)\n",
"fig.show()\n"
]
},
{
"cell_type": "markdown",
"id": "cell-18",
"metadata": {},
"source": [
"<a id=\"section-6\"></a>\n",
"## 6 · Scenarios (🟡)\n",
"\n",
"Scenarios stress the overlay on two levers: **adoption** scales every\n",
"nominal value (including the initial outlay), and **risk delta** widens or\n",
"narrows the TEI risk adjustments — added to benefit risk, subtracted from\n",
"cost risk, clamped at zero.\n",
"\n",
"One counterintuitive consequence, worth stating: the **conservative**\n",
"scenario *lowers* costs PV as well as benefits — 80% adoption shrinks\n",
"every cost line and the clamp zeroes all this study's cost paddings (510%\n",
"risk factors, all under the 10% delta). The case direction is still\n",
"conservative: NPV and ROI both fall.\n"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "cell-19",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:20.554518Z",
"iopub.status.busy": "2026-07-09T19:15:20.554277Z",
"iopub.status.idle": "2026-07-09T19:15:20.564822Z",
"shell.execute_reply": "2026-07-09T19:15:20.564005Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Adoption</th>\n",
" <th>Risk Δ</th>\n",
" <th>Benefits PV</th>\n",
" <th>Costs PV</th>\n",
" <th>NPV</th>\n",
" <th>ROI %</th>\n",
" <th>Payback (months)</th>\n",
" </tr>\n",
" <tr>\n",
" <th>Scenario</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>conservative</th>\n",
" <td>80%</td>\n",
" <td>+10%</td>\n",
" <td>10,543,494</td>\n",
" <td>3,026,631</td>\n",
" <td>7,516,863</td>\n",
" <td>248</td>\n",
" <td>3</td>\n",
" </tr>\n",
" <tr>\n",
" <th>moderate</th>\n",
" <td>100%</td>\n",
" <td>+0%</td>\n",
" <td>14,840,637</td>\n",
" <td>4,057,170</td>\n",
" <td>10,783,466</td>\n",
" <td>266</td>\n",
" <td>3</td>\n",
" </tr>\n",
" <tr>\n",
" <th>aggressive</th>\n",
" <td>115%</td>\n",
" <td>-5%</td>\n",
" <td>18,021,962</td>\n",
" <td>4,883,285</td>\n",
" <td>13,138,677</td>\n",
" <td>269</td>\n",
" <td>3</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Adoption Risk Δ Benefits PV Costs PV NPV ROI % \\\n",
"Scenario \n",
"conservative 80% +10% 10,543,494 3,026,631 7,516,863 248 \n",
"moderate 100% +0% 14,840,637 4,057,170 10,783,466 266 \n",
"aggressive 115% -5% 18,021,962 4,883,285 13,138,677 269 \n",
"\n",
" Payback (months) \n",
"Scenario \n",
"conservative 3 \n",
"moderate 3 \n",
"aggressive 3 "
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# ── Scenario sweep over the client overlay ───────────────────────────\n",
"scen_summaries = {\n",
" s: compute_summary(apply_scenario(overlay_benefits, s),\n",
" apply_scenario(overlay_costs, s), DISCOUNT_RATE)\n",
" for s in SCENARIOS\n",
"}\n",
"scen_df = pd.DataFrame([{\n",
" \"Scenario\": s,\n",
" \"Adoption\": f\"{SCENARIOS[s]['adoption']:.0%}\",\n",
" \"Risk Δ\": f\"{SCENARIOS[s]['risk_delta']:+.0%}\",\n",
" \"Benefits PV\": r[\"benefits_pv\"],\n",
" \"Costs PV\": r[\"costs_pv\"],\n",
" \"NPV\": r[\"npv\"],\n",
" \"ROI %\": round(r[\"roi_pct\"], 1),\n",
" \"Payback (months)\": round(r[\"payback_months\"], 2)\n",
" if r[\"payback_months\"] is not None else float(\"nan\"),\n",
"} for s, r in scen_summaries.items()]).set_index(\"Scenario\")\n",
"display(scen_df)\n"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "cell-20",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:20.567700Z",
"iopub.status.busy": "2026-07-09T19:15:20.567440Z",
"iopub.status.idle": "2026-07-09T19:15:20.587490Z",
"shell.execute_reply": "2026-07-09T19:15:20.586666Z"
}
},
"outputs": [
{
"data": {
"application/vnd.plotly.v1+json": {
"config": {
"plotlyServerURL": "https://plot.ly"
},
"data": [
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#1baf7a",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "Benefits PV",
"type": "bar",
"x": [
"conservative",
"moderate",
"aggressive"
],
"y": [
10543493.914350111,
14840636.814425241,
18021962.2539444
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#e34948",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "Costs PV",
"type": "bar",
"x": [
"conservative",
"moderate",
"aggressive"
],
"y": [
3026631.4049586775,
4057170.398196844,
4883285.090157775
]
},
{
"hovertemplate": "%{fullData.name}: %{y:$,.0f}<extra></extra>",
"marker": {
"color": "#2a78d6",
"line": {
"color": "#fcfcfb",
"width": 2
}
},
"name": "NPV",
"type": "bar",
"x": [
"conservative",
"moderate",
"aggressive"
],
"y": [
7516862.509391434,
10783466.416228397,
13138677.163786625
]
}
],
"layout": {
"bargap": 0.45,
"barmode": "group",
"font": {
"color": "#52514e",
"family": "system-ui, -apple-system, \"Segoe UI\", sans-serif",
"size": 12
},
"height": 420,
"hovermode": "x unified",
"legend": {
"font": {
"color": "#52514e",
"size": 11
},
"orientation": "h",
"x": 0,
"y": -0.1,
"yanchor": "top"
},
"margin": {
"b": 80,
"l": 70,
"r": 30,
"t": 70
},
"paper_bgcolor": "#fcfcfb",
"plot_bgcolor": "#fcfcfb",
"template": {
"data": {
"bar": [
{
"error_x": {
"color": "#2a3f5f"
},
"error_y": {
"color": "#2a3f5f"
},
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "bar"
}
],
"barpolar": [
{
"marker": {
"line": {
"color": "#E5ECF6",
"width": 0.5
},
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "barpolar"
}
],
"carpet": [
{
"aaxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"baxis": {
"endlinecolor": "#2a3f5f",
"gridcolor": "white",
"linecolor": "white",
"minorgridcolor": "white",
"startlinecolor": "#2a3f5f"
},
"type": "carpet"
}
],
"choropleth": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "choropleth"
}
],
"contour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "contour"
}
],
"contourcarpet": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "contourcarpet"
}
],
"heatmap": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "heatmap"
}
],
"histogram": [
{
"marker": {
"pattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
}
},
"type": "histogram"
}
],
"histogram2d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2d"
}
],
"histogram2dcontour": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "histogram2dcontour"
}
],
"mesh3d": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"type": "mesh3d"
}
],
"parcoords": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "parcoords"
}
],
"pie": [
{
"automargin": true,
"type": "pie"
}
],
"scatter": [
{
"fillpattern": {
"fillmode": "overlay",
"size": 10,
"solidity": 0.2
},
"type": "scatter"
}
],
"scatter3d": [
{
"line": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatter3d"
}
],
"scattercarpet": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattercarpet"
}
],
"scattergeo": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergeo"
}
],
"scattergl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattergl"
}
],
"scattermap": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermap"
}
],
"scattermapbox": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scattermapbox"
}
],
"scatterpolar": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolar"
}
],
"scatterpolargl": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterpolargl"
}
],
"scatterternary": [
{
"marker": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"type": "scatterternary"
}
],
"surface": [
{
"colorbar": {
"outlinewidth": 0,
"ticks": ""
},
"colorscale": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"type": "surface"
}
],
"table": [
{
"cells": {
"fill": {
"color": "#EBF0F8"
},
"line": {
"color": "white"
}
},
"header": {
"fill": {
"color": "#C8D4E3"
},
"line": {
"color": "white"
}
},
"type": "table"
}
]
},
"layout": {
"annotationdefaults": {
"arrowcolor": "#2a3f5f",
"arrowhead": 0,
"arrowwidth": 1
},
"autotypenumbers": "strict",
"coloraxis": {
"colorbar": {
"outlinewidth": 0,
"ticks": ""
}
},
"colorscale": {
"diverging": [
[
0,
"#8e0152"
],
[
0.1,
"#c51b7d"
],
[
0.2,
"#de77ae"
],
[
0.3,
"#f1b6da"
],
[
0.4,
"#fde0ef"
],
[
0.5,
"#f7f7f7"
],
[
0.6,
"#e6f5d0"
],
[
0.7,
"#b8e186"
],
[
0.8,
"#7fbc41"
],
[
0.9,
"#4d9221"
],
[
1,
"#276419"
]
],
"sequential": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
],
"sequentialminus": [
[
0.0,
"#0d0887"
],
[
0.1111111111111111,
"#46039f"
],
[
0.2222222222222222,
"#7201a8"
],
[
0.3333333333333333,
"#9c179e"
],
[
0.4444444444444444,
"#bd3786"
],
[
0.5555555555555556,
"#d8576b"
],
[
0.6666666666666666,
"#ed7953"
],
[
0.7777777777777778,
"#fb9f3a"
],
[
0.8888888888888888,
"#fdca26"
],
[
1.0,
"#f0f921"
]
]
},
"colorway": [
"#636efa",
"#EF553B",
"#00cc96",
"#ab63fa",
"#FFA15A",
"#19d3f3",
"#FF6692",
"#B6E880",
"#FF97FF",
"#FECB52"
],
"font": {
"color": "#2a3f5f"
},
"geo": {
"bgcolor": "white",
"lakecolor": "white",
"landcolor": "#E5ECF6",
"showlakes": true,
"showland": true,
"subunitcolor": "white"
},
"hoverlabel": {
"align": "left"
},
"hovermode": "closest",
"mapbox": {
"style": "light"
},
"paper_bgcolor": "white",
"plot_bgcolor": "#E5ECF6",
"polar": {
"angularaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"radialaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"scene": {
"xaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"yaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
},
"zaxis": {
"backgroundcolor": "#E5ECF6",
"gridcolor": "white",
"gridwidth": 2,
"linecolor": "white",
"showbackground": true,
"ticks": "",
"zerolinecolor": "white"
}
},
"shapedefaults": {
"line": {
"color": "#2a3f5f"
}
},
"ternary": {
"aaxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"baxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
},
"bgcolor": "#E5ECF6",
"caxis": {
"gridcolor": "white",
"linecolor": "white",
"ticks": ""
}
},
"title": {
"x": 0.05
},
"xaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
},
"yaxis": {
"automargin": true,
"gridcolor": "white",
"linecolor": "white",
"ticks": "",
"title": {
"standoff": 15
},
"zerolinecolor": "white",
"zerolinewidth": 2
}
}
},
"title": {
"font": {
"color": "#0b0b0b",
"size": 16
},
"text": "<b>Scenario comparison — client overlay</b><br><span style='font-size:12px;color:#898781'>adoption × risk-delta stress on the same drivers</span>",
"x": 0.02,
"xanchor": "left"
},
"xaxis": {
"linecolor": "#c3c2b7",
"showgrid": false,
"tickfont": {
"color": "#898781"
},
"type": "category"
},
"yaxis": {
"gridcolor": "#e1e0d9",
"tickfont": {
"color": "#898781"
},
"tickformat": "$~s",
"zerolinecolor": "#c3c2b7",
"zerolinewidth": 1.5
}
}
}
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig = go.Figure()\n",
"_scen = list(scen_summaries)\n",
"for _name, _key, _color in [(\"Benefits PV\", \"benefits_pv\", BEN_TOTAL),\n",
" (\"Costs PV\", \"costs_pv\", COST_TOTAL),\n",
" (\"NPV\", \"npv\", NPV_COLOR)]:\n",
" fig.add_trace(bar(_scen, [scen_summaries[s][_key] for s in _scen],\n",
" _name, _color))\n",
"fig.update_layout(barmode=\"group\")\n",
"tei_layout(fig, \"Scenario comparison — client overlay\",\n",
" subtitle=\"adoption × risk-delta stress on the same drivers\",\n",
" height=420)\n",
"fig.show()\n"
]
},
{
"cell_type": "markdown",
"id": "cell-21",
"metadata": {},
"source": [
"<a id=\"section-7\"></a>\n",
"## 7 · Verification & assertions\n",
"\n",
"The gate re-derives the case from the engine and asserts: the verbatim\n",
"anchor is intact (AI-token line at $0 included); the engine reproduces\n",
"Forrester's published totals within $5; the overlay is the identity at\n",
"composite scale; and the structural identities hold at **any** widget\n",
"setting. It must pass in a headless `nbconvert --execute` run — that is\n",
"this study's regression check.\n"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "cell-22",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:20.590288Z",
"iopub.status.busy": "2026-07-09T19:15:20.590029Z",
"iopub.status.idle": "2026-07-09T19:15:20.607459Z",
"shell.execute_reply": "2026-07-09T19:15:20.606722Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"All assertions passed.\n",
" reproduction Δ vs PDF: benefits -1.19 · costs +0.40 · npv -1.58\n"
]
}
],
"source": [
"def _approx(got, want, tol=0.5):\n",
" assert abs(got - want) <= tol, f\"got {got:,.2f}, want {want:,.2f}\"\n",
"\n",
"\n",
"# ── Anchor integrity — the verbatim record is intact (unconditional) ─\n",
"_approx(BENEFITS_VERBATIM[0][\"year_values\"][\"1\"], 680_000)\n",
"_approx(COSTS_VERBATIM[1][\"initial\"], 1_190_000)\n",
"_tok = next(r for r in COSTS_VERBATIM if r[\"field_key\"] == \"genesys_ai_tokens\")\n",
"assert all(v == 0 for v in _tok[\"year_values\"].values()) # the $0 AI line, verbatim\n",
"assert ASSUMPTIONS[\"agents_fte\"] == 600\n",
"assert ASSUMPTIONS[\"weekly_interactions\"] == 80_000\n",
"assert (COMPOSITE.agents_fte, COMPOSITE.ai_tokens_annual) == (600, 0.0)\n",
"\n",
"# ── Published reproduction — engine defaults, explicit args ──────────\n",
"_c = compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM, 0.10)\n",
"_approx(_c[\"benefits_pv\"], PUBLISHED[\"benefits_pv\"], tol=5) # Δ 1.19 (PDF rounding)\n",
"_approx(_c[\"costs_pv\"], PUBLISHED[\"costs_pv\"], tol=5) # Δ +0.40\n",
"_approx(_c[\"npv\"], PUBLISHED[\"npv\"], tol=5) # Δ 1.58\n",
"assert round(_c[\"roi_pct\"]) == PUBLISHED[\"roi_pct\"] # 265.79 → 266\n",
"_approx(_c[\"payback_months\"], 3.33, tol=0.01) # not headlined in the PDF\n",
"_approx(_c[\"initial_costs\"], 1_309_000)\n",
"_approx(_c[\"benefits_by_year\"][2026], 5_816_960, tol=1)\n",
"_approx(_c[\"costs_by_year\"][2028], 1_105_080, tol=1)\n",
"\n",
"# ── Overlay identity + scaling behaviour (explicit args) ─────────────\n",
"_ob, _oc = overlay_rows(COMPOSITE)\n",
"_id = compute_summary(_ob, _oc, 0.10)\n",
"_approx(_id[\"benefits_pv\"], _c[\"benefits_pv\"], tol=0.01) # identity at composite\n",
"_hb, _hc = overlay_rows(ClientDrivers(agents_fte=300))\n",
"_approx(next(r for r in _hb if r[\"field_key\"] == \"legacy_retirement\")\n",
" [\"year_values\"][\"1\"], 340_000) # agents-driven halves\n",
"_approx(next(r for r in _hb if r[\"field_key\"] == \"self_service_savings\")\n",
" [\"year_values\"][\"1\"], 2_329_600) # interactions-driven unmoved\n",
"_tb, _tc = overlay_rows(ClientDrivers(ai_tokens_annual=250_000))\n",
"_t = compute_summary(_tb, _tc, 0.10)\n",
"_approx(_t[\"costs_pv\"] - _c[\"costs_pv\"], 621_713, tol=1) # tokens priced → PV moves\n",
"\n",
"# ── Scenario pin (explicit args) ─────────────────────────────────────\n",
"_s = compute_summary(apply_scenario(BENEFITS_VERBATIM, \"conservative\"),\n",
" apply_scenario(COSTS_VERBATIM, \"conservative\"), 0.10)\n",
"_approx(_s[\"npv\"], 7_516_862.51, tol=1)\n",
"\n",
"# ── Structural ties — hold at ANY widget state (unconditional) ───────\n",
"_approx(client[\"npv\"], client[\"benefits_pv\"] - client[\"costs_pv\"], tol=0.01)\n",
"_approx(client[\"roi_pct\"], client[\"npv\"] / client[\"costs_pv\"] * 100, tol=0.01)\n",
"for _y in YEARS:\n",
" _approx(client[\"net_by_year\"][_y],\n",
" client[\"benefits_by_year\"][_y] - client[\"costs_by_year\"][_y], tol=0.01)\n",
"_approx(client[\"cumulative_net_by_year\"][YEARS[-1]],\n",
" sum(client[\"net_by_year\"].values()) - client[\"initial_costs\"], tol=0.01)\n",
"_approx(sum(r[\"pv\"] for r in client[\"rows\"][\"benefits\"]), client[\"benefits_pv\"], tol=0.01)\n",
"_approx(sum(r[\"pv\"] for r in client[\"rows\"][\"costs\"]), client[\"costs_pv\"], tol=0.01)\n",
"\n",
"# ── Live state — only when the sidebar sits at the composite defaults ─\n",
"if _at_default:\n",
" _approx(client[\"benefits_pv\"], 14_840_636.81, tol=1) # engine-exact\n",
" _approx(client[\"npv\"], PUBLISHED[\"npv\"], tol=5)\n",
" _approx(client[\"payback_months\"], 3.33, tol=0.01)\n",
"\n",
"backstage(\"All assertions passed.\")\n",
"backstage(f\" reproduction Δ vs PDF: benefits \"\n",
" f\"{_c['benefits_pv'] - PUBLISHED['benefits_pv']:+,.2f} · \"\n",
" f\"costs {_c['costs_pv'] - PUBLISHED['costs_pv']:+,.2f} · \"\n",
" f\"npv {_c['npv'] - PUBLISHED['npv']:+,.2f}\")\n"
]
},
{
"cell_type": "markdown",
"id": "cell-23",
"metadata": {},
"source": [
"<a id=\"section-8\"></a>\n",
"## 8 · Data appendix — for the machines\n",
"\n",
"Everything below renders **backstage only** (JupyterLab / nbconvert\n",
"exports): markdown tables plus one JSON block of model state. Carried in\n",
"the exports, this is the payload a downstream LLM — or, on the roadmap,\n",
"Athena as the study repository — consumes directly. On the Mercury stage\n",
"it stays hidden.\n"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "cell-24",
"metadata": {
"execution": {
"iopub.execute_input": "2026-07-09T19:15:20.610247Z",
"iopub.status.busy": "2026-07-09T19:15:20.609930Z",
"iopub.status.idle": "2026-07-09T19:15:20.632690Z",
"shell.execute_reply": "2026-07-09T19:15:20.631995Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"#### Composite organization (verbatim assumptions 🟢)\n",
"\n",
"| Assumption | Value 🟢 |\n",
"|:-------------------------|:---------------|\n",
"| annual revenue | $2,500,000,000 |\n",
"| employees | 10,000 |\n",
"| agents fte | 600 |\n",
"| concurrent licenses | 400 |\n",
"| weekly interactions | 80,000 |\n",
"| interaction minutes | 12 min |\n",
"| self service rate before | 15% |\n",
"| self service rate after | 25% |\n",
"| mttr saved minutes | 2 min |\n",
"| agent hourly rate | $28/hr |\n",
"| agent annual salary | $58,240 |\n",
"| revenue impacted | $500,000,000 |\n",
"| revenue lift | 1.5% |\n",
"| gross margin | 8% |\n",
"| discount rate | 10% |\n",
"| analysis years | 3 years |\n",
"\n",
"#### Benefits — client overlay (risk-adjusted $)\n",
"\n",
"| Benefit | Driver | Risk adj | 2026 | 2027 | 2028 | 3-yr RA | PV |\n",
"|:------------------------------------------------------------------------------------------------------|:-------------|:-----------|----------:|----------:|----------:|-----------:|-----------:|\n",
"| Retirement of legacy systems with CX Cloud adoption | agents | -5% | 646,000 | 883,500 | 883,500 | 2,413,000 | 1,981,225 |\n",
"| Cost savings from reallocated workers and avoided seasonal hires with increased customer self-service | interactions | -15% | 1,980,160 | 1,980,160 | 1,980,160 | 5,940,480 | 4,924,365 |\n",
"| CX agent efficiency gains | interactions | -10% | 2,620,800 | 2,620,800 | 2,620,800 | 7,862,400 | 6,517,542 |\n",
"| Incremental sales from agent assist capabilities | revenue | -5% | 570,000 | 570,000 | 570,000 | 1,710,000 | 1,417,506 |\n",
"| TOTAL | | | 5,816,960 | 6,054,460 | 6,054,460 | 17,925,880 | 14,840,637 |\n",
"\n",
"#### Costs — client overlay (risk-adjusted $)\n",
"\n",
"| Cost | Driver | Risk adj | Initial | 2026 | 2027 | 2028 | PV |\n",
"|:----------------------------------------|:----------|:-----------|----------:|----------:|----------:|----------:|----------:|\n",
"| CX Cloud solution costs (licenses) | agents | +5% | 0 | 882,000 | 882,000 | 882,000 | 2,193,403 |\n",
"| Implementation and deployment cost | fixed | +10% | 1,309,000 | 0 | 0 | 0 | 1,309,000 |\n",
"| Ongoing management costs | fixed | +10% | 0 | 223,080 | 223,080 | 223,080 | 554,767 |\n",
"| Genesys AI Experience token consumption | ai_tokens | +0% | 0 | 0 | 0 | 0 | 0 |\n",
"| TOTAL | | | 1,309,000 | 1,105,080 | 1,105,080 | 1,105,080 | 4,057,170 |\n",
"\n",
"#### KPIs — published composite vs client overlay\n",
"\n",
"| | Forrester composite (published 🟢) | Client overlay (🟡) |\n",
"|:--------------|:-------------------------------------|:-----------------------|\n",
"| Benefits PV | $14,840,638 | $14,840,637 |\n",
"| Costs PV | $4,057,170 | $4,057,170 |\n",
"| NPV | $10,783,468 | $10,783,466 |\n",
"| ROI | 266% | 266% |\n",
"| Payback | not headlined | 3.3 months (~Apr 2026) |\n",
"| Discount rate | 10% | 10% |\n",
"\n",
"#### Scenarios (client overlay)\n",
"\n",
"| Scenario | Adoption | Risk Δ | Benefits PV | Costs PV | NPV | ROI % | Payback (months) |\n",
"|:-------------|:-----------|:---------|--------------:|-----------:|-----------:|--------:|-------------------:|\n",
"| conservative | 80% | +10% | 10,543,494 | 3,026,631 | 7,516,863 | 248 | 3 |\n",
"| moderate | 100% | +0% | 14,840,637 | 4,057,170 | 10,783,466 | 266 | 3 |\n",
"| aggressive | 115% | -5% | 18,021,962 | 4,883,285 | 13,138,677 | 269 | 3 |\n",
"\n",
"#### Model state (JSON)\n",
"\n",
"```json\n",
"{\n",
" \"study\": \"202512_TEI_Genesys_CX_Cloud\",\n",
" \"source\": \"Forrester TEI of CX Cloud, by Genesys and Salesforce (Dec 2025, commissioned by Genesys and Salesforce)\",\n",
" \"published\": {\n",
" \"benefits_pv\": 14840638,\n",
" \"costs_pv\": 4057170,\n",
" \"npv\": 10783468,\n",
" \"roi_pct\": 266,\n",
" \"discount_rate\": 0.1,\n",
" \"analysis_years\": 3\n",
" },\n",
" \"reproduction\": {\n",
" \"benefits_pv\": 14840636.81,\n",
" \"costs_pv\": 4057170.4,\n",
" \"npv\": 10783466.42,\n",
" \"roi_pct\": 265.79,\n",
" \"payback_months\": 3.33,\n",
" \"payback_note\": \"Forrester does not headline a payback for this study\"\n",
" },\n",
" \"client\": {\n",
" \"drivers\": {\n",
" \"agents_fte\": 600,\n",
" \"weekly_interactions\": 80000,\n",
" \"annual_revenue\": 2500000000.0,\n",
" \"ai_tokens_annual\": 0.0,\n",
" \"discount_rate\": 0.1,\n",
" \"scenario\": \"moderate\"\n",
" },\n",
" \"benefits_by_year\": {\n",
" \"2026\": 5816960,\n",
" \"2027\": 6054460,\n",
" \"2028\": 6054460\n",
" },\n",
" \"costs_by_year\": {\n",
" \"2026\": 1105080,\n",
" \"2027\": 1105080,\n",
" \"2028\": 1105080\n",
" },\n",
" \"net_by_year\": {\n",
" \"2026\": 4711880,\n",
" \"2027\": 4949380,\n",
" \"2028\": 4949380\n",
" },\n",
" \"cumulative_net_by_year\": {\n",
" \"2026\": 3402880,\n",
" \"2027\": 8352260,\n",
" \"2028\": 13301640\n",
" },\n",
" \"initial_costs\": 1309000,\n",
" \"kpis\": {\n",
" \"benefits_pv\": 14840637,\n",
" \"costs_pv\": 4057170,\n",
" \"npv\": 10783466,\n",
" \"roi_pct\": 265.79,\n",
" \"payback_months\": 3.33,\n",
" \"payback_label\": \"3.3 months (~Apr 2026)\"\n",
" },\n",
" \"scenarios\": {\n",
" \"conservative\": {\n",
" \"benefits_pv\": 10543494,\n",
" \"costs_pv\": 3026631,\n",
" \"npv\": 7516863,\n",
" \"roi_pct\": 248.36\n",
" },\n",
" \"moderate\": {\n",
" \"benefits_pv\": 14840637,\n",
" \"costs_pv\": 4057170,\n",
" \"npv\": 10783466,\n",
" \"roi_pct\": 265.79\n",
" },\n",
" \"aggressive\": {\n",
" \"benefits_pv\": 18021962,\n",
" \"costs_pv\": 4883285,\n",
" \"npv\": 13138677,\n",
" \"roi_pct\": 269.05\n",
" }\n",
" }\n",
" },\n",
" \"driver_map\": {\n",
" \"benefits\": {\n",
" \"legacy_retirement\": \"agents\",\n",
" \"self_service_savings\": \"interactions\",\n",
" \"agent_efficiency\": \"interactions\",\n",
" \"agent_assist_sales\": \"revenue\"\n",
" },\n",
" \"costs\": {\n",
" \"cx_cloud_licenses\": \"agents\",\n",
" \"implementation\": \"fixed\",\n",
" \"ongoing_management\": \"fixed\",\n",
" \"genesys_ai_tokens\": \"ai_tokens\"\n",
" }\n",
" },\n",
" \"ai_token_note\": \"Published study models $0 Genesys AI Experience token consumption; benefits B/C/D depend on token-billed AI capabilities. Price via drivers.ai_tokens_annual.\"\n",
"}\n",
"```\n"
]
}
],
"source": [
"# ── Data appendix — LLM-readable dump of every model output ──────────\n",
"# Renders backstage only (JupyterLab / nbconvert exports) — hidden on\n",
"# the Mercury stage, where the narrative and figures carry the story.\n",
"import json as _json\n",
"\n",
"\n",
"def _section(title, df, **kw):\n",
" backstage(f\"\\n#### {title}\\n\")\n",
" backstage(df.to_markdown(floatfmt=\",.0f\", **kw))\n",
"\n",
"\n",
"_section(\"Composite organization (verbatim assumptions 🟢)\",\n",
" assumptions_df, index=False)\n",
"_section(\"Benefits — client overlay (risk-adjusted $)\", benefits_df)\n",
"_section(\"Costs — client overlay (risk-adjusted $)\", costs_df)\n",
"_section(\"KPIs — published composite vs client overlay\", kpis_fmt)\n",
"_section(\"Scenarios (client overlay)\", scen_df)\n",
"\n",
"backstage(\"\\n#### Model state (JSON)\\n\")\n",
"backstage(\"```json\")\n",
"backstage(_json.dumps({\n",
" \"study\": \"202512_TEI_Genesys_CX_Cloud\",\n",
" \"source\": (\"Forrester TEI of CX Cloud, by Genesys and Salesforce \"\n",
" \"(Dec 2025, commissioned by Genesys and Salesforce)\"),\n",
" \"published\": PUBLISHED,\n",
" \"reproduction\": {\n",
" \"benefits_pv\": round(composite[\"benefits_pv\"], 2),\n",
" \"costs_pv\": round(composite[\"costs_pv\"], 2),\n",
" \"npv\": round(composite[\"npv\"], 2),\n",
" \"roi_pct\": round(composite[\"roi_pct\"], 2),\n",
" \"payback_months\": round(composite[\"payback_months\"], 2),\n",
" \"payback_note\": \"Forrester does not headline a payback for this study\",\n",
" },\n",
" \"client\": {\n",
" \"drivers\": {\n",
" \"agents_fte\": AGENTS_FTE,\n",
" \"weekly_interactions\": WEEKLY_INTERACTIONS,\n",
" \"annual_revenue\": ANNUAL_REVENUE,\n",
" \"ai_tokens_annual\": AI_TOKENS_ANNUAL,\n",
" \"discount_rate\": DISCOUNT_RATE,\n",
" \"scenario\": SCENARIO,\n",
" },\n",
" \"benefits_by_year\": {str(y): round(client[\"benefits_by_year\"][y]) for y in YEARS},\n",
" \"costs_by_year\": {str(y): round(client[\"costs_by_year\"][y]) for y in YEARS},\n",
" \"net_by_year\": {str(y): round(client[\"net_by_year\"][y]) for y in YEARS},\n",
" \"cumulative_net_by_year\": {str(y): round(client[\"cumulative_net_by_year\"][y]) for y in YEARS},\n",
" \"initial_costs\": round(client[\"initial_costs\"]),\n",
" \"kpis\": {\n",
" \"benefits_pv\": round(client[\"benefits_pv\"]),\n",
" \"costs_pv\": round(client[\"costs_pv\"]),\n",
" \"npv\": round(client[\"npv\"]),\n",
" \"roi_pct\": round(client[\"roi_pct\"], 2),\n",
" \"payback_months\": round(client[\"payback_months\"], 2)\n",
" if client[\"payback_months\"] is not None else None,\n",
" \"payback_label\": client[\"payback_label\"],\n",
" },\n",
" \"scenarios\": {\n",
" s: {\"benefits_pv\": round(r[\"benefits_pv\"]),\n",
" \"costs_pv\": round(r[\"costs_pv\"]),\n",
" \"npv\": round(r[\"npv\"]),\n",
" \"roi_pct\": round(r[\"roi_pct\"], 2)}\n",
" for s, r in scen_summaries.items()\n",
" },\n",
" },\n",
" \"driver_map\": {\"benefits\": BENEFIT_DRIVERS, \"costs\": COST_DRIVERS},\n",
" \"ai_token_note\": (\"Published study models $0 Genesys AI Experience token \"\n",
" \"consumption; benefits B/C/D depend on token-billed AI \"\n",
" \"capabilities. Price via drivers.ai_tokens_annual.\"),\n",
"}, indent=2))\n",
"backstage(\"```\")\n"
]
}
],
"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"
},
"widgets": {
"application/vnd.jupyter.widget-state+json": {
"state": {
"14c2d1e5a0644752a2731aca143939b2": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"2247afa5bfb04ad8905333e3f64caf67": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.number.NumberInputWidget",
"_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "Weekly interactions — composite 80,000",
"layout": "IPY_MODEL_e2772f6f5be849508a98ba9fd5de0dd1",
"layout_path": null,
"max": 2000000.0,
"min": 1000.0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"step": 5000.0,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 80000.0
}
},
"2b2aa32e5ae340dfab0c7f0de2791359": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"55da27f443ca4021895933ad5d96f23b": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.number.NumberInputWidget",
"_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "Annual revenue ($) — composite 2.5B",
"layout": "IPY_MODEL_14c2d1e5a0644752a2731aca143939b2",
"layout_path": null,
"max": 100000000000.0,
"min": 100000000.0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"step": 100000000.0,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 2500000000.0
}
},
"6367ab66c77b4dcba88690962f8f466b": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"76c10bd18e9c466daec54721bc92b435": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.select.SelectWidget",
"_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"choices": [
"8%",
"10% (Forrester)",
"12%"
],
"disabled": false,
"hidden": false,
"label": "Discount rate",
"layout": "IPY_MODEL_fdb25a02906540a49bcd2fb7791aba21",
"layout_path": null,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": "10% (Forrester)"
}
},
"9173bebf5a34432e8e4819f906db4add": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.number.NumberInputWidget",
"_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "Genesys AI tokens ($/yr) — study modeled $0",
"layout": "IPY_MODEL_2b2aa32e5ae340dfab0c7f0de2791359",
"layout_path": null,
"max": 5000000.0,
"min": 0.0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"step": 25000.0,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 0.0
}
},
"979e73c2734f41dca452ea6800cad0b8": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "2.0.0",
"model_name": "HTMLStyleModel",
"state": {
"_model_module": "@jupyter-widgets/controls",
"_model_module_version": "2.0.0",
"_model_name": "HTMLStyleModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "StyleView",
"background": null,
"description_width": "",
"font_size": null,
"text_color": null
}
},
"ac40d1aeb52245df84aad900dc195c77": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"b01d658cd1294c5382cf6c64284d4781": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"b2751bf42368472ebf73f08317e9cde9": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.number.NumberInputWidget",
"_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"disabled": false,
"hidden": false,
"label": "CX agents (FTE) — composite 600",
"layout": "IPY_MODEL_6367ab66c77b4dcba88690962f8f466b",
"layout_path": null,
"max": 20000.0,
"min": 25.0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"step": 25.0,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 600.0
}
},
"c7bf126b79ee49b2bb7140ec4fb10b96": {
"model_module": "@jupyter-widgets/controls",
"model_module_version": "2.0.0",
"model_name": "HTMLModel",
"state": {
"_dom_classes": [],
"_model_module": "@jupyter-widgets/controls",
"_model_module_version": "2.0.0",
"_model_name": "HTMLModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/controls",
"_view_module_version": "2.0.0",
"_view_name": "HTMLView",
"cell_id": "",
"description": "",
"description_allow_html": false,
"layout": "IPY_MODEL_ac40d1aeb52245df84aad900dc195c77",
"layout_path": null,
"placeholder": "",
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"style": "IPY_MODEL_979e73c2734f41dca452ea6800cad0b8",
"tabbable": null,
"tooltip": null,
"value": "<div style=\"font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: normal; line-height: 1.65; color: #0f172a; word-break: break-word;\"><p style=\"margin: 0 0 1em;\"><b>Jump to section</b><ol style=\"padding-left:1.2em;margin:6px 0; margin: 0 0 1em; padding-left: 1.4em;\"><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-1');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('1 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Composite organization</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-2');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('2 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Client inputs</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-3');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('3 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Benefits</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-4');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('4 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Costs &amp; the $0 AI line</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-5');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('5 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Business case</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-6');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('6 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Scenarios</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-7');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('7 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Verification &amp; assertions</a></li><li style=\"margin:2px 0\"><a style=\"cursor:pointer;text-decoration:underline; color: #007bff; text-decoration: underline; text-underline-offset: 0.14em;\" onclick=\"var el=document.getElementById('section-8');if(!el){document.querySelectorAll('h1,h2').forEach(function(h){if(!el&&h.textContent.trim().indexOf('8 ')===0){el=h;}});}if(el)el.scrollIntoView({behavior:'smooth',block:'start'});\">Data appendix</a></li></ol></p></div>"
}
},
"ce038c3719d240a1a635401ac4a6208e": {
"model_module": "anywidget",
"model_module_version": "~0.11.*",
"model_name": "AnyModel",
"state": {
"_anywidget_id": "mercury.select.SelectWidget",
"_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ",
"_dom_classes": [],
"_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ",
"_model_module": "anywidget",
"_model_module_version": "~0.11.*",
"_model_name": "AnyModel",
"_view_count": null,
"_view_module": "anywidget",
"_view_module_version": "~0.11.*",
"_view_name": "AnyView",
"cell_id": "",
"choices": [
"conservative",
"moderate",
"aggressive"
],
"disabled": false,
"hidden": false,
"label": "Scenario",
"layout": "IPY_MODEL_b01d658cd1294c5382cf6c64284d4781",
"layout_path": null,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": "moderate"
}
},
"e2772f6f5be849508a98ba9fd5de0dd1": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
},
"fdb25a02906540a49bcd2fb7791aba21": {
"model_module": "@jupyter-widgets/base",
"model_module_version": "2.0.0",
"model_name": "LayoutModel",
"state": {
"_model_module": "@jupyter-widgets/base",
"_model_module_version": "2.0.0",
"_model_name": "LayoutModel",
"_view_count": null,
"_view_module": "@jupyter-widgets/base",
"_view_module_version": "2.0.0",
"_view_name": "LayoutView",
"align_content": null,
"align_items": null,
"align_self": null,
"border_bottom": null,
"border_left": null,
"border_right": null,
"border_top": null,
"bottom": null,
"display": null,
"flex": null,
"flex_flow": null,
"grid_area": null,
"grid_auto_columns": null,
"grid_auto_flow": null,
"grid_auto_rows": null,
"grid_column": null,
"grid_gap": null,
"grid_row": null,
"grid_template_areas": null,
"grid_template_columns": null,
"grid_template_rows": null,
"height": null,
"justify_content": null,
"justify_items": null,
"left": null,
"margin": null,
"max_height": null,
"max_width": null,
"min_height": null,
"min_width": null,
"object_fit": null,
"object_position": null,
"order": null,
"overflow": null,
"padding": null,
"right": null,
"top": null,
"visibility": null,
"width": null
}
}
},
"version_major": 2,
"version_minor": 0
}
}
},
"nbformat": 4,
"nbformat_minor": 5
}