$$ % Define your custom commands here \newcommand{\bmat}[1]{\begin{bmatrix}#1\end{bmatrix}} \newcommand{\E}{\mathbb{E}} \newcommand{\P}{\mathbb{P}} \newcommand{\S}{\mathbb{S}} \newcommand{\R}{\mathbb{R}} \newcommand{\S}{\mathbb{S}} \newcommand{\norm}[2]{\|{#1}\|_{{}_{#2}}} \newcommand{\pd}[2]{\frac{\partial #1}{\partial #2}} \newcommand{\pdd}[2]{\frac{\partial^2 #1}{\partial #2^2}} \newcommand{\vectornorm}[1]{\left|\left|#1\right|\right|} \newcommand{\abs}[1]{\left|{#1}\right|} \newcommand{\mbf}[1]{\mathbf{#1}} \newcommand{\mc}[1]{\mathcal{#1}} \newcommand{\bm}[1]{\boldsymbol{#1}} \newcommand{\nicefrac}[2]{{}^{#1}\!/_{\!#2}} \newcommand{\argmin}{\operatorname*{arg\,min}} \newcommand{\argmax}{\operatorname*{arg\,max}} \newcommand{\dd}{\operatorname{d}\!} $$

estimation.joint

estimation.joint

Phase 2: joint estimation of physiological state and metabolic parameters.

The idea is standard and worth stating plainly: append the unknown parameters to the state vector and filter the lot together. The augmented state

\[ z_k = \bigl(\underbrace{T, F, G_c, G_n, A}_{x_k},\; \underbrace{\eta}_{\text{parameters}},\; \underbrace{\omega}_{\text{waist}}\bigr) \in \mathbb{R}^{16} \]

evolves with the physiological state following the energy-balance dynamics and the parameter block following a slow random walk. Each morning’s weigh-in then updates both – where you are, and what kind of metabolism produced that trajectory. Parameter uncertainty contracts as evidence accumulates, which is exactly the desired behavior when early food logs are less reliable than later ones. The last two entries calibrate the optional tape measure (offset and slope of waist against fat mass); they are personal constants like the metabolic parameters and are carried and relaxed the same way, but they never enter the dynamics. They are added only when some episode has a waist reading: the unscented transform’s sigma-point spread depends on the state dimension, so two idle entries would perturb every other estimate slightly for no information.

Two design choices deserve comment.

Why a random walk on parameters that are supposedly constant. Giving the parameter block a small process noise is what keeps the filter from locking in early. With zero process noise the parameter covariance shrinks monotonically and the filter becomes deaf to later evidence – including evidence that its early estimate was wrong. The random walk is a modeling fiction that buys robustness, and it is also not entirely a fiction: maintenance energy genuinely does drift with season, activity, and age.

Pooling across episodes by sequential Bayes. Episodes share your metabolism but not your state: you started each attempt at a different weight with a different glycogen load. So between episodes the physiological block is reset from that episode’s first weigh-in, while the parameter posterior carries forward as the next episode’s prior. Because episodes may be years apart, the carried-forward covariance is relaxed back toward the prior at a rate set by the calendar gap – what your metabolism was in 2023 is evidence about 2026, but weaker evidence than last month’s. Past episodes without a food log are not filtered here at all: they identify the noise in phase 1 and nothing else, and filtering them would let a misspecified “intake unknown, near maintenance” assumption bend the parameters (see the comment in fit_joint).

Classes

Name Description
EpisodeFit Filtering and smoothing output for one episode.
JointFit Result of phase 2.
ParameterSummary Posterior summary of one parameter in natural units.

EpisodeFit

estimation.joint.EpisodeFit(name, dates, filtered, smoothed)

Filtering and smoothing output for one episode.

Methods

Name Description
scale_trend_kg Smoothed true scale weight (tissue plus both water compartments).
scale_trend_kg
estimation.joint.EpisodeFit.scale_trend_kg()

Smoothed true scale weight (tissue plus both water compartments).

JointFit

estimation.joint.JointFit(
    episodes,
    transform,
    eta_mean,
    eta_cov,
    params,
    summaries,
    total_loglik,
    noise,
    waist_transforms=None,
    waist_eta_mean=None,
    waist_eta_cov=None,
    n_waist=0,
)

Result of phase 2.

eta_mean and eta_cov cover the nine metabolic parameters only; the waist calibration block is kept apart in waist_* so that everything downstream of the metabolic posterior is unaffected by it.

Methods

Name Description
maintenance_interval_kcal Posterior median and central interval for maintenance energy, kcal/day.
maintenance_kcal Estimated non-adaptive maintenance energy at a given weight, kcal/day.
waist_summaries Posterior of the waist offset (cm) and slope (cm per kg fat).
weight_stable_intake_kcal Intake at which weight would hold steady, kcal/day.
maintenance_interval_kcal
estimation.joint.JointFit.maintenance_interval_kcal(
    weight_lb,
    *,
    q=0.9,
    n_samples=20000,
    seed=0,
)

Posterior median and central interval for maintenance energy, kcal/day.

Returns (low, median, high).

This, and not the individual coefficients, is the quantity your data actually pins down. The intercept and the mass slope are strongly negatively correlated – over the narrow range of body mass in one dataset, raising one and lowering the other leaves the fitted trajectory almost unchanged – so each has a wide marginal posterior while their combination at any particular weight is well determined. Quoting either coefficient alone would overstate the uncertainty; quoting them as independent would understate it. Propagating the full covariance through the combination is the only honest option, and it is why this method samples rather than adding variances.

maintenance_kcal
estimation.joint.JointFit.maintenance_kcal(weight_lb)

Estimated non-adaptive maintenance energy at a given weight, kcal/day.

Excludes the thermic effect of food and adaptive thermogenesis, so it is not the intake at which weight would hold steady. Use weight_stable_intake for that.

waist_summaries
estimation.joint.JointFit.waist_summaries(n_samples=20000, seed=0)

Posterior of the waist offset (cm) and slope (cm per kg fat).

weight_stable_intake_kcal
estimation.joint.JointFit.weight_stable_intake_kcal(weight_lb, tef_fraction=0.1)

Intake at which weight would hold steady, kcal/day.

Solves I = c0 + c1*W + tef_fraction * I for I, i.e. accounts for the fact that eating more itself costs energy to process. Ignores any adaptation currently in effect, which if you are mid-deficit will make this an overestimate of what you can actually eat without regaining.

ParameterSummary

estimation.joint.ParameterSummary(name, mean, median, q05, q95, prior_center)

Posterior summary of one parameter in natural units.

Quantiles come from transforming Monte Carlo draws of the unconstrained posterior, not from a delta-method linearisation. The transforms are strongly nonlinear – exponential and logistic – so the induced posteriors are skewed, and a symmetric interval would misrepresent them.

Attributes

Name Description
shrinkage How far the posterior median moved from the prior, in relative terms.

Functions

Name Description
fit_joint Run joint state-parameter estimation across all episodes, in date order.
initial_body_fat_fraction Body-fat fraction to initialize the fat-mass state.
state_frame Smoothed state trajectory as plain arrays, keyed by state name.

fit_joint

estimation.joint.fit_joint(
    episodes,
    cfg,
    noise,
    *,
    prior_scale=1.0,
    param_drift=PARAM_DRIFT_PER_DAY,
    substeps=4,
    spec=None,
    n_samples=20000,
    seed=0,
    dof=None,
)

Run joint state-parameter estimation across all episodes, in date order.

Parameters

Name Type Description Default
noise NoiseModel Process and observation noise. The observation standard deviation should come from ~wdm.estimation.noise_id.identify_noise rather than the default, which is a population guess. required
prior_scale float Multiplier on the prior parameter covariance. Above 1 to express less confidence in the literature priors. 1.0
param_drift float Per-day random-walk scale on the unconstrained parameters. PARAM_DRIFT_PER_DAY
dof float | None Student-t degrees of freedom for the observation noise, or None for Gaussian with the gross-error gate. Use the same choice as phase 1. None

initial_body_fat_fraction

estimation.joint.initial_body_fat_fraction(cfg, weight_kg)

Body-fat fraction to initialize the fat-mass state.

Uses the configured value when supplied. Otherwise falls back to the Deurenberg equation, which predicts body-fat percentage from BMI, age, and sex. Its standard error is around 4 percentage points – poor for a single person, which is precisely why the initial covariance on fat mass is set wide and why fat mass is reported with an interval rather than a number.

state_frame

estimation.joint.state_frame(fit)

Smoothed state trajectory as plain arrays, keyed by state name.