☰
K-Means聚类与机器学习:新生儿败血症ICU住院时长影响因素分析
2026/10/15 23:55:13 网站建设 项目流程

1. 项目背景与核心问题拆解

新生儿重症监护室里,每一个小时都牵动着整个家庭的神经。败血症在新生儿群体中并不罕见,尤其是早产儿和低出生体重儿,免疫系统尚未发育成熟,一旦发生感染,病情进展可能非常迅速。临床医生需要在极短的时间内判断:这个孩子的生命体征组合到底意味着什么?他大概需要在ICU里待多久?哪些指标是真正影响住院时长的关键因素?

这个项目要解决的就是上述问题。它用K-Means聚类算法对新生儿败血症患儿的生命体征做“画像”分析,再结合机器学习方法研究ICU住院时长的影响因素。说得直白一点,就是把一群小患者的体温、心率、呼吸频率、血压、血氧饱和度、白细胞计数等指标扔给算法,让算法自己去找出“哪些孩子的情况比较像”,然后进一步分析“为什么有些孩子住得久、有些住得短”。

我第一次接触这个方向的时候,心里其实是有疑问的:聚类分析在临床上到底有没有实际价值?后来跟几位做新生儿科的医生聊过之后才明白,临床上的“分型”很多时候依赖医生的个人经验和有限的指南,而数据驱动的聚类可以提供一个更客观的视角。比如,同样是败血症,有的孩子主要表现为呼吸系统症状,有的则以循环系统不稳定为主,还有的可能先出现体温波动。这些不同的表现模式,如果能够被算法识别出来,对后续的个体化治疗和资源分配都有参考意义。

这个项目适合谁来看?如果你是对机器学习在医疗领域应用感兴趣的学生或开发者,这个项目可以给你一个完整的实战案例;如果你是临床研究人员,想了解如何用数据方法辅助分析,这里面的思路和踩坑经验也有参考价值;如果你只是对“聚类到底能干什么”感到好奇,那这篇文章会从数据预处理一直讲到结果解读,尽量把每一步都讲清楚。

注意:本文涉及的所有数据均为模拟数据,不包含任何真实患者信息。项目中的代码和参数设置基于常见实践进行合理补全,实际应用时需要根据具体数据集进行调整。

2. 数据准备与特征工程:从原始指标到可用的分析素材

2.1 数据来源与字段说明

做任何机器学习项目,数据都是第一位的。这个项目的数据通常来自医院的电子病历系统或ICU监护设备导出的生命体征记录。一个典型的数据集可能包含以下字段:

字段名称含义数据类型备注
patient_id患者编号字符串脱敏后的唯一标识
gestational_age胎龄(周)数值影响败血症风险的重要因素
birth_weight出生体重(克)数值低体重儿风险更高
temperature体温(摄氏度)数值可能波动较大
heart_rate心率(次/分)数值新生儿正常范围与成人差异大
respiratory_rate呼吸频率(次/分)数值败血症时可能增快
systolic_bp收缩压(mmHg)数值循环系统指标
diastolic_bp舒张压(mmHg)数值循环系统指标
spo2血氧饱和度(%)数值呼吸功能指标
wbc白细胞计数数值感染指标
crpC反应蛋白数值炎症指标
platelet血小板计数数值可能影响凝血功能
icu_losICU住院时长(天)数值回归任务的目标变量
sepsis_type败血症类型分类早发型/晚发型

这些字段里,生命体征类指标(体温、心率、呼吸频率、血压、血氧)是聚类分析的主要输入,而实验室指标(白细胞、CRP、血小板)和人口学特征(胎龄、出生体重)既可以作为聚类的辅助特征,也可以作为后续影响因素分析的候选变量。

2.2 数据清洗:那些你必须处理的“脏数据”

真实世界的数据从来不会干干净净地等着你。我在处理这类数据时,遇到过以下几种典型问题:

缺失值处理。生命体征数据通常是连续监测的,但导出时可能出现某个时间段没有记录的情况。如果某个患者的缺失比例超过30%,我一般会考虑直接剔除该样本;如果缺失比例较低,可以用该患者其他时间点的均值或中位数填充。对于实验室指标,由于不是每天检测,缺失可能更多,这时候需要根据临床意义决定是填充还是保留为缺失。

异常值识别。新生儿的心率正常范围大约在120-160次/分,但监护设备偶尔会记录到明显偏离的数值,比如心率突然变成0或者300。这些往往是设备误差或导联脱落导致的。我的做法是先用箱线图或3σ原则标记可疑值,然后结合临床知识判断:如果某个值在生理上完全不可能,就直接删除;如果只是极端但可能的值,就保留但做缩尾处理。

