Skip to content

CHANNEL_UNITS in hf_config.py mislabels stored data scale (HR is Hz, not bpm) #102

Description

@NarayanSchuetz

Summary

CHANNEL_UNITS in src/data/processing/hf_config.py mislabels the stored scale of the heart-rate channel (ch5): it declares "bpm", but the stored values are beats/second (Hz). The same wrong label is repeated in the imputation channel table. Meanwhile the downstream (xgboost) code correctly documents HR as beats/sec and converts ×60 for reporting — so the repo contradicts itself and consumers can silently be off by 60×.

Energy (ch6) is NOT affected"cal/min" is correct (values are small calories; 1000 cal = 1 kcal, so e.g. ~3000 cal/min ≈ 3 kcal/min during exercise is plausible). An earlier draft of this issue wrongly flagged energy; that concern is retracted.

Evidence (heart rate, ch5 hk_watch:HKQuantityTypeIdentifierHeartRate)

CHANNEL_UNITS[5] == "bpm", but raw observed values in the xs split are:

stat value
min 0.667
p5 0.983
median 1.467
p95 2.283
max 3.333

(norm stats: mean 1.264, std 0.305)

  • As bpm, a median HR of 1.47 is physically impossible.
  • As Hz (beats/sec): 1.467 × 60 = 88 bpm; full range ≈ 40–200 bpm — physiological.

So the stored HR channel is in beats/sec (Hz), while CHANNEL_UNITS declares bpm.

Repo-wide audit (incl. markdown)

Wrong — label the stored ch5 stream as bpm (should be beats/sec / Hz):

  • src/data/processing/hf_config.py:35CHANNEL_UNITS[5] = "bpm" ← canonical source of the mislabel
  • src/imputation_evaluation/README.md:101| 5 | watch_hr | Continuous | bpm |

Correct — already document HR as beats/sec (the repo already knows):

  • src/downstream_evaluation/models/xgboost/constants.py:24WATCH_HR = 5 # Stored as beats/second, multiply by 60 for bpm
  • src/downstream_evaluation/models/xgboost/constants.py:108"unit": "beats/sec (×60 for bpm)"
  • src/downstream_evaluation/models/xgboost/extractors.py:531Note: HR is stored as beats/second (channel 5), multiply by 60 for bpm. (plus ×60 conversions at lines ~293/304 and docstring line 68)
  • src/imputation_evaluation/masking/intensity_failure.py + src/imputation_evaluation/config.py — defensively auto-detect Hz vs bpm (mean_hr < 10 → hz, 1 Hz = 60 BPM)

Out of scope (different channels — NOT the ch5 minute stream):

  • data/labels/survey_documentation/healthkit_watch_metrics/Watch_RestingHeartRate.md, Watch_WalkingHeartRateAverage.md, summary.md — these document the label-target metrics RestingHeartRate / WalkingHeartRateAverage (separate HealthKit summary quantities); their bpm is plausibly correct in their own right.

Energy / other continuous channels:

  • cal/min for ch6 is consistent and correct across hf_config.py:36, xgboost constants.py:25/109, and imputation_evaluation/README.md:102.
  • steps/min (ch0/3), m/min (ch1/4), count/min (ch2) are internally consistent across the same three sources (stored scale not independently re-verified here, but no contradiction found).

Likely cause

CHANNEL_UNITS documents the source HealthKit unit (HealthKit reports HR in bpm), but the processing pipeline stored the values on a per-second (Hz) scale. Consumers that treat the declared provenance unit as the stored unit inherit the mismatch.

Suggested fix

  1. Change CHANNEL_UNITS[5] in hf_config.py from "bpm""beats/sec" (or "Hz"), matching the xgboost docs; optionally note "×60 for bpm".
  2. Fix the imputation_evaluation/README.md channel table row 5 to beats/sec.
  3. Consider a single shared source of truth for channel units so hf_config.py and the xgboost/imputation docs cannot drift again.

How it surfaced

Found while reporting per-channel VQ-VAE reconstruction RMSE: HR RMSE printed as "bpm" but the value (~0.05) and the reconstruction plots (HR ≈ 0.8–1.4) only make sense in Hz. Reproducible standalone from the raw dataset, independent of any branch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdocumentationImprovements or additions to documentation

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions