医疗数据分析实战:从数据清洗到特征工程的完整流程与避坑指南
2026/8/28 7:51:01 网站建设 项目流程

1. 项目概述与核心价值

最近在复盘一个关于肿瘤疾病诊疗的经济学分析项目,感触最深的就是数据清洗和特征工程这个环节。这个项目听起来是数学建模,但内核其实是一个典型的数据科学问题:我们手头有一堆来自医院信息系统、医保结算、患者随访记录的原始数据,目标是要构建一个能评估不同诊疗方案经济负担的模型。数据清洗和特征工程,就是决定这个模型是“纸上谈兵”还是“真能落地”的关键分水岭。

很多刚接触这类项目的朋友,可能会把重心直接放在模型算法上,比如纠结用线性回归还是随机森林。但根据我多年的经验,在医疗经济数据分析里,你花在模型调参上的时间,可能远不如花在理解数据、清洗数据、构造特征上的时间来得有价值。一个特征构造得好,比如准确计算了“次均药品费用占比”或“住院期间并发症发生强度”,其带来的模型效果提升,可能比换好几个算法都明显。这个项目最终能产出有说服力的结论,八成功劳要归功于前期扎实的数据准备工作。接下来,我就把这个过程中的核心思路、具体操作以及踩过的坑,系统地梳理一遍,希望能给正在处理类似复杂、多源数据的你一些实在的参考。

2. 数据理解与清洗策略设计

2.1 数据源探查与问题诊断

我们拿到的数据通常不是从一个漂亮的Excel里导出来的,而是多个系统的“缝合怪”。在这个肿瘤诊疗项目中,数据主要来自三块:住院病案首页(包含诊断、手术、费用明细)、医保结算明细(包含报销比例、自付金额)、以及部分患者随访调查表(包含生活质量评分、后续治疗情况)。第一步不是写代码,而是“看”数据。

我用pandas加载数据后,第一件事就是用.info().describe().head()进行快速扫描。这里有几个关键发现和对应的处理策略:

  1. 缺失值模式分析:医疗数据中,缺失值很少是“完全随机缺失”的。例如,“化疗药物费用”字段的缺失,很可能意味着该患者未接受化疗;而“医保报销比例”缺失,则可能是数据录入遗漏。对于前者,我们选择用0或一个标志位(如“未发生”)进行填充,因为这本身就是一种有效信息。对于后者,则需要根据患者医保类型(从其他字段关联)进行合理插补或标记为待核查。
  2. 异常值识别:费用类数据是异常值的重灾区。一个简单的df[‘总费用’].describe()就能看到最大值可能高达均值的数十倍。这里不能武断地用3σ原则剔除,因为肿瘤治疗中,确实存在因病情复杂、使用昂贵靶向药或发生严重并发症而导致费用极高的真实个案。我的策略是结合业务逻辑设定阈值:比如,单日住院费用超过所在科室历史平均值的5倍,则标记出来,回溯原始病历进行人工复核。
  3. 数据一致性校验:不同来源的数据对同一实体的描述可能不一致。例如,病案首页的“主要诊断”是ICD-10编码,而医保数据里可能是中文名称。我们需要建立映射表进行统一。更棘手的是,患者的“住院ID”可能在两个系统里不完全匹配,这就需要通过“患者姓名”、“住院日期”等多个字段进行模糊匹配和关联。

注意:在医疗数据清洗中,任何直接删除或修改原始数据的操作都必须谨慎,并且最好保留修改日志。我通常会新建一个“数据清洗日志”的DataFrame,记录下每一行被修改的字段、原值、新值以及修改原因(如“根据业务规则填充”、“关联表匹配”)。

2.2 结构化清洗流程实施

明确了问题,就可以制定一个可重复的清洗流水线。我习惯用pandas配合自定义函数,构建一个模块化的清洗脚本。

