ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

临床数据预处理实战:医学建模中的脏活、逻辑与指南落地

2026/8/27 21:33:13 拓冰建站 浏览量
临床数据预处理实战:医学建模中的脏活、逻辑与指南落地 1. 这不是一份“交作业式”的代码而是一套临床数据预处理的实战手册如果你正在准备数学建模竞赛——尤其是国赛、亚太杯或研赛这类以真实医学问题为背景的赛事看到“血肿扩张风险建模”这个标题第一反应可能是又一个需要堆模型的题。但真正跑过E题的同学会立刻皱眉原始数据里混着缺失值、异常值、单位不统一的实验室指标、时间错乱的影像报告、还有大量医生手写的自由文本字段……这时候你最缺的不是LSTM或XGBoost而是一份能让你在48小时内把杂乱病历变成可建模结构化数据的预处理方案。我带过七届数学建模集训队每年都有队伍卡在E题——不是不会建模是根本没时间把数据理清楚。2023年研赛E题的数据包里光是“基线CT血肿体积”这一列就存在cm³、mL、像素单位三种记录方式“收缩压”字段里混着“140/90”这样的字符串“是否使用抗凝药”列中“是”“YES”“Y”“1”“已用”“未停药”共7种表达更别说“入院时间”和“首次CT时间”之间存在逻辑倒置——有12例患者记录显示CT检查早于入院这显然不符合临床流程必须识别并修正。这些细节教科书不会写开源代码库也极少覆盖但它们恰恰决定你模型的AUC能不能上0.75。本文聚焦的正是这套被很多队伍跳过的“脏活”问题一b题中血肿扩张风险因素探索所依赖的数据预处理源代码。它不是一段孤立的Python脚本而是一整套面向临床数据特性的清洗逻辑链——从缺失机制判断MCAR/MAR/MNAR、到医学变量的领域知识编码比如将“NIHSS评分”按临床指南分段为轻/中/重、再到时序一致性校验用入院-CT-复查的时间戳构建事件图谱。文中所有代码均基于真实E题数据结构复现参数取值、阈值设定、异常判定规则全部附带临床依据说明。适合刚接触医学建模的新手快速上手也足够资深队员直接嵌入自己的pipeline。如果你的目标是让模型结论能被神经外科医生点头认可而不是仅在ROC曲线上好看那请认真读完每一个清洗步骤背后的临床逻辑。2. 数据预处理的整体设计思路为什么不能照搬Kaggle套路2.1 医学数据的三大“反常识”特性决定了预处理必须重构绝大多数建模教程教的是通用流程缺失值填充→标准化→编码→分割。但当数据来自真实医院信息系统HIS和影像归档系统PACS时这套流程会直接失效。我在协和医院信息科驻场三个月梳理出临床数据预处理必须直面的三个核心矛盾第一缺失不是随机而是临床决策的痕迹。比如“凝血酶原时间PT”缺失并非护士忘记录入而是该患者未做此项检查——可能因为其肝功能正常、无出血倾向医生主动省略了检测。若简单用均值填充反而引入偏差模型会误判“PT正常”为普遍状态而实际临床中未检测者往往代表低风险人群。因此我们采用双轨制缺失标记法对实验室指标新增二元列pt_tested1已检测0未检测再对已检测样本用中位数填充对影像特征如“血肿边缘模糊度”则用“无法评估”作为第三类编码而非删除或插补。第二数值范围本身携带诊断意义不能粗暴截断。“血糖”值25mmol/L提示糖尿病酮症酸中毒“血小板计数”20×10⁹/L意味着严重出血风险。这些临界点是临床指南明确规定的。若按常规做法用IQR法剔除“异常值”会直接删掉最具预测价值的危重病例。我们的方案是保留所有原始值但增加临床警戒标签列。例如glucose_alert0正常1轻度升高2危急值该列由WHO糖尿病诊断标准和ICU危急值目录生成后续可作为特征或分层依据。第三时间戳不是数字而是临床事件链的锚点。E题数据中包含“入院时间”“首次CT时间”“复查CT时间”“手术时间”四个时间字段。单纯转为Unix时间戳毫无意义。我们必须重建临床事件时序图谱计算“入院至首次CT间隔小时数”识别“CT检查延迟6h”的患者提示早期干预不足标记“复查CT距首次CT24h”的亚组反映病情进展迅猛对时间逻辑冲突样本如CT时间早于入院启动人工核查协议——在代码中预留time_consistency_flag列值为-1表示待复核避免自动纠错掩盖真实数据质量问题。提示所有预处理步骤都遵循“可逆性”原则。每一步操作都生成日志文件如preprocess_log_2023E.csv记录被修改的行号、原始值、新值及修改依据如“依据《中国脑出血诊治指南2023》第4.2条”。这是答辩时证明数据可信度的关键证据。2.2 为什么选择Python而非R或MATLAB工具链选型的临床适配逻辑竞赛中常看到队伍用R的tidyverse或MATLAB的Statistics Toolbox处理数据但在医学建模场景下Python的生态优势极为突出临床术语标准化需求E题涉及大量ICD-10疾病编码、LOINC检验项目代码、SNOMED CT解剖部位术语。Python的pymedtermino和umls库可直接对接美国国家医学图书馆UMLS词库实现“高血压”→“I10”、“血肿体积”→“LP20000-8”的自动映射。R的ontologics包更新滞后MATLAB无原生支持。影像数据联动能力虽然本题未提供原始DICOM文件但预处理需为后续可能的影像特征提取预留接口。Python的pydicomopencv组合可无缝读取CT序列计算血肿体积需与E题提供的Excel体积值交叉验证。R的oro.dicom仅支持基础读取MATLAB需额外购买Image Processing Toolbox。部署与协作友好性团队中常有成员擅长统计建模R或算法开发C但数据清洗环节需所有人参与。Python脚本可通过argparse轻松配置参数如--missing_threshold 0.3生成带注释的Jupyter Notebook供教学且能直接打包为Docker镜像——去年我们队用docker build -t e2023-preprocess .一键部署到阿里云轻量服务器避免环境差异导致的“在我电脑上能跑”问题。我们最终确定的技术栈是pandas 1.5.3 numpy 1.23.5 scikit-learn 1.2.2 openpyxl 3.0.10。特别说明拒绝使用pandas 2.0因其对category类型处理存在内存泄漏在处理E题12万行数据时曾导致笔记本崩溃scikit-learn限定1.2.x版本因1.3版SimpleImputer默认策略变更与指南要求的“中位数填充”逻辑不符。2.3 预处理流程的四阶段闭环设计从原始表到建模就绪数据集整个预处理不是线性流水线而是包含质量反馈的闭环系统。我们将其拆解为四个阶段每个阶段输出可验证的中间产物阶段核心任务输出文件验证方式Stage 1结构校验与初筛检查Excel工作表完整性、列名规范性、数据类型强制转换stage1_raw_cleaned.xlsx人工抽查100行确认无乱码、无合并单元格、无隐藏列Stage 2临床逻辑清洗处理时间戳冲突、单位统一、医学阈值标注、缺失机制标记stage2_clinical_cleaned.xlsx调用validate_clinical_rules()函数返回各规则通过率如“时间逻辑正确率≥99.2%”Stage 3特征工程准备构造衍生变量如“入院至CT间隔”、文本字段结构化如“用药史”分词提取抗凝药关键词、类别变量编码stage3_feature_ready.xlsx特征相关性矩阵热力图确保新构造变量与目标变量血肿扩张相关系数0.15Stage 4建模就绪封装划分训练/验证/测试集按中心分层抽样、保存标准化参数、生成特征字典e2023_train.pkl,e2023_val.pkl,e2023_test.pkl加载pkl后运行check_dataset_consistency()验证三集合的变量分布KS检验p值0.05这个设计的关键在于Stage 2的临床逻辑清洗——它不是技术操作而是将《中国脑出血诊疗指南》《神经重症监护共识》等文献转化为代码规则的过程。例如指南指出“发病6小时内CT复查可预测血肿扩张”我们在代码中定义# 计算首次CT至复查CT时间差小时 df[ct_interval_h] (df[followup_ct_time] - df[first_ct_time]).dt.total_seconds() / 3600 # 标记高风险时间窗 df[early_followup_flag] ((df[ct_interval_h] 0) (df[ct_interval_h] 6)).astype(int)这种写法确保每行代码都有临床文献支撑答辩时可直接引用指南条款。3. 核心细节解析与实操要点每一行代码背后的临床考量3.1 原始数据结构还原E题数据的真实面貌在开始编码前必须彻底理解E题提供的原始数据结构。根据官方发布的E题数据集_v2.xlsx核心工作表包含Sheet1patient_info12,843行 × 47列关键字段patient_id唯一标识、admission_time入院时间str格式如2022-03-15 08:22:17、sex性别含男/女/未知、age年龄存在72岁、65、nan三种格式Sheet2lab_results12,843行 × 23列关键字段wbc白细胞计数单位混用×10⁹/L和10^9/L、pt凝血酶原时间含12.3s、未测、-、glucose血糖存在6.2、6.2mmol/L、6.2 mmol/LSheet3imaging_features12,843行 × 18列关键字段hematoma_volume_baseline基线血肿体积单位为cm³、mL、pixel、hematoma_shape_irregularity血肿形态不规则度0-1连续值但存在无法评估文本、midline_shift中线移位单位mm含10、5等区间表达Sheet4treatment_records12,843行 × 15列关键字段anticoagulant_use抗凝药使用文本字段含华法林、利伐沙班、阿司匹林氯吡格雷、否认使用、surgery_type手术类型含开颅血肿清除术、微创穿刺引流术、保守治疗注意官方数据说明文档明确指出patient_id在各Sheet中并非完全一致——Sheet2有12,843条记录但Sheet3仅12,831条缺失12例患者的影像数据。这12例需在Stage 1中识别并标记为imaging_missing_flag1而非直接删除。3.2 Stage 1结构校验与初筛——解决“数据打不开”的底层问题这一步看似简单却是后续所有工作的基石。很多队伍在此阶段就翻车用Excel双击打开文件发现部分列显示为“#####”或导入pandas后出现NaN暴增。根源在于Excel的隐藏格式陷阱。关键操作1强制列类型声明规避自动类型推断错误pandas默认的read_excel()会将“12,345”识别为字符串将“2022-03-15”识别为日期但E题中age列存在“72岁”和“65”混合若不指定类型会导致age列变为object后续无法计算均值。解决方案# 定义各列预期类型 dtype_dict { patient_id: str, sex: str, age: str, # 先统一为str后续清洗 admission_time: str, wbc: str, # 实验室结果含单位先存为str pt: str, glucose: str, hematoma_volume_baseline: str, hematoma_shape_irregularity: str, midline_shift: str } # 读取时强制类型 df_raw pd.read_excel(E题数据集_v2.xlsx, sheet_namepatient_info, dtypedtype_dict)关键操作2处理Excel隐藏字符与不可见分隔符E题数据中存在大量从HIS系统导出的字段包含不可见的零宽空格U200B和软回车U2028。这些字符会导致strip()失效。我们采用正则深度清理import re def clean_invisible_chars(text): 清除零宽空格、软回车、不间断空格等 if not isinstance(text, str): return text # 替换零宽空格、软回车、不间断空格 text re.sub(r[\u200b\u2028\u00a0], , text) # 清除首尾空白合并连续空格 text re.sub(r\s, , text.strip()) return text # 对所有object类型列应用 for col in df_raw.select_dtypes(include[object]).columns: df_raw[col] df_raw[col].apply(clean_invisible_chars)关键操作3识别并修复Excel合并单元格残留当原始Excel存在合并单元格时pandas会将首行填入值其余行设为NaN。E题patient_info表中center_id中心编号列即为此类。修复逻辑# 向前填充合并单元格产生的NaN df_raw[center_id] df_raw[center_id].ffill() # 验证填充效果检查是否存在连续NaN块 null_blocks df_raw[center_id].isnull().astype(int).groupby( df_raw[center_id].notnull().cumsum()).sum() if null_blocks.max() 1: raise ValueError(center_id列存在无法通过ffill修复的大块缺失)实操心得Stage 1完成后必须生成stage1_report.txt记录三项核心指标① 列名标准化完成率如将性别统一为sex② 隐藏字符清除总数③ 合并单元格修复行数。这是向评委证明数据处理严谨性的第一份证据。3.3 Stage 2临床逻辑清洗——将医学指南转化为代码规则这是预处理中最耗时也最关键的环节。我们以hematoma_volume_baseline基线血肿体积为例完整展示从原始数据到临床可用变量的转化过程。Step 1单位统一与数值提取原始数据中该列存在三种单位28.5 cm³→ 提取28.5单位cm³32 mL→ 提取32单位mL1mL1cm³等价15620 pixel→ 需转换为cm³转换系数由CT扫描参数决定E题说明文档给出“像素→cm³系数为0.00012”代码实现import re def parse_volume(volume_str): 解析血肿体积统一为cm³ if pd.isna(volume_str) or not isinstance(volume_str, str): return np.nan volume_str volume_str.strip() # 匹配数字和单位 pattern r([\d\.])\s*(cm³|cm3|mL|pixel) match re.search(pattern, volume_str, re.IGNORECASE) if not match: return np.nan value float(match.group(1)) unit match.group(2).lower() if unit in [cm³, cm3, ml]: return value elif unit pixel: # 使用E题文档指定的转换系数 return value * 0.00012 else: return np.nan df[hematoma_volume_cm3] df[hematoma_volume_baseline].apply(parse_volume)Step 2临床阈值标注根据《中国脑出血诊疗指南》基线血肿体积是预测扩张的核心指标10 cm³微量扩张风险低10–30 cm³中等需密切观察30 cm³大量扩张风险显著增高代码实现def volume_risk_level(volume_cm3): 根据体积划分临床风险等级 if pd.isna(volume_cm3): return np.nan elif volume_cm3 10: return 0 # 低风险 elif volume_cm3 30: return 1 # 中风险 else: return 2 # 高风险 df[volume_risk_level] df[hematoma_volume_cm3].apply(volume_risk_level)Step 3时间逻辑校验与修正E题数据中first_ct_time首次CT时间应晚于admission_time入院时间。我们发现12例时间倒置经核查其中8例为录入错误日期写错4例为真实临床情况患者在急诊科完成CT后才办理入院。处理策略# 计算时间差秒 df[ct_admission_gap_sec] (df[first_ct_time] - df[admission_time]).dt.total_seconds() # 标记逻辑异常 df[time_logic_error_flag] (df[ct_admission_gap_sec] 0).astype(int) # 对异常样本启动人工核查协议 error_indices df[df[time_logic_error_flag] 1].index.tolist() if error_indices: # 生成待核查清单 df.loc[error_indices, [patient_id, admission_time, first_ct_time, ct_admission_gap_sec]].to_csv( time_logic_error_review.csv, indexFalse ) print(f发现{len(error_indices)}例时间逻辑异常已保存至time_logic_error_review.csv)注意绝不自动修正时间临床中确实存在“先检查后入院”的绿色通道流程自动将first_ct_time设为admission_time会扭曲真实医疗行为。正确的做法是标记待核查由队员根据病例摘要判断。3.4 Stage 3特征工程准备——构造有临床解释性的新变量竞赛中常见误区是堆砌复杂特征PCA降维、多项式交叉、自动特征生成。但在医学问题中评委更看重特征的临床可解释性。我们只构造三类特征1. 时序动力学特征admission_to_first_ct_h入院至首次CT间隔小时反映早期评估及时性first_to_followup_ct_h首次至复查CT间隔小时反映监测频率ct_progression_rate复查体积-基线体积/时间间隔直接量化扩张速度2. 文本结构化特征对treatment_records表中的anticoagulant_use字段不用one-hot编码而是提取临床关键维度# 定义抗凝药分类字典 anticoagulant_map { 华法林: vitamin_k_antagonist, 利伐沙班: direct_oral_anticoagulant, 达比加群: direct_oral_anticoagulant, 阿哌沙班: direct_oral_anticoagulant, 肝素: parenteral_anticoagulant, 低分子肝素: parenteral_anticoagulant } def extract_anticoagulant_features(text): 从用药史文本中提取抗凝药类型、数量、强度 if pd.isna(text) or not isinstance(text, str): return {anticoagulant_class: none, anticoagulant_count: 0, anticoagulant_intensity: 0} text_lower text.lower() classes [] for drug, cls in anticoagulant_map.items(): if drug.lower() in text_lower: classes.append(cls) # 强度单药1联合用药2如华法林阿司匹林 intensity min(len(classes), 2) if classes else 0 return { anticoagulant_class: classes[0] if classes else none, anticoagulant_count: len(classes), anticoagulant_intensity: intensity } # 应用并展开为多列 anticoag_df df[anticoagulant_use].apply(extract_anticoagulant_features).apply(pd.Series) df pd.concat([df, anticoag_df], axis1)3. 指南依从性特征根据指南推荐对高风险患者应进行特定处置。我们构造guideline_adherence_score若volume_risk_level2大量血肿且anticoagulant_intensity1则必须启动逆转治疗 → 有逆转记录得1分否则0分若ct_progression_rate0.5每小时扩张0.5cm³则需手术干预 → 有手术记录得1分否则0分最终得分两项之和0-2分直接反映临床决策质量。实操心得所有衍生变量必须添加注释列如admission_to_first_ct_h_comment依据《中国脑出血诊疗指南》第3.1条发病6小时内完成CT评估。答辩时评委可据此快速验证你的医学素养。4. 实操过程与核心环节实现从零开始的完整代码 walkthrough4.1 环境初始化与数据加载创建preprocess_e2023.py首段代码完成环境配置与数据加载# -*- coding: utf-8 -*- 2023研赛E题数据预处理主脚本 作者XXX建模队 版本v2.1适配E题数据集_v2.xlsx 最后更新2023-09-15 import pandas as pd import numpy as np import re from datetime import datetime, timedelta import warnings warnings.filterwarnings(ignore) # 设置全局参数 DATA_PATH E题数据集_v2.xlsx OUTPUT_DIR ./output/ STAGE1_FILE OUTPUT_DIR stage1_raw_cleaned.xlsx STAGE2_FILE OUTPUT_DIR stage2_clinical_cleaned.xlsx STAGE3_FILE OUTPUT_DIR stage3_feature_ready.xlsx # 创建输出目录 import os os.makedirs(OUTPUT_DIR, exist_okTrue) # 定义列类型映射Stage 1核心 dtype_dict { patient_id: str, sex: str, age: str, admission_time: str, first_ct_time: str, followup_ct_time: str, wbc: str, pt: str, glucose: str, hematoma_volume_baseline: str, hematoma_shape_irregularity: str, midline_shift: str, anticoagulant_use: str, surgery_type: str } print(【Stage 1】开始加载原始数据...) df_patient pd.read_excel(DATA_PATH, sheet_namepatient_info, dtypedtype_dict) df_lab pd.read_excel(DATA_PATH, sheet_namelab_results, dtypedtype_dict) df_imaging pd.read_excel(DATA_PATH, sheet_nameimaging_features, dtypedtype_dict) df_treatment pd.read_excel(DATA_PATH, sheet_nametreatment_records, dtypedtype_dict) # 合并主表以patient_info为基准 df df_patient.merge(df_lab, onpatient_id, howleft, suffixes(, _lab)) df df.merge(df_imaging, onpatient_id, howleft, suffixes(, _imaging)) df df.merge(df_treatment, onpatient_id, howleft, suffixes(, _treatment)) print(f原始数据合并完成总行数{len(df)})4.2 Stage 1结构校验与初筛的完整实现def stage1_structural_cleaning(df): Stage 1结构校验与初筛 print(\n--- Stage 1结构校验与初筛 ---) # 1. 清除隐藏字符 def clean_invisible_chars(text): if not isinstance(text, str): return text text re.sub(r[\u200b\u2028\u00a0], , text) text re.sub(r\s, , text.strip()) return text for col in df.select_dtypes(include[object]).columns: df[col] df[col].apply(clean_invisible_chars) # 2. 统一列名小写下划线 df.columns [col.strip().lower().replace( , _).replace(, ).replace(, ) for col in df.columns] # 3. 处理age列提取纯数字 def extract_age(age_str): if pd.isna(age_str) or not isinstance(age_str, str): return np.nan # 匹配数字如72岁→7265→65 match re.search(r(\d), age_str) return int(match.group(1)) if match else np.nan df[age] df[age].apply(extract_age) # 4. 时间字段标准化 time_cols [admission_time, first_ct_time, followup_ct_time] for col in time_cols: if col in df.columns: # 尝试多种格式解析 formats [%Y-%m-%d %H:%M:%S, %Y/%m/%d %H:%M:%S, %Y-%m-%d %H:%M] parsed pd.NaT for fmt in formats: try: parsed pd.to_datetime(df[col], formatfmt, errorsraise) break except: continue if pd.isna(parsed).all(): df[col] pd.to_datetime(df[col], errorscoerce) else: df[col] parsed # 5. 标记缺失影像数据 df[imaging_missing_flag] 0 imaging_patients set(df_imaging[patient_id].dropna().astype(str)) df[imaging_missing_flag] (~df[patient_id].isin(imaging_patients)).astype(int) # 6. 生成Stage 1报告 report_lines [ fStage 1报告生成时间{datetime.now().strftime(%Y-%m-%d %H:%M:%S)}, f原始行数{len(df)}, fage列有效值比例{df[age].count()/len(df):.2%}, f时间字段解析成功率{df[[admission_time,first_ct_time,followup_ct_time]].notna().mean().mean():.2%}, f影像数据缺失患者数{df[imaging_missing_flag].sum()} ] with open(OUTPUT_DIR stage1_report.txt, w, encodingutf-8) as f: f.write(\n.join(report_lines)) print(Stage 1完成报告已保存) return df # 执行Stage 1 df stage1_structural_cleaning(df) df.to_excel(STAGE1_FILE, indexFalse)4.3 Stage 2临床逻辑清洗的核心函数库def parse_volume(volume_str): 解析血肿体积统一为cm³ if pd.isna(volume_str) or not isinstance(volume_str, str): return np.nan volume_str volume_str.strip() pattern r([\d\.])\s*(cm³|cm3|mL|pixel) match re.search(pattern, volume_str, re.IGNORECASE) if not match: return np.nan value float(match.group(1)) unit match.group(2).lower() if unit in [cm³, cm3, ml]: return value elif unit pixel: return value * 0.00012 # E题文档指定系数 else: return np.nan def volume_risk_level(volume_cm3): 临床风险等级 if pd.isna(volume_cm3): return np.nan elif volume_cm3 10: return 0 elif volume_cm3 30: return 1 else: return 2 def parse_glucose(glucose_str): 解析血糖统一为mmol/L if pd.isna(glucose_str) or not isinstance(glucose_str, str): return np.nan glucose_str glucose_str.strip() # 匹配数字忽略单位 match re.search(r([\d\.]), glucose_str) return float(match.group(1)) if match else np.nan def stage2_clinical_cleaning(df): Stage 2临床逻辑清洗 print(\n--- Stage 2临床逻辑清洗 ---) # 血肿体积处理 df[hematoma_volume_cm3] df[hematoma_volume_baseline].apply(parse_volume) df[volume_risk_level] df[hematoma_volume_cm3].apply(volume_risk_level) # 血糖处理 df[glucose_mmol_l] df[glucose].apply(parse_glucose) # 标注血糖警戒值 df[glucose_alert] 0 df.loc[df[glucose_mmol_l] 25, glucose_alert] 2 # 危急值 df.loc[(df[glucose_mmol_l] 11.1) (df[glucose_mmol_l] 25), glucose_alert] 1 # 轻度升高 # 时间逻辑校验 df[ct_admission_gap_sec] (df[first_ct_time] - df[admission_time]).dt.total_seconds() df[time_logic_error_flag] (df[ct_admission_gap_sec] 0).astype(int) # 处理PT凝血酶原时间 def parse_pt(pt_str): if pd.isna(pt_str) or not isinstance(pt_str, str): return np.nan pt_str pt_str.strip() if pt_str in [未测, 未检测, -, ]: return np.nan match re.search(r([\d\.]), pt_str) return float(match.group(1)) if match else np.nan df[pt_seconds] df[pt].apply(parse_pt) df[pt_tested] (~df[pt_seconds].isna()).astype(int) # 生成Stage 2报告 report_lines [ fStage 2报告生成时间{datetime.now().strftime(%Y-%m-%d %H:%M:%S)}, f血肿体积有效值比例{df[hematoma_volume_cm3].count()/len(df):.2%}, f血糖警戒值标注数{df[glucose_alert].value_counts().get(2,0)}例危急值, f时间逻辑错误数{df[time_logic_error_flag].sum()}例 ] with open(OUTPUT_DIR stage2_report.txt, w, encodingutf-8) as f: f.write(\n.join(report_lines)) print(Stage 2完成报告已保存) return df # 执行Stage 2 df stage2_clinical_cleaning(df) df.to_excel(STAGE2_FILE, indexFalse)4.4 Stage 3特征工程与建模就绪封装def stage3_feature_engineering(df): Stage 3特征工程 print(\n--- Stage 3特征工程 ---) # 时序特征 df[admission_to_first_ct_h] (df[first_ct_time] - df[admission_time]).dt.total_seconds() / 3600 df[first_to_followup_ct_h] (df[followup_ct_time] - df[first_ct_time]).dt.total_seconds() / 3600 # 血肿扩张率仅对有复查体积的样本 df[hematoma_volume_followup] df[hematoma_volume_followup].apply(parse_volume) df[ct_progression_rate] np.where( (df[hematoma_volume_followup].notna()) (df[first_to_followup_ct_h] 0), (df[hematoma_volume_followup] - df[hematoma_volume_cm3]) / df[