Skip to content
Mathematics6 min

Monotone by construction

A progress bar that ran backwards, and why an invariant belongs in the definition of a metric rather than in a test that catches it later.

In one of our products, a bar shows how much of a beneficial daily exposure a user has accumulated. During testing it read sixty-nine per cent after sixty-one minutes, and fifteen per cent after seventy-seven minutes. Nothing had been lost. The user had simply continued doing the thing the bar was measuring.

The bug was not in the rendering. It was in the definition.

The broken formulation

The score multiplied an accumulated benefit by a penalty term that grew as risk approached a personal threshold:

S(t) = B(t) · (1 − P(t))

B non-decreasing, P increasing — the product is neither.

Each factor is defensible on its own. B(t) accumulates and never decreases. P(t) is a sigmoid that rises as the accumulated dose approaches the threshold. Their product, however, is free to fall — and it falls hardest exactly when the user is most engaged, because that is where the sigmoid collapses fastest.

A quantity presented to a user as progress carries an implicit contract: it does not go down while they are making progress. The formula above violated that contract, and no amount of tuning the sigmoid's steepness would have fixed it. The property we needed was structural.

The fix: compose only non-decreasing functions

We rewrote the score as a weighted sum of saturating terms, each one a non-decreasing function of a quantity that itself only accumulates:

S(t) = w₁ · min(1, A₁(t) / a₁) + w₂ · min(1, A₂(t) / a₂)

A composition of non-decreasing functions is non-decreasing. The invariant is now a theorem, not a hope.

The risk term did not disappear. It moved to where it belonged: its own indicator, with its own scale, its own thresholds and its own warnings. Two distinct facts about the user's state are now shown as two distinct numbers, which is also — not coincidentally — more honest than folding them into one.

When two things a user needs to know are combined into a single number, the number usually stops being able to say either of them.

The general rule

Before a quantity is exposed in an interface, write down the properties it must satisfy. Monotone in time. Bounded in [0, 1]. Continuous under a small change of input. Invariant under a unit change. Then check that the formula guarantees them, rather than checking that the current parameters happen to produce them.

  • Monotonicity — does it ever decrease when the underlying activity continues?
  • Boundedness — can a long session push it past 100, or a strange input below 0?
  • Continuity — does a one-minute difference in input ever produce a visible jump?
  • Saturation — does it behave sensibly in the limit, or does it grow without bound?

These are four lines of analysis on a whiteboard. They are considerably cheaper than discovering, after release, that the number your product is built around is capable of contradicting itself in front of a user.