Forecasting Model
Bioloupe Forecasting Model
Section titled “Bioloupe Forecasting Model”Pharmaceutical forecasting domain model: patient flow, addressable population, market share, revenue calculations, and Monte Carlo simulation.
For technical architecture (Jotai state, React Query, component patterns), see ARCHITECTURE.md.
Table of Contents
Section titled “Table of Contents”- Introduction & Model Overview
- Patient Flow Model
- Addressable Population Calculations
- Market Share Model
- Revenue/Sales Calculations
- Monte Carlo Simulation
- Data Inputs Reference
1. Introduction & Model Overview
Section titled “1. Introduction & Model Overview”Purpose
Section titled “Purpose”The Bioloupe forecasting model predicts pharmaceutical sales revenue by modeling the patient journey from disease incidence through treatment and calculating market share capture across multiple therapy lines.
High-Level Flow
Section titled “High-Level Flow”flowchart TB
A[Annual Incidence] --> B[Addressable Population]
B --> C[Drug Treated Patients]
C --> D[Market Share Applied]
D --> E[New Patients per Therapy Line]
E --> F[Revenue Calculation]
F --> G[Total Sales Forecast]
Key Business Concepts
Section titled “Key Business Concepts”| Concept | Description |
|---|---|
| Incidence | New disease cases per year in a population |
| Addressable Population | Patients who can potentially receive treatment |
| Therapy Line | Sequential treatment stage (1L, 2L, 3L+) |
| Peak Share | Maximum market share a product can achieve |
| Loss of Exclusivity (LoE) | When patent protection ends and generics enter |
| Class Share | Percentage of patients suitable for the therapy class (adjusts Peak Share) |
2. Patient Flow Model
Section titled “2. Patient Flow Model”The model tracks patients through a funnel from initial diagnosis to treatment across multiple therapy lines.
Patient Population Funnel
Section titled “Patient Population Funnel”flowchart TD
subgraph Incidence["Disease Incidence"]
I[Base Incidence] --> EVO[Incidence Evolution Array<br/>compound growth per year]
end
subgraph StageSplit["Stage Split (Solid Tumors Only)"]
EVO --> |Solid Tumor| ES[Early Stage %]
EVO --> |Solid Tumor| MS[Metastatic %]
EVO --> |Hematology| TH[Full Incidence]
ES --> E2E[+ Early to Early Relapse %]
ES --> E2M[Early to Met Relapse %]
E2M --> MS
end
subgraph Addressable["Addressable Population (1L)"]
E2E --> BF1["x Biomarker Factor"]
BF1 --> EA["Early Addressable x HC%"]
MS --> BF2["x Biomarker Factor"]
BF2 --> MA["Met Addressable x HC%"]
TH --> BF3["x Biomarker Factor"]
BF3 --> TA["Therapy Addressable x HC%"]
end
Disease Categories
Section titled “Disease Categories”The model handles two main disease categories differently:
| Category | Stage Split |
|---|---|
| Solid Tumors | Yes — Early Stage + Metastatic |
| Hematology | No — Full incidence flows to therapy |
Disease type is 'Solid Tumor' | 'Hematology' (see DiseaseConfig in src/features/reports/forecasting/types.ts). Specific indications are configured via reference data from the API.
Solid Tumors split into Early Stage and Metastatic populations. Early-stage patients may relapse into the metastatic pool. Hematology cancers are systemic from diagnosis, so all patients flow directly to therapy lines without stage separation.
Formula difference:
Solid Tumor Met Addressable = (Incidence x Met%) + (Incidence x Early% x Relapse%)Hematology Addressable = Incidence x Healthcare Access%HA is the scenario-level value from the SummaryPanel, applied once at the top of the patient flow.
Therapy Line Transitions
Section titled “Therapy Line Transitions”flowchart TD
A[1L Addressable Population] --> |Treatment Rate %| B[1L Drug Treated Patients]
B --> |Market Share %| C[1L New Patients on Product]
B --> |Pool minus Retreatment| D[Available for 2L]
D --> |Transition Rate %| E[2L Drug Treated Patients]
E --> |Market Share %| F[2L New Patients on Product]
E --> |Pool minus Retreatment| G[Available for 3L]
G --> |Transition Rate %| H[3L Drug Treated Patients]
H --> |Market Share %| I[3L New Patients on Product]
Key Transition Parameters:
- Drug Treatment Rate: Percentage of eligible patients who receive any treatment (applied at stage entry only — see Important note below)
- Transition Rate: Percentage of patients who move from one therapy line to the next
- Retreatment Factor: Patients staying on current therapy (reduces pool for next line)
- Re-Treatment from Prior Lines Toggle (
retreatmentOption, per-line UI): OFF (default) — drug-exposed patients are excluded from subsequent-line pools (pool − newPatients × 1). ON — drug-exposed patients remain in subsequent-line pools and can be captured again by the product’s market share (pool − 0).
Important: Drug Treatment Rate applies at the funnel entry point of each stage group only — the early line, the first metastatic line (Met1), and the first therapy line (hematology 1L). Custom lines participate in their effective stage group (stageCategory ?? "therapy") and are applicable only when they are the first member of that group by array order; in practice that means custom lines appended after a reference line are non-applicable. For non-first lines the UI hides the Drug Treatment Rate field and shows an inline note pointing back at the source line, and the compute/report/MC/Tornado paths skip DTR (factor = 1); transitioned patients are considered eligible. Gated by isDTRApplicable() in src/features/forecasting/math/forecasting.ts.
2.1 Biomarker Filtering
Section titled “2.1 Biomarker Filtering”Concept
Section titled “Concept”Some cancer indications have biomarker-defined sub-populations — subsets of patients who test positive for a specific biomarker. For example, Oropharyngeal Cancer has an HPV Positive sub-population representing ~68% of cases.
When a biomarker filter is active, the model applies a funnel multiplier to the first-line addressable population. Biomarker does not modify incidence — it filters within the patient flow funnel.
Formula
Section titled “Formula”biomarkerFactor = (biomarkerPercentage / 100) x (biomarkerTestingRate / 100) [when active] = 1.0 [when inactive]Applied in the funnel:
Addressable Population (incidence + relapse -- unchanged by biomarker) | x biomarkerFactor <- applied here (first line only) | x Healthcare Access % | x Neo/Adj Factor (if applicable) | x Drug Treatment Rate | = Drug Treated Patients- Biomarker Percentage: prevalence of the biomarker in the disease population (e.g., 68%)
- Testing Rate: percentage of patients actually tested for this biomarker (e.g., 93%)
Example: HPV Positive, Oropharyngeal Cancer (USA)
Section titled “Example: HPV Positive, Oropharyngeal Cancer (USA)”| Parameter | Value |
|---|---|
| Addressable Population | 15,004 |
| HPV Positive Prevalence | 68% |
| HPV Testing Rate | 93% |
| biomarkerFactor | 0.68 x 0.93 = 0.6324 |
| Filtered Addressable | 15,004 x 0.6324 = 9,489 |
Behavior
Section titled “Behavior”- Biomarker filtering is optional (toggle OFF by default)
- Only one biomarker can be active per indication at a time
- Applied to first lines only (Early 1L, Met 1L, Therapy 1L for hematology); subsequent lines inherit the filtered count via transition rates
- Toggling OFF sets factor to 1.0 (no filtering)
- Incidence count is never modified — biomarker is a derived multiplier via
biomarkerFactorAtom
Key files:
- Atoms:
biomarkerFactorAtom,activeBiomarkerAtom,toggleBiomarkerAtominsrc/features/reports/forecasting/atoms.ts - Computation:
biomarkerFactorparam oncalculateLineFunnelForDisplay,calculateLinePatients,calculateLineSales,calculateTreatmentEligiblePatients - UI:
FirstLineExtrascomponent inLinePanel.tsx
Testing Rate Over Time
Section titled “Testing Rate Over Time”The biomarker testing rate can project year-over-year via the “Expected to change?” toggle. When enabled, the testing rate compounds annually starting from startYear, capped at max:
testingRate(year) = testingRate(year - 1) x (1 + min/100) for year >= startYear = min(testingRate(year), max) clamped each step = baseTestingRate for year < startYearThe biomarker factor at a given year becomes:
biomarkerFactor(year) = (biomarkerPercentage / 100) x (testingRate(year) / 100)Growth is shared across early and metastatic first lines where the same biomarker is active (one config per bm:{biomarkerId} instance row). Computed via computeBiomarkerFactor(year, percentage, testingRateBase, growth) in src/features/forecasting/math/forecasting.ts.
2.1.1 Transplant Eligibility Split (Hematology — AML / MM / DLBCL / FL / Hodgkin Lymphoma)
Section titled “2.1.1 Transplant Eligibility Split (Hematology — AML / MM / DLBCL / FL / Hodgkin Lymphoma)”Concept
Section titled “Concept”For hematologic malignancies where stem cell transplant is a conditioning step, a therapy line may forecast only a slice of the addressable population: the transplant-eligible cohort, the non-eligible cohort, or some other defined subgroup. The user picks one cohort per line and sets a single percentage applied as a multiplier (mirrors the Neo/Adj split pattern).
Formula
Section titled “Formula”transplantSplitFactor = transplantSplitPercent / 100 [when toggle ON for line L] = 1 [when toggle OFF]- When disabled or not allowlisted, factor = 1.0 (no effect).
- The
transplantSplitSetting("eligible"/"non-eligible"/"other") is a label — does not change the math, but drives the smart default.
Setting-aware default
Section titled “Setting-aware default”When the toggle flips ON (and the user hasn’t manually overridden the percent), the percent auto-fills from the disease-level transplantEligible reference value E:
| Setting | Auto-fill default |
|---|---|
eligible | E (or 50 if no reference) |
non-eligible | 100 − E (or 50 if no reference) |
other | 100 |
Where It Fits in the Patient Funnel
Section titled “Where It Fits in the Patient Funnel”Addressable Population | x Healthcare Access | x Neo/Adj Factor (early-stage solid tumors only) | x Transplant Split Factor <- NEW, per line, hematology only | x Drug Treatment Rate | = Drug Treated PatientsApplied per-line in calculateLinePatients and calculateLineFunnelForDisplay via getTransplantSplitFactor(line).
UI Behavior
Section titled “UI Behavior”- Allowlist: AML, MM, DLBCL, FL, Hodgkin Lymphoma (case-insensitive). Block hidden for all other indications.
- Toggle: User enables/disables the split per line.
- Segmented control: Eligible | Non-eligible | Other.
- Percentage input: Single percentage (0–100%) applied as a multiplier (default = setting-aware as above).
- No Monte Carlo / Tornado wiring: the percent row hides the “Add to Monte Carlo” toggle.
Key files:
- Type:
UnifiedLineData.transplantSplitEnabled/transplantSplitSetting/transplantSplitPercentinsrc/features/reports/forecasting/types.ts - Computation:
getTransplantSplitFactor()+getTransplantSplitDefault()insrc/features/reports/forecasting/sections/Configuration/transplant.ts - UI: Inline block in
LinePanel.tsx(gated onisTransplantAllowlisted(indication)next to the Treatment Rate row) - DOCX: single-emission block in
reporting/collectReportData.ts(mirrors neoAdj)
2.2 Neoadjuvant / Adjuvant Split (Early Stage Solid Tumors Only)
Section titled “2.2 Neoadjuvant / Adjuvant Split (Early Stage Solid Tumors Only)”Concept
Section titled “Concept”For early-stage solid tumors, patients may receive treatment in different settings:
- Neoadjuvant = treatment before surgery (shrink the tumor, then operate)
- Adjuvant = treatment after surgery (operate, then treat to prevent recurrence)
- Other = other treatment settings (e.g. maintenance, palliative)
The user selects one category and sets a single percentage that acts as a simple multiplier on the eligible population.
Formula
Section titled “Formula”neoAdjFactor = neoAdjPercent / 100- When disabled or not applicable, factor = 1.0 (no effect)
- The selected category (
neoAdjSetting) is a label only — does not affect the math
Default: 100% = factor 1.0 (no reduction)
Where It Fits in the Patient Funnel
Section titled “Where It Fits in the Patient Funnel”Addressable Population | x Healthcare Access | x [Setting] Split <- neoAdjPercent / 100 | x Drug Treatment Rate | = Drug Treated PatientsThe neo/adj factor is applied before drug treatment rate (see calculateLineFunnelForDisplay in src/features/forecasting/math/forecasting.ts). The toggle is per-line — any early-stage line can have it enabled independently. In sales/Monte Carlo calculations, the factor applies to the first line only.
UI Behavior
Section titled “UI Behavior”- Toggle: User enables/disables the split per line
- Segmented control: Pick one of 3 categories: Neoadjuvant, Adjuvant, or Other
- Percentage input: Single percentage (0-100%) applied as a multiplier (default 100%)
- Rollback: Resets percentage to 100%
Key files:
- Type:
UnifiedLineData.neoAdjSettingandneoAdjPercentinsrc/features/reports/forecasting/types.ts - Computation:
getNeoAdjFactor()insrc/features/forecasting/math/forecasting.ts - UI: Segmented control + single percentage in
LinePanel.tsx
2.3 Custom Variables (Addressable Population Multipliers)
Section titled “2.3 Custom Variables (Addressable Population Multipliers)”Concept
Section titled “Concept”Users can define up to MAX_CUSTOM_VARIABLES custom multiplier variables per therapy line (see constants.ts). Each custom variable represents a percentage applied multiplicatively to the addressable population, allowing users to model additional filtering factors not covered by the built-in parameters.
Formula
Section titled “Formula”customMultiplier = product(customVar.value / 100) -- product of all custom variable percentagesApplied in the patient funnel (order matches code):
Addressable Population | x Healthcare Access % | x Neo/Adj Factor (if applicable -- see section 2.2) | x Drug Treatment Rate | x customMultiplier <- product of all custom variable percentages | = Drug Treated PatientsFirst-Line Coupling
Section titled “First-Line Coupling”First-line (1L) custom variables are synced to incidenceVars.customVariables for use in Monte Carlo and Tornado simulations. This ensures probabilistic analyses include the custom variable effects.
Multi-Line Support
Section titled “Multi-Line Support”For 2L+ lines, the system uses findMatchingLineCustomVariable() to locate matching custom variables across lines, enabling consistent filtering across the therapy cascade.
Key files:
- Computation:
getCustomMultiplier()(display paths — year-agnostic) andgetCustomMultiplierForYear(vars, year)(live Model/SalesChart andcalculateTreatmentEligiblePatients— compounds viaapplyChangeableValuewhenchangeable && startYear !== null) insrc/features/forecasting/math/forecasting.ts - Matching:
findMatchingLineCustomVariable()insrc/features/forecasting/math/forecasting.ts - Constant:
MAX_CUSTOM_VARIABLESinsrc/features/reports/forecasting/constants.ts
Growth Projection (“Expected to change?”)
Section titled “Growth Projection (“Expected to change?”)”Custom variables already carry {changeable, min, max, startYear} and project year-over-year via applyChangeableValue (src/features/forecasting/math/forecasting.ts). The same growth mechanism is now wired for built-in Healthcare Access (scenario-level), Drug Treatment Rate (per-line), and Biomarker Testing Rate (per-biomarker) — see §2.1 Testing Rate Over Time and §3 Healthcare Access.
3. Addressable Population Calculations
Section titled “3. Addressable Population Calculations”Incidence Calculation
Section titled “Incidence Calculation”Per-year incidence is read from the incidenceEvolution array, which is built by compounding the per-year growth rate off the base:
incidence[0] = round(base)incidence[i] = round(incidence[i-1] x (1 + growthRate / 100))Example:
- Base Incidence (year 0): 50,000 patients
- Growth Rate: 0.5% per year
incidence[6] = round(50,000 x (1 + 0.005)^6) ~= 51,515 patientsThe array is built by buildIncidenceEvolution() in src/features/forecasting/math/incidence-evolution.ts and consumed year-by-year in src/features/forecasting/math/forecasting.ts via incidenceEvolution[yearIndex]. Growth rates can be edited per transition; see the Evolution Arrays section for storage details. Geo-level variation is applied upstream at cascade time via the per-capita incidenceBase fallback.
Addressable Population by Disease Category
Section titled “Addressable Population by Disease Category”Solid Tumors (Stage-Aware)
Section titled “Solid Tumors (Stage-Aware)”Solid tumors split into Early Stage and Metastatic populations with relapse transitions:
Early Addressable:
Early Addressable = Incidence x EarlyStage% x (1 + EarlyToEarlyRelapse%) x HealthcareAccess%Metastatic Addressable:
Met Addressable = (Incidence x MetStage% + Incidence x EarlyStage% x EarlyToMetRelapse%) x HealthcareAccess%Code note:
computeAddressable()returns raw stage addressable counts. The sharedbuildLineFunnelMap()helper insrc/features/forecasting/math/forecasting.tsmultiplies the population-level Healthcare Access into stage addressable before invokingcalculateLineFunnelForDisplay, so the funnel function itself no longer applies HA per line. BothPatientFlowTimeline(the patient-flow tree UI) andLinePanel(the configuration panel) consume that helper, so their addressable/eligible counts cannot drift. The sales path (calculateLinePatients) reads HA from the population row via thepopulationHealthcareAccessparam and applies it once at the first line viaapplyFilters.
Example (Breast Cancer):
- Incidence: 50,000
- Early Stage: 70%
- Metastatic: 30%
- Early to Early Relapse: 15%
- Early to Met Relapse: 10%
- Healthcare Access: 95%
Early Addressable = 50,000 x 0.70 x (1 + 0.15) x 0.95 = 38,238 patientsMet Addressable = (50,000 x 0.30 + 50,000 x 0.70 x 0.10) x 0.95 = 17,575 patientsHematology (Direct Flow)
Section titled “Hematology (Direct Flow)”Hematology indications do not have stage splits:
Addressable = Incidence x HealthcareAccess%Stage Mix Parameters
Section titled “Stage Mix Parameters”| Parameter | Description |
|---|---|
| Early Stage % | Initial early-stage diagnosis |
| Localized % | Sub-breakdown: localized early stage (optional) |
| Locally Advanced % | Sub-breakdown: locally advanced (III-IVB by default; III-IIIB for lung and breast, optional) |
| Metastatic % | Initial metastatic diagnosis (derived: 100 - Early%) |
| Unknown Stage % | Patients with unknown stage at diagnosis (optional) |
| Early to Early Relapse % | Early patients who relapse but stay early |
| Early to Met Relapse % | Early patients who progress to metastatic |
Actual values are sourced per indication from the API reference data.
Note: Early Stage % + Metastatic % + Unknown Stage % = 100%. When Unknown Stage % is not provided, it auto-derives as the residual: max(0, 100 − Early% − Met%).
Early Stage Sub-Variables (Solid Tumors Only)
Section titled “Early Stage Sub-Variables (Solid Tumors Only)”Some solid tumors have granular staging data that breaks Early Stage into Localized and Locally Advanced (III-IVB) sub-populations. These sub-variables are display-only — the computation layer always uses the combined earlyStagePercent.
Behavior matrix:
| Data Available | Parent (Early Stage %) | Localized | Locally Advanced | Example |
|---|---|---|---|---|
| Both subs | Read-only (auto-sum) | Editable | Editable | Oropharyngeal: 15.8 + 72.9 = 88.7% |
| Parent + one sub | Editable | Read-only | Read-only | Partial reference data |
| Parent only | Editable | Hidden | Hidden | Most indications (today’s behavior) |
| No data | Fallback chain | Hidden | Hidden | Missing geo-specific data |
Auto-computation: When both sub-variables are present, the parent is derived:
earlyStagePercent = earlyStageLocalizedPercent + earlyStageLocallyAdvancedPercentmetStagePercent = 100 - earlyStagePercentReset behavior: Resetting a sub-variable also resets the parent and metastatic percentage to maintain consistency.
Healthcare Access
Section titled “Healthcare Access”Healthcare access percentages are sourced per geography and indication from the API reference data. The fallback default is DEFAULT_INCIDENCE_DATA.healthcareAccess in src/features/reports/forecasting/constants.ts.
Growth Over Time
Section titled “Growth Over Time”Healthcare Access (scenario-level) and Drug Treatment Rate (per-line) can each project year-over-year via the “Expected to change?” toggle. When enabled, the value compounds annually starting from startYear, capped at max:
v(year) = v(year - 1) x (1 + min/100) for year >= startYear = min(v(year), max) clamped each step = baseValue for year < startYearImplemented by applyChangeableValue(year, baseValue, config) in src/features/forecasting/math/forecasting.ts. The growth config is a ChangeableConfig ({changeable, min, max, startYear}) defined in types.ts.
- Scenario-level scope (HA): Healthcare Access is a single scenario-level value. It is applied once at the top of the patient flow — multiplied into the stage addressable population before any line-level math runs. Subsequent therapy lines no longer apply HA. The user edits HA in the SummaryPanel (Patient Flow summary), and a single
healthcareAccessGrowthrow stores the optional “Expected to change?” projection. - Per-line scope (TR): Drug Treatment Rate remains per-line, with a per-line growth config; absent rows = growth OFF.
- Per-line override (HA): To model line-specific access penalties (e.g., access drops at later therapy lines, regional sub-population effects), users add a Custom Variable on the affected line. Custom variables multiply that line’s eligible pool via
getCustomMultiplier. Example: a line with HA penalty 85% is modeled by adding a custom variable named e.g. “Late-line access penalty” with value85. - Persistence: Stored as JSON-encoded
ChangeableConfigininstanceRowsAtomunder companion row nameshealthcareAccessGrowth({line: null}) andtreatmentRateGrowth(per-line). - Defaults on toggle ON: rate
0%, max100%, startYear2030(DEFAULT_GROWTH_CONFIGinconstants.ts).
4. Market Share Model
Section titled “4. Market Share Model”Peak Share Calculation
Section titled “Peak Share Calculation”Peak Share is the absolute maximum share a product reaches over the forecast. It is determined by competitive positioning. The product approaches Peak Share via the Speed-to-Peak uptake curve and may decline before LoE due to market events or competitive dynamics; once LoE hits, share also erodes via the LoE curve. A user-set Peak Share override (via the Custom Input toggle) wins over the Launch Order / Best-in-Class auto-recommendation.
flowchart LR
subgraph Inputs["Peak Share Inputs"]
LO[Launch Order<br/>1st to 10th]
BIC[Best-in-Class?<br/>Yes/No]
DVC[Delay vs Competition<br/>Quarters]
end
subgraph Calculation["Peak Share Calculation"]
LO --> BASE[Base Share from Matrix]
BIC --> BONUS[Best-in-Class Bonus]
DVC --> PENALTY[Delay Penalty]
BASE --> PS[Peak Share %]
BONUS --> PS
PENALTY --> PS
end
Formulas:
Peak Share (within class):
PeakShare = min(100, max(0, BaseShare[LaunchOrder] + BestInClassBonus - DelayPenalty))Effective Peak Share (used in market share calculations):
EffectivePeakShare = CustomEffectivePeakShare (if user has toggled Custom Input) OR PeakShare x (ClassShare / 100) (Bioloupe Guidance -- default)Where:
BaseSharecomes from theLAUNCH_ORDERSmatrix inconstants.ts(indexed by launch order and number of competitors)BestInClassBonusfromBEST_IN_CLASSarray (indexed by number of competitors)DelayPenalty = DelayQuarters > COMPETITION_THRESHOLD_QUARTERS ? DelayQuarters x COMPETITION_FACTOR_MULTIPLIER : 0(seeconstants.tsfor current values)ClassShareis the percentage of patients suitable for the therapy class (default 100%)
Example:
- Launch Order: 2nd to market (with 3 competitors)
- Best in Class: Yes
- Delay: 6 quarters behind first entrant
- Base Share (2nd position, 3 competitors): from
LAUNCH_ORDERS - Best-in-Class Bonus (3 competitors): from
BEST_IN_CLASS - Delay Penalty: 6 x
COMPETITION_FACTOR_MULTIPLIER
Verify current lookup values in
LAUNCH_ORDERSandBEST_IN_CLASSarrays inconstants.ts.
Peak Share = 29% + 30% - 3% = 56%Uptake Curves (Speed to Peak)
Section titled “Uptake Curves (Speed to Peak)”Products don’t achieve peak share immediately. The uptake curve determines how quickly market share ramps up. Peak Share is the ceiling; the uptake curve controls acceleration toward it.
All uptake curves are defined in the UPTAKE_CURVE constant in src/features/reports/forecasting/constants.ts. Curves are named {N} Year {Speed} (e.g., “3 Year Fast”). The number indicates years to reach 100% of peak share.
Note: Curves are named like “3 Year Medium”, “5 Year Fast”, etc. The number indicates years to reach 100% of peak share. A product with 60% peak share using “3 Year Fast” reaches 60% x 60% = 36% in Year 1, 60% x 85% = 51% in Year 2, and the full 60% in Year 3.
Market Share Over Time
Section titled “Market Share Over Time”flowchart LR
subgraph PreLaunch["Pre-Launch"]
PL[Market Share = 0%]
end
subgraph Ramp["Ramp Period"]
PS[Peak Share] --> UC[Uptake Curve Applied]
UC --> MS1[Growing Share]
end
subgraph Mature["Mature Period"]
MS1 --> PEAK[At Peak Share]
end
subgraph PostLoE["Post-LoE"]
PEAK --> ER[Erosion Curve]
ER --> FINAL[Declining Share]
end
PreLaunch --> Ramp
Ramp --> Mature
Mature --> PostLoE
Year N Market Share Formula:
MarketShare[Year] = UptakeCurve[YearOffset] x EffectivePeakShare x (1 - ErosionRate[YearsPostLoE])Where EffectivePeakShare = CustomEffectivePeakShare (if set) or PeakShare x (ClassShare / 100)
First Year Weighting (for mid-year launches):
WeightedShare = (Uptake[Y1] x (13 - LaunchMonth) + Uptake[Y0] x (LaunchMonth - 1)) / 12Year-1 launch semantics
Section titled “Year-1 launch semantics”- Launch date is the first day of the stated month. A
launchof2025-08-01means Aug 1, 2025; that calendar year contributes 5 months of revenue (Aug–Dec). - Year-1 of Speed-to-Peak is the 12-month rolling window starting at the launch date — not the launch calendar year. The
WeightedShareformula above prorates the launch calendar year so the rolling-12-month uptake reads correctly across the launch-year / first-full-year boundary. - Year-0 (pre-launch row) market share is 0; the forecast horizon shows one pre-launch year of zeros to make the launch transition visible.
- Per-input clamp, not cross-line: every per-line market-share input is clamped 0–100% on entry, but the cross-line sum (Σ shares across all selected therapy lines) is intentionally not clamped — that surface lets users model competitive scenarios where lines compete for share.
Market events (additive impacts)
Section titled “Market events (additive impacts)”A market event is a one-time, persistent share adjustment that begins at the event’s date. Events are intentionally additive — not multiplicative — because they model discrete competitive or commercial inflections (a new competitor, a label expansion, a guideline change) rather than scaling the existing uptake.
MarketShare[Year] = ( UptakeCurve[YearOffset] x EffectivePeakShare + EventImpact[Year] ) x LoEImpact[Year]EventImpact[Year] = sum over all events whose startDate ≤ Year of: event.impactPercent x UptakeCurve[Year − event.year] (same Speed-to-Peak as the line)Behavior:
- Events use the same Speed-to-Peak uptake curve as the line they sit on, so a 5pp event under “3 Year Slow” ramps to its full 5pp impact over 3 years.
- Multiple events stack additively (their impacts sum into a single
EventImpact[Year]). - Negative impacts are allowed (e.g., a competitor launch with
impactPercent: -10subtracts 10pp). - Events are permanent — there is no end date. Once active, an event continues to contribute its impact for the remainder of the forecast.
- Events are gated by the line’s
isSelectedflag; deselected lines contribute 0 to both share and event impact. - Event impact is added on top of ramped share, then multiplied by
LoEImpact, so post-LoE erosion attenuates the event’s contribution alongside the base share.
Loss of Exclusivity (LoE) Erosion
Section titled “Loss of Exclusivity (LoE) Erosion”After LoE, market share erodes due to generic/biosimilar competition. Small molecules face rapid generic erosion; biologics erode more slowly due to biosimilar complexity.
Erosion curves are defined in MOLECULE_SHARE_EROSION and BIOLOGICS_SHARE_EROSION in src/features/reports/forecasting/constants.ts. Index i of the curve is the erosion percentage for the ith calendar year post-LoE (i = 0 is the year before LoE = 0% erosion; i = 1 is the LoE year; i ≥ N plateaus at the last tabulated value).
LoE Year Weighting (for mid-year LoE) — calendar year Y containing the LoE month M is split into a pre-LoE segment (months 1..M−1) and a post-LoE segment (months M..12):
LoEImpact[Y] = 1 - ( Erosion[i] x (13 - M) / 12 // post-LoE segment + Erosion[i-1] x (M - 1) / 12 ) // pre-LoE segmentwhere i = Y - LoEYear + 1 (so for Y = LoEYear, i = 1 → reads Erosion[1] for post-LoE months and Erosion[0] = 0 for pre-LoE months, yielding partial-year erosion). Years prior to LoE return LoEImpact = 1.0.
Behavior summary
Section titled “Behavior summary”- Jan-1 LoE = full calendar year of erosion. A
loeDateof2037-01-01makes the entire year 2037 post-LoE — Jan 1 is the first post-LoE day, soM = 1and the formula degenerates to1 - Erosion[1]. - Mid-year LoE prorates erosion via the partial-year blend formula above (months 1..M−1 use the prior-year erosion
Erosion[i-1]; months M..12 use the LoE-year erosionErosion[i]). - No early erosion before LoE. For any calendar year
Y < LoEYear, the LoE pipeline short-circuits toLoEImpact = 1.0(no share loss). - Erosion plateaus past the tabulated curve. The curves in
MOLECULE_SHARE_EROSION/BIOLOGICS_SHARE_EROSIONare 21 entries long; for any year more than 20 years past LoE the lookup clamps to the final tabulated value (no extrapolation). - Past-LoE drugs are out of scope. The LoE date picker disables past calendar years (
minYear = currentYearon the MonthPicker) — the model is for products with at least their LoE still ahead. - Erosion applies to share, not price. The Net Price Over Time computation has no LoE term; the price formula continues unchanged across the LoE boundary.
5. Revenue/Sales Calculations
Section titled “5. Revenue/Sales Calculations”Revenue Calculation Flow
Section titled “Revenue Calculation Flow”flowchart LR
NP[New Patients] --> CALC((x))
PRICE[Net Price per Month] --> CALC
CALC --> CALC2((x))
COMP[Compliance %] --> CALC2
CALC2 --> CALC3((x))
MOT[Months of Therapy] --> CALC3
CALC3 --> DIV[/ 1,000,000]
DIV --> SALES[Line Sales $M]
Net Price Over Time
Section titled “Net Price Over Time”Price typically changes annually from the launch price:
NetPrice[Year] = LaunchPrice x (1 + AnnualPriceChange%)^(Year - LaunchYear)Net price is independent of LoE. The price formula continues unchanged post-LoE — LoE only erodes share. If the price is meant to drop after generic entry, the user expresses that via the annualNetPriceChange schedule itself, not via the LoE pipeline. Likewise, the launch year sets the baseline (Year ≤ LaunchYear → NetPrice = LaunchPrice); price changes start compounding from year 1 onward.
Example:
- Launch Price: $15,000/month
- Annual Price Change: -2%
- Year of Launch: 2025
| Year | Calculation | Net Price |
|---|---|---|
| 2025 | $15,000 x (0.98)^0 | $15,000 |
| 2026 | $15,000 x (0.98)^1 | $14,700 |
| 2027 | $15,000 x (0.98)^2 | $14,406 |
| 2030 | $15,000 x (0.98)^5 | $13,537 |
Sales Formula
Section titled “Sales Formula”LineSales ($M) = sum(CohortPatients[yearOffset] x NetPrice x Compliance% x MonthsThisYear[yearOffset]) / 1,000,000MonthsOfTherapy is the total time a typical patient stays on the drug — not a protocol length, not a treatment-cycle length. A value of 24 means the average patient is on the drug for 24 months; revenue from that cohort spans 24 months of billing.
MonthsOfTherapy is distributed across calendar years using cohort logic — each cohort year contributes up to 12 months. For example, a 24-month therapy generates 12 months of revenue in year 0 (from that year’s new patients) and 12 months in year 1 (from the prior year’s cohort). When MonthsOfTherapy <= 12, only one cohort year contributes and the formula reduces to NewPatients x NetPrice x Compliance% x MonthsOfTherapy / 1M.
Example (1L Therapy):
- New Patients: 5,000
- Net Price: $12,000/month
- Compliance: 85%
- Months of Therapy: 10
LineSales = 5,000 x $12,000 x 0.85 x 10 / 1,000,000 = $510MTotal Sales
Section titled “Total Sales”TotalSales = Sum of LineSales across all therapy lines (1L + 2L + 3L...)Complete Worked Example
Section titled “Complete Worked Example”Scenario: Oncology product, 2nd line therapy, Year 3 post-launch
| Input | Value |
|---|---|
| Addressable Population | 30,000 |
| Treatment Rate | 80% |
| Transition Rate (from 1L) | 60% |
| Market Share (Year 3) | 35% |
| Net Price | $14,000/month |
| Compliance | 90% |
| Months of Therapy | 8 |
Calculation:
1. Eligible Pool = 30,000 - 1L_Patients retained (Assume 15,000 available after 1L retention)
2. 2L Eligible = 15,000 x 60% x 80% = 7,200 patients
3. New Patients = 7,200 x 35% = 2,520 patients
4. Sales = 2,520 x $14,000 x 0.90 x 8 / 1,000,000 = $254M6. Monte Carlo Simulation
Section titled “6. Monte Carlo Simulation”Purpose
Section titled “Purpose”Monte Carlo simulation quantifies forecast uncertainty by running thousands of scenarios with randomly sampled variable values and aggregating the distribution of outcomes.
Architecture
Section titled “Architecture”The simulation count is configurable and persisted per-scenario. Results are isolated between scenario tabs via independent Jotai stores. For execution architecture (worker lifecycle, dual-path execution, auto-run/stale logic, store pinning), see ARCHITECTURE.md.
Simulation Flow
Section titled “Simulation Flow”flowchart TD
subgraph Inputs["Variable Definitions"]
V1["Variable 1<br/>Min: 10 | Mode: 15 | Max: 25"]
V2["Variable 2<br/>Min: 50 | Mode: 70 | Max: 80"]
VN["Variable N<br/>Min: 5 | Mode: 8 | Max: 12"]
end
subgraph Simulation["Monte Carlo Engine"]
V1 --> SAMPLE[Sample from Distributions]
V2 --> SAMPLE
VN --> SAMPLE
SAMPLE --> |"1K-100K iterations"| MODEL[Run Full Sales Model]
MODEL --> COLLECT[Collect Results]
end
subgraph Outputs["Analysis Outputs"]
COLLECT --> DIST[Distribution of Outcomes]
COLLECT --> PERC["Percentiles<br/>P10 / P50 / P90"]
COLLECT --> TORNADO[Tornado Chart]
end
Variable Types
Section titled “Variable Types”Variables that can be simulated:
| Category | Variables |
|---|---|
| Incidence | Early Stage %, De Novo %, Unknown Stage % (when >0), Early-to-Early Relapse %, Early-to-Met Relapse % |
| Treatment | Healthcare access, Treatment rate — for solid tumors this splits into Early Treatment Rate (first early-stage line) and Metastatic Treatment Rate (first met-stage line) when those stages are present; hematology and early-only / met-only solid tumor scenarios fall back to a single Treatment Rate. See deriveDefaultSimulationVariables.ts. |
| Market | Peak share (per line), Months of therapy (per line) |
| Pricing | Launch price |
| Custom | Custom multiplier variables (up to MAX_CUSTOM_VARIABLES per line, when changeable) |
Note: Transition rate, Compliance, and Annual price change are not currently simulated.
Incidence Evolution: Monte Carlo and Tornado analyses use the evolved incidence values from Custom Inputs. For each simulated year, getYearIncidence(year) applies the growth rates defined in the Incidence Evolution table.
Distribution Types
Section titled “Distribution Types”| Distribution | Use Case | Parameters |
|---|---|---|
| Triangular | Most common — when you have min/max/mode | Min, Mode, Max |
| Normal | Symmetric uncertainty | Mean, Std Dev |
| Uniform | Equal probability across range | Min, Max |
Tornado Analysis
Section titled “Tornado Analysis”The tornado chart ranks variables by their impact on forecast uncertainty:
For each variable:1. Hold all other variables at most likely values2. Run model at variable's minimum value -> Low result3. Run model at variable's maximum value -> High result4. Impact = |High - Low|5. Rank variables by impact (largest at top)Variables at the top of the tornado have the greatest influence on uncertainty. Focus validation efforts on high-impact variables.
Percentile Interpretation
Section titled “Percentile Interpretation”| Percentile | Meaning |
|---|---|
| P10 | 10% chance results will be lower (pessimistic) |
| P50 | Median outcome (50% above, 50% below) |
| P90 | 90% chance results will be lower (optimistic) |
Key files: src/features/reports/forecasting/sections/MonteCarlo/
7. Data Inputs Reference
Section titled “7. Data Inputs Reference”Disease Configuration Structure
Section titled “Disease Configuration Structure”Each indication requires a DiseaseConfig — see src/features/reports/forecasting/types.ts. Includes disease type, incidence, stage mix (for solid tumors), and retreatment factor. Fallback values in STAGE_MIX_DEFAULTS (src/features/reports/forecasting/constants.ts).
Geographic Adjustments
Section titled “Geographic Adjustments”The initialization flow is geo-aware: each geography derives its own baseIncidence, growthRate, healthcareAccess, stage mix, and treatment line configuration from the API data. When geo-specific data is unavailable, the fallback chain is: Requested geo -> USA -> EU5 -> Japan -> field-level defaults.
Per-Capita Incidence Fallback
Section titled “Per-Capita Incidence Fallback”When the selected geography lacks its own incidenceBase for a cancer, the fallback cascade sources the value from another geo (USA -> EU5 -> Japan). To preserve epidemiological plausibility (USA’s raw patient count is ~2.8x Japan’s because the USA population is ~2.8x larger), the sourced value is normalized per-capita using each geography’s popBase:
finalBase = round(sourceBase x targetPopBase / sourcePopBase)Example: Japan has no CRC incidenceBase. Cascade sources USA (154,270). USA popBase = 344.1M; Japan popBase = 124.4M. Result: round(154,270 x 124.4 / 344.1) ~= 55,773.
The derivation is surfaced in the amber fallback banner tooltip at the top of the Configuration section (2-line sub-block showing method and formula).
Degraded path: if either popBase is missing, the raw source value is copied 1:1 without per-capita normalization (today’s pre-fallback behavior).
Scope: applies to incidenceBase only. incidenceGrowth fallback copies 1:1 — growth rates are already cancer/geo-specific percentages that embed all epidemiological factors.
Default Values
Section titled “Default Values”Stage mix defaults: see STAGE_MIX_DEFAULTS in src/features/reports/forecasting/constants.ts. Line parameter defaults (treatment rate, compliance, months of therapy): see LINE_DEFAULTS in constants.ts and the DEFAULT object in src/features/reports/forecasting/sections/Configuration/atoms.ts.
Unknown Stage Redistribution
Section titled “Unknown Stage Redistribution”When stage distribution includes an “Unknown” category, the unknown portion is redistributed proportionally across known stages:
knownTotal = earlyStagePercent + metStagePercenteffectiveEarlyPct = earlyStagePercent / knownTotaleffectiveMetPct = metStagePercent / knownTotal
earlyIncidence = totalIncidence x effectiveEarlyPctmetIncidence = totalIncidence x effectiveMetPctExample: Incidence=154,270, Early=71%, Met=23%, Unknown=6%
knownTotal = 71 + 23 = 94effectiveEarly = 71/94 = 75.53% -> 116,523 patientseffectiveMet = 23/94 = 24.47% -> 37,747 patientsTotal accounted: 154,270 (100%)When unknownStagePercent = 0 (default), knownTotal = 100% and the formula reduces to the identity — no change from direct percentage usage.
Unknown is logically a child of Early Stage. The UI constraint earlyStagePercent + metStagePercent + unknownStagePercent = 100% is enforced as:
- When Unknown changes:
earlyStagePercent = max(0, 100 - metStagePercent - unknownStagePercent)(Early absorbs the change; Met stays fixed). Sub-variables (Localized, Locally Advanced) scale proportionally. - When Early changes:
metStagePercent = max(0, 100 - earlyStagePercent - unknownStagePercent)(Met absorbs the change; Unknown stays fixed).
Projection Timeline
Section titled “Projection Timeline”Projection length is PROJECTION_DATA_LENGTH = 21 in constants.ts — 1 pre-launch year (zeros) + 20 post-launch years. The pre-launch row anchors the chart at firstSelectedLaunchYear − 1 so the launch transition is visible; the 20 post-launch years are the active forecast horizon. Default visible range is DEFAULT_VISIBLE_LABELS in SalesChart.tsx. Data points are annual.
Launch year flexibility. The launch picker accepts any calendar year — past (already-launched products), present, or future. The LoE picker is the only date input that disables past years; LoE is bound to firstSelectedLaunch + marketExclusivityYears bidirectionally — editing LoE shifts all line launches by the same month delta (see ARCHITECTURE.md §“bidirectional sync (loeDate)”).
Evolution Arrays (incidenceEvolution, netPriceEvolution)
Section titled “Evolution Arrays (incidenceEvolution, netPriceEvolution)”Both are stored per {geo, indication} in instanceRows as number[] of length PROJECTION_DATA_LENGTH — always arrays, never collapsed to a scalar even when values are identical.
- Fresh load: derived client-side, not returned by the API.
incidenceEvolution=buildIncidenceEvolution(baseIncidence, growthRate, PROJECTION_DATA_LENGTH)(inputs from statistics geoConfig).netPriceEvolution=[0, 0, ...]. When the toggle is enabled, pre-filled withannualNetPriceChange; when disabled, reset to[].
- Saved model load: both arrive as arrays inside
FullSnapshot.instanceRows(GET/api/forecasting/:id). - Related scalar:
annualNetPriceChangeis a separate per-{geo, indication}row (default2fromDEFAULT_ASSUMPTION_DATA), overridden by the API only on saved-model load. - Companion slot —
incidenceEditedYears: sortednumber[]of transition indices (0-based, between consecutive evolution years) the user has explicitly edited. Hydrated toReadonlySet<number>on read and threaded throughapplyGrowthRateChangeso non-edited downstream cells snap toreferenceRatewhile user-edited cells are preserved.resetGrowthRateAtYearAtomdrops the rolled-backyearIndexfrom the set;resetAllGrowthRatesAtomclears it entirely. Legacy snapshots without the slot load as an empty Set. Invariant: user-edited is explicit, not derived — the app never infers edits from value comparison.
Common gotchas:
- The geo fallback chain is: Requested geo -> USA -> EU5 -> Japan -> field-level defaults (per-capita normalization applied to
incidenceBasewhen the cascade fires) - Healthcare access percentages are API-sourced per indication, not hardcoded
- Unknown stage redistribution preserves total incidence by proportionally scaling early/met percentages
- Compliance default is defined in
Configuration/atoms.ts(not 100% — verify current value there)