Skip to content

Insights: a steady 22 kg loss is reported as a 180-day plateau and as volatile #1453

Description

@ahmetabdullahgultekin

The Insights screen can contradict itself: it reports a large, steady change at the top of the card and "no significant change" at the bottom of the same card, and it labels a perfectly smooth trend as volatile.

Reproducing it

No hardware needed — the project's own demo generator produces it:

About → Developer Tools → Scenario: Trend Progress, Time Range: Last 6 months → Generate Demo Data → Insights

What the card then shows, simultaneously:

Start / Now 100.21 kg → 78.10 kg (−22.11 kg)
Rate −3.74 kg / month
Chips short-term ↘ long-term ↘ volatile plateau 180 days
Summary "No significant change for 180 days — your body may be adapting."

A 22 kg loss over six months is being described as a 180-day plateau.

Cause: both come from using the raw standard deviation on a trending series

1. Plateau — MeasurementInsightsUseCase.kt:178-193

val threshold = stdDev * 0.5f
for (i in dataPoints.indices.reversed().drop(1)) {
    if (abs(dataPoints[i + 1].second - dataPoints[i].second) > threshold) break
    plateauStartIndex = i
}

The step between two consecutive measurements is compared against half the standard deviation of the entire series. On a trending series those quantities have completely different scales:

series      181 points, 100.16 -> 78.05 kg
stdDev      6.42 kg
threshold   3.21 kg        (stdDev * 0.5)
largest consecutive step   0.22 kg
steps above threshold      0
=> loop never breaks, the whole series is reported as a plateau

The relationship is inverted: the stronger the trend, the larger the stdDev, the higher the threshold, and the longer the reported plateau. The cleanest weight-loss curve produces the longest "no change" message.

2. Volatility — MeasurementInsightsUseCase.kt:142-151

val stdDev    = stdDev(rawValues, mean)
val relStdDev = if (mean != 0f) stdDev / mean else 0f

On a trending series the raw stdDev measures the span of the trend, not variability around it:

relStdDev   6.42 / 89.15 = 7.20%
thresholds  STABLE < 1%, MODERATE < 3%
=> HIGH ("volatile"), for a visually smooth line

Any successful weight-loss run of more than ~3% of body weight is therefore labelled volatile — the users most likely to open Insights.

Suggested direction

Both need the trend removed before measuring spread:

  • Plateau: derive the threshold from local step sizes (e.g. stdDev of consecutive differences) rather than from the spread of the whole series — or test the plateau window directly, i.e. "total change within the candidate window is below X".
  • Volatility: measure residuals rather than raw values — stdDev of consecutive differences, or residuals from a linear fit.

A shared detrendedStdDev() helper would cover both.

I'd be glad to send a PR with a fix and unit tests in MeasurementInsightsUseCaseTest.kt — but this changes the behaviour of an algorithm you designed, so I'd rather ask first whether you want it, and in which direction.

Minor, same screen

The plateau chip truncates: it renders as 180 günlük durağ in Turkish (180 days plateau fits in English). Probably worth letting that chip wrap or shortening the string.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIndicates an unexpected problem or unintended behavior

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions