Skip to Content
DocsHow it worksMathematical model

Mathematical model

The purpose of the model is to transform product behavior into an explainable risk score using business-defined signals.

The core pipeline is:

event history -> signal intensities -> signal impacts -> raw score -> risk score

Each step is intentionally simple and inspectable.

1. Signals normalize behavior into intensity

Every signal returns an intensity between 0 and 1.

0 = inactive 1 = fully active

This normalization is what lets very different product concepts share the same scoring system.

For example:

  • “recent billing failure”;
  • “no app open recently”;
  • “low usage”;
  • “strong usage”;

all become the same mathematical shape:

intensity ∈ [0, 1]

2. Intensity becomes impact

Once a signal has an intensity, its contribution is linear:

impact = weight * intensity

A positive weight increases risk.

A negative weight reduces risk.

That means the same model can represent both churn signals and retention signals.

billing failure -> positive weight strong usage -> negative weight

3. Impacts become score

The score is additive:

rawScore = baseRisk + sum(signal impacts) riskScore = clamp(rawScore, 0, 100)

rawScore is allowed to go below 0 or above 100.

riskScore is the bounded display score.

This distinction matters. If rawScore is 132, the displayed risk is still 100, but the raw score tells you the user is far beyond the critical threshold.

4. Score becomes risk level

Risk levels are derived from the clamped riskScore.

riskScore < medium -> low riskScore < high -> medium riskScore < critical -> high otherwise -> critical

The thresholds are configurable, but they must remain ordered:

medium < high < critical

This keeps the score numeric while still giving applications a simple label to display or use in workflows.

5. Time is modeled with half-life decay

Tiaude uses exponential decay:

decay(ageDays, halfLifeDays) = 2 ^ (-ageDays / halfLifeDays)

This gives a natural product interpretation:

age = 0 -> contribution is 1 age = one half-life -> contribution is 0.5 age = two half-lives -> contribution is 0.25 age = three half-lives -> contribution is 0.125

Half-life is easier to reason about than arbitrary decay constants.

A developer can configure:

halfLifeDays: 7

and understand that the signal loses half of its strength every seven days.

Signal family: occurrence

An occurrence signal measures the remaining strength of the latest matching event.

intensity = decay(age since latest matching event, halfLifeDays)

If no event ever matched, intensity is 0.

Use it for events that are important when they happen, then fade over time.

Examples:

  • billing failure;
  • support complaint;
  • failed import;
  • cancellation intent.

A billing failure from one hour ago should matter more than a billing failure from three months ago. occurrence captures that directly.

Signal family: absence

An absence signal measures the lack of a recent expected event.

intensity = 1 - decay(age since latest matching event, halfLifeDays)

If no event ever matched, intensity is 1.

This is useful for inactivity-style risk.

Examples:

  • no app open;
  • no activation event;
  • no project created;
  • no core workflow completed.

Immediately after an app open, inactivity risk is low. As that event gets older, the absence signal rises smoothly.

This is one of the useful symmetries in the model:

occurrence = presence curve absence = complement of presence curve

Signal family: frequency_above

A frequency_above signal measures repeated recent usage.

It uses a decayed count instead of a raw count.

Linear mode:

intensity = min(decayedCount / threshold, 1)

Binary mode:

intensity = decayedCount >= threshold ? 1 : 0

With a negative weight, this becomes a retention signal.

Example:

strong recent feature usage -> lower churn risk

Signal family: frequency_below

A frequency_below signal measures insufficient recent usage.

Linear mode:

intensity = 1 - min(decayedCount / threshold, 1)

Binary mode:

intensity = decayedCount < threshold ? 1 : 0

This is useful when risk should decrease as usage approaches the desired threshold.

Example:

less than 3 recent core actions -> higher churn risk

The linear mode avoids hard cliffs. A user close to the threshold is treated differently from a user with no recent usage at all.

Why frequency uses decayed count

A strict time window like “events in the last 30 days” requires keeping event timestamps or replaying history.

Tiaude instead uses a decayed accumulator.

When time advances:

decayedCount = decayedCount * decay(deltaDays, halfLifeDays)

When a matching event arrives:

decayedCount = decayedCount + 1

That gives the model a compact memory of recent activity.

Older events never disappear abruptly. They fade continuously.

Confidence is not risk

confidence measures how much relevant evidence the scorer has seen.

It is not the probability that the user will churn.

confidence = min(1, log(1 + relevantEventCount) / log(1 + targetRelevantEvents))

The logarithm is intentional.

The first few relevant events add a lot of confidence. Later events still help, but with diminishing returns.

This avoids treating a score based on one relevant event as equally reliable as a score based on many relevant events.

Freshness is not computation time

freshness measures the age of the latest relevant event.

It is based on:

lastRelevantEventAt

not only on:

computedAt

That distinction matters.

A score can be recomputed today and still be stale if the last relevant user event happened months ago.

computedAt = today lastRelevantEventAt = 90 days ago freshness = stale

This prevents refreshScore() from creating a false sense of fresh data.

Reasons and actions

A signal is considered active when:

intensity > 0

Active signals produce reasons.

Recommended actions are only produced by active signals that increase risk:

impact > 0

Negative-weight signals can explain why risk is lower, but they do not produce recommended actions.

This keeps the output practical:

  • reasons explain the score;
  • actions suggest what to do next.

The model in compact form

The full scoring model can be summarized as:

for each signal: intensity = signal-specific function(state, now) impact = weight * intensity rawScore = baseRisk + sum(impact) riskScore = clamp(rawScore, 0, 100) riskLevel = thresholds(riskScore) confidence = evidence(relevantEventCount) freshness = age(lastRelevantEventAt)