心跳信号分类预测:从386分到33名,这套代码到底做对了什么

发布时间:2026/10/14 11:32:45
心跳信号分类预测:从386分到33名,这套代码到底做对了什么
简介这份资源面向数据挖掘零基础学习者与天池竞赛入门选手围绕心跳信号分类预测赛题提供一套可复现的实战代码方案帮助读者从零理解数据预处理、特征工程到模型训练与提交的完整流程。压缩包共2个文件均为Python脚本整体约4KB分别承担训练测试流程与结果提交生成两类任务便于对照学习建模思路与竞赛提交规范。资源标题中标注的386分、33名成绩说明该方案在真实竞赛中具备一定上分参考价值适合想快速提分、了解竞赛节奏的读者借鉴。目前已有2642人学习下载热度较高。通过阅读代码读者可掌握数据清洗、标准化、特征提取、模型选择与评估等关键环节并理解如何将训练好的模型应用于新数据、按平台要求输出预测结果从而形成一套可迁移到其他赛题的入门方法论。1. 心跳信号分类预测从 386 分到 33 名这套代码到底做对了什么第一次拿到心跳信号分类这个赛题很多人会以为它是个普通的二分类任务给一段信号判断它是不是异常心跳。但真正跑过一遍就会发现难点根本不在模型选型上而在信号本身——单条样本只有 205 个采样点信噪比低正负样本比例悬殊而且不同类别的波形差异非常细微。我见过不少新手直接套一个随机森林或者 XGBoostAUC 卡在 0.6 左右就上不去了然后开始怀疑人生。这套代码之所以能拿到 386 分、33 名的成绩核心不在于用了多复杂的深度网络而在于它把特征工程和模型融合这两件事做扎实了。它适合两类人一类是刚接触数据挖掘、想找一个完整项目跑通全流程的新手另一类是做信号处理相关任务、想看看别人怎么把原始波形转成可用特征的从业者。接下来我会把这套方案拆开从数据理解、特征构造、模型训练到融合提分一步步讲清楚每一步为什么这么做、参数怎么调、哪里容易翻车。2. 数据理解与特征工程205 个采样点里能榨出什么2.1 先搞清楚数据长什么样再动手心跳信号分类的数据通常以 CSV 格式给出每一行是一条样本包含 id、heartbeat_signals 和 label 三列。heartbeat_signals 是一个用逗号分隔的字符串长度固定为 205代表一段心跳信号的幅值序列。label 是类别标签一般有 4 个类别其中 0 表示正常心跳1、2、3 表示不同类型的异常。拿到数据后第一件事不是急着建模而是先做探索性分析。我一般会先看类别分布因为这类医学信号数据往往存在严重的类别不平衡。如果正常样本占了 90% 以上那准确率这个指标就完全不可信了必须用 AUC 或者 F1 来评估。import pandas as pd import numpy as np import matplotlib.pyplot as plt train pd.read_csv(train.csv) test pd.read_csv(testA.csv) # 查看类别分布 print(train[label].value_counts()) print(train[label].value_counts(normalizeTrue)) # 把信号字符串转成数值数组 def parse_signals(df): signals df[heartbeat_signals].str.split(,, expandTrue).astype(float) return signals.values train_signals parse_signals(train) test_signals parse_signals(test) print(train_signals.shape, test_signals.shape)这段代码做了两件事一是确认类别比例二是把字符串格式的信号转成 NumPy 数组。注意str.split之后要加expandTrue否则返回的是一列列表对象后面没法直接转 float。转完之后建议检查一下有没有 NaN虽然这类赛题数据一般比较干净但万一有缺失值后面所有特征都会出问题。2.2 统计特征均值、方差、偏度、峰度一个都不能少原始信号是 205 维的时序数据直接丢给树模型效果不会太好因为树模型对连续值的切分是逐维进行的205 维里很多维度其实是冗余的。更有效的做法是先提取统计特征把每条信号压缩成几十维的向量。我常用的统计特征包括均值、标准差、最大值、最小值、中位数、四分位数、偏度、峰度、过零率、均方根。这些特征分别刻画了信号的中心趋势、离散程度、分布形状和波动特性。对于心跳信号来说偏度和峰度尤其重要因为异常心跳的波形往往在形状上和正常心跳有差异。from scipy import stats def extract_stat_features(signals): feats [] for sig in signals: row [] row.append(np.mean(sig)) row.append(np.std(sig)) row.append(np.max(sig)) row.append(np.min(sig)) row.append(np.median(sig)) row.append(np.percentile(sig, 25)) row.append(np.percentile(sig, 75)) row.append(stats.skew(sig)) row.append(stats.kurtosis(sig)) row.append(np.sqrt(np.mean(sig**2))) # 过零率 zero_crossings np.where(np.diff(np.sign(sig)))[0] row.append(len(zero_crossings)) feats.append(row) return np.array(feats) stat_train extract_stat_features(train_signals) stat_test extract_stat_features(test_signals) print(stat_train.shape)这里有几个参数需要注意。np.percentile的第二个参数是百分位25 和 75 分别对应下四分位和上四分位。stats.skew和stats.kurtosis默认计算的是 Fisher 偏度和超额峰度如果你希望得到 Pearson 峰度需要加参数fisherFalse。过零率计算的是信号穿过零点的次数对于心跳信号来说这个值可以反映信号的振荡频率。2.3 频域特征FFT 之后取前 N 个幅值时域统计特征只能刻画信号的整体分布无法捕捉周期性和频率成分。心跳信号本质上是准周期信号频域特征往往能提供额外的区分信息。最常见的做法是对信号做快速傅里叶变换然后取幅值谱的前 N 个分量作为特征。def extract_fft_features(signals, n_components20): feats [] for sig in signals: fft_vals np.abs(np.fft.fft(sig)) # 只取前一半因为对称 half len(fft_vals) // 2 feats.append(fft_vals[:n_components]) return np.array(feats) fft_train extract_fft_features(train_signals, n_components20) fft_test extract_fft_features(test_signals, n_components20)n_components这个参数控制取多少个频率分量。取太少会丢失信息取太多会引入噪声。我一般会先取 20 到 30 个然后看特征重要性再调整。注意 FFT 的结果是对称的所以只需要取前一半。另外如果信号长度不是 2 的幂次np.fft.fft会自动补零但补零会改变频率分辨率这一点在做对比实验时要注意。2.4 特征拼接与标准化别让量纲毁了模型把统计特征和频域特征拼在一起之后维度大概在 30 到 50 之间。这时候不同特征的量纲差异很大比如均值的范围可能在 -1 到 1 之间而过零率的范围可能是 0 到 100。对于树模型来说量纲影响不大但对于逻辑回归、SVM 或者神经网络来说不标准化会直接导致模型不收敛。from sklearn.preprocessing import StandardScaler X_train np.hstack([stat_train, fft_train]) X_test np.hstack([stat_test, fft_test]) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)注意fit_transform只在训练集上调用测试集必须用训练集的均值和方差来transform。这个细节很多人会搞错如果在测试集上也fit_transform就相当于引入了未来信息线下验证分数会虚高线上提交直接翻车。3. 模型训练与调参为什么 LightGBM 是这套方案的主力3.1 选 LightGBM 而不是 XGBoost 的理由在这类结构化特征任务上梯度提升树系列一直是首选。XGBoost 和 LightGBM 都能用但我更倾向于 LightGBM原因有三个第一LightGBM 基于直方图的决策树算法训练速度比 XGBoost 快很多尤其是在特征维度几十到几百的时候第二LightGBM 原生支持类别特征虽然这个赛题里 label 是数值但如果你在特征工程里构造了类别型特征LightGBM 可以直接处理第三LightGBM 的num_leaves参数比 XGBoost 的max_depth更直观调参时更容易控制模型复杂度。import lightgbm as lgb from sklearn.model_selection import StratifiedKFold from sklearn.metrics import roc_auc_score params { objective: multiclass, num_class: 4, metric: multi_logloss, boosting_type: gbdt, learning_rate: 0.05, num_leaves: 31, max_depth: -1, min_child_samples: 20, subsample: 0.8, colsample_bytree: 0.8, reg_alpha: 0.1, reg_lambda: 0.1, random_state: 42, n_jobs: -1 }这里objective设为multiclassnum_class设为 4因为 label 有 4 个类别。metric用multi_logloss因为这是一个多分类问题。learning_rate设为 0.05 是一个比较保守的值配合较多的迭代次数可以获得更好的效果。num_leaves设为 31 是 LightGBM 的默认值如果过拟合可以降到 15 或 7。subsample和colsample_bytree都设为 0.8用来增加模型的随机性防止过拟合。3.2 五折交叉验证不只是为了评估交叉验证在这套方案里不只是用来评估模型性能更重要的是用来生成袋外预测。具体做法是把训练集分成 5 折每次用 4 折训练、1 折验证同时用训练好的模型对测试集做预测。5 折跑完之后测试集上会得到 5 组预测概率取平均作为最终的测试集预测结果。skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) oof_preds np.zeros((X_train_scaled.shape[0], 4)) test_preds np.zeros((X_test_scaled.shape[0], 4)) for fold, (trn_idx, val_idx) in enumerate(skf.split(X_train_scaled, y_train)): X_trn, X_val X_train_scaled[trn_idx], X_train_scaled[val_idx] y_trn, y_val y_train[trn_idx], y_train[val_idx] dtrain lgb.Dataset(X_trn, labely_trn) dval lgb.Dataset(X_val, labely_val, referencedtrain) model lgb.train( params, dtrain, num_boost_round1000, valid_sets[dval], callbacks[lgb.early_stopping(50), lgb.log_evaluation(100)] ) oof_preds[val_idx] model.predict(X_val, num_iterationmodel.best_iteration) test_preds model.predict(X_test_scaled, num_iterationmodel.best_iteration) / 5early_stopping(50)表示如果验证集上的 logloss 在 50 轮内没有下降就停止训练。num_iterationmodel.best_iteration确保用的是验证集上最好的那一轮模型而不是最后一轮。test_preds累加之后除以 5得到的是 5 折模型的平均预测这种集成方式通常比单模型稳定得多。3.3 多分类 AUC 的计算方式多分类问题的 AUC 计算和二分类不太一样。sklearn 的roc_auc_score默认支持multi_class参数可以设为ovr或ovo。ovr是 One-vs-Rest把每个类别分别当作正类其余当作负类计算 AUC 后取平均。ovo是 One-vs-One两两类别之间计算 AUC 再平均。我一般用ovr因为它对类别不平衡更鲁棒。from sklearn.metrics import roc_auc_score auc roc_auc_score(y_train, oof_preds, multi_classovr, averagemacro) print(fOOF AUC: {auc:.4f})averagemacro表示对每个类别的 AUC 取算术平均不考虑类别样本量。如果你的类别非常不平衡也可以考虑averageweighted按样本量加权。4. 避坑与排查这套方案里最容易翻车的五个地方4.1 信号解析时 dtype 不对导致特征全为 NaN现象跑完特征提取代码后发现特征矩阵里全是 NaN模型训练直接报错。原因pd.read_csv读取 heartbeat_signals 列时默认是 object 类型str.split之后得到的仍然是字符串。如果直接.astype(float)而不加expandTrue返回的是一列列表无法转成数值。解决确保str.split(,, expandTrue)之后再.astype(float)并且用print(df.head())确认转换后的数据确实是数值。4.2 交叉验证时数据泄露导致线下分数虚高现象线下交叉验证 AUC 达到 0.95线上提交只有 0.65。原因在交叉验证之前就对全量数据做了标准化或者特征选择导致验证集的信息泄露到了训练过程中。解决标准化和特征选择必须放在交叉验证的循环内部每次只用训练折的数据来 fit验证折和测试集只做 transform。4.3 类别不平衡导致模型偏向多数类现象模型在多数类上表现很好但在少数类上几乎全部预测错误。原因没有处理类别不平衡模型为了降低整体损失倾向于把所有样本预测为多数类。解决在 LightGBM 中设置is_unbalanceTrue或者scale_pos_weight也可以在训练时对少数类样本进行过采样。另外评估指标要用 AUC 而不是准确率。4.4 早停轮数设得太大导致过拟合现象验证集 logloss 先下降后上升但模型仍然继续训练。原因early_stopping的 rounds 参数设得太大比如设了 200模型在验证集恶化之后还继续跑了 200 轮才停。解决把early_stopping的 rounds 设在 30 到 50 之间同时配合较小的 learning_rate让模型有足够的时间收敛但不会过拟合。4.5 测试集预测时用了错误的迭代轮数现象测试集预测结果和验证集表现差距很大。原因model.predict时没有指定num_iteration默认用的是最后一轮模型而不是验证集上最好的那一轮。解决始终用num_iterationmodel.best_iteration来做预测确保用的是早停时保存的最优模型。5. 进阶技巧用概率校准和模型融合再提两个点5.1 概率校准让预测结果更可靠LightGBM 输出的概率并不一定是校准好的尤其是在类别不平衡的情况下。概率校准的思路是找一个映射函数把模型输出的原始概率映射到真实概率。常用的方法有 Platt Scaling 和 Isotonic Regression。Platt Scaling 假设映射是一个 Sigmoid 函数适合样本量较小的情况Isotonic Regression 是非参数方法适合样本量较大的情况。from sklearn.calibration import CalibratedClassifierCV from sklearn.model_selection import train_test_split # 用校准集来拟合校准器 X_trn, X_cal, y_trn, y_cal train_test_split( X_train_scaled, y_train, test_size0.2, stratifyy_train, random_state42 ) base_model lgb.LGBMClassifier(**{k: v for k, v in params.items() if k ! objective}, objectivemulticlass, num_class4) calibrated CalibratedClassifierCV(base_model, methodisotonic, cvprefit) calibrated.fit(X_cal, y_cal) cal_preds calibrated.predict_proba(X_test_scaled)注意cvprefit表示 base_model 已经训练好了校准器直接在校准集上拟合。如果cv设为整数CalibratedClassifierCV会自己做交叉验证但那样就不能用早停后的最优模型了。5.2 多模型融合LightGBM XGBoost CatBoost单模型的提升空间有限融合多个模型往往能再涨一两个点。我一般会选三个梯度提升树模型LightGBM、XGBoost 和 CatBoost。这三个模型的算法细节不同预测结果有一定的互补性。融合方式有加权平均和 Stacking 两种加权平均更简单Stacking 效果通常更好但容易过拟合。from xgboost import XGBClassifier from catboost import CatBoostClassifier # XGBoost xgb_model XGBClassifier( n_estimators1000, learning_rate0.05, max_depth6, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda0.1, objectivemulti:softprob, num_class4, eval_metricmlogloss, early_stopping_rounds50, random_state42, n_jobs-1 ) # CatBoost cat_model CatBoostClassifier( iterations1000, learning_rate0.05, depth6, l2_leaf_reg3, loss_functionMultiClass, eval_metricMultiClass, random_seed42, verbose100, early_stopping_rounds50 ) # 加权平均融合 final_preds 0.4 * lgb_preds 0.3 * xgb_preds 0.3 * cat_preds权重怎么定我一般先用等权平均跑一版然后根据每个模型在验证集上的 AUC 来调整权重。AUC 高的模型给更大的权重但差距不要拉得太大否则融合就退化成单模型了。如果追求更精细的融合可以用逻辑回归做 Stacking把三个模型的输出概率作为特征再训练一个元学习器。5.3 一个容易被忽略的提分点后处理多分类问题有时候可以通过后处理来提分。比如如果某些类别的预测概率非常接近可以考虑用阈值调整来改变预测结果。另外如果赛题允许提交概率那就不需要做 argmax直接提交概率往往能获得更好的 AUC。# 如果提交概率直接用 test_preds submission pd.DataFrame(test_preds, columns[label_0, label_1, label_2, label_3]) submission.insert(0, id, test[id]) submission.to_csv(submission.csv, indexFalse)如果提交的是类别标签那就用np.argmax(test_preds, axis1)。但要注意argmax 会丢失概率信息如果评估指标是 AUC提交概率通常比提交类别标签分数更高。这套方案我从头跑过不止一遍最大的体会是特征工程决定了上限模型融合决定了下限。很多人一上来就调模型参数调了半天涨了 0.5 个点但回头把特征重新构造一下可能直接涨 5 个点。另外交叉验证的代码一定要写对数据泄露是新手最容易踩的坑线下分数虚高会给你虚假的信心线上提交直接打回原形。希望帮到你。本文还有配套的精品资源点击获取