import pandas as pd import numpy as np def load_and_inspect(data_path): """加载数据并输出基础信息""" df = pd.read_csv(data_path, encoding='gbk') # 医疗数据常用编码 print(f"数据形状: {df.shape}") print(df.info()) print(df.describe(include='all')) return df def handle_missing_values(df, strategy_dict): """根据字段策略处理缺失值""" for col, strategy in strategy_dict.items(): if strategy == 'zero_fill': df[col].fillna(0, inplace=True) elif strategy == 'mode_fill': df[col].fillna(df[col].mode()[0], inplace=True) elif strategy == 'forward_fill': df[col].fillna(method='ffill', inplace=True) elif isinstance(strategy, dict): # 基于分组的填充,如按科室填充平均费用 group_col = strategy['group_by'] fill_value = df.groupby(group_col)[col].transform('mean') df[col].fillna(fill_value, inplace=True) # 标记缺失也是一种处理方式 df[col+'_is_missing'] = df[col].isna().astype(int) return df def detect_and_cap_outliers(df, col, method='iqr', cap_quantile=0.99): """检测并处理异常值(缩尾处理)""" if method == 'iqr': Q1 = df[col].quantile(0.25) Q3 = df[col].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR # 对于费用数据,通常只处理极大值,极小值可能是数据错误 df[col] = np.where(df[col] > upper_bound, upper_bound, df[col]) elif method == 'quantile': upper_limit = df[col].quantile(cap_quantile) df[col] = np.where(df[col] > upper_limit, upper_limit, df[col]) return df # 示例调用 df_raw = load_and_inspect('住院费用.csv') strategy = {'西药费': 'zero_fill', '手术费': 'mode_fill', '日均费用': {'group_by': '科室'}} df_cleaned = handle_missing_values(df_raw, strategy) df_cleaned = detect_and_cap_outliers(df_cleaned, '总费用', method='quantile', cap_quantile=0.995)

这个流程的关键在于策略的可解释性。比如对“西药费”用0填充缺失,是基于“缺失即未发生”的业务判断。对“日均费用”按科室分组填充均值,是因为不同科室(如肿瘤内科和ICU)的费用基准差异巨大,整体均值填充会引入严重偏差。

3. 面向经济学分析的特征工程构建

数据洗干净了,只是得到了“干净的原料”。特征工程才是把这些原料烹饪成“模型可消化美食”的过程。对于肿瘤诊疗经济学分析,特征构建必须紧扣“经济”和“诊疗”这两个核心维度。

3.1 从原始字段到业务特征

原始数据字段多是记录性的,如“住院天数”、“总费用”、“药品费”。我们需要从中提炼出能反映诊疗模式、资源消耗效率和经济效益的衍生特征。以下是我构建的部分核心特征类别:

  1. 费用结构特征

    • 药品费用占比 = 药品费 / 总费用:反映治疗对药物的依赖程度。靶向治疗高的患者,此比例通常很高。
    • 检查费用占比:反映诊断和监测的强度。
    • 日均费用 = 总费用 / 住院天数:衡量医疗资源的日均消耗速度,是评估效率的关键指标。
    • 自付比例 = 自付金额 / 总费用:直接反映患者的经济负担。
  2. 诊疗过程特征

    • 是否手术:从手术费>0或手术名称非空衍生出的二值特征,是影响总费用的最显著因素之一。
    • 治疗阶段:根据住院次数、距离首次确诊时间等,人工定义或聚类生成“初始治疗”、“巩固治疗”、“姑息治疗”等阶段标签。
    • 并发症标志:从诊断编码中提取是否伴有严重感染、出血、器官衰竭等并发症,这些会大幅提升费用和降低疗效。
    • 多学科诊疗(MDT)标志:从会诊记录字段中提取,可能预示着更合理的资源规划和更好的预后。
  3. 时间序列与聚合特征(针对多次住院患者):

    • 累计住院天数累计总费用
    • 费用增长率:本次住院日均费用与前次住院日均费用的比值。
    • 住院间隔:两次住院之间的天数,可能反映病情稳定程度。
# 特征构造示例代码 def create_economic_features(df): """构造经济学相关特征""" # 费用结构 df['drug_cost_ratio'] = df['药品费'] / (df['总费用'] + 1e-5) # 防止除零 df['exam_cost_ratio'] = df['检查费'] / (df['总费用'] + 1e-5) df['avg_daily_cost'] = df['总费用'] / df['住院天数'] df['out_of_pocket_ratio'] = df['患者自付金额'] / (df['总费用'] + 1e-5) # 诊疗过程 df['is_surgery'] = (df['手术费'] > 0) | (df['手术名称'].notna()) # 基于ICD-10编码判断并发症 (简化示例) complication_codes = ['J18', 'I50', 'N17'] # 肺炎、心衰、肾衰编码示例 df['has_complication'] = df['次要诊断编码'].apply(lambda x: any(code in str(x) for code in complication_codes)) # 聚合特征(假设df已按患者ID和住院时间排序) df['cumulative_total_cost'] = df.groupby('患者ID')['总费用'].cumsum() return df

3.2 特征编码与标准化