时间窗口对齐。不同指标的记录频率不一样,生命体征可能是每小时甚至每分钟记录一次,而实验室指标可能每天只测一次。做聚类时,通常需要把数据聚合到患者层面,比如取每个患者入住ICU后前24小时的生命体征均值、最大值、最小值等统计量作为特征。

import pandas as pd import numpy as np # 假设原始数据已经加载为df # 计算每个患者前24小时的统计特征 def extract_features(group): return pd.Series({ 'temp_mean': group['temperature'].mean(), 'temp_max': group['temperature'].max(), 'temp_min': group['temperature'].min(), 'hr_mean': group['heart_rate'].mean(), 'hr_max': group['heart_rate'].max(), 'hr_min': group['heart_rate'].min(), 'rr_mean': group['respiratory_rate'].mean(), 'sbp_mean': group['systolic_bp'].mean(), 'spo2_mean': group['spo2'].mean(), 'wbc_mean': group['wbc'].mean(), 'crp_mean': group['crp'].mean(), 'platelet_mean': group['platelet'].mean(), 'gestational_age': group['gestational_age'].iloc[0], 'birth_weight': group['birth_weight'].iloc[0], 'icu_los': group['icu_los'].iloc[0] }) features_df = df.groupby('patient_id').apply(extract_features).reset_index()

2.3 特征标准化:为什么这一步不能省

K-Means是基于距离的算法,它计算样本之间的欧氏距离。如果某个特征的数值范围远大于其他特征,比如白细胞计数可能是5-30,而体温只有36-39,那么白细胞对距离的影响就会远远超过体温。这显然不合理,因为从临床角度看,体温变化0.5度可能比白细胞变化5个单位更有意义。

所以,标准化是必须的。常用的方法有两种:Z-score标准化和Min-Max归一化。我一般倾向于Z-score,因为它对异常值的鲁棒性稍好一些,而且保留了数据的分布形态。

from sklearn.preprocessing import StandardScaler feature_cols = ['temp_mean', 'temp_max', 'temp_min', 'hr_mean', 'hr_max', 'hr_min', 'rr_mean', 'sbp_mean', 'spo2_mean', 'wbc_mean', 'crp_mean', 'platelet_mean', 'gestational_age', 'birth_weight'] scaler = StandardScaler() X_scaled = scaler.fit_transform(features_df[feature_cols])

实操心得:标准化之后,建议再检查一下每个特征的均值和标准差,确保均值接近0、标准差接近1。如果某个特征标准化后仍然偏离很大,可能是原始数据中存在极端异常值,需要回头处理。

3. K-Means聚类:如何给新生儿败血症“画像”

3.1 为什么选择K-Means而不是其他聚类算法

聚类算法有很多种,DBSCAN、层次聚类、高斯混合模型各有优劣。这个项目选择K-Means,主要有几个考虑:

第一,K-Means的计算效率高,对于几千到几万条记录的数据集,跑一次聚类通常只需要几秒到几分钟,适合快速迭代。第二,K-Means的结果解释性相对直观,每个簇可以用中心点来代表,医生更容易理解“这个簇的孩子平均体温偏高、心率偏快”这样的描述。第三,K-Means对球形簇的假设在生命体征数据上通常不会太离谱,因为经过标准化后,各维度尺度一致,簇的形状往往比较接近球形。

当然,K-Means也有明显的局限。它需要预先指定簇的数量K,对初始中心点敏感,而且对异常值不够鲁棒。所以在实际项目中,我通常会配合肘部法则和轮廓系数来确定K,并用多次随机初始化来降低局部最优的风险。

3.2 确定最佳簇数:肘部法则与轮廓系数双验证

确定K值是K-Means最关键的步骤之一。K太小,簇内差异大,画像不够精细;K太大,簇太多,临床解释困难。我的做法是同时看两个指标:

肘部法则。计算不同K值下的簇内平方和(WCSS),然后画出WCSS随K变化的曲线。通常曲线会在某个点出现明显的“拐弯”,那个点就是比较合适的K。

轮廓系数。它衡量每个样本与自身簇内其他样本的相似度,以及与最近簇样本的差异度。轮廓系数越接近1,聚类效果越好。

from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score import matplotlib.pyplot as plt wcss = [] sil_scores = [] K_range = range(2, 11) for k in K_range: kmeans = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42) labels = kmeans.fit_predict(X_scaled) wcss.append(kmeans.inertia_) sil_scores.append(silhouette_score(X_scaled, labels)) # 绘制肘部法则图 plt.figure(figsize=(12, 5)) plt.subplot(1, 2, 1) plt.plot(K_range, wcss, 'bo-') plt.xlabel('K值') plt.ylabel('簇内平方和') plt.title('肘部法则') plt.subplot(1, 2, 2) plt.plot(K_range, sil_scores, 'ro-') plt.xlabel('K值') plt.ylabel('轮廓系数') plt.title('轮廓系数') plt.tight_layout() plt.show()

在实际运行中,我通常会看到K=3或K=4时轮廓系数出现峰值,而肘部法则的拐点也大致在这个范围。结合临床可解释性,最终选择K=4的情况比较多——四个簇分别对应“相对稳定型”“呼吸受累型”“循环不稳定型”和“炎症反应强烈型”。

3.3 聚类结果解读:每个簇到底代表什么

聚类跑完之后,最重要的一步是解读每个簇的临床含义。我一般会计算每个簇在各特征上的均值,然后跟整体均值做对比,找出每个簇的“特征标签”。

簇编号体温心率呼吸频率收缩压血氧白细胞CRP临床画像
0正常正常正常正常正常正常正常相对稳定型
1偏高偏快明显偏快正常偏低偏高偏高呼吸受累型
2偏低偏快偏快偏低偏低偏高明显偏高循环不稳定型
3波动大明显偏快偏快偏低偏低明显偏高明显偏高炎症反应强烈型

这个表格是我根据常见数据模式整理的,实际项目中每个簇的特征需要根据数据具体计算。解读的时候,一定要结合临床知识,不能只看数字。比如,心率偏快在新生儿中可能是疼痛、发热、血容量不足等多种原因导致的,不能简单归因于败血症本身。

注意事项:聚类结果的可解释性依赖于特征的选择和标准化方式。如果发现某个簇很难用临床逻辑解释,可能需要回头检查特征工程是否有问题,或者考虑增加/减少特征。

4. ICU时长影响因素研究:从聚类标签到回归分析

4.1 把聚类标签作为新特征

聚类分析本身只是第一步。更有价值的是,把聚类得到的簇标签作为一个新的类别特征,加入到后续的回归模型中,看看不同“画像”的患者在ICU住院时长上是否有显著差异。

这个思路的逻辑是:聚类捕捉了生命体征之间的复杂交互模式,这些模式可能比单个指标更能反映病情的整体严重程度。比如,一个患者可能体温只是轻微偏高,心率也只是轻微偏快,但两者同时出现且伴随血氧下降,这种组合模式可能比单独看任何一个指标都更有预测力。

# 将聚类标签合并回原始特征数据 features_df['cluster'] = labels # 查看不同簇的ICU住院时长分布 los_by_cluster = features_df.groupby('cluster')['icu_los'].describe() print(los_by_cluster)

从常见的数据模式来看,相对稳定型的患者ICU时长通常最短,中位数可能在5-7天;呼吸受累型可能稍长,7-10天;循环不稳定型和炎症反应强烈型往往最长,可能超过10天甚至更久。当然,这只是一个大致趋势,具体数值取决于数据集和临床实践。

4.2 回归模型的选择与比较

研究影响因素,回归分析是常用工具。这个项目里,目标变量是ICU住院时长(连续数值),所以可以用线性回归、岭回归、Lasso回归、随机森林回归、梯度提升回归等。我一般会先跑一个线性回归作为基线,然后尝试树模型看看是否有非线性关系。

from sklearn.linear_model import LinearRegression, Ridge, Lasso from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor from sklearn.model_selection import cross_val_score from sklearn.metrics import mean_absolute_error, r2_score # 准备特征和目标变量 X = features_df[feature_cols + ['cluster']] y = features_df['icu_los'] # 对类别特征cluster做独热编码 X = pd.get_dummies(X, columns=['cluster'], drop_first=True) # 定义模型列表 models = { '线性回归': LinearRegression(), '岭回归': Ridge(alpha=1.0), 'Lasso回归': Lasso(alpha=0.1), '随机森林': RandomForestRegressor(n_estimators=100, random_state=42), '梯度提升': GradientBoostingRegressor(n_estimators=100, random_state=42) } # 交叉验证比较 for name, model in models.items(): scores = cross_val_score(model, X, y, cv=5, scoring='r2') print(f"{name}: R2均值={scores.mean():.3f}, 标准差={scores.std():.3f}")

根据我的经验,树模型在这类数据上通常表现更好,因为生命体征与住院时长之间的关系往往是非线性的。比如,体温与住院时长的关系可能是U型的——体温过低和过高都可能导致住院时间延长,而正常体温对应最短时长。线性回归很难捕捉这种模式。

4.3 特征重要性分析:哪些因素真正影响ICU时长

