Day 4 提過,手錶與戒指拿到的是 PPG 的脈波間期,身體一動,它和 ECG 量到的 HRV 就對不太上。所以多數廠商選在睡眠時算:睡眠時身體最平穩、干擾最少,PPG 算出來的數字才夠接近 ECG 的 HRV。
但「在睡眠時算」只回答了一半。一整夜有兩三萬個心跳間距,Day 4 算的是 5 分鐘一段的 RMSSD,一夜切下來有近百段,app 早上卻只給一個數。從一夜的心跳間距到那一個數,中間還要決定:取睡眠的哪一段、視窗切多長、最後怎麼把近百個視窗壓成一個數。RHR 也一樣,字面上是清醒靜止時的心率,有些廠商卻取夜間的值;Apple 的 HRV 甚至不在睡眠時量。
今天先整理各家的公開說明,再用一個合成的一夜,把幾種壓法實際算一遍。
早上 app 給的一個 HRV、一個 RHR,不是某個時刻量到的讀數,是一串選擇的結果:取哪段時間、切多長的視窗、每段算什麼、最後怎麼把一整夜壓成一個數。各家廠商的選擇都不一樣。
各家公開說明裡的做法:
| 廠商 | nightly HRV | RHR |
|---|---|---|
| Oura | 睡眠中每 5 分鐘一個樣本,取整夜平均;另外給最大值 | 夜間每 10 分鐘一個值,同時給整夜平均與最低值 |
| Garmin | 睡眠中每 5 分鐘一段,取整段睡眠的平均* | 24 小時內最低的 30 分鐘平均* |
| Polar | 入睡後約前 4 小時的 RMSSD | 取同一個 4 小時視窗* |
| WHOOP | 睡眠中的加權平均,偏重最後一段慢波睡眠*;自家文件的說法不一致 | 同 HRV 的加權方式* |
| Fitbit | 過去 24 小時內最長、且超過 3 小時的那段睡眠,算 RMSSD | 每天估一次,和夜間的睡眠心率分開;算法沒公開 |
| Apple | SDNN,清醒靜止時在背景量,預設每 4 小時一次 | 排除睡眠時段,估清醒休息時的最低心率 |
* 依 Dial et al. 2025 對各家文件的整理。其餘取自各家的公開說明與 Apple 2024 年的白皮書。
光指標就不一致:五家用 RMSSD(Oura、Garmin、WHOOP 的部分依 Dial 2025),Apple 用 SDNN,而且 Apple 的 HRV 不是一晚一個數。
字面上,RHR 是清醒、靜止時的心率。表裡至少有三種做法:
所以「RHR 就是睡覺時的心率」只對一部分裝置成立。夜間取值和臨床說的靜止心率是不同的量測情境;兩者差多少,我沒查到來源,所以夜間 RHR 不能直接拿去對靜止心率的參考區間。
挑睡眠有量測上的理由。腕戴與戒指拿到的是 PPG 的脈波間期,嚴格說是 PRV;靜止時 PRV 與 HRV 夠接近,活動時一致性明顯變差(Schäfer & Vagedes 2013,見 Day 4)。睡眠是一天裡最長、條件也最固定的靜止時段。
但睡眠不是均勻的一段。Grosicki & Presby 2025 引用 Trinder 2001 的結果:慢波睡眠的心率比 REM 低 3.5%,標準化高頻 HRV 高 123.7%。所以取整夜、取前 4 小時、偏重慢波睡眠,會得到不同的數字。Polar 的說法是前幾個小時比整夜平均更能反映恢復;WHOOP 偏重最後一段慢波睡眠。兩家都有自己的理由,但都沒有公開到外人能重現的程度。偏重某個睡眠分期還多一層不確定:分期同樣是推估,而且比分辨睡或醒更難,Dial 等人引用的統合分析,四分期的準確度約 60–75%。
常有人說 HR 和 HRV 負相關。這句話要先講清楚是哪一層:跨人比、同一個人跨夜比、同一夜跨視窗比,是三件事,一層的結果不能拿去推另一層。
我查到有公開說明的三家,都是拿當晚的數字跟個人的近期基準比:Oura 拿當晚的最低 RHR 對照過去約 2 個月的平均,Polar 對照過去 28 天,Garmin 累積 3 週建立基準之後,拿 7 日平均去比。廠商呈現的是「偏離自己多少」,不是數值本身的高低。Day 4 已經談過 HRV 為什麼不是越高越好;至於「恢復不佳」「該休息」這類判讀,沒有可對照的參考標準,本系列只當輔助觀察。
今天要看的是聚合這一步,所以需要每一段的真值都已知的資料。我用合成的一夜:8 段各自平穩的間期序列接起來,每段內是獨立常態亂數(seed=5),共 28,289 個間期、8.00 小時;各段平均心率 52.1–68.2 bpm,RMSSD 22.7–64.7 ms。每段的平均間期與標準差是我設的,沒有文獻依據,不對應真實的睡眠分期。
from wearable_ai.hrv.nightly import nightly_value, window_table
from wearable_ai.synthetic import piecewise_rr
rr, seg = piecewise_rr(NIGHT_SEGMENTS, pattern="jitter", seed=5) # 8 段,參數見 nightly_windows.py
table = window_table(rr, window_s=300, keep_partial=False) # 96 個 5 分鐘視窗
nightly_value(table, "rmssd_ms", "mean") # 43.82
nightly_value(table, "rmssd_ms", "lowest") # 20.61
nightly_value(table, "rmssd_ms", "fixed", period_s=(0, 14_400)) # 52.17

