医疗时序数据预测这件事,我前前后后做过几个相关项目之后,最大的感受就是:准确率当然要追,但“稳”才是真正决定方案能不能上线的硬指标。心率监护曲线、ICU床位占用、药品库存波动,这些数据一旦预测结果忽高忽低,压根没法交给临床或后勤去用。Kats是我在对比过一堆时间序列工具之后,觉得在医疗时序上最顺手的一个。
Kats是某海外大厂开源的时间序列分析全家桶,把预测、异常检测、特征提取、回测评估都统一到了一套API里。跟自己去拼statsmodels、Prophet、sklearn那套流程相比,Kats最大的价值不是某个模型多强,而是“同一份数据、同一个接口,可以把一整个候选模型池都跑一遍,还能用一致的回测口径去比较”,这对医疗场景太重要了——因为医疗时序往往数据量不大、噪声又高,你需要在不同模型之间快速试错,找到那个稳定能用的,而不是追求最花哨的。
这篇就围绕我用Kats处理医疗时序数据的完整过程来讲:从数据预处理的坑,到模型选型的思路,到回测评估怎么做得严谨,再到实际踩过的几个坑。适合手里有医疗或健康相关的时序数据、想快速做出可靠预测方案的朋友,也适合想入门Kats、但不想只看官方文档干巴巴例子的读者。
1. 医疗时序的“不好惹”和Kats的应对思路
1.1 医疗序列到底难在哪
医疗时序数据跟电商销量、服务器负载这类序列有个本质区别:它背后是生理系统或医疗系统,动态行为受大量不可控因素支配。
先说噪声。ICU里一条心率曲线,病人翻个身、咳嗽一下、护士调个输液速度,曲线立刻给你蹦几个尖峰。这些尖峰从算法角度看是“异常”,但从临床看可能只是正常操作,你既不能一刀切删掉,也不能让模型去硬拟合。
再说非平稳。医疗序列很少安安分分待在均值附近。病情恶化时,血压可能一路下行;术后恢复期,各项指标又会有个缓慢抬升的趋势;到了深夜,心率整体往下走,凌晨三四点最低,这又是节律性的成分。趋势、周期、突变揉在一起,线性模型经常招架不住。
还有一个很现实的约束——数据量。一个病人住几天院,能用的干净数据可能只有几百个点,几十个病人攒在一起,结构又因人而异,没法直接拼成一个大训练集。深度学习方法在这种量级下基本发挥不出来,传统统计模型和轻量级机器学习反而更稳。
最后是“可解释性”压力。临床或运营人员拿到预测结果,第一反应永远是“为什么”。你说一个黑盒模型给了个预测值,对方不敢用的。Kats全家桶里主力还是ARIMA、Prophet、Theta这类有明确结构和参数的模型,预测完之后你能说出是趋势主导还是周期主导,这在医疗场景里是刚需。
1.2 Kats对这些难题的回应
Kats的核心不是某个模型,而是一套工具体系。首先,所有数据进来先要做成TimeSeriesData对象,这个对象强制你规范时间和数值两列,从源头上解决“导入数据乱七八糟”的问题。
其次,Kats把大量经典模型封装出统一接口:fit()训练、predict(steps=n)预测、plot()可视化。这意味着你可以在同一份数据上,几分钟内把ARIMA、Prophet、Theta、STLF甚至集成模型全部跑一遍,不用给每个模型单独写一套调用逻辑。
第三是回测能力。BackTester可以模拟滚动预测场景,每次用过去一段窗口训练,预测未来若干步,然后滚动下一步。这在医疗时序里尤其关键,因为很多医疗预测任务本质上就是“滚动预报”——比如每个小时更新一次未来24小时的心率趋势。用回测得到的误差指标,比单次划分训练/测试集更接近真实上线表现。
最后,Kats还提供了异常检测模块,比如CUSUM检测器、离群点检测器。这在医疗数据清洗阶段非常有用:先用无监督方法找出可疑片段,再结合临床规则人工审查,比纯靠阈值卡数据高效得多。
所以选择Kats不是因为它能把某个模型做到90分,而是它把“试错、验证、上线”的整个循环帮你简化了,这对医疗这种要求稳扎稳打的场景,价值远大于某个模型单点的精度提升。
2. 预处理做不对,预测全白费
很多人在Kats上报错、结果离谱,最后发现都是数据预处理没做好。医疗数据尤其明显,这一环节往往要占整个项目50%以上的时间。
2.1 时间戳统一与重采样:先让数据“等间隔”
Kats对数据的第一个要求就是等间隔时间序列。但医疗系统导出的数据往往不是等间隔的:心电监护仪理论上5分钟一个点,实际可能因为网络、设备缓冲出现3分钟、7分钟交错的间隔;HIS系统导出的床位记录可能白天密集、夜间稀疏。
解决办法是先转成Pandas的DatetimeIndex,然后按目标频率重采样,比如统一为5min或1h。重采样时注意聚合方式——心率、血压这类生理量用均值;计数类数据(比如某时段就诊人数)用求和;采样率下降时可以直接取最后一个观测值。
import pandas as pd TZ = 'Asia/Shanghai' # 按医院实际时区替换 df['time'] = pd.to_datetime(df['time'], unit='ms') # 统一时区,否则模型预测出来的“凌晨低谷”可能出现在错误时刻 df['time'] = df['time'].dt.tz_localize('UTC').dt.tz_convert(TZ).dt.tz_localize(None) df = df.set_index('time') # 重采样到5分钟,数值列取均值 df_resampled = df['value'].resample('5min').mean()时区这个坑我在实际项目中栽过。某次做跨院数据对比,一部分设备时间戳是UTC,一部分是本地时间,没统一就扔进Kats,结果模型预测出来的“凌晨低谷”出现在早上,闹出笑话。所以重采样前一定先把时区对齐,再检查一下序列的时间范围是否符合预期。
2.2 缺失值策略:不是填完就完事
医疗数据缺值是常态。设备离线、护士摘掉传感器、患者外出检查,都会产生空洞。常见的缺失值处理有前向填充、线性插值、移动平均。我的经验是:
- 短于5分钟的缺口,直接前向填充,不会引入明显偏差。
- 5到30分钟的缺口,用线性插值,注意两端不要出现断崖。
- 超过30分钟甚至1小时的大缺口,不建议硬填,直接把这个区间拆掉,或者单独标记成“非连续段”。
为什么不能一味填充?因为长时间缺失往往意味着“那段时间发生了特殊事件”——比如患者转科、设备故障。这时候填出来的数据是假的,模型学到的是虚幻的平稳,上线遇到真缺口就会露馅。
# 5分钟粒度下,最多前向填充1个点(约5分钟) df_ffill = df_resampled.ffill(limit=1) # 或者用线性插值,限制最大连续插值数量 df_interp = df_resampled.interpolate(method='linear', limit=6)一个更稳的做法是给每条数据打上“填充标签”,建模时当作辅助信息,或者至少做敏感性分析:分别用填充和不填充的数据训练,看结果差异大不大。如果差异大,说明模型对缺失很敏感,那就得考虑分段建模,而不是依赖填充。
2.3 异常值清洗:临床阈值与统计方法双管齐下
生理信号异常值,处理原则跟普通业务数据不一样。心率30到200之间有临床意义,超出这个范围大概率是传感器脱落或干扰,直接剔除是安全的。但在这个范围内,有些值虽然统计上“异常”(比如心率从80突然掉到45),可能恰恰是病情变化,不能随便剔除。
所以我的清洗策略是分层。
第一层,硬阈值过滤。心率小于30或大于200、血氧饱和度小于50这类超出生理极限的值,直接置为缺失,再走缺失值流程。
第二层,局部突变识别。用滑动窗口计算中位数和MAD(绝对中位差),把偏离中位数超过3倍MAD的点标记出来,人工复核。相比Z-score,MAD对异常值本身不敏感,更适合生理信号这种本身就带尖峰的数据。
import numpy as np def mad_filter(series, window=24, n_sig=3.0): roll_median = series.rolling(window, center=True).median() roll_mad = (series - roll_median).abs().rolling(window, center=True).median() return series[(series - roll_median).abs() <= n_sig * 1.4826 * roll_mad]注意这里1.4826是把MAD换算到标准差的常数,适用于正态分布假设。窗口头尾会出现NaN,会被自然过滤掉,在短片段里可能浪费数据,可以配合边界填充使用。
清洗不是越猛越好。你把所有看着不顺眼的点都删掉,序列是会变得“好看”,但预测模型对正常波动的敏感度也丢了,临床场景尤其忌讳这种过度平滑。我的原则是:只处理能明确归因于干扰的点,正常范围内的波动一律保留。
3. 模型选型:Kats全家桶里谁最适合上
预处理完了,到选模型环节。Kats里模型一堆,但医疗场景我用的最多的其实就是ARIMA、Prophet、Theta、Holt-Winters,偶尔用一下Ensemble。
3.1 为什么我不一上来就搞深度学习
很多初学者觉得时序预测难度高,就想上LSTM、Transformer。但医疗时序普遍数据量小、信噪比低、非平稳性强,深度学习模型很容易在训练集上“背答案”,验证集上崩溃。加上Kats对这类模型的接入相对简单,但它强在传统模型的系统封装。对绝大多数医疗预测任务,一个调好参数的ARIMA或Prophet已经能覆盖需求,而且出问题好排查。
深度学习在医疗时序不是没用,而是应用场景更多在“多变量、多序列联合建模”这种大工程上——比如同时输入十几个生命体征预测脓毒症风险。这种是另一个层级的事,跟单序列稳步预测的目标不一样。做Kats这种单序列或低维预测,经典模型是性价比之选。
3.2 几个模型的选型对照
| 模型 | 最适合的场景 | 医疗场景典型用途 | 主要弱点 |
|---|---|---|---|
| ARIMA | 差分后近似平稳的序列 | 床位占用率、设备使用量 | 对突变和长周期适应差 |
| Prophet | 有明显周期和节假日的序列 | 药品需求、门诊量、日周期生命体征 | 序列太短或太长时表现不稳 |
| Theta | 小样本快速基线 | 任意任务的第一个baseline | 模型简单,复杂模式学不全面 |
| Holt-Winters | 趋势加季节性叠加的序列 | 血库库存、耗材月消耗 | 参数敏感,需要手动调 |
| Ensemble | 不确定选谁时兜底 | 生产环境多模型加权 | 训练时间翻倍,解释性下降 |
对单变量医疗时序,我常用的组合是:先用Theta跑一遍baseline,然后ARIMA和Prophet都跑,加一个Ensemble兜底,最后在回测上比较SMAPE和MAE选最优。
3.3 参数怎么定
ARIMA的p、d、q可以用Kats提供的一些搜索思路,或者直接用pmdarima这类工具的auto_arima跑一遍作为参考。比如某次我做心率预测,auto_arima给出的建议是ARIMA(2,1,2),d=1表示做了一阶差分,因为心率原始序列不平稳;p和q都不大,符合“健康”的简约模型原则。
Prophet里我最常调的两个参数是seasonality_mode和changepoint_prior_scale。医疗数据的季节性一般是加法而不是乘法——凌晨心率不会因为整体水平高就成比例地波动,所以additive比较合理。changepoint_prior_scale控制趋势突变点的灵活性,默认0.05,如果预测曲线过于僵硬或者过于扭曲,可以适当往0.01或0.1方向调。
别迷信auto_arima或默认参数。模型这玩意,参数要放在你的具体任务里看结果。我的习惯是:固定训练集,先粗跑一遍,看预测曲线是否平滑、是否滞后、是否出现极端值,再针对性调参,而不是一上来就追求数字最优。
4. 实操:从原始数据到回测评估一条龙
下面拿一个具体例子走完整流程,场景是“某医院心血管病区,用过去72小时的心率数据,滚动预测未来6小时的心率走势”。这个需求很典型——心电监护数据密集、噪声大、有昼夜节律,非常适合演示Kats的操作。
4.1 整理数据并构造TimeSeriesData
原始数据可能是设备导出的csv,列名乱七八糟。第一步先规整成两列:time和value。然后构造Kats的时间序列对象。
import numpy as np import pandas as pd from kats.consts import TimeSeriesData np.random.seed(42) n = 24 * 7 # 7天,每小时一个点 time = pd.date_range('2024-03-01', periods=n, freq='h') # 模拟带日周期和缓慢趋势的心率数据 trend = np.linspace(80, 76, n) cyclic = 8 * np.sin(2 * np.pi * np.arange(n) / 24) noise = np.random.normal(0, 3, n) value = trend + cyclic + noise df = pd.DataFrame({'time': time, 'value': value}) ts_data = TimeSeriesData(time=df['time'], value=df['value'])真实场景中这一部分就是预处理环节的落地。构造完成后,可以用ts_data.plot()看一眼数据长什么样,确认没有明显的断档和尖刺,再进入训练。
这里有个细节:TimeSeriesData内部会检查时间是否有重复、是否为空值。如果报错,多半是前面预处理没做干净,回到第2步排查,不要强行灌进去。
4.2 模型训练与预测的完整代码
以ARIMA为例:
from kats.models.arima import ARIMAModel, ARIMAParams params = ARIMAParams(p=2, d=1, q=2) model = ARIMAModel(data=ts_data, params=params) model.fit() fcst = model.predict(steps=6, alpha=0.05)predict返回的DataFrame包含fcst列和置信区间上下界。医疗场景里,置信区间比点预测更重要。心率预测的均值是76,但如果95%区间是60到92,那临床看到的信号差不多是“范围很宽,别太当真”;如果区间是74到78,说明模型对当前状态比较确定,可以做下一步决策。
Prophet的写法也差不多:
from kats.models.prophet import ProphetModel, ProphetParams params = ProphetParams(seasonality_mode='additive', interval_width=0.95) model = ProphetModel(data=ts_data, params=params) model.fit() fcst = model.predict(steps=6)注意Prophet的训练比ARIMA慢,数据太长时先降采样或限制训练窗口。对于心率预测这种连续滚动更新的场景,我一般只用最近72小时的数据训练,更早的数据对当前状态帮助有限,反而拖慢速度。
Ensemble用法:
from kats.models.ensemble import EnsembleParams, EnsembleModel from kats.models.theta import ThetaModel, ThetaParams params = EnsembleParams([ {'model': ARIMAModel, 'params': ARIMAParams(p=2, d=1, q=2)}, {'model': ProphetModel, 'params': ProphetParams(seasonality_mode='additive')}, {'model': ThetaModel, 'params': ThetaParams(m=24)}, ]) model = EnsembleModel(data=ts_data, params=params) model.fit() fcst = model.predict(steps=6)m=24是因为我们用小时数据,日周期对应24个点。这个参数经常被忽略,直接导致Theta模型感知不到日周期。Ensemble的好处是即使某个基模型拉胯,加权之后整体也不太会崩,适合做生产环境的兜底方案。
4.3 用BackTester做滚动回测
单次划分训练集、测试集验证,对医疗时序来说不够。因为医疗预测是持续滚动进行的,你需要知道模型在连续多轮预测中的稳定性。Kats的BackTester正是干这个的。
from kats.backtesting import BackTester bt = BackTester( data=ts_data, model_class=ARIMAModel, params=ARIMAParams(p=2, d=1, q=2), error_metrics=['smape', 'mae', 'mse'], steps=6, ) bt.run() bt.plot()回测思路是:从序列的某个位置开始,每轮用train_percentage或window_length指定的窗口训练,预测未来steps步,然后窗口滑动,重复多次。最后得到每一步的误差分布,而不是单个误差值。
我看到很多入门者只报一个平均SMAPE,这是不够的。医疗场景里,我还会额外看两个东西:
- 误差的逐段分布。如果误差在凌晨时段明显高于白天,说明模型对夜间节律把握不好,需要单独调或引入光照、活动这类外生变量。
- 极端误差尾部。平均误差5%,但如果偶尔来一次30%的偏差,上线后就会造成一次不小的误报。我会统计误差超过某个阈值(比如心率预测偏差大于10次/分)的比例,确保它在可控范围。
用回测去比较ARIMA和Prophet,比各跑一次预测然后看谁“更像真的”靠谱得多,因为回测模拟了上线后的滚动更新机制,样本量是若干个预测轮次,统计上更可信。
4.4 评估指标怎么选
Kats的error_metrics支持smape、mae、mse、rmse等。医疗时序评估,我建议至少同时看SMAPE和MAE,有条件再加一个自定义的临床误差指标。
SMAPE的优势是对称、无量纲,适合比较不同量级的数据(比如心率和血压)。但也有人指出SMAPE在真实值为0时无定义,好在医疗数据极少为0,可放心用。
MAE则是绝对误差,可以直接和临床可接受范围对照。比如心率预测MAE是3次/分,那基本在可接受范围内;如果是15次/分,这个预测基本没法用于告警决策,就得重新建模。
我自己在后端评估时还会算一个P95误差:把每轮回测的绝对误差排序,取95分位。这个指标能告诉你最坏情况有多差,比平均值更能暴露模型的脆弱点。
5. 实踩过的坑与排查经验
最后这块,我把几个高频问题整理出来,基本覆盖了用Kats做医疗时序预测时最常见的翻车点。
5.1 数据太短,模型训不动
Kats里ARIMA这类模型对序列长度有要求。我遇到过拿两天数据就想去预测未来24小时的情况,结果模型fit半天,预测出来基本是一条水平线。因为两天数据连一个完整的日周期都覆盖不住,模型压根学不到节律。
处理办法:
- 数据长度至少是预测长度的3倍以上,最好是5到10倍。
- 如果历史数据确实有限,就缩短预测步长,从24小时改成6小时再看。
- 模型选型也跟着变,Theta这种参数少的模型在小样本下更稳,ARIMA其次,Prophet很吃数据量。
5.2 预测曲线“无脑平移”,滞后严重
这是非常常见的现象:预测值约等于最后一个观测值往未来平推,曲线一点变化都没有。医学上这叫“预测退化成last observation carried forward”。
出现这种情况,通常意味着这个序列接近随机游走,或者模型的差分、趋势设置有问题。排查路径:
- 先ADF检验看平稳性。如果p值显著大于0.05,说明序列不平稳,ARIMA需要提高差分阶数。
- 看ACF图,若自相关衰减缓慢,说明趋势成分没提取干净。
- 也检查一下是不是清洗过度,把有效波动都抹平了。
解决思路包括:换成能显式建模趋势的Prophet,或者在预处理阶段先做一次趋势提取(比如去掉滑动均值),对残差建模,再加回趋势。
5.3 回测指标不错,上线就不行
这是“分布偏移”的经典症状。医疗数据在不同时段、不同病区可能有完全不同的统计特征。模型在3月份数据上回测得很好,4月份上线就飘了,大概率是4月份病人构成变了、用药方案变了、监测设备换了,导致序列分布整体偏移。
我的做法是:在部署前做“时段外回测”,只用更早的数据训练,预测最近一段时期的数据,专门验证模型在未知时段的表现。另外,监控数据分布漂移也很重要——如果上线后输入数据的均值、方差跟训练时明显不同,就不要迷信模型的预测结果,先重建再上线。
5.4 长序列下训练特别慢
Kats封装了Prophet,Prophet本来就不擅长超长序列。有次我拿半年逐分钟的ICU数据(近26万个点)直接训练Prophet,fit一步跑了十几分钟还没结束。后来把数据先重采样成5分钟,又限制了训练窗口长度(只取最近14天),速度立刻上来了,预测精度几乎没掉。
经验是:
- 医疗时序做预测,不一定要“全部历史数据都用上”。很多任务最近几周的窗口就够。
- 重采样是降维的利器,5分钟粒度对大多数临床应用足够。
- Ensemble里塞太多慢模型,回测总时长会爆炸,我的做法是先跑单个模型,筛选出两三个表现最好的,再做Ensemble。
下面把问题整理成一张速查表,方便后续排查对照:
| 现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| fit不收敛或报错 | 数据存在缺失或时间戳重复 | 检查TimeSeriesData前处理 | 回到第2节做重采样和缺失清洗 |
| 预测是一条水平线 | 序列太短、模型太弱、差分不当 | ADF检验、看数据长度 | 缩短步长、换Theta/Prophet、加差分处理 |
| 预测滞后半个周期 | 季节项没调对 | 检查周期参数m或seasonality_mode | 调整Prophet季节类型、检查重采样粒度 |
| 回测好但上线崩 | 数据分布偏移 | 时段外回测、监控均值方差 | 定期重建模型、缩小训练窗口 |
| 训练速度极慢 | 序列过长或模型复杂 | 查看数据点数和基模型数量 | 重采样、截断训练窗口、减少Ensemble基数 |
我个人的体会是,Kats在医疗时序上最大的价值不是“某个模型巧夺天工”,而是它把“快速试错、严格回测、稳定上线”这整套流程变得可复制。反复踩坑之后,我现在做任何医疗时序预测项目,都固定走同一套模板:先花大力气把数据清洗和重采样做扎实,然后用基线模型确认量级,再用回测对比候选模型,最后单独测试最坏情况误差是否可接受。这四步走完,模型基本不会在真实场景里给你“惊喜”。
最后再分享一个小技巧:如果护理人员或临床医生质疑预测结果,别急着调模型,先画图让他们标一标“哪些时段他们本来就是有把握的”。你会发现模型和人的判断差距,往往不在算法,而在数据预处理阶段丢失的护理记录、用药事件这些上下文信息。把事件标记加进模型流程,比换任何高级模型都管用。