Score decay

The classification process depends on scores that are increased and decreased as a visitor engages with a website or other digital experience. While those scores are accurate at the time they're calculated, over time, they become less accurate. Uniform compensates for that by automatically reducing stale scores over time. This is described as "decay.

With default settings, Context doesn't perform decay but it's a simple addition to the configuration to add a decay algorithm. Context comes with a simple linear decay implementation that decays scores at a linear rate the longer a visitor is inactive. With default linear decay settings of a 1-day grace period, 30-day decay duration, and 95% decay cap for example:

  • Return visit in less than 1 day: no decay - within grace period
  • Return visit in 16 days: decay is 50% of total score in each dimension (1 day grace + 15 days is half of 30-day default decay rate)
  • Return visit in 31 days: decay is 95% of total score in each dimension (1 day grace + 30 days, decay cap 95%)

Uniform provides a decay function that reduces a score by the same amount every time it runs. The following example demonstrates how to activate linear decay.

import { Context, createLinearDecay } from '@uniformdev/context'; const manifest = { project: {} }; const options = { decayRate: 0.1 }; const context = new Context({ manifest, consent: true, decay: createLinearDecay(options), });

This function creates a linear decay function that can be assigned to the Context. Linear decay involves reducing a score by the same amount each day the decay runs.

The following describes how decay works over time and with continuing engagement. Consider the following settings:

  • Scores will decay by 10% each day (decay rate).

  • The most a score will decay in a day is 95% (decay cap).

  • Decay will start after 1 day (grace period).

    DayNew activityDecayTotal scoreNotes
    11000100No decay due to grace period.
    201090Decay is 10% of the previous day's score (100).
    30981Decay is 10% of the previous day's score (90).
    4208.192.9Decay is 10% of the previous day's score (81). Decay doesn't apply to the new activity score.
    509.2983.61Decay is 10% of the previous day's score (92.9).

info

For more information on linear decay, see the package reference.

To implement your custom decay function, simply implement a function with the following signature and set it on the Context object initializer: decay: customDecay():

See full working example for Next.js in this repo.

const context = new Context({ ... // Example: custom decay override decay: customDecay(), }); // ... // example: implement your own decay function export function customDecay(options?: LinearDecayOptions): DecayFunction { /** gracePeriod: * The length of time before decay starts, in msec. * Default: 1 day (8.64e7) */ /**decayRate: * How much the score decays per day (decimal, 0-1). * Default: decay over 30 days (1/30) * * Note: the grace period is not included in this rate, * so if the grace period is 1 day and the decay rate is 1/30, * it would take 31 days to hit max decay. */ /** decayCap * The maximum amount of decay that can occur at once (decimal, 0-1) * Default: 95% (0.95) */ const { gracePeriod = 8.64e7, decayRate = 1 / 30, decayCap = 0.95 } = options ?? {}; return function linearDecay({ now, lastUpd, scores, sessionScores, onLogMessage }) { // brand new data, no decay if (typeof lastUpd !== 'number') { return false; } const timeSinceLastUpdate = now - lastUpd; const timeSinceGracePeriod = timeSinceLastUpdate - gracePeriod; // grace period not elapsed yet if (timeSinceGracePeriod <= 0) { return false; } const timeSinceGracePeriodInDays = timeSinceGracePeriod / 8.64e7; const decayFactor = 1 - Math.min(decayCap, timeSinceGracePeriodInDays * decayRate); if (decayFactor <= 0) { return false; } decayVector(scores, decayFactor); decayVector(sessionScores, decayFactor); onLogMessage?.(['info', 140, `linear decay factor ${decayFactor.toPrecision(6)}`]); return true; }; } function decayVector(vector: ScoreVector, decay: number) { for (const key in vector) { if (key.startsWith('$')) { continue; } vector[key] *= decay; } }