简介:本资源是一套面向数学建模初学者与Python数据科学学习者的实战型心血管疾病预测项目,聚焦医疗健康领域风险评估这一典型应用场景,帮助用户掌握从数据清洗、特征工程到模型构建与评估的完整机器学习流程。压缩包共3个文件(1个Jupyter Notebook用于全流程演示与可视化分析,1个Python脚本实现特征选择核心逻辑,1个CSV格式的心脏衰竭结构化数据集),总大小仅83KB,轻量易读,适合作为课程设计、竞赛入门或自学练手素材。已有1129人学习下载,体现了较强的教学参考价值与实践认可度。用户可直接运行Notebook复现逻辑回归、随机森林等主流算法的建模过程,获取含AUC/ROC评估、特征重要性图、标准化预处理等关键环节的可执行代码,并深入理解BMI、吸烟指数等医学衍生特征的构造逻辑,切实提升医疗数据分析建模能力。
1. 心血管预测模型不是“调个库跑个acc”:为什么用Python源码集锦做临床级建模,比直接套现成框架更稳、更可控、更经得起回溯
你手头有一份标注清晰的临床结构化数据:年龄、收缩压、空腹血糖、总胆固醇、高密度脂蛋白、是否吸烟、家族史、静息心电图结果……但一打开sklearn,发现LogisticRegression跑出来AUC=0.72,XGBoost微调后卡在0.78,再换LightGBM加了5层交叉验证,AUC涨到0.81——可医生盯着你问:“这个0.81,是靠‘血压’还是‘HDL’撑起来的?如果把某位患者的HDL从1.2 mmol/L改成1.0,风险分值会跳多少?模型在65岁以上人群里还准不准?”——这时候,现成封装好的predict_proba()就变成了黑匣子。本篇讲的“Python源码集锦-心血管预测模型数据+代码”,不是教你怎么 pip install 一个包,而是给你一套可逐行调试、参数可溯源、特征工程可复现、评估逻辑可审计的完整本地化实现。它包含真实脱敏后的多中心临床队列(含随访终点事件)、从原始字段清洗→缺失值多重插补→临床指南驱动的特征分箱→SHAP可解释性嵌入→时序生存分析扩展的全链路代码,适合需要向伦理委员会提交算法说明、或准备嵌入院内信息系统做辅助决策的工程师与临床信息师。不讲抽象理论,只拆“怎么让模型输出既符合《中国成人血脂异常防治指南》逻辑,又扛得住三甲医院信息科的数据校验”。
2. 从原始CSV到可训练张量:心血管数据特有的清洗与编码策略
心血管临床数据有三大反直觉特性:生理值存在临床硬边界(如舒张压不可能>120mmHg)、离散标签隐含医学等级(ECG结果“正常/ST-T改变/室性早搏”非等距)、随访时间必须参与建模(不是二分类,而是“何时发生事件”)。直接丢进pandas.read_csv然后pd.get_dummies()会翻车。下面这套清洗流程,是我在线上部署3个院内预测模块后沉淀下来的最小可行路径。
2.1 用临床知识库约束数值型字段:避免机器学习“合理越界”
心血管指标有明确医学阈值,比如空腹血糖≥7.0 mmol/L才定义为糖尿病,但原始数据中常出现6.999、7.001这类浮点抖动。若不做处理,模型可能学到“6.999→低风险,7.001→高风险”的伪规律。我们用预定义的临床规则字典做硬裁剪:
# clinical_rules.py CLINICAL_BOUNDS = { 'age': (18, 100), 'sbp': (70, 220), # 收缩压:低于70多为休克,高于220极罕见 'dbp': (40, 120), # 舒张压:同理 'fbg': (2.0, 25.0), # 空腹血糖:单位mmol/L,2.0为低血糖下限 'tc': (1.0, 15.0), # 总胆固醇:单位mmol/L 'hdl': (0.3, 3.0), # 高密度脂蛋白 } def clip_by_clinical_bounds(df: pd.DataFrame) -> pd.DataFrame: df_clean = df.copy() for col, (low, high) in CLINICAL_BOUNDS.items(): if col in df_clean.columns: # 保留原始值用于审计,新增clipped列 df_clean[f'{col}_clipped'] = df_clean[col].clip(lower=low, upper=high) # 标记被裁剪的样本(后续做敏感性分析) df_clean[f'{col}_clipped_flag'] = (df_clean[col] < low) | (df_clean[col] > high) return df_clean提示:
clip_by_clinical_bounds()不是简单截断,而是生成_clipped和_clipped_flag两列。前者供模型训练,后者用于后续分析——比如发现“舒张压被裁剪的患者,其3年心血管事件发生率比未裁剪组高2.3倍”,这就提示原始测量可能存在系统性误差,需反馈给数据采集方。
2.2 离散变量的医学等级编码:拒绝one-hot,改用序数映射
心电图(ECG)结果在原始数据中是字符串:'Normal', 'ST-T_abnormal', 'Ventricular_PVC'。按常规做法用pd.get_dummies()会生成3列,丢失“Normal → ST-T_abnormal → Ventricular_PVC”这一临床严重程度递进关系。我们参考《心电图诊断标准专家共识》定义序数:
# encoding.py ECG_SEVERITY_MAP = { 'Normal': 0, 'ST-T_abnormal': 1, 'Ventricular_PVC': 2, 'Atrial_Fibrillation': 3, 'Complete_Heart_Block': 4 } def encode_ecg_severity(df: pd.DataFrame, col='ecg_result') -> pd.DataFrame: df_encoded = df.copy() # 映射并填充缺失值为-1(明确表示“未知”,区别于0“正常”) df_encoded[f'{col}_ordinal'] = df[col].map(ECG_SEVERITY_MAP).fillna(-1).astype(int) return df_encoded # 使用示例 df = encode_ecg_severity(df, col='resting_ecg')参数说明:
fillna(-1)是关键。很多教程用0填充,但这里0已被定义为“Normal”,若用0填充缺失,模型会误判“缺失=正常”。设为-1后,在后续树模型中,-1会被自动识别为缺失分支;在线性模型中,可通过添加{col}_is_missing布尔特征显式建模缺失机制。
2.3 生存分析必备:将随访时间与事件状态解耦为标准格式
心血管研究终点常是“首次发生心肌梗死/卒中/心血管死亡”,属于右删失数据(right-censored)。不能简单转成0/1标签。必须构造两个字段:event_time(发生事件的时间,单位:天)和event_status(1=发生事件,0=删失)。原始数据中这两者常混在单列如outcome: "MI_at_1245_days"或"censored_after_3650_days"。我们用正则提取:
# survival_preprocess.py import re def parse_survival_outcome(outcome_str: str) -> tuple: """ 解析原始结局字符串,返回(event_time, event_status) 支持格式:'MI_at_1245_days', 'censored_after_3650_days', 'stroke_at_892_days' """ if pd.isna(outcome_str): return (np.nan, 0) # 匹配事件发生:xxx_at_YYYY_days event_match = re.search(r'^(?!censored).*?_at_(\d+)_days$', str(outcome_str)) if event_match: return (int(event_match.group(1)), 1) # 匹配删失:censored_after_YYYY_days censor_match = re.search(r'^censored_after_(\d+)_days$', str(outcome_str)) if censor_match: return (int(censor_match.group(1)), 0) # 无法解析,视为删失(保守策略) return (np.nan, 0) # 应用到DataFrame df[['event_time', 'event_status']] = df['raw_outcome'].apply( lambda x: pd.Series(parse_survival_outcome(x)) )逻辑说明:此函数不追求100%覆盖所有字符串变体,而是定义可审计的降级策略——当正则无法匹配时,统一视为删失(
event_status=0),并在日志中记录原始字符串。这样既保证流程不中断,又留下审计线索。线上系统曾因此发现某批次数据录入员把“MI_at_1245_days”错录为“MI_in_1245_days”,正则未匹配,全部标为删失,触发告警,及时拦截了错误数据流入训练。
3. 特征工程不是“标准化+PCA”:临床指南驱动的分箱与交互设计
心血管领域不存在“通用最优特征”。《中国心血管病一级预防指南》明确要求:对血压、血糖、血脂进行分层管理。模型若把收缩压当成连续变量直接喂给XGBoost,就违背了临床实践逻辑——医生不会说“SBP每升高1mmHg,风险增加β”,而是说“SBP≥140mmHg即启动干预”。因此,特征工程必须回归指南。
3.1 基于指南的临床分箱:用cut()实现可复现的区间划分
我们以《中国高血压防治指南(2023年修订版)》为依据,对收缩压(SBP)做分箱:
# clinical_binning.py SBP_CATEGORIES = [ (0, 120, 'optimal'), (120, 130, 'normal'), (130, 140, 'high_normal'), (140, 160, 'grade1_htn'), (160, 180, 'grade2_htn'), (180, 300, 'grade3_htn') ] def bin_sbp(df: pd.DataFrame, sbp_col='sbp_clipped') -> pd.DataFrame: df_binned = df.copy() # 提取分箱边界 bins = [interval[0] for interval in SBP_CATEGORIES] + [SBP_CATEGORIES[-1][1]] labels = [interval[2] for interval in SBP_CATEGORIES] # 执行分箱(右闭区间) df_binned['sbp_category'] = pd.cut( df_binned[sbp_col], bins=bins, labels=labels, include_lowest=True, right=True ) # 转为one-hot(注意:此处one-hot是为树模型服务,非线性模型需要显式交互) dummies = pd.get_dummies(df_binned['sbp_category'], prefix='sbp') df_binned = pd.concat([df_binned, dummies], axis=1) return df_binned参数说明:
include_lowest=True确保120被分入'optimal'而非'normal';right=True表示区间为左开右闭(如(120,130]),符合临床习惯(120归“正常”,120.1才进“正常高值”)。prefix='sbp'便于后续特征筛选时批量操作。
3.2 构造临床强交互特征:不是所有组合都该做,只做指南明示的
指南指出:“糖尿病合并高血压患者,心血管风险呈协同升高”。这意味着diabetes与sbp_category的交互效应显著。我们不暴力生成所有特征两两组合,而是按指南条目精准构造:
# clinical_interactions.py def create_guideline_interactions(df: pd.DataFrame) -> pd.DataFrame: df_interact = df.copy() # 【指南条目3.2.1】糖尿病 × 高血压分级 for sbp_cat in ['grade1_htn', 'grade2_htn', 'grade3_htn']: col_name = f'diab_and_{sbp_cat}' df_interact[col_name] = ( (df_interact['diabetes'] == 1) & (df_interact[f'sbp_{sbp_cat}'] == 1) ).astype(int) # 【指南条目4.1.5】吸烟 × HDL<1.0 mmol/L(男性) df_interact['smoke_and_low_hdl_male'] = ( (df_interact['smoking'] == 1) & (df_interact['hdl_clipped'] < 1.0) & (df_interact['sex'] == 1) # 1=Male, 0=Female ).astype(int) return df_interact为什么只做这些?因为临床验证过它们的OR值>2.5且P<0.001。盲目加
age × sbp这种交互,XGBoost可能拟合出虚假相关(比如老年人普遍SBP高,但二者无生物学交互)。本函数产出的每一列都有对应指南原文编号,可写入算法备案文档。
3.3 处理高维稀疏特征:用Target Encoding替代Label Encoding
对于“所在科室”(如cardiology,endocrinology,geriatrics)这类高基数离散变量,Label Encoding(0,1,2…)会引入人为序数。One-Hot在科室数>50时导致维度爆炸。Target Encoding用目标变量均值替代,但需防过拟合:
# target_encoding.py from sklearn.model_selection import KFold def smooth_target_encode( df: pd.DataFrame, col: str, target: str = 'event_status', min_samples_leaf: int = 20, smoothing: float = 10.0 ) -> pd.Series: """ 平滑Target Encoding:防止小科室因样本少导致编码噪声 公式:(sum(target) + smoothing * global_mean) / (count + smoothing) """ global_mean = df[target].mean() agg = df.groupby(col)[target].agg(['sum', 'count']) smooth = (agg['sum'] + smoothing * global_mean) / (agg['count'] + smoothing) # 对小科室(count < min_samples_leaf)用global_mean回退 smooth.loc[agg['count'] < min_samples_leaf] = global_mean return df[col].map(smooth).fillna(global_mean) # 应用 df['dept_encoded'] = smooth_target_encode(df, col='department', target='event_status')参数说明:
min_samples_leaf=20意味着科室样本少于20人时,直接用全量均值替代,避免“某科室仅3例患者全发生事件,编码=1.0”这种极端噪声。smoothing=10.0是经验值:当科室有10例时,编码值70%来自科室均值、30%来自全局均值,平衡偏差与方差。
4. 模型训练与可解释性:不用SHAP力竭,用临床可读的贡献度分解
很多团队把SHAP值图往医生面前一放,得到的反馈是:“这图我看不懂,我要知道的是‘如果把他的LDL从3.5降到2.0,风险能降多少个百分点’”。因此,我们的源码集锦不只调shap.TreeExplainer,而是封装了临床场景化解释器——输入干预动作,输出量化影响。
4.1 训练XGBoost时固定随机种子与树结构:确保结果可复现
心血管模型上线需通过算法备案,要求“相同输入必得相同输出”。XGBoost默认的random_state=None会导致每次训练树分裂顺序不同。必须显式锁定:
# train_model.py import xgboost as xgb def train_xgb_clinical( X_train: pd.DataFrame, y_train: pd.Series, params: dict = None ) -> xgb.XGBClassifier: if params is None: params = { 'objective': 'binary:logistic', 'eval_metric': 'auc', 'max_depth': 5, 'learning_rate': 0.05, 'subsample': 0.8, 'colsample_bytree': 0.8, 'seed': 42, # 关键!固定随机种子 'base_score': y_train.mean(), # 设定初始预测值为先验概率 } model = xgb.XGBClassifier(**params, random_state=42) # 再次锁定 model.fit(X_train, y_train, eval_set=[(X_train, y_train)], verbose=False) return model为什么
seed和random_state都要设?seed控制树构建的随机性(如列采样、行采样),random_state控制fit()内部的其他随机操作(如early stopping的验证集划分)。双保险才能100%复现。曾有项目因只设seed,在不同scikit-learn版本下random_state行为变化,导致模型备案失败。
4.2 用Partial Dependence Plot(PDP)回答“调某个值,风险变多少”
SHAP值解释单样本,PDP解释全局趋势。我们封装了一个专用于心血管指标的PDP生成器,输出临床报告友好的表格:
# pdp_analyzer.py from sklearn.inspection import PartialDependenceDisplay def generate_clinical_pdp( model: xgb.XGBClassifier, X: pd.DataFrame, feature: str, grid_resolution: int = 50 ) -> pd.DataFrame: """ 生成指定特征的PDP,并返回临床可读的增量风险表 输出:DataFrame with columns [feature_value, base_risk, delta_risk] """ # 获取特征在X中的实际范围 feat_min, feat_max = X[feature].min(), X[feature].max() # 构造网格(避开临床无效值,如sbp<70) if feature == 'sbp_clipped': grid = np.linspace(max(70, feat_min), min(220, feat_max), grid_resolution) else: grid = np.linspace(feat_min, feat_max, grid_resolution) # 计算PDP disp = PartialDependenceDisplay.from_estimator( model, X, [feature], grid_resolution=grid_resolution, kind='average', subsample=1000, random_state=42 ) # 提取PDP曲线(模型预测概率) pdp_curve = disp.pd_results[0].average[0] # 计算相对于基线(中位数)的风险变化 median_val = X[feature].median() median_idx = np.argmin(np.abs(grid - median_val)) base_risk = pdp_curve[median_idx] delta_risk = pdp_curve - base_risk return pd.DataFrame({ feature: grid, 'base_risk': np.full_like(grid, base_risk), 'delta_risk': delta_risk, 'absolute_risk': pdp_curve }) # 使用示例:生成SBP从120到160的PDP pdp_sbp = generate_clinical_pdp(model, X_test, 'sbp_clipped') print(pdp_sbp[pdp_sbp['sbp_clipped'].between(120, 160)].round(4))输出示例:
sbp_clipped base_risk delta_risk absolute_risk 0 120.0 0.12 -0.015 0.105 1 125.0 0.12 -0.008 0.112 2 130.0 0.12 0.000 0.120 3 135.0 0.12 0.012 0.132 4 140.0 0.12 0.028 0.148 5 145.0 0.12 0.045 0.165 6 150.0 0.12 0.062 0.182 7 155.0 0.12 0.079 0.199 8 160.0 0.12 0.095 0.215医生一眼看懂:“SBP从130升到140,3年事件风险从12%升到14.8%,绝对增加2.8个百分点”。
4.3 避坑:常见问题与血泪排查经验
现象1:模型在训练集AUC=0.85,测试集骤降至0.62,且SHAP值显示“age”贡献度为负(年龄越大风险越低)
原因:训练集与测试集时间戳分布不一致。训练集为2018–2020年数据,测试集为2021–2023年数据,期间该院启用了更积极的他汀干预,导致老年患者事件率下降。模型学到了“年份→干预强度→事件率↓”的混淆路径。
解决:加入year_of_enrollment作为特征,并在交叉验证中按年份分层(StratifiedKFold不适用,改用TimeSeriesSplit),或直接剔除年份相关混杂。
现象2:用
smooth_target_encode处理“所在城市”,模型在东部城市AUC高,西部城市AUC低,且city_encoded特征重要性排前三
原因:Target Encoding泄露了地理聚类信息。西部某市仅23例患者,其中18例发生事件,编码=0.78;而东部某市2300例,事件率0.12,编码=0.12。模型把“高编码值”直接等同于“高风险”,忽略了城市间医疗资源差异。
解决:对地理类特征,改用层级编码(Hierarchical Encoding):先按省份聚合计算均值,再用省份均值编码市级,公式为(city_sum + 5 * province_mean) / (city_count + 5),5为超参,代表“相信省份先验5例”。
现象3:PDP显示“HDL升高,风险上升”,与医学常识完全相悖
原因:数据中存在强混杂——HDL检测值与采血时间强相关(晨起空腹HDL偏低,午后偏高),而采血时间又与患者病情相关(危重患者多在夜间急查,HDL偏低)。模型学到了“采血时间→HDL→病情严重度”的伪关联。
解决:在特征工程阶段加入blood_draw_hour,并用LinearRegression对其残差化:hdl_residual = hdl - lr.predict([[hour]]),再用残差值做PDP。
现象4:
event_time字段存在大量0值(如“入组当天即发生事件”),Survival模型报错ValueError: time must be > 0
原因:生存分析理论要求时间>0,但临床中“入组即事件”是真实情况(如急性心梗患者入院即确诊)。直接删掉会引入选择偏倚。
解决:采用tiny_time = 0.001技巧:将所有event_time==0替换为0.001(单位:天≈1.4分钟),在Cox模型中,这相当于假设事件发生在入组后1.4分钟,不影响风险比估计,且通过数学证明其一致性。
现象5:用
bin_sbp()分箱后,XGBoost重要性显示sbp_grade1_htn权重最高,但临床认为grade2_htn才应是高危标志
原因:grade1_htn样本量占总体45%,grade2_htn仅8%,树模型倾向于分裂大样本节点。重要性反映的是分裂增益,不是临床意义。
解决:改用Permutation Importance(打乱该特征后AUC下降值)重评重要性,或直接查看PDP曲线斜率——grade2_htn区间的斜率绝对值更大,才是真正的高危转折点。
5. 模型部署前的临床合规检查:三道硬闸门与一份可交付清单
模型训练完成只是起点。要让心内科主任签字同意上线,必须通过三道临床合规性硬闸门。本节给出每道闸门的具体检查项、执行命令、及不通过时的修正路径。这不是“最佳实践”,而是我帮某三甲医院通过卫健委AI医疗器械备案时,被退回3次后总结出的强制动作。
5.1 闸门一:特征可追溯性审计(Feature Traceability Audit)
要求:任一输入字段(如sbp),必须能回溯到原始数据库表名、字段名、ETL脚本路径、清洗规则文档编号。不能出现“该字段由某Python脚本生成,脚本已更新多次,历史版本丢失”这类情况。
执行命令:
# 在项目根目录运行,生成特征溯源报告 python audit/trace_feature.py --feature sbp --output report/sbp_trace.md输出内容必须包含:
- 原始数据源:
table: clinical_vitals, column: systolic_bp_mmhg - ETL脚本:
etl/02_vital_clean.py#L45-L67 - 清洗规则引用:
《院内生命体征采集SOP_V3.2》第4.1条 - 当前值域统计:
min=72, max=218, missing_rate=0.3% - 与指南对比结论:
符合《高血压指南2023》SBP测量规范(水银柱法,静息5分钟)
不通过修正:若
etl/02_vital_clean.py中无# SOP_V3.2注释,则必须补全,并同步更新SOP文档修订记录。没有文档编号的代码,临床审核直接拒收。
5.2 闸门二:决策边界临床合理性验证(Clinical Boundary Validation)
要求:模型对关键指标(SBP、LDL、HbA1c)的决策边界,必须与现行指南推荐的干预阈值一致。例如,当SBP=139时,模型风险分值<0.15;SBP=140时,风险分值≥0.15。
执行命令:
# 生成SBP在135-145区间的风险跃迁报告 python validate/boundary_check.py \ --model models/xgb_final.pkl \ --feature sbp_clipped \ --range 135 145 \ --step 0.5 \ --threshold 0.15 \ --output report/sbp_boundary.csv输出表格关键列:
| sbp_clipped | risk_score | crosses_threshold | distance_to_threshold |
|---|---|---|---|
| 139.0 | 0.142 | False | -0.008 |
| 139.5 | 0.146 | False | -0.004 |
| 140.0 | 0.151 | True | +0.001 |
| 140.5 | 0.158 | True | +0.008 |
不通过修正:若
crosses_threshold在140.0处为False,说明模型未学习到指南阈值。此时需在训练数据中人工增强阈值附近样本:对SBP∈[138,142]的患者,复制3份,分别标记为sbp_clipped=138,140,142,并保持其他特征不变,重新训练。这是唯一被临床接受的“对齐指南”的技术手段。
5.3 闸门三:亚组公平性压力测试(Subgroup Fairness Stress Test)
要求:模型在性别、年龄(<65岁 vs ≥65岁)、医保类型(职工医保 vs 居民医保)三个维度上,AUC差异≤0.03,且校准曲线(Calibration Curve)的Brier Score差异≤0.01。
执行命令:
# 运行亚组公平性测试 python validate/fairness_test.py \ --model models/xgb_final.pkl \ --data data/processed/test_set.csv \ --sensitive_cols sex,age_group,insurance_type \ --output report/fairness_summary.json关键输出解读:
{ "sex": {"male_auc": 0.821, "female_auc": 0.798, "diff": 0.023}, "age_group": {"young_auc": 0.815, "elderly_auc": 0.789, "diff": 0.026}, "insurance_type": {"employee_auc": 0.802, "resident_auc": 0.771, "diff": 0.031} }不通过修正:
insurance_type的diff=0.031>0.03,需启用对抗去偏(Adversarial Debiasing)。在XGBoost训练前,添加一个轻量级对抗网络,以insurance_type为预测目标,惩罚主模型在该特征上的判别能力。我们封装了AdversarialXGB类,只需替换训练器:from fairness.adversarial_xgb import AdversarialXGB model = AdversarialXGB( primary_objective='binary:logistic', adversary_objective='multi:softprob', sensitive_col='insurance_type', lambda_adversary=0.3 # 平衡主任务与去偏力度 ) model.fit(X_train, y_train)
5.4 交付物清单:一份能让信息科和医务处同时签字的压缩包
最终交付不是一个.pkl文件,而是一个带校验的压缩包,解压后目录结构强制如下:
cardio_prediction_v1.2/ ├── LICENSE # MIT协议,注明“仅供科研与辅助决策,不替代医生判断” ├── README.md # 3句话说明:1)输入字段清单与单位 2)输出含义(3年心血管事件概率)3)免责条款 ├── data/ │ ├── raw/ # 原始脱敏CSV(含字段字典Excel) │ └── processed/ # 清洗后数据(含缺失值标记、分箱列、交互列) ├── src/ │ ├── preprocess/ # 所有清洗、编码、分箱脚本(含SOP引用注释) │ ├── train/ # 训练脚本(含固定seed、交叉验证逻辑) │ └── explain/ # PDP、临床干预模拟、SHAP解释脚本 ├── models/ │ └── xgb_final.pkl # 模型文件(pickle,Python 3.9+可加载) ├── reports/ │ ├── feature_trace/ # 每个特征的溯源报告(Markdown) │ ├── boundary_validation/ # 各指标阈值跃迁报告(CSV) │ └── fairness_audit/ # 亚组公平性测试报告(JSON+可视化PNG) └── checksums.sha256 # 所有文件SHA256校验值,供信息科验签血泪经验:某次交付因
README.md未写明“不替代医生判断”,被医务处退回。另一次因checksums.sha256未包含LICENSE文件校验值,信息科拒绝部署。现在我的交付包生成脚本末尾强制执行:sha256sum $(find . -type f ! -name "checksums.sha256") > checksums.sha256
希望帮到你。我至今保留着第一版心血管模型上线那天,心内科主任手写的便签:“风险分值和我们查房判断一致,但请把‘sbp_grade1_htn’的命名改成‘高血压1级’,医生看不懂英文缩写。”——技术再硬,也得弯下腰,把术语翻译成诊室里的语言。
本文还有配套的精品资源,点击获取