SensorFM 这类以语义通道为单位组织输入的模型,通常不能直接吃手环导出的原始文件:手环侧给出的是厂商私有的字段名、嵌套结构和各家不同的采样率,模型侧要的是统一通道名、统一单位、基本恒定采样间隔的时间序列。判断顺序建议是:先看文件结构,再做字段到语义通道的映射,再比采样间隔,最后看坐标系,然后决定哪些差异靠预处理消化、哪些只能换数据源。
不同品牌手环的导出数据,通道名、嵌套层级、采样率通常都不一致,SensorFM 一般不能直接读。可以先做四件事:确认文件是列式表格还是嵌套结构、把设备字段映射到统一语义通道、用相邻时间戳差值判断采样是否均匀、用静止段重力方向判断坐标系是否一致。通道缺失、量程饱和、时间戳整段缺失这三类问题,预处理补不回来,只能换数据源或放弃该段。
先看清导出文件的实际结构
先别急着写解析代码,先看文件头和字段列表。列式 CSV/TSV 和按记录嵌套的 JSON(或由其他格式转出的 JSON)在解析方式上完全不同,按错字段解析出来的往往是一列时间或一列序号,而不是传感器通道。
# 列式文件:只看头部和总行数
head -n 3 export.csv
wc -l export.csv
# 嵌套 JSON:看顶层键和第一条记录的键
python -c "import json;d=json.load(open('export.json'));print(list(d)[:20]);print(json.dumps(d[list(d)[0]] if isinstance(d,dict) else d[0], ensure_ascii=False)[:500])"
看字段时顺手记录三件事:总行数或记录条数、时间戳首尾之间的时间跨度、包含多少个候选通道。行数与时间跨度一起看能反推平均采样间隔,如果时间跨度是八小时而只有几百条记录,这更像是降采样后的摘要数据而不是原始波形。通道数要连嵌套层级一起数,有些设备把心率放在类似 metrics.heart 的下一层,同级还有置信度字段,只取第一层会漏掉。
把设备字段翻译成统一语义通道
同一类数据在不同品牌里叫法很多,映射表要落到单位和量程上;只统一名字不统一单位,后续仍然会错。下面这张表是通用骨架,第一列请替换成你实际导出文件里的原文。
| 设备字段名(示例) | 语义通道 | 单位 | 量程 / 取值 | 备注 |
|---|---|---|---|---|
| hr / heartRate / heart_rate | heart_rate | 次/分 | 常见 30–220 | 需确认是瞬时值还是窗口均值 |
| acc_x / accX / accelerometer.x | acc_x | g 或 mg | ±2g、±4g 等 | 满量程需从导出说明或配置确认 |
| spo2 / SpO2 | spo2 | % | 0–100 | 多数设备只给百分比,不给原始 PPG |
| timestamp / ts / timeMillis | timestamp | 秒或毫秒 | — | 需确认单位、时区与起始基准 |
| ppg_raw / ir_raw | 无对应通道 | — | — | 模型侧通常没有对应输入,需换源或丢弃 |
| sleep_stage / activity_code | 无对应通道 | 枚举 | — | 枚举定义各家不同,不能直接当数值列 |
表里最后两行要单独标出来:这类字段在模型侧没有明确的语义通道可以落,硬塞成数值列只会引入不可解释的输入。映射完成后,建议随机抽几条记录,拿设备自带页面或导出工具的显示值对一遍心率和时间戳,确认单位换算方向没有反。
比对采样间隔是否恒定
手环数据常见三种情况:固定间隔、带抖动的事件驱动、以及不同通道各自采样率。用相邻时间戳差值看分布最快。
python - <<'PY'
import csv
from collections import Counter
ts=[]
with open('export.csv') as f:
r=csv.DictReader(f)
for row in r:
ts.append(float(row['timestamp'])) # 按实际字段名替换
d=[b-a for a,b in zip(ts,ts[1:])]
d.sort()
n=len(d)
print('条数', n)
print('最小', d[0], '中位', d[n//2], '最大', d[-1])
print(Counter(round(x,3) for x in d).most_common(10))
PY
中位数大致代表主采样间隔,最大值如果明显偏离中位数,说明存在丢包或暂停段。建议按段落处理:连续多个间隔接近中位数的段落保留;只有个别尖峰的段落可以重采样;如果一整段的间隔都远大于中位数,比如中间有几小时没有记录,这段原始信息已经丢失,重采样只会补出平坦的假数据,建议丢弃并记录缺口位置。
判断佩戴方向是否需要重定向
腕戴和手持的三轴坐标系朝向不同,同一个抬手动作在两组数据里可能表现为相反的符号。判断方法是用静止段的重力方向做基准:设备静止时合加速度主要来自重力,哪个轴承担主要分量,哪个轴就大致指向地。
python - <<'PY'
import csv, math
rows=[]
with open('export.csv') as f:
r=csv.DictReader(f)
for row in r:
rows.append((float(row['acc_x']),float(row['acc_y']),float(row['acc_z'])))
still=[(x,y,z) for x,y,z in rows if abs(math.sqrt(x*x+y*y+z*z)-1.0)<0.08]
if still:
n=len(still)
print('静止样本', n)
print('均值 g:', sum(p[0] for p in still)/n, sum(p[1] for p in still)/n, sum(p[2] for p in still)/n)
PY
把两组数据(例如已知基准的设备和你手上的手环)静止段的均值向量拿出来比:如果主分量落在不同轴上,通常需要一次轴交换或符号翻转;如果主分量符号相反,至少要做一次取反。这一步做完要再用同一段静止数据复算一次,确认主分量回到同一个轴。重定向只解决轴向问题,解决不了佩戴松紧带来的幅度差异,幅度差异建议放到归一化阶段处理,不要靠旋转硬凑。
列出必须预处理与只能换源的分界
把上面几步的结果落到一张分界清单上,再决定是否继续往下走。
- 通道缺失:模型必需的语义通道在导出文件里完全没有,例如模型要三轴加速度,而设备只导出心率和步数。这类问题通常在映射阶段就会暴露,预处理补不出来,只能换数据源,或者确认该通道是否属于可选输入后再决定是否降级使用。
- 量程饱和:原始值长时间贴在上限或下限附近不变,例如加速度长时间停在同一个固定值。这是传感器量程截断,插值和外推都不能还原被削掉的峰值,建议丢弃饱和段,或改用更大量程的导出方式(如果设备支持)。
- 时间戳缺失:整段没有绝对时间戳、只有序号,或者只有起始时间而没有每条的偏移。缺一条两条可以按恒定间隔线性补齐;整段缺失时无法判断真实采样间隔,补出来的时间轴不可信,这一段只能放弃。
反过来,单位换算、字段重命名、列顺序重排、降采样、坐标系旋转这些差异,通常在预处理阶段就能消化,做完后拿一小段已知数据回放验证即可。判断的底线是:凡是要靠推测去补的信息,先假设它补不回来;能换源就换源,换不了就明确标注这段数据的不可用范围,不要让带着未知不确定性的数据进入模型输入。