构造好的特征里有分类变量(如治疗阶段肿瘤分期),也有数值变量。它们需要经过编码和标准化才能喂给模型。

  • 分类变量编码:对于有序分类(如肿瘤分期I, II, III, IV),我常用标签编码序数编码,因为其本身包含顺序信息。对于无序分类(如手术类型),必须使用独热编码。但要注意,如果类别很多,独热编码会造成维度爆炸。此时可以考虑对低频类别进行归并(如“其他”),或使用目标编码(需小心过拟合)。
  • 数值变量标准化:由于我们构造的特征量纲差异巨大(总费用可能几万,药品费用占比在0~1之间),必须进行标准化。我通常使用RobustScalerZ-Score标准化。对于存在明显偏态分布的费用数据,先取对数再标准化的效果往往更好,因为这能使其分布更接近正态。
from sklearn.preprocessing import OrdinalEncoder, OneHotEncoder, RobustScaler from sklearn.compose import ColumnTransformer # 假设已有特征DataFrame: X categorical_ordinal = ['肿瘤分期'] # 有序分类 categorical_nominal = ['手术类型', '参保类型'] # 无序分类 numerical = ['avg_daily_cost', 'drug_cost_ratio', 'cumulative_total_cost', '住院天数'] # 定义转换器 preprocessor = ColumnTransformer( transformers=[ ('ord', OrdinalEncoder(categories=[['I','II','III','IV']]), categorical_ordinal), ('nom', OneHotEncoder(handle_unknown='ignore', sparse_output=False), categorical_nominal), ('num', RobustScaler(), numerical) ]) X_processed = preprocessor.fit_transform(X)

实操心得:在划分训练集和测试集之前,就定义好ColumnTransformer,然后在训练集上fit_transform,在测试集上只做transform。这是避免数据泄露的铁律。特征工程的所有参数(如编码的类别、缩放器的均值和方差)都必须仅从训练集中学习。

4. 特征选择与评估

特征不是越多越好。无关或冗余的特征会降低模型效率,甚至导致过拟合。在经济学分析中,我们更追求特征的可解释性。

4.1 基于统计与模型的特征筛选

我通常会采用一个多阶段的筛选流程:

  1. 方差过滤:使用VarianceThreshold移除方差接近0的特征(例如,某个手术类型只有1个样本,独热编码后某一列全是0)。
  2. 相关性分析:计算特征与目标变量(如总费用住院天数)的相关系数(数值用Pearson,分类用Spearman或方差分析)。同时,检查特征之间的多重共线性。高相关的特征(如总费用药品费)可以只保留一个,或构建成比例特征(如之前提到的药品费用占比)。
  3. 模型特征重要性:使用一个简单的树模型(如ExtraTreesRegressor)快速训练,查看其feature_importances_属性。这能给出一个基于模型的特征效用初步排名。
  4. 递归特征消除:对于最终确定的模型,可以使用RFECV进行递归特征消除交叉验证,这是一个更精确但计算量更大的方法,它能找到最优特征子集。
from sklearn.feature_selection import VarianceThreshold, RFECV from sklearn.ensemble import ExtraTreesRegressor from sklearn.model_selection import train_test_split # 假设 X_processed, y 已准备好 X_train, X_test, y_train, y_test = train_test_split(X_processed, y, test_size=0.2, random_state=42) # 1. 方差过滤 selector_variance = VarianceThreshold(threshold=0.01) # 移除方差小于0.01的特征 X_train_variance = selector_variance.fit_transform(X_train) # 2. & 3. 使用树模型评估重要性 et_model = ExtraTreesRegressor(n_estimators=100, random_state=42) et_model.fit(X_train_variance, y_train) importances = et_model.feature_importances_ # 将重要性排序并与特征名对应... # 4. 递归特征消除 (耗时,建议在特征集不大时使用) rfecv = RFECV(estimator=et_model, step=1, cv=5, scoring='neg_mean_squared_error') rfecv.fit(X_train_variance, y_train) print(f"最优特征数量: {rfecv.n_features_}") X_train_selected = rfecv.transform(X_train_variance)

4.2 经济学意义的特征评估

除了统计指标,每个入选的特征都必须有合理的经济学或临床解释。例如,在最终模型中,如果并发症标志的特征权重很高,这符合“并发症增加治疗难度和成本”的常识。如果某个特征重要性高但难以解释,就需要回溯数据源头,检查是否引入了数据泄露或虚假关联。

