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:35 — CHANNEL_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:24 — WATCH_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:531 — Note: 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
- Change
CHANNEL_UNITS[5] in hf_config.py from "bpm" → "beats/sec" (or "Hz"), matching the xgboost docs; optionally note "×60 for bpm".
- Fix the
imputation_evaluation/README.md channel table row 5 to beats/sec.
- 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.
Summary
CHANNEL_UNITSinsrc/data/processing/hf_config.pymislabels 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 thexssplit are:(norm stats: mean 1.264, std 0.305)
So the stored HR channel is in beats/sec (Hz), while
CHANNEL_UNITSdeclares bpm.Repo-wide audit (incl. markdown)
Wrong — label the stored ch5 stream as
bpm(should bebeats/sec/ Hz):src/data/processing/hf_config.py:35—CHANNEL_UNITS[5] = "bpm"← canonical source of the mislabelsrc/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:24—WATCH_HR = 5 # Stored as beats/second, multiply by 60 for bpmsrc/downstream_evaluation/models/xgboost/constants.py:108—"unit": "beats/sec (×60 for bpm)"src/downstream_evaluation/models/xgboost/extractors.py:531—Note: 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 metricsRestingHeartRate/WalkingHeartRateAverage(separate HealthKit summary quantities); theirbpmis plausibly correct in their own right.Energy / other continuous channels:
cal/minfor ch6 is consistent and correct acrosshf_config.py:36, xgboostconstants.py:25/109, andimputation_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_UNITSdocuments 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
CHANNEL_UNITS[5]inhf_config.pyfrom"bpm"→"beats/sec"(or"Hz"), matching the xgboost docs; optionally note "×60 for bpm".imputation_evaluation/README.mdchannel table row 5 tobeats/sec.hf_config.pyand 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.