iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
AI Engineering

30 天拆解 Wearable × AI:從穿戴裝置生理訊號到AI健康洞察系列 第 18 篇

Day 18|Sleep × HRV × Resting HR:單一指標夠嗎?

  • 分享至 

  • xImage
  •  

今天為什麼研究這個?

Day 17 把單一指標和 personal baseline 比較,標出偏離日;但一個 rmssd 警報,還不足以說明那天發生了什麼。睡眠時間、夜間 HRV 與 resting HR 若一起變動,能否提供較完整的情境?若方向互相牴觸,又該怎麼寫,才不會只挑支持某個故事的數字?

今天用 LifeSnaps 公開資料集的日粒度資料,把三個指標放到同一個 ID 的同一個解讀日,畫靜態多面板圖並挑案例檢查。先約定日期與標記規則,再看哪些天訊號一致、哪些天互相矛盾,以及三者都可比較的日子剩多少。這是跨指標的描述性實驗;同日出現的變化不會自動證明睡眠造成 HRV 或 RHR 改變。

Concept

1. 同一列,不等於同一段時間

多表 join 要先知道每個欄位的日期從哪裡來。LifeSnaps 的日粒度 CSV 已由資料集作者彙成每個 (id, date) 一列;前處理紀錄顯示,睡眠只取 mainSleep,日期來自 dateOfSleep;rmssd 取匯出檔 Daily Heart Rate Variability Summary 的 data.timestamp 日期;resting_hr 取 RHR 紀錄的 data.value.date。這不是直接把三份原始表依時刻相接。分析時還要依收集期間加上「輪」:對齊與建立 baseline 都以 (id, 輪, date) 為鍵,不能讓同一人的兩輪歷史接在一起。

Fitbit Web API 的睡眠文件把 dateOfSleep 定義為睡眠結束日;Web API 的 HRV 文件說 HRV 對應主睡眠及其日期。但後者不是 LifeSnaps 匯出欄位的直接規格。本專案在完整日粒度 CSV 檢查,有 rmssd 的日期同日有睡眠紀錄達 99.5%,平移 ±1 天後為 90.1%/90.0%;加上 API 定義,才推測 LifeSnaps 的夜間 HRV 大致歸到醒來日,並沒有原始時間戳可逐筆證實。

RHR 是每日估計值,不能當作同一晚的睡眠心率;公開文件沒有說它對應哪一段時間。本專案看到 RHR 同日有睡眠紀錄的比例為 79.4%,平移 ±1 天是 75.4%/77.5%,也不足以證明「醒來日的 RHR 反映那一晚」。同一列只是選定的解讀日,不表示三個指標來自同一個量測時窗。另有一個 CSV 層級的限制:作者對同鍵多筆主睡眠用 groupby(...).first(),pandas 會逐欄取第一個非缺值;若同日確有多筆,minutesAsleep 與其他睡眠欄位可能不是同一筆紀錄。作者也把「每人每天是否只有一段主睡眠」留作待查,本文不能把彙整列當成已核對的原始睡眠紀錄。

2. 先算可比較的交集,再看方向

三個欄位的覆蓋範圍不同。本系列採用的 LifeSnaps 兩輪資料中,minutesAsleep 有值的為 66 個 ID、resting_hr 為 68 個 ID,rmssd 只有 43 個 ID。minutesAsleep 只計主睡眠,這裡的「睡眠較短」不是一整天含小睡的總睡眠量。不能拿睡眠有值的所有日子當三指標分析的分母,也不能把缺值補成「沒有偏離」。要先按 (id, 輪, date) 對齊,再逐項確認當天有值與各自 baseline 是否足以判定,報出原始日數和可比較交集;交集的實際大小留給今天的實驗計算。

比較也要在各 ID、各輪、各指標內進行:minutesAsleep 是分鐘、rmssd 是毫秒、resting_hr 是每分鐘心跳數,原始數值不能直接加總。本篇暫定沿用 Day 17 主結果 A 組的 z_mean_sd、兩側門檻 3.0(min_run = 1),逐項保留 decidable 與 flag,再看相對於各自 baseline 的方向與幅度。Day 17 曾比較三種方法,但門檻只用合成資料的 rmssd、resting_hr 校準;minutesAsleep 沒校準,不能說三個指標有相近的誤報率。今日先把「主睡眠較短、RMSSD 較低、RHR 較高」視為一種待檢查的方向組合,而非預設的生理定律。

3. 「一致」和「矛盾」只是證據標記

在看案例前先固定標記口徑。三項都可判定且都標旗、方向同屬上述組合或同屬其反向(主睡眠較長、RMSSD 較高、RHR 較低),才標為這組規則下的「一致」,並另記是哪一個方向;可判定日若同時有朝上述方向與反向的標旗,標為「矛盾」。其餘可判定日分開標「部分偏離」(有標旗,但不足以歸入前兩類)或「無偏離」(三項皆未標旗)。任一項缺值、baseline 不足或尚未建立,就標「無法判定」,另記是哪一項及原因,不能和有資料的「無偏離」共用分母;LifeSnaps 的日粒度 CSV 沒有品質欄位,今天不做品質判定。每項的 decidable、flag 和方向都保留,連同 Day 17 的方法、門檻與版本記下,避免看完圖才改規則。

