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>
68 lines
2.4 KiB
Python
68 lines
2.4 KiB
Python
"""
|
|
Scenario stress — transplanted from the retired shared ``core/calculations/scenarios.py``
|
|
with identical semantics.
|
|
|
|
Forrester TEI risk-adjusts benefits *down* and costs *up*; scenarios stress
|
|
both levers:
|
|
|
|
* ``adoption`` scales nominal values (``year_values`` and ``initial``).
|
|
* ``risk_delta`` is *added* to a benefit's risk factor and *subtracted*
|
|
from a cost's (conservative = more uncertainty on benefits, less padding
|
|
on costs), then clamped to [0, 1].
|
|
|
|
``"moderate"`` is the identity — the headless default reproduces the
|
|
published study. Note the counterintuitive corollary: the conservative
|
|
scenario *lowers* costs PV, because 80% adoption shrinks consumption-priced
|
|
usage and the clamp caps cost padding.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from copy import deepcopy
|
|
|
|
SCENARIOS: dict[str, dict[str, float]] = {
|
|
"conservative": {"adoption": 0.80, "risk_delta": 0.10},
|
|
"moderate": {"adoption": 1.00, "risk_delta": 0.00},
|
|
"aggressive": {"adoption": 1.15, "risk_delta": -0.05},
|
|
}
|
|
|
|
|
|
def apply_scenario(
|
|
items: list[dict],
|
|
scenario: str = "moderate",
|
|
*,
|
|
multipliers: dict[str, dict[str, float]] | None = None,
|
|
table: str | None = None,
|
|
) -> list[dict]:
|
|
"""
|
|
Deep-copied value rows with the scenario applied; inputs are not mutated.
|
|
|
|
Each row needs ``year_values`` (year-string → float), optionally
|
|
``initial`` and ``risk_adjustment``, and a ``table`` of ``"benefits"``
|
|
or ``"costs"`` (or pass ``table=`` to force one) — the table decides the
|
|
sign of ``risk_delta``.
|
|
"""
|
|
cfg = (multipliers or SCENARIOS).get(scenario)
|
|
if cfg is None:
|
|
raise KeyError(f"Unknown scenario: {scenario!r}")
|
|
adoption = float(cfg.get("adoption", 1.0))
|
|
risk_delta = float(cfg.get("risk_delta", 0.0))
|
|
|
|
out: list[dict] = []
|
|
for raw in items:
|
|
item = deepcopy(raw)
|
|
item_table = item.get("table") or table or "benefits"
|
|
item["table"] = item_table
|
|
|
|
item["year_values"] = {
|
|
k: float(v) * adoption for k, v in item["year_values"].items()
|
|
}
|
|
if item.get("initial") is not None:
|
|
item["initial"] = float(item["initial"]) * adoption
|
|
|
|
ra = float(item.get("risk_adjustment") or 0.0)
|
|
new_ra = ra + risk_delta if item_table == "benefits" else ra - risk_delta
|
|
item["risk_adjustment"] = max(0.0, min(1.0, new_ra))
|
|
out.append(item)
|
|
return out
|