# Score decay in Uniform

> Learn to manage scores of visitor dimensions so they do not become stale.

Source: https://docs.uniform.app/docs/guides/classification/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%)

## Activate linear decay

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.

```typescript
import { Context, createLinearDecay } from '@uniformdev/context';

const manifest = { project: {} };

const options = { decayRate: 0.1 };

const context = new Context({ 
  manifest,
  consent: true,
  decay: createLinearDecay(options),
});
```

## `createLinearDecay`

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).

  | Day | New activity | Decay | Total score | Notes |
  | --- | --- | --- | --- | --- |
  | 1 | 100 | 0 | 100 | No decay due to grace period. |
  | 2 | 0 | 10 | 90 | Decay is 10% of the previous day's score (100). |
  | 3 | 0 | 9 | 81 | Decay is 10% of the previous day's score (90). |
  | 4 | 20 | 8.1 | 92.9 | Decay is 10% of the previous day's score (81). Decay doesn't apply to the new activity score. |
  | 5 | 0 | 9.29 | 83.61 | Decay is 10% of the previous day's score (92.9). |

> **Info:**
>
> For more information on linear decay, see the
> [package reference](https://sdk.uniform.app/context.createlineardecay).

## Implement custom decay

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](https://github.com/uniformdev/examples/blob/main/examples/nextjs-starter/lib/uniform/uniformContext.ts).

```typescript

 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;
  }
}

```