這是嚴格的三項同日標旗條件。Day 17 在 LifeSnaps 的 rmssd 只有 40.0% 目標日可判定,可判定日的標旗率依方法為 2.9%–4.8%;再要求睡眠與 RHR 同日同方向標旗,「一致」日可能很少。這是看圖前對門檻與可用資料的預期,交集還沒有計算;即使最後很少,也不能據此推論三個連續指標彼此無關。

每個案例的 evidence bundle 至少要有解讀日期、各指標觀測值與單位、相對 baseline 的方向和幅度、可判定/缺值狀態,以及規則版本。三個方向一致,只表示這些量測在這套對齊與門檻下共同出現;矛盾也不表示其中一個裝置數值必然錯。兩者都不能單憑相關性推斷原因,後續文字應描述觀測和不確定性,而不是替當事人下健康結論。

Hands-on

Dataset

LifeSnaps 日粒度 CSV,限兩輪收集期間。解讀日是每個 (id, 輪) 內三項任一有值的日子,共 3,527 天、68 個 ID;三欄同列皆非空的有 1,959 列、43 個 ID,這只表示欄位有值。

這裡說「ID」而不說「人」。兩組 ID(621e2ed6/621e346f、621e3410/621e367e)第 1 輪的三項數值分別有 47、44 天完全相同,其他欄位只有 steps、distance、bpm 不同,來源沒有查證,不能斷言是同一人。另有 29 列 rmssd、1 列 minutesAsleep 的匯出值為 0,依預先定的規則照有效觀測處理。

Method

  1. 以 (id, 輪, date) 對齊三個指標,各自在每輪內建 baseline:z_mean_sd、兩側門檻 3.0、deviation-v2,再用上述五類標記(cross-metric-v1)。
  2. RHR 日期敏感度:睡眠與 rmssd 留在 d,把原日期 d−1、d、d+1 的 RHR 判定對齊到 d,分母固定為主結果的解讀日。RHR 的 baseline 依原本日期計算,不跨輪;平移後落到輪外或解讀日之外的列另外計數。
  3. 事後敏感度檢查(看完主結果才決定,不取代主表):A 把零值改成缺值、連 baseline 一起重算;B 每組疑似重複 ID 的第 1 輪只留一個,兩種保留法都跑。

Code

long = cm.run_metrics(df, BASELINE_CONFIG, DEV_CONFIG)     # 每個 (id, 輪)、每個指標各自判定
wide = cm.align_metrics(long, cm.METRICS, targets=targets)  # 一個解讀日一列;沒有列的指標記 missing_value
labels = cm.label_days(wide)                                # 逐列呼叫 classify_day

def classify_day(sides, decidable):
    expected = set(EXPECTED_SIDE)  # 三個指標;省略輸入檢查
    if not all(decidable.values()):
        return UNDECIDABLE, NO_PATTERN
    if all(sides[m] == EXPECTED_SIDE[m] for m in expected):
        return CONSISTENT, PATTERN_EXPECTED
    if all(sides[m] == REVERSE_SIDE[m] for m in expected):
        return CONSISTENT, PATTERN_REVERSE
    flagged = [m for m in expected if sides[m] != dev.NO_SIDE]
    if flagged:
        has_expected = any(sides[m] == EXPECTED_SIDE[m] for m in flagged)
        has_reverse = any(sides[m] == REVERSE_SIDE[m] for m in flagged)
        return (CONTRADICTORY if has_expected and has_reverse else PARTIAL), NO_PATTERN
    return NO_DEVIATION, NO_PATTERN

結果與意外