如果树模型表现更好,可以用特征重要性来识别关键影响因素。随机森林和梯度提升都可以输出特征重要性,通常以“不纯度减少”或“排列重要性”来衡量。

# 以随机森林为例 rf = RandomForestRegressor(n_estimators=100, random_state=42) rf.fit(X, y) # 特征重要性 importance_df = pd.DataFrame({ 'feature': X.columns, 'importance': rf.feature_importances_ }).sort_values('importance', ascending=False) print(importance_df.head(15))

从常见结果来看,出生体重、胎龄、CRP峰值、血小板最低值、血氧最低值等往往是排名靠前的特征。聚类标签的重要性通常也能排进前几位,说明生命体征的组合模式确实携带了独立于单个指标的信息。

实操心得:特征重要性只能说明“这个特征对预测有用”,不能直接推断因果关系。比如,出生体重低与ICU时长长相关,可能是因为低体重儿本身发育不成熟,而不是低体重“导致”了住院时间长。解读时要保持谨慎。

5. 常见问题与排查技巧实录

5.1 聚类结果不稳定怎么办

K-Means对初始中心点敏感,不同随机种子可能得到不同的聚类结果。如果你发现每次跑出来的簇标签都不一样,可以尝试以下方法:

  • 增加n_init参数的值,让算法用更多组初始中心点,选择最优的那一组。scikit-learn默认是10,可以调到20或30。
  • 使用k-means++初始化方法,它比随机初始化更稳定。
  • 如果数据量不大,可以考虑用层次聚类先确定大致的簇结构,再用K-Means细化。

5.2 某个簇的样本量特别少怎么办

有时候聚类结果会出现一个簇只有几个样本的情况。这通常意味着这个簇可能是异常值组成的,或者K值选得太大。我的处理方式是:先检查这些样本的原始数据,看看是不是记录错误;如果数据没问题,可以考虑把这个簇合并到最近的簇,或者重新选择K值。

5.3 回归模型R²很低怎么排查

如果回归模型的R²只有0.2-0.3,不要急着换模型。先检查以下几点:

  • 特征是否足够?ICU时长受很多因素影响,包括感染类型、抗生素使用、并发症等,如果特征里只有生命体征,R²低是正常的。
  • 目标变量是否需要变换?ICU时长通常呈右偏分布,取对数后可能更接近正态,回归效果会更好。
  • 是否存在非线性关系?可以尝试加入特征的平方项或交互项,或者直接用树模型。

5.4 临床解读时容易犯的错误

最后说几个我在跟临床医生交流时发现的常见误解:

  • 把聚类簇当成诊断分型。聚类只是数据驱动的分组,不等于临床诊断。一个患者可能同时符合多个簇的特征。
  • 忽略时间维度。生命体征是动态变化的,用均值做聚类会丢失很多信息。如果数据允许,可以考虑用时间序列聚类方法。
  • 过度解读特征重要性。特征重要性高不代表这个因素就是“原因”,可能只是相关。

6. 项目扩展与个人经验分享

这个项目做完之后,我觉得还有几个方向可以继续挖。一是把时间序列信息利用起来,比如用LSTM或Transformer对生命体征的时间序列做建模,可能会比简单的统计特征效果更好。二是引入更多临床变量,比如抗生素使用天数、机械通气时长、并发症情况等,这些对ICU时长的预测价值可能更高。三是做外部验证,用不同医院或不同时间段的数据来检验模型的泛化能力。

我个人在实际操作中的体会是,医疗数据分析项目最难的不是算法,而是对数据的理解和临床知识的积累。同样的特征,在不同临床背景下可能有完全不同的含义。比如,新生儿心率偏快可能是感染,也可能是疼痛或发热,如果不了解这些,很容易得出错误的结论。所以,做这类项目一定要多跟临床人员沟通,不要自己闷头调参。

另外,代码的可复现性很重要。我习惯把数据预处理、特征工程、建模、评估的每一步都写成函数或类,这样换一个数据集时只需要改数据加载部分,其他逻辑可以复用。这个项目里,我把特征提取和标准化封装成了一个Pipeline,用起来很方便。

from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans pipeline = Pipeline([ ('scaler', StandardScaler()), ('kmeans', KMeans(n_clusters=4, init='k-means++', n_init=20, random_state=42)) ]) pipeline.fit(features_df[feature_cols]) features_df['cluster'] = pipeline.predict(features_df[feature_cols])

最后再分享一个小技巧:在做聚类之前,可以先对特征做相关性分析,把高度相关的特征合并或剔除。比如收缩压和舒张压通常高度相关,可以只保留平均动脉压。这样既能减少维度,又能避免某些特征被重复计算权重。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询