Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt

Use this file to discover all available pages before exploring further.

Maturity Ramp

When a new offer goes live, its propensity posterior is wide — we don’t yet know its true acceptance rate. Showing it at full ranking confidence is reckless (we might over-spend on a dud) but suppressing it entirely is also wrong (we’ll never learn). The maturity ramp is the gating decision that controls cold-start exposure as evidence accumulates. Kaireon ships two maturity ramps. The default — Bayesian Confidence-Bound Maturity Ramp (BCB-MR) — is the better one and the rest of this page covers it. A legacy linear-count ramp is kept for back-compat with tenants who tuned around a fixed interaction threshold.

Why BCB-MR exists

Count-based maturity gates — “treat the model as immature until it has accumulated N responses” — are the conventional choice in next-best-action systems. The approach is intuitive but has two real failure modes:
  1. Low-volume offers are trapped. A campaign for 100 high-value enterprise customers will never accumulate a 5,000-response gate. The model is forever immature even when its posterior is already tight — useful signal is thrown away.
  2. High-volume volatile offers are prematurely “done”. An offer with thousands of trials at a seasonal volatile rate still has a wide posterior. A pure count gate declares it mature even when it deserves continued exploration.
The signal we actually want is posterior credible-interval width — “how much room is left for the true rate to be?”. An offer is mature when we can pin down its rate within an acceptable confidence band.

The algorithm

For each candidate offer at decision time:
  1. Look up the most-specific ModelAdaptation row (the scope hierarchy: offer → channel → direction → category → global).
  2. Compute the Wilson score interval for the Bernoulli proportion p̂ = positives / (positives + negatives) at the configured confidence level (default 95%, z = 1.96). Wilson 1927 is the canonical closed-form CI; well-behaved at boundaries (no blow-up at p̂ = 0 or 1, unlike the Wald interval).
  3. Let width = upper − lower. If width ≤ widthThreshold (default 0.20), the offer is matureexposureProbability = 1.0.
  4. Otherwise the offer is immature. Compute a decaying cold-start floor:
    floor(n) = baseFloor / √(1 + n / decayHalfLife)
    
    At n=0 the floor is baseFloor (default 0.50). At n=100 with default decay it’s ≈ 0.15. The decay mirrors UCB1’s √(2 ln t / n) shrinkage but applied to the floor rather than an exploration bonus.
  5. Return exposureProbability = max(floor(n), wilsonLower). The max ensures we never penalize evidence we already have — an offer with strong early positives (8/10 → Wilson lower ≈ 0.49) gets at least that lower-bound exposure even if the decay-floor shrank below it.
The caller then does a deterministic per-customer/per-day roll: hash(customerId + offerId + today) ≤ exposureProbability decides whether THIS impression includes the offer.

Worked examples

The table below shows real exposure decisions at default config (widthThreshold = 0.20, baseFloor = 0.50, z = 1.96, decayHalfLife = 10).
PositivesNegativesWilson lowerWilson upperWidthSourceExposure
000.001.001.00no_evidence0.500
190.0180.4040.386floor0.354
370.1080.6030.496floor0.354
820.4900.9430.452ci_gated0.490
40600.3090.4970.188mature1.000
2008000.1760.2260.050mature1.000
The 8/10 row is the interesting one: the offer is clearly persuasive (μ̂ = 0.80) but the CI is still wide, so it’s ci_gated — exposure is the Wilson lower bound 0.49. As soon as evidence tightens the CI below 0.20 width, the offer flips to mature.

Configuration

Per-tenant settings (Settings → Models → Maturity Ramp):
SettingDefaultEffect
maturityRampMode"bayesian_ci"Set to "legacy_count" to use the count-based ramp.
maturityWidthThreshold0.20CI width below which an offer is mature. Lower = stricter.
maturityRampColdStartFloor0.50Maximum cold-start exposure floor (the baseFloor in the formula).
modelMaturityThreshold100Used only when maturityRampMode = "legacy_count". The number of impressions that flip an offer to mature. Set to 0 to disable the legacy ramp entirely.

Why this design works

  1. Low-volume offers aren’t trapped. A campaign for 100 enterprise customers can mature once its posterior tightens, regardless of raw response count.
  2. High-volume volatile offers stay in exploration. An offer with thousands of trials but a still-wide CI keeps getting exploration traffic instead of being declared “done” by a count threshold.
  3. The threshold is principled. A 0.20 width at 95% means “the true rate is known to within ±0.10” — a tunable that business owners can reason about, with a clear posterior interpretation rather than a chosen convention.
  4. The maturity event is observable. Operators see a single interpretable number — “this offer’s posterior CI width is 0.18, just below the 0.20 threshold” — instead of “evidence count is 4837 of 5000”. Combined with direction-scoped adaptations, this surfaces directly in the Model Health UI.

References

  • Wilson, E.B. (1927). “Probable inference, the law of succession, and statistical inference.” JASA 22:209–212. The canonical Bernoulli proportion CI.
  • Auer, Cesa-Bianchi, Fischer (2002). “Finite-time Analysis of the Multiarmed Bandit Problem.” Machine Learning 47:235–256. UCB1 — origin of the √(ln t / n) shrinkage form adapted here as a decaying floor.
  • Chapelle & Li (2011). “An Empirical Evaluation of Thompson Sampling.” NeurIPS. Empirical justification for Bayesian bandit-style exposure.

Code

  • Library: platform/src/lib/ml/maturity.ts
  • Tests: platform/src/lib/ml/__tests__/maturity.test.ts
  • Call site: platform/src/lib/pipeline-runner.ts (applyMaturityRamp)