Last updated · ~10 min read · written by Claude, reviewed for accuracy
TrendBody is two small numerical models stitched together. A 2-state Kalman filter estimates your underlying weight and its drift rate from noisy daily scale readings. A linearized energy-balance equation from Hall et al. (2011) converts that drift rate into an implied caloric surplus or deficit, accounting for metabolic adaptation. Everything else in the app — Body Clock, forecasting, cycle adjustment — is preprocessing or post-processing around these two models. This page describes both, then lists the cases where they break.
Why a Kalman filter
A daily body weight reading is the sum of a slow signal (true mass change from energy balance) and a fast signal (water, sodium, glycogen, gut contents, hydration, clothing, scale error, and time-of-day variation). The slow signal lives in a kilogram-per-week range. The fast signal swings 0.5–2 kg per day. Naive moving averages destroy phase information and treat every input equally, including outliers. We want something better:
- handles irregular sampling (missed days, weekly cadence);
- handles missing data by widening uncertainty, not faking it;
- produces a velocity estimate, not just a smoothed value;
- produces calibrated uncertainty so downstream code knows when not to trust the trend.
A 2-state Kalman filter delivers all four. It is the minimum-variance Bayesian estimator for a linear-Gaussian system, which is a reasonable first-order model of body weight on the time scale of weeks. It has been the standard tool for navigation, tracking, and sensor fusion since Apollo, and it is not new science — we just applied it to a domain that has historically used 7-day rolling averages.
State, transition, observation
The state vector is two-dimensional: the underlying weight w (kg) and its drift rate v (kg/day).
Between measurements separated by dt days, the state evolves under constant-velocity dynamics with additive Gaussian process noise:
We use the diagonal-Q simplification rather than the full integrated white-noise form. The two process-noise terms are tuned:
q_w controls how much the underlying weight state is allowed to random-walk per day beyond what the constant-velocity model predicts. It is set conservatively so the trend can adapt to real physiological changes without chasing noise. Day-to-day water/glycogen fluctuation is a measurement phenomenon and is captured by R, not Q. q_v is small — the filter allows your trend slope to change, but slowly, on the order of weeks. Set q_v too high and the trend chases noise; too low and it lags real changes (diet starts, holidays). The current values are conservative defaults, tuned against simulated and self-test trajectories — not user data, of which we collect none. Everything runs on your device.
The observation model is straightforward — the scale measures weight directly, so the measurement matrix is H = [1, 0] and the measurement noise variance R defaults to 0.7 kg² (≈0.84 kg standard deviation). When the preprocessor supplies a per-measurement noise (see below), it overrides the default.
The recursion
For each new measurement z at time t:
# predict
x_pred = F · x
P_pred = F · P · Fᵀ + Q
# innovation
y = z - H · x_pred # scalar
S = H · P_pred · Hᵀ + R # scalar = P_pred[0,0] + R
# outlier gate (see next section)
if |y| > 4 · √S:
R_eff = R · 100
S = P_pred[0,0] + R_eff
else:
R_eff = R
# update
K = P_pred · Hᵀ / S # 2-vector
x = x_pred + K · y
P = (I - K·H) · P_pred · (I - K·H)ᵀ + K · R_eff · Kᵀ # Joseph formInitialization sets x = [first_weight, 0]ᵀ and P = diag(5.0, 0.1). The initial weight uncertainty is generous so the second and third measurements move the trend quickly; the initial velocity prior is tight around zero so the filter doesn't mistake the first noisy step for a real drift.
Why Joseph form. The textbook covariance update P = (I - K·H) · P_pred is algebraically equivalent to the Joseph form above, but numerically less stable under floating-point arithmetic. Over many updates, accumulated rounding can make P drift away from symmetric positive-definiteness, which causes innovation variance to go negative and the filter to diverge silently. The Joseph form is symmetric by construction and preserves PD as long as R> 0. We also enforce symmetry explicitly by averaging the off-diagonal entries after each update — belt and suspenders.
Outlier gating
A single bad reading — heavy clothes, post-sodium bloat from takeaway food, a child standing on the scale, a misread digit — can otherwise yank the trend by hundreds of grams in one step. We gate on the normalized innovation:
Four sigma is a rare event under the model (roughly 1-in-16,000). If you're seeing one of those, the model is wrong about the measurement, not about you. Inflating R by 100× makes the Kalman gain near zero, so the outlier is effectively ignored for this step but still recorded — the next consistent reading will pull the trend back in cleanly. We chose 100× over a hard reject because a true rapid change (intentional refeed, illness) should still register if it persists across multiple readings.
Per-measurement noise (Body Clock)
Body weight follows a daily cycle: lowest in the morning after overnight fluid loss, highest in the evening after food and hydration. The swing is typically 0.5–2 kg. Rather than force users to weigh themselves at the same time every day (which kills adherence), TrendBody learns the per-user diurnal phase and assigns higher measurement variance to readings taken outside the user's usual window. The Kalman filter then naturally down-weights those readings — they still inform the trend, just less.
This is the right place for that adjustment. A morning weigh-in and an evening weigh-in are not equally informative about your underlying mass, and pretending they are is exactly the kind of unmodeled noise that destroys the SNR of naive smoothers.
From trend velocity to caloric balance
The Kalman filter gives us v in kg/day. Translating that into an implied energy balance requires a body-composition model, because losing 1 kg of pure fat is not energetically equivalent to losing 1 kg of mixed tissue, and your daily expenditure changes as your weight changes (metabolic adaptation). We use the linearized form of the Hall body-weight model:
ΔEI is the implied change in daily intake relative to baseline maintenance. v is the Kalman-filtered drift rate. ΔBW is your current weight minus a baseline reference. Both ρ and εare population-averaged values from Hall's linearization of his full ODE. Note that ρ = 9100 kcal/kg is slightly lower than pure adipose tissue density (~9500 kcal/kg) because the linearization assumes a mixed fat-and-lean-tissue composition for weight change; the full nonlinear ODE provides slightly better accuracy at the extremes but linearization is well-justified in the BMI 18–35 range where this app is intended to be used.
Citation: Hall KD, Sacks G, Chandramohan D, Chow CC, Wang YC, Gortmaker SL, Swinburn BA. Quantification of the effect of energy imbalance on bodyweight. The Lancet. 2011;378(9793):826–37. PMID 21872751.
For goal forecasting, we forward-simulate the nonlinear ODE rather than the linearization, because the linear approximation accumulates error over months-long projections.
Limitations
- Cold start. The first ~14 days of trend are dominated by initialization uncertainty and a small sample. The velocity estimate is effectively a guess in week 1. We show wider confidence intervals during this period, but a user looking at it on day 3 and expecting an actionable caloric estimate will be disappointed — and right to be.
- Sparse sampling makes velocity unobservable. If you weigh in once a week or less, the filter can't distinguish a true slope change from a single noisy reading. We still produce an estimate, but the confidence interval reflects this — and at very sparse cadence, the band is wide enough that the trend isn't actionable. Daily weighing isn't required, but several times per week is.
- Hall's ρ and ε are population averages. Individual variation in body composition, NEAT, and metabolic flexibility can make the personalized error band wider than the filter's formal uncertainty suggests. We don't try to personalize ρ or ε from data because doing so reliably requires a metabolic chamber, not a bathroom scale.
- The model assumes Gaussian noise. Real measurement noise is not Gaussian — it has heavier tails (clothing, sodium, scale errors). The 4σ gate is our concession to this; a fully principled approach would use a robust filter (e.g., a Huberized observation model). The added complexity hasn't earned its keep in our testing yet.
- We do not validate caloric balance against a metabolic ward. The estimate is a model output, not a measurement. It is directionally useful — "you're in a 400 kcal deficit" is meaningful; treating "413 kcal/day" as gospel is not. The app displays the value with deliberately rounded precision for this reason.
- Disordered eating contexts. The math doesn't know who's using it. We design the defaults conservatively (caloric balance hidden by default, behavior-not-identity framing, amber/emerald instead of red/green) but if you have an active eating disorder, please consult a healthcare professional before using any weight tracking tool.
Is this AI?
Worth answering directly, because the honest answer has two halves and they're easy to conflate.
The app contains no machine learning. No neural network, no embeddings, no LLM, no model weights shipped in the binary. The Kalman filter and Hall's equation predate the deep-learning era by decades; both are deterministic and inspectable. For a problem this small — one scalar time series per user — the classical estimator is more accurate, more interpretable, and far easier to ship privately than anything learned. Every number above is a closed-form computation running locally on your device. You can check the arithmetic by hand.
The code and this article, however, were written by AI — about as much of it as you'd guess. Most of the Swift was written by Claude Opus (Anthropic's LLM) over a few weekends of pair-programming, and this engineering write-up was drafted by Claude directly from that Swift source, then reviewed for accuracy. We're telling you this for the same reason the rest of the page exists: inspectability. An AI helped build the estimator; the estimator itself is not AI. Those are different claims, and the second one is the one that matters when you're deciding whether to trust the math — because you don't have to trust the prose. The model is a textbook 2-state Kalman filter and a published 2011 equation. Every constant and step is on this page; verify them against the citation rather than against our word.
Found a bug or have a quibble with the math?
TrendBody is built by one independent developer. If you spot something — wrong constant, missed citation, a tuning parameter you think we should revisit — email support@trendbody.app. Engineering feedback is genuinely welcome and gets read.
Back to the plain-English science page, or what the app actually does.