原本以為 實際發現
我以為三欄都有值的日子,經過 baseline 篩選後仍會保留相當一部分;缺值會是主要障礙,但不一定是各指標不同的障礙。 三項都有值 1,959 天,三項都可判定只剩 617 天(28 個 ID),占解讀日 17.5%。無法判定的 2,910 天裡,rmssd 最常見的原因是缺值(1,552),resting_hr 是 baseline 尚未穩定(1,344)。
我預期三項同日同方向標旗會少,但仍可能找到少數一致或方向互相牴觸的日子;部分偏離裡也許會有不少兩項標旗。 一致 0 天、矛盾 0 天。617 個可判定日中 572 天無偏離、45 天部分偏離;45 天裡 41 天只有一項標旗,4 天兩項,沒有一天三項都標旗。
既然先挑了「主睡眠較短、RMSSD 較低、RHR 較高」作待檢查的組合,我以為部分偏離日的標旗會較常落在這組方向。 部分偏離日中,只有預期方向標旗的 23 天、只有反向的 22 天。兩項標旗的 4 天有 3 天同屬預期方向,其中 2 天的 RMSSD 標旗是匯出值 0。
我以為 RHR 的日期前後挪一天,整體日數和大多數個別日子的分類都會差不多。 RHR 改用前一天或後一天的判定,部分偏離 43–45 天、一致仍是 0,只有偏移 −1 出現 1 天矛盾;被丟掉的 RHR 列都沒有標旗。但個別日子會換類:偏移 −1、+1 各有 10 天由無偏離變成部分偏離、10 天反過來。
我以為把匯出值 0 當缺值,主要只會拿掉那些由零值造成的向下標旗;其他日子的 baseline 與分類應該變動不大。 部分偏離日的 12 個 RMSSD 向下標旗,6 個是匯出值 0。改成缺值並重算 baseline 後,這 6 天都變成無法判定,另有 2 天由無偏離變成部分偏離,部分偏離 45 → 41(事後檢查)。另外,RMSSD 有 147/966 個可判定日的判定下界低於 0,這些日子不可能出現向下標旗。
我擔心兩組疑似重複 ID 會把同樣的偏離算兩次;若每組只留一個 ID,部分偏離的日數與比例可能明顯改變。 兩組疑似重複 ID 占三項可判定日 72 天、部分偏離 4 天。每組第 1 輪只留一個 ID 時,可判定 617 → 581、部分偏離 45 → 43,共同解讀日的分類一天都沒變(事後檢查)。

https://ithelp.ithome.com.tw/upload/images/20261001/20184206At9UCMogCC.png
ID 621e375b 第 1 輪:2021-07-09 主睡眠 179 分向下、RHR 75.1 bpm 向上標旗,RMSSD 未標旗,兩項同屬預期方向,仍是部分偏離

https://ithelp.ithome.com.tw/upload/images/20261001/201842067bief5QWFE.png
ID 621e3267 第 1 輪:2021-07-18 RMSSD 96.4 ms 向上、RHR 48.5 bpm 向下標旗,兩項同屬反向,仍是部分偏離;這一輪 RMSSD 的判定下界全部低於 0

https://ithelp.ithome.com.tw/upload/images/20261001/20184206PmZZshSgfC.png
ID 621e3236 第 2 輪:2021-12-24 主睡眠 193 分向下標旗,RMSSD 的匯出值是 0,也被標為向下

Limitations

這次只用 A 組的 z_mean_sd、兩側門檻 3.0 與「三項都標旗才算一致」的規則;minutesAsleep 的門檻沒有像 RMSSD、RHR 那樣在合成資料上校準,三者的標旗率也不能當成相同誤報代價。三項都可判定的只有 617/3,527 個解讀日(17.5%),因此一致與矛盾各 0 天,只能說這套資料與門檻下沒有標出這兩類,不能推成指標間沒有關聯。RHR 的實際量測時窗未知;平移 ±1 天雖讓各類總數接近,仍有個別日子在無偏離與部分偏離間互換,偏移 +1 也是事後回看隔天數值,不能當作當天可用的證據。

LifeSnaps 的日粒度 CSV 沒有可核對的品質欄或事件真值,零值的來源也不明。主結果把零值當觀測,使 12 個 RMSSD 向下標旗中有 6 個是匯出值 0;事後把零值改缺值並重算 baseline,這 6 天全變成無法判定,顯示部分案例依賴這個資料口徑。兩組 ID 在第 1 輪的三項數值長時間相同,但來源未查證,不能斷言是同一人;只留每組一個 ID 的事後檢查改了分母,沒有改動共同解讀日的分類。這些敏感度結果不驗證零值原因或 ID 身分,也不能把偏離標記當成健康事件的準確率,更不能由同日共變推斷睡眠造成 HRV 或 RHR 的變化。

這對 AI Engineering 的意義

三項都標旗才算「一致」的規則,在真實資料上一天都沒觸發;若只看單一指標,又有 41 天是一項標旗撐起的部分偏離。要交給 LLM 的單位應該是 evidence bundle:哪幾項標旗、方向、幅度,哪幾項無法判定、原因,以及規則版本。2,910 個無法判定日也要以「無法判定」的狀態傳下去,不能被當成沒事而刪掉。

分類由 deterministic code 依預先寫好的規則產生,LLM 只負責描述,不負責判斷哪天算一致或矛盾。敏感度檢查顯示,去掉疑似重複 ID,共同解讀日的分類不變,但部分偏離 45 → 43;換 RHR 的日期歸屬,各類總數幾乎不變,每種偏移卻各有 20 天在無偏離與部分偏離間互換;把零值改成缺值,45 個部分偏離日中有 6 天變成無法判定。所以 Day 19 的 feature contract 除了數值和 decidable,還要能帶資料本身的疑點,例如「匯出值為 0」與「ID 可能不是獨立個體」。


上一篇
Day 17|Deviation Detection:什麼是今天跟平常不一樣?
系列文
30 天拆解 Wearable × AI:從穿戴裝置生理訊號到AI健康洞察 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言