本文要处理的问题:把五十多项原始生理指标,收敛成用户能读懂、且长期稳定的少数几个分数。
一、问题:指标越多,用户越不读
可穿戴设备采集能力的提升,并不自动带来使用率的提升。
以一款主打夜间监测的指戴设备为例。它的采集端覆盖50多项指标:睡眠分期(浅睡、深睡、快速眼动)、入睡耗时、静息心率、心率变异性、呼吸频率、血氧、体温波动、日间活动量、卡路里、压力水平。设备体积比上一代缩减约40%,宽度6.09毫米,厚度2.28毫米,重量不到3克,为的就是让佩戴者可以在夜间无感地戴着。
如果把这些指标原样呈现,用户面对的是几十条曲线。大多数人看完头一屏就会退出,第二天不再打开。
真正的瓶颈不在采集能力,而在原始指标与可执行判断之间,缺了一层收敛逻辑。指标是数据,判断是结论,中间那层是需要自己建的。
这一层如果缺失,采集能力越强,用户越容易被淹没。设备测得出更多,用户读得懂的却没有变多,两者之间的差距,就是产品体验上的落差。
二、方案:采集、特征、展示三层分离
工程上的做法是把链路拆成三层,每层只对上一层负责,不跨层取值。
- 采集层:负责设备接入与单位统一,输出带时间戳的标准化序列;
- 特征层:负责窗口聚合与基线计算,输出相对值而不是原始读数;
- 展示层:负责把特征映射到少数几个分数,并给出分级。
目录结构
health-metrics/
├── collect/ # 原始指标接入与单位归一
│ ├── sources.yaml
│ └── normalizer.py
├── features/ # 窗口聚合与个人基线
│ └── windows.py
├── scoring/ # 分数聚合与分级
│ ├── rules.yaml
│ └── grade.py
└── config/
└── display.yaml
指标映射配置
权重与方向必须显式写出来,不要藏在代码里。
metrics:
readiness:
window: nightly
inputs:
- key: resting_heart_rate
weight: 0.25
direction: lower_better
- key: hrv_rmssd
weight: 0.30
direction: higher_better
- key: body_temperature_delta
weight: 0.20
direction: stable_better
- key: previous_day_load
weight: 0.25
direction: lower_better
display:
range: [0, 100]
crown_threshold: 85
rest_hint_threshold: 70
字段设计上有两个关键取舍。
其一是方向。心率、体温波动这类指标并非越高越好,也并非越低越好,而是存在一个个人相对稳定的区间。配置层如果不声明方向,特征层就没法做归一,展示层拿到的分数会随基线漂移。
其二是窗口。恢复状态依赖跨天数据,睡眠分数依赖当夜数据。把窗口显式写进配置,才能避免同一份数据在两处被算成两个口径。
三、分级阈值与展示规则
分数本身没有意义,有意义的是分数对应的动作。
| 分数区间 | 展示形态 | 建议动作 |
| 85–100 | 强调图标 | 维持当日计划 |
| 70–84 | 常规展示 | 按计划执行 |
| 0–69 | 收敛提示 | 降低强度、优先恢复 |
阈值不应拍脑袋给定。可行的做法是把历史样本按分位数切开,再让分层结果与用户自报状态做一致性对齐。
分级的价值不在于分得多细,而在于每一档都能对应一个动作。三个分数、三档区间,已经可以覆盖绝大多数日常判断。
四、验证:分数是否稳定
指标体系的验证,重点不是准确率有多高,而是同一份输入能否得到同一个输出,以及分数能否稳定地落在合理区间内。
checks:
- name: 窗口完整性
rule: nightly_window_minutes >= 240
- name: 缺失值比例
rule: missing_ratio <= 0.15
- name: 分数单调性
rule: monotonic(readiness, resting_heart_rate) == true
- name: 阈值可达性
rule: rate(score >= 85) between 0.05 and 0.25
这一条检查尤其容易被忽略。如果某个分级在样本里几乎从不出现,或者几乎总是出现,那这一档实际上没有区分能力,界面上的图标也就成了装饰。
五、踩坑记录
坑一:先算分,再定方向。早期版本直接把各项指标加权求和,结果出现“静息心率升高、分数反而变高”的情况。根因是方向字段缺失,加权时把越低越好的指标当成了越高越好。方向必须在配置层声明,不能留给下游猜。
坑二:用原始读数代替个人基线。同样是静息心率 62,对不同的人含义不同。改用相对个人基线的偏离度之后,分数的跨用户可比性明显改善。
坑三:展示层直接读原始指标。一旦展示层能绕开特征层取数,口径就会分叉。正确做法是在数据访问上做限制,让展示层只能拿到已经收敛过的特征。
# 反例:展示层直接读原始序列
score = raw["hrv_rmssd"] * 0.3 + raw["resting_heart_rate"] * 0.25
# 正例:只读特征层输出,并带上人员标识与窗口
features = load_features(user_id, window="nightly")
score = aggregate(features, rules="scoring/rules.yaml")
坑四:把阈值写死在代码里。阈值会随着样本分布变化而调整,写死在代码里意味着每次调整都要发版。放进配置文件,用 your-domain.test 这类占位域名承载内网配置中心即可。
六、小结
从50项指标到3个分数,真正的工作量不在采集,而在收敛:统一单位、声明方向、按窗口归类、把阈值外置、再给每一档配一个动作。做完整条链路之后会发现,用户要的从来不是更多数据,而是更少的犹豫。