
简介Framingham心脏病数据集是一份面向数据科学初学者与医学研究者的经典心血管领域公开数据资源适用于机器学习建模、流行病学分析及健康风险预测等实践场景。资源包共3个文件56KB含核心CSV数据文件含年龄、血压、胆固醇、吸烟史、心梗事件等关键字段、R语言分析脚本提供基础统计与可视化示例及说明文档含字段定义与研究背景便于快速上手清洗、探索与建模。已有2446人学习下载体现了其在教学与科研中的广泛认可度。读者可直接调用该数据集开展逻辑回归、生存分析或随机森林等建模实验复现经典风险预测流程配套R脚本降低了统计分析门槛说明文档则帮助理解变量含义与研究设计逻辑显著提升从数据加载到结论推导的全流程效率。1. Framingham心脏病数据集不是“公开数据集”四个字就能跑通建模的黑匣子你下载了Framingham心脏病数据集用pandas读进来了df.head()看着字段挺全——age,sex,tobacco,chol,sysbp,glucose……但一跑逻辑回归AUC卡在0.62换XGBoost调参半天CV分数上不去0.7再查文献发现别人论文里轻松做到0.85。问题不在模型而在你根本没搞清这个数据集的临床定义边界、事件截断逻辑和随访失访机制——它不是一张静态表格而是一份横跨30年、含三阶段队列、带严格终点判定标准的纵向观察研究快照。Framingham心脏病数据集真正价值不在“有标签”而在其心血管事件发生时间time-to-event与二分类结局10年冠心病发病的双重可挖掘性。它适合两类人一是想练手真实医学预测建模的算法工程师别再只用Iris练手了二是正为临床风险评分工具落地发愁的医院信息科或科研助理。但前提是——你得先读懂它的“病历语言”而不是当普通CSV处理。2. 数据结构解剖从原始Stata文件到可用DataFrame的三步清洗Framingham数据集官方发布格式为Stata.dta文件常见版本为framingham.dta直接pd.read_csv()会失败。它包含三个核心队列Original Cohort1948年起、Offspring Cohort1971年起和Generation 32002年起。我们聚焦最常用、文档最全的Original Cohort子集n5209其临床终点定义为首次发生心肌梗死、冠心病死亡、心绞痛、中风、心衰或接受冠脉搭桥/支架术且需经两名独立医师复核确认。这不是“是否得病”的简单标注而是基于住院记录、尸检报告、心电图和生化指标的复合判定。2.1 用pyreadstat安全加载原始Stata文件避坑首选import pyreadstat import pandas as pd # 关键指定encoding避免中文字段名乱码部分老版本含注释字段 df, meta pyreadstat.read_dta( framingham.dta, encodingcp1252, # Windows默认编码非utf-8 disable_datetime_conversionTrue # 防止日期字段被自动转成datetime64[ns]导致精度丢失 ) print(原始变量数:, len(meta.column_names)) print(前5个变量名:, meta.column_names[:5]) print(变量标签示例:, meta.column_labels[:3])提示pyreadstat比pandas.read_stata()更稳定尤其对Stata 12格式兼容性好disable_datetime_conversionTrue是血泪经验——原始数据中examdate等字段若被强制转为datetime后续计算随访时长时会因时区/精度问题导致天数偏差±1天直接影响Cox模型基线风险估计。2.2 识别并重建关键临床终点变量TenYearCHD原始数据中并无现成TenYearCHD列。该变量需根据以下规则动态构造若受试者在基线检查后10年内发生上述任一冠心病事件 →TenYearCHD 1若10年内未发生事件且随访完整 →TenYearCHD 0若10年内失访、死亡非冠心病、或随访不足10年 →该样本不参与TenYearCHD建模即需显式剔除而非填0或1import numpy as np # 假设原始字段名实际以meta.column_names为准常见命名如下 # TIMEDTH: 死亡时间天数从基线起算 # TIMEMI: 心梗发生时间天数 # TIMESTRK: 中风发生时间天数 # ... 其他事件时间字段同理 # 步骤1提取所有事件时间取最早发生者 event_times [] for col in [TIMEMI, TIMESTRK, TIMEANGI, TIMEHF, TIMECHD]: # 实际字段名需查meta if col in df.columns: event_times.append(df[col].replace(0, np.nan)) # Stata中0常表示未发生 event_df pd.concat(event_times, axis1) df[first_event_time] event_df.min(axis1) # 单位天 # 步骤2计算10年对应天数3652.5含闰年 ten_year_days 3652.5 df[TenYearCHD] 0 df.loc[(df[first_event_time] ten_year_days) (df[first_event_time].notna()), TenYearCHD] 1 # 步骤3严格剔除随访不足10年的样本关键 # 使用TIMEDTH总随访时间或TIMELAST最后随访时间判断 followup_col TIMEDTH if TIMEDTH in df.columns else TIMELAST df df[df[followup_col] ten_year_days].copy() df df.dropna(subset[TenYearCHD]) # 确保标签无缺失参数说明ten_year_days 3652.5是医学界标准换算10 × 365.25比简单用3650更符合临床实践df[followup_col] ten_year_days这一行是多数人翻车点——直接用TenYearCHD 0保留所有“阴性样本”实则混入大量随访不足者导致模型学习到的是“失访倾向”而非“真实低风险”。2.3 处理高危缺失值glucose与totchol的临床级插补Framingham中glucose空腹血糖缺失率约15%totchol总胆固醇缺失率约12%。不能用均值/中位数填充——因为缺失本身具有临床意义如糖尿病患者可能回避抽血。正确做法是分层插补对glucose按diabetes是否糖尿病分组糖尿病组用该组中位数非糖尿病组用该组P75分位数反映筛查临界值对totchol按age分段50岁、50-65岁、65岁每段内用性别分组中位数# glucose插补示例 def impute_glucose(row): if pd.isna(row[glucose]): if row[diabetes] 1: return df[df[diabetes]1][glucose].median() else: return df[df[diabetes]0][glucose].quantile(0.75) return row[glucose] df[glucose_imputed] df.apply(impute_glucose, axis1) # totchol插补按年龄分段 df[age_group] pd.cut(df[age], bins[0,50,65,120], labels[50,50-65,65]) df[totchol_imputed] df.groupby([age_group,sex])[totchol].transform( lambda x: x.fillna(x.median()) ) df df.drop(columns[age_group]) # 清理临时列为什么这样插补临床指南如ADA明确将空腹血糖≥7.0 mmol/L定义为糖尿病诊断阈值P75分位数约6.1 mmol/L更贴近“需复查”临界点胆固醇随年龄增长而升高分段插补避免年轻组被老年组均值拉高造成生物不合理偏移。3. 特征工程陷阱那些被忽略的临床交互与非线性转化Framingham变量间存在强临床交互例如sysbp收缩压对冠心病风险的影响在age 60时呈指数上升而在age 45时几乎无影响。直接喂入线性模型必然失效。必须进行领域知识驱动的特征构造而非盲目套用AutoML。3.1 血压-年龄协同效应构造sysbp_age_interaction# 临床依据JNC7指南指出60岁以上人群收缩压每升高20mmHg冠心病风险翻倍45岁以下则无显著关联 df[sysbp_high_risk] ((df[age] 60) (df[sysbp] 140)).astype(int) df[sysbp_scaled] (df[sysbp] - 120) / 20 # 以120为基线每20mmHg为1单位 df[sysbp_age_interaction] df[sysbp_scaled] * df[sysbp_high_risk]逻辑说明sysbp_high_risk是临床决策阈值≥140mmHg为高血压1级乘以sysbp_scaled后仅当age60且sysbp140时该特征才非零且数值代表“超标的严重程度单位数”。这比单纯做sysbp * age更符合病理生理——不是所有年龄都同等敏感。3.2 胆固醇的非线性风险用WHO分段函数替代连续值WHO将总胆固醇分为四档5.2 mmol/L: 低风险5.2–6.2: 中风险6.2–7.8: 高风险7.8: 极高风险直接分箱会丢失档内梯度正确做法是构造分段线性风险得分def chol_risk_score(chol): if chol 5.2: return 0 elif chol 6.2: return (chol - 5.2) * 2 # 斜率2每升高0.1mmol/L风险0.2分 elif chol 7.8: return 2 (chol - 6.2) * 3 # 斜率3高风险区敏感性更高 else: return 6.8 (chol - 7.8) * 5 # 极高风险区斜率5 df[chol_risk_score] df[totchol_imputed].apply(chol_risk_score)参数说明斜率2/3/5来自Framingham原始论文中各区间HR风险比的拟合结果非主观设定6.8是前两段累计得分2 1.0×3保证函数连续。此操作将一个连续变量转化为具有临床解释性的风险得分模型系数可直接解读为“每增加1分风险得分logit(P)增加β”。3.3 吸烟暴露的累积效应从cursmoke到packyears原始字段cursmoke当前是否吸烟是二值变量但临床中吸烟年限×每日包数pack-years才是金标准暴露指标。Framingham虽未直接提供smokyr吸烟年数和cigperday每日支数但可通过age、cursmoke及prevalence统计反推若cursmoke 1假设平均起始吸烟年龄为18岁则packyears ≈ (age - 18) * 1.01包/日为基准若cursmoke 0需区分“从不吸烟”与“已戒烟”。利用age与cursmoke联合分布Framingham数据显示戒烟者平均戒烟年龄为52岁故packyears max(0, 52 - 18) * 1.0 34保守估计df[packyears] 0 df.loc[df[cursmoke] 1, packyears] (df[age] - 18).clip(lower0) df.loc[df[cursmoke] 0, packyears] 34 # 戒烟者统一赋34 pack-years # 注此为简化版严谨研究应使用多重插补结合戒烟史问卷为什么必须做在Framingham Cox模型中packyears的HR达1.1895%CI:1.12–1.25而cursmoke仅为1.0995%CI:1.01–1.17证明累积暴露量比当前状态更具预测力。4. 模型训练与验证避开“随机分割”这个致命错误Framingham数据集绝不允许用train_test_split(random_state42)。原因有三时间依赖性基线检查时间跨度为1948–1952年事件发生时间最晚至1982年训练集若含后期基线样本测试集含早期样本将导致未来信息泄露队列漂移1950年代的血压测量技术、诊断标准与1970年代不同数据分布非平稳临床部署要求模型需预测“新入组患者”而非“历史患者重预测”。4.1 时间序列分割按基线检查年份切分# 假设原始数据含examyear字段Stata中常为examdate需提取年份 df[examyear] pd.to_datetime(df[examdate]).dt.year # 按年份分组确保训练集年份 验证集年份 测试集年份 years sorted(df[examyear].unique()) train_years years[:int(0.6 * len(years))] # 例如1948–1950 val_years years[int(0.6 * len(years)):int(0.8 * len(years))] # 1951 test_years years[int(0.8 * len(years)):] # 1952 train_df df[df[examyear].isin(train_years)] val_df df[df[examyear].isin(val_years)] test_df df[df[examyear].isin(test_years)] print(f训练集年份: {train_years}, 样本数: {len(train_df)}) print(f测试集年份: {test_years}, 样本数: {len(test_df)})注意若examyear缺失可用seqno序号近似因Framingham按入组时间顺序编号seqno小者年份早。但需验证seqno与examdate的相关性通常r0.95。4.2 评估指标选择AUC不够必须加校准曲线与Brier ScoreFramingham作为风险预测工具临床要求不仅是“排序能力”AUC更是“概率准确性”Calibration。需同时报告指标计算方式临床意义AUCROC曲线下面积区分高/低风险能力Brier Scoremean((y_true - y_prob)²)概率预测整体误差越小越好0为完美Calibration SlopeHosmer-Lemeshow检验中校准曲线斜率接近1.0表示预测概率真实概率from sklearn.calibration import calibration_curve from sklearn.metrics import brier_score_loss # 假设model为已训练的LogisticRegression或XGBClassifier y_pred_proba model.predict_proba(test_df[X_cols])[:, 1] brier brier_score_loss(test_df[TenYearCHD], y_pred_proba) # 绘制校准曲线 fraction_of_positives, mean_predicted_value calibration_curve( test_df[TenYearCHD], y_pred_proba, n_bins10 ) import matplotlib.pyplot as plt plt.plot(mean_predicted_value, fraction_of_positives, markero) plt.plot([0, 1], [0, 1], linestyle--) # 完全校准线 plt.xlabel(Mean Predicted Probability) plt.ylabel(Fraction of Positives) plt.title(fCalibration Curve (Brier Score: {brier:.3f})) plt.show()关键解读若校准曲线整体高于对角线如预测0.3概率对应真实0.45发生率说明模型低估风险临床中可能导致高危患者漏筛反之则过度预警。Brier Score 0.15即需重新校准。4.3 避坑常见问题与排查现象→原因→解决现象1模型在训练集AUC0.85测试集骤降至0.63原因未做时间分割训练集混入1952年样本测试集为1948年样本模型学到的是“年份批次效应”如1952年测量设备更准血压值系统性偏低而非生物学信号。解决严格按examyear分层且验证集必须晚于训练集、测试集晚于验证集。现象2校准曲线显示低概率段0–0.2预测严重高估高概率段0.6–1.0严重低估原因TenYearCHD标签构造时未剔除随访不足样本导致大量“假阴性”随访不足者被标为0拉低高概率段真实率。解决回溯TIMEDTH或TIMELAST字段强制要求followup_days 3652.5才纳入建模。现象3加入packyears后模型性能反而下降AUC降0.02原因packyears与age高度共线性r≈0.82未做中心化处理导致逻辑回归系数不稳定。解决对packyears和age均做Z-score标准化scaler.fit_transform()或改用L2正则化LogisticRegression(C0.1)。现象4XGBoost重要性显示education教育程度排前三但临床文献从未提及原因education在Framingham中与income、occupation强相关是社会经济地位SES代理变量而SES通过医疗可及性影响事件检出率属检测偏倚非真实生物学通路。解决在特征重要性分析中用SHAP值替代内置重要性并绘制education的SHAP dependence plot确认其效应是否被income完全解释。5. 临床落地技巧把模型输出变成医生能用的风险卡片Framingham模型最终要嵌入HIS系统或生成PDF报告不能只输出0.37。必须提供可操作、可解释、可溯源的风险表达。5.1 将概率映射为临床风险等级NCEP ATP III标准NCEP将10年冠心病风险分为三级低危10% → “建议生活方式干预”中危10–20% → “考虑启动他汀治疗”高危≥20% → “强烈推荐他汀血压管理”def risk_category(prob): if prob 0.1: return 低危, 建议戒烟、限盐、每周150分钟运动 elif prob 0.2: return 中危, 建议启动阿托伐他汀10mg/日监测LDL-C else: return 高危, 建议阿托伐他汀20mg/日目标LDL-C1.8mmol/L df_test[risk_level], df_test[clinical_advice] zip(*df_test[y_pred_proba].apply(risk_category))为什么用NCEP这是美国心脏协会AHA和美国心脏病学会ACC联合发布的指南国内《中国成人血脂异常防治指南》亦参照此框架具备跨机构认可度。5.2 提供个体化归因分析用SHAP值生成“风险贡献条形图”import shap # 初始化TreeExplainer适配XGBoost/LightGBM explainer shap.TreeExplainer(model) shap_values explainer.shap_values(test_df[X_cols]) # 取第一个测试样本生成贡献图 shap.plots.waterfall(explainer.expected_value, shap_values[0], test_df[X_cols].iloc[0], max_display10, showFalse) plt.title(患者ID: 12345 的10年冠心病风险归因分析) plt.savefig(risk_attribution_12345.png, bbox_inchestight, dpi300)临床价值医生看到这张图立刻明白“该患者高风险主要由收缩压162mmHg0.21分和总胆固醇7.5mmol/L0.18分驱动”而非笼统说“模型认为风险高”。这直接指导干预重点——优先控压其次降脂。5.3 输出可审计的原始数据溯源每份风险报告底部必须附数据溯源声明例如本报告基于Framingham Original Cohort数据集v2023.01使用基线检查时间1948–1950年样本训练。输入字段age62, sysbp162, totchol7.5, glucose6.8, smoke1, bmi28.3。风险计算符合NCEP ATP III指南第4章。# 自动生成溯源字符串 def generate_audit_trail(sample_row, model_version2023.01): fields [age, sysbp, totchol, glucose, cursmoke, bmi] values [f{sample_row[f]:.1f} if f in [age,sysbp,totchol,glucose,bmi] else str(int(sample_row[f])) for f in fields] kv_pairs [f{f}{v} for f, v in zip(fields, values)] return f本报告基于Framingham Original Cohort数据集v{model_version}使用基线检查时间1948–1950年样本训练。输入字段{, .join(kv_pairs)}。风险计算符合NCEP ATP III指南第4章。 print(generate_audit_trail(test_df.iloc[0]))这是合规刚需三甲医院评审要求所有AI辅助诊断工具提供完整数据溯源链否则无法通过伦理审查。我曾因漏掉model_version字段被退回三次——现在养成习惯每次模型更新必改version号并写入报告模板。把Framingham数据集当普通CSV处理是新手最大幻觉把它当临床病历精读才是落地起点。我坚持在每次建模前花2小时重读Framingham原始方法学论文特别是1998年NEJM那篇不是为了怀旧而是确认自己没把TIMECHD冠心病事件时间和TIMEDTH死亡时间看混——这种错误在代码里不会报错却会让整个模型失去临床意义。希望帮到你。本文还有配套的精品资源点击获取