我会制作一个“特征护照”表格,记录每个特征的定义、构造方法、预期方向(与目标正/负相关)、以及实际模型中的重要性排名。这份文档对于向临床专家或管理方解释模型至关重要。

特征名定义构造方法预期与总费用的关系实际重要性排名业务解释
avg_daily_cost日均费用总费用/住院天数正相关1直接反映资源消耗强度
has_complication是否发生并发症从诊断编码提取正相关2并发症显著增加治疗复杂度和成本
drug_cost_ratio药品费用占比药品费/总费用正相关3高占比可能意味着使用了昂贵靶向药
is_surgery是否接受手术手术费>0或手术名称非空正相关4手术本身及围手术期管理增加费用

5. 完整数据管道搭建与自动化

在实际项目中,数据清洗和特征工程不是一次性任务。新数据会不断产生,模型需要定期更新。因此,构建一个可复用的、自动化的数据管道是工程上的最佳实践。

我通常使用sklearn.pipeline.Pipeline将清洗、编码、特征构造、选择等步骤封装起来。虽然自定义的特征构造步骤需要写成Transformer,但这能保证整个流程的一致性和可重复性。

from sklearn.base import BaseEstimator, TransformerMixin from sklearn.pipeline import Pipeline # 自定义特征构造转换器 class EconomicFeatureCreator(BaseEstimator, TransformerMixin): def __init__(self): pass def fit(self, X, y=None): return self def transform(self, X): X_new = X.copy() # 这里放入之前 create_economic_features 函数中的逻辑 X_new['avg_daily_cost'] = X_new['总费用'] / X_new['住院天数'] # ... 其他特征构造 return X_new # 构建完整管道 full_pipeline = Pipeline([ ('economic_features', EconomicFeatureCreator()), ('preprocessor', preprocessor), # 前面定义的ColumnTransformer ('variance_selector', VarianceThreshold(threshold=0.01)), ('feature_selector', RFECV(ExtraTreesRegressor(), cv=5)) ]) # 在训练集上拟合整个管道 full_pipeline.fit(X_train, y_train) # 直接转换新数据 X_new_processed = full_pipeline.transform(X_new_raw)

这个管道的好处是,你保存full_pipeline这个对象后,对于任何新的原始数据,只需要调用一次.transform(),就能得到模型可以直接使用的、经过完全一致处理的特征向量,极大减少了出错的概率和重复劳动。

6. 常见陷阱与避坑指南

在肿瘤诊疗经济学分析的数据准备过程中,我踩过不少坑,这里总结几个最典型的:

  1. 泄露未来信息:这是最致命的错误。例如,用“本次住院总费用”去计算“本次住院的日均费用”是合理的。但如果你用“患者所有住院的总费用”的平均值作为特征,去预测其中某一次住院的费用,这就泄露了未来的信息。务必确保每个样本的特征信息仅来自于该样本发生之前或同时的数据。
  2. 忽略数据的时间戳:医疗数据有强烈的时间顺序。一定要确保在按时间划分训练集/测试集时,测试集的时间都在训练集之后,以模拟真实的预测场景。
  3. 过度清洗“异常值”:肿瘤治疗中,天价账单可能是真实且重要的个案,代表了某种极端但真实的诊疗路径。盲目剔除会损失关键信息,导致模型无法预测高值。更好的方法是将其保留,或使用更稳健的模型(如分位数回归)。
  4. 特征构造脱离业务:闭门造车构造的特征可能数学上漂亮,但业务上无法解释。例如,构造一个“费用与天数的非线性交互项”,如果临床专家无法理解其含义,这个特征在模型落地时就会受到质疑。一定要与领域专家保持沟通。
  5. 独热编码的维度灾难:对于像“手术操作编码”这样可能有上百个类别的字段,直接独热编码会产生大量稀疏列。可以考虑:a) 只保留高频类别,其余归为“其他”;b) 使用编码体系的上层类别(如将具体手术编码映射到“根治术”、“姑息术”、“探查术”等大类);c) 使用嵌入或目标编码(需谨慎防止过拟合)。

数据处理和特征工程是一项兼具艺术性和科学性的工作,尤其在医疗经济领域,它要求我们在技术严谨性和业务洞察力之间找到平衡。没有一劳永逸的模板,最好的方法就是深入理解你的数据来源、业务背景,然后通过迭代式地探索、构造、验证,逐步打磨出那些能让模型“发光”的特征。这个过程很耗时,但当你看到模型因为一个精心构造的特征而性能大幅提升时,那种成就感是无可替代的。

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

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

立即咨询