上圖是 96 個 5 分鐘視窗的 RMSSD,下圖是心率,兩條折線都在幾個平台之間跳動。三條水平線是三種聚合:前 4 小時平均 52.2 與 56.4、整夜平均 43.8 與 58.9、最低 5 分鐘 20.6 與 52.0;前 4 小時以淺綠底標出
| 96 個 5 分鐘視窗 | 整夜平均 | 最低 5 分鐘 | 前 4 小時平均 |
|---|---|---|---|
| RMSSD(ms) | 43.82 | 20.61 | 52.17 |
| 心率(bpm) | 58.91 | 51.96 | 56.42 |
中位數只當對照:
| 5 分鐘視窗 | 整夜平均 | 整夜中位數 | 前 4 小時平均 | 前 4 小時中位數 |
|---|---|---|---|---|
| RMSSD(ms) | 43.82 | 43.53 | 52.17 | 55.31 |
| 心率(bpm) | 58.91 | 58.35 | 56.42 | 55.47 |
整夜兩者差 0.29 ms、0.56 bpm,前 4 小時差 3.14 ms、0.95 bpm。這份資料沒有離群的視窗,所以平均與中位數很接近;真實資料不是這樣,見 Limitations 第 4 條。
| 原本以為 | 實際發現 |
|---|---|
| 主流廠商的 RHR 多取睡覺時的心率 | 只對一部分成立。Oura、Polar、WHOOP 取睡眠期間;Apple 早期機型排除睡眠,Fitbit 把 RHR 和睡眠心率分開,Garmin 在 24 小時內找最低的 30 分鐘(Concept 第 2 節) |
| 睡覺時干擾少、心率穩定,所以取這段算 HRV | 取睡眠有量測上的理由,但睡眠不是均勻的一段:慢波睡眠的心率比 REM 低 3.5%,標準化高頻 HRV 高 123.7%。而「睡眠」本身是推估出來的(Concept 第 1、3 節) |
| 整夜的數字取中位數 | 有公開說明聚合方式的廠商都寫平均,沒有一家寫中位數。在合成資料上兩者只差 0.29 ms;nsr001 的 5 分鐘視窗卻差 7.68 ms(平均 32.77、中位數 25.09) |
| 不滿 5 分鐘的最後一段也算(Day 4 的做法) | 8.90 秒、10 個間期的尾段成了 RMSSD 最低的一窗(18.78 ms),整夜中位數也從 43.53 掉到 36.08 ms。現在尾段不算 |
Grosicki & Presby 2025 把「定義對齊」拆成三個維度:指標定義(RMSSD 或 SDNN)、時間視窗(前 4 小時、整夜或特定睡眠分期)、平均或加權方式。只要有一個維度不同,兩個數字就不是同一個量。Dial 2025 讓 13 人戴五款裝置、累積 536 夜,拿去跟 ECG 胸帶比對時,就得逐台處理:Polar 的參考值改成只取前 4 小時;Garmin 的 RHR 因為不知道是哪 30 分鐘而排除;WHOOP 的加權方式無法重現,只能拿整夜平均去比。最後這一點正是那封信的批評。兩位作者受僱於 WHOOP,原文有揭露;Dial 等人回應說,問題出在廠商公開的資訊不足以讓外人對齊。
今天的數字把這三個維度落到一張表上。同一張視窗表,只換聚合方式,RMSSD 就從 20.61 變到 52.17 ms;只換視窗長度,最低一窗從 18.18 變到 21.92 ms;多留一個 8.9 秒的尾段,中位數掉 7.45 ms。這些差異都不會出現在 hrv_nightly_ms = 43.82 這個欄位名裡。所以 nightly metric 交給 LLM 時,metadata 至少要帶:指標(RMSSD 或 SDNN)、時段與它怎麼判定、window_s、聚合方式、尾段怎麼處理、用了幾個視窗。少了這些,模型分不出兩個「HRV」能不能比,例如把 Oura 的整夜平均和 Polar 的前 4 小時放進同一條趨勢線。
還有兩個「最低」。最低心率與最低 RMSSD 來自不同的視窗,除非 feature 帶著時間戳,LLM 不該把它們寫成同一個時刻的事;而 Garmin 的最低 30 分鐘連時間都不給。