Files
Robert Helewka 64fb83257d feat: add GenesysCX study and fix Streamlit chart key collisions
- Add 202512_GenesysCX TEI study (config, seed data, notebooks, README)
  with NPV $10.8M / ROI 266% including AI-token cost line
- Add explicit `key` parameter to all chart wrappers in app/components
  to prevent StreamlitDuplicateElementId errors when the same figure
  type renders across Summary/Benefits/Costs tabs
- Render benefits bar and cost pie charts on their respective tabs
- Add benefits_vs_costs_by_year chart wrapper
2026-06-10 14:26:49 -04:00

82 lines
3.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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.
"""
Implementation rollout & ramp model.
Captures the gap between **when Genesys starts billing** (contract
start) and **when each region actually goes live**:
- The platform licence commit bills in full from contract start; the
vendor's *ramp period* compensates with a first-year credit
(typical: 6-month ramp → 50% Y1 discount on the platform commit).
- AI token usage (per-user and consumption meters) starts only when a
site goes live, and bills for the months the site is live in each
model year.
- Benefits likewise accrue only from go-live (the scenario realization
curve then models adoption maturity *within* the live period).
A site with ``go_live_month = m`` is live for ``12*year m`` months of
the first ``year`` years (clamped to 0..12 per year). So NAM at month 6
is live 6 months of Y1; EMEA at month 9 → 3 months; AUZ/APAC at month
12 → 0 months in Y1 and fully live from Y2.
"""
from __future__ import annotations
from dataclasses import dataclass, field
MONTHS_PER_YEAR = 12
@dataclass
class RolloutPlan:
#: ISO date Genesys starts billing the licence commit (informational,
#: surfaced in UI/exports; the model works in months-from-start).
contract_start: str | None = None
#: Total build duration, months (informational).
build_months: int = 12
#: Vendor ramp period, months. Documentation for the Y1 credit below.
ramp_months: int = 6
#: First-year credit on the platform licence commit. Typical
#: 6-month ramp = 50% discount in year 1; years 2+ bill in full.
first_year_platform_discount: float = 0.5
#: site_name -> go-live month (months after contract start).
#: Sites absent from the map are treated as live from day 0.
go_live_month: dict[str, int] = field(default_factory=dict)
def __post_init__(self) -> None:
if not 0.0 <= self.first_year_platform_discount <= 1.0:
raise ValueError("first_year_platform_discount must be within 0-1")
if self.ramp_months < 0 or self.build_months < 0:
raise ValueError("ramp_months/build_months must be >= 0")
for site, m in self.go_live_month.items():
if m < 0:
raise ValueError(f"{site}: go_live_month must be >= 0")
# ── Availability ────────────────────────────────────────────────
def live_months_in_year(self, site_name: str, year: int) -> int:
"""Months ``site_name`` is live during model year ``year`` (1-based)."""
go_live = self.go_live_month.get(site_name, 0)
live_by_year_end = max(0, MONTHS_PER_YEAR * year - go_live)
live_by_prev_year_end = max(0, MONTHS_PER_YEAR * (year - 1) - go_live)
return min(MONTHS_PER_YEAR, live_by_year_end - live_by_prev_year_end)
def fraction_live(self, site_name: str, year: int) -> float:
return self.live_months_in_year(site_name, year) / MONTHS_PER_YEAR
# ── Billing ─────────────────────────────────────────────────────
def platform_factor(self, year: int) -> float:
"""Fraction of the full platform commit billed in ``year``."""
return 1.0 - self.first_year_platform_discount if year == 1 else 1.0
#: Behaviour identical to the pre-rollout model: everything live from
#: day 0, no ramp credit.
NO_ROLLOUT = RolloutPlan(
build_months=0, ramp_months=0, first_year_platform_discount=0.0
)