Skip to content

[oura][analytics] Compute active calories for an Oura ring from its own MET stream (0x50) — the decode is now day-sum-validated against Oura's export, and Oura's own number is that exact sum #2242

Open

Description

@pipiche38

Summary

An Oura-only wearer currently gets no on-device calorie estimate: the Today "Calories" tile is imported-first (Apple Health / Health Connect active_kcal), and DailyMetric.activeKcalEst — the HR-only Keytel/Harris-Benedict estimate — is never fed by the ring (OuraStreamMapping does not touch MET, and the ring's HR does not reach the estimator either).
Against the wearer's own Oura export the current activeKcalEst correlates at r = −0.10 with Oura's active_calories.

The ring already sends the right signal. 0x50 activity_info carries one MET value per 60 s (decoded on both platforms by decodeActivityInfo, but Tier B: logged to oura-activity.jsonl, never scored — OURA_PROTOCOL.md §6, "NOT ground-truth-validated"). Two things changed that this week:

  1. The decode is now validated at day scale against Oura's own numbers. The account export
    (dailyactivity.csv) carries a 1440-minute met series per day at 60 s / one decimal — the same object as the wire values. Over 16 overlapping days (47k decoded minutes): Σ over minutes with MET ≥ 1.5 of (MET − 1), NOOP vs export, matches on 10/10 clean days within 1–8 % (e.g. 947 / 947, 308 / 304, 418 / 414). The four days NOOP reads 15–25 % low are the known sidecar coverage holes (minutes missing during drains), not decode error. Minute-for-minute the export is re-binned (best r 0.79 at a −2 min shift), so validation must compare sums over shared minutes.
  2. Oura's active_calories is, by construction, that sum. Over 574 full-wear days (before the May-2025
    HR-intensity update): active_calories = k × Σ_{MET≥1.5}(MET − 1) with r = 0.992, single-k MAPE 12.9 %. The excess form beats full MET decisively (r 0.976 vs 0.871). total − active sits at a constant ≈ RMR. From
    late 2025 the account's k steps up (HR intensity folded in) and r drops to 0.976 — still the same shape. This is what Oura documents: "1.5 METs as the lowest threshold for activities that contribute to your daily active calorie burn", and what Kristiansson et al. 2023 (BMC Med Res Methodol 23:50, Gen 2) state: "AEE is defined and starts accumulating at > 1.5 MET", relative to an undisclosed profile-based RMR.

Proposal

Compute, for an Oura ring as the active device:

active_kcal(day) = Σ over 60 s MET samples with MET ≥ 1.5 of (MET − 1) × RMR_kcal_per_day / 1440
  • RMR from the revised Harris-Benedict NOOP already has (Calories.restingKcalPerS, profile weight / height / age / sex). Zero free parameters — it is the definition of a MET-minute. Oura's own k is person-specific and undisclosed; the goal is a defensible estimate of the same quantity, not a reproduction of Oura's number.
  • Coverage-aware: the sum must be scaled to the covered minutes (or missing minutes filled as 1.0 MET) and the coverage exposed, because the MET stream has holes during drains — a naive sum undercounts silently.
  • Feed DailyMetric.activeKcalEst (and the Today tile's active_kcal sparkline) for ring days, both platforms, so the MetricsCache / Trends path stays unchanged. Resting share via the same RMR so total = resting + active like Oura.
  • Ship default-off, Experimental, per the project's rule for a derived physiological signal, with the validation
    below in the PR; flip the default once it holds on a second wearer.
  • This replaces the earlier idea of wiring the ring's HR into the Keytel estimator: Keytel is fitted for exercise HR
    ranges, MET is the all-day signal Oura itself uses, and only the MET path has a ground truth to validate against.

Validation plan (already possible with data on file)

  • 08-13 → 08-26: NOOP active_kcal vs the export's active_calories over the same days — expect r ≈ 0.99 and a stable ratio (the ratio IS Oura's k / our RMR-based constant; it should not drift day to day). Current baseline r = −0.10.
  • Day-sum check of the decode on every new capture (Σ_{≥1.5}(MET−1) over shared minutes vs the export) is the regression test for the decode itself; it lifts 0x50 from Tier B for this use.
  • Honest ceiling to state in the UI copy: Oura's own AEE is only within ±46–90 % of research accelerometers in free living (Kristiansson 2023, MAPE), and the ring's MET under-estimates hard efforts (runs MAPE 20–30 % vs calorimetry). An estimate, not a measurement.

Not in scope

Steps (0x7E/0x7F are model features, not counts — #9 on open_oura), strain/effort from MET (killed by the swim falsifier), and any change to WHOOP's calorie path.

Activity

  1. 121 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions