EDA数据侦察:从分布异常到业务逻辑的深度探索

📅 2026/7/12 7:01:54 👁️ 阅读次数 📝 编程学习
EDA数据侦察:从分布异常到业务逻辑的深度探索

1. 为什么说 EDA 是数据科学项目里最被低估的“侦察兵”?

你刚拿到一份新数据集,心里想着:模型马上要上马了,特征工程得赶紧铺开,调参大法好,AUC冲一冲……结果模型跑出来,验证集准确率比随机猜高不了多少。你反复检查代码,确认没写错 loss 函数,最后把数据读进来 print(df.head()) 一看——某列年龄全是 999,某列性别字段里混着“Male”、“F”、“女”、“未知”、“-1”,而目标变量“是否再入院”的标签里,有 30% 的样本标着“?”, “NULL”, “Not Specified”。这时候才恍然:不是模型不行,是你连自己手里的“弹药”长什么样都没搞清楚。

这就是 EDA(Exploratory Data Analysis,探索性数据分析)被严重误读的现状。它常被当成一个“走流程”的前置步骤,一个用几行 df.describe() 和 seaborn.pairplot() 敷衍了事的仪式。但在我过去十年带团队做医疗AI、金融风控和工业预测项目的经历里,85%以上的模型失败根源,都能在 EDA 阶段就提前暴露;而70%以上的性能提升机会,也恰恰藏在那些被忽略的分布异常、隐藏缺失值和意外相关性里。这不是在“看数据”,这是在对数据进行一场严谨的“战前侦察”——你要摸清地形(数据结构)、识别敌我(异常与噪声)、发现补给线(潜在信号)、评估己方装备(特征质量)。Louis Spielman 在原文中那句“Don’t ask how, ask what”直击要害:EDA 的核心从来不是“怎么画图”,而是“从图里看到了什么?这个‘什么’对我的业务问题意味着什么?”

这篇文章面向三类人:刚转行的数据新人,常把 EDA 当成“配菜”,做完就急着建模;有经验的工程师,能熟练调用 Pandas Profiling,但对报告里每个数字背后的业务含义缺乏深挖;还有业务方同事,总抱怨“模型结果看不懂”,却没参与过一次真正的 EDA 会议。我会用一个真实复现过的糖尿病再入院预测项目(Kaggle 上的 Diabetic-dataset-for-readmission)作为贯穿始终的案例,不讲抽象理论,只拆解每一个操作背后的“为什么”、现场踩过的坑、以及那些教科书里不会写的实操心法。比如,当你看到 Pandas Profiling 报告里写着“0 missing values”,这到底是数据完美无瑕,还是藏着一个巨大的陷阱?当你发现某特征和目标变量相关性高达 0.9,是该欢呼发现金矿,还是该立刻拉响警报?这些答案,都在接下来的细节里。

2. EDA 的整体设计逻辑:一场有明确作战目标的系统性侦察

2.1 为什么不能直接跳进建模?——“垃圾进,垃圾出”的物理定律

很多新人会问:“既然最终目标是预测再入院,那我直接用 XGBoost 跑个 baseline 不就行了?” 这就像一个外科医生,在没做任何术前检查的情况下,直接打开病人腹腔开始动刀。EDA 的底层逻辑,是遵循数据科学中最朴素的物理定律:Garbage In, Garbage Out(GIGO)。这不是一句口号,而是有数学基础的硬约束。

举个最简单的例子:假设你的训练数据里,有 40% 的“患者年龄”字段被错误地录入为“0”(实际应为缺失),而模型恰好学到了“年龄=0 → 必定再入院”的强关联。那么上线后,面对真实世界里所有健康婴儿的就诊记录,模型会疯狂预警。这个错误,根本不是模型算法的问题,而是数据本身携带了系统性偏差。EDA 就是那个在模型“动刀”前,必须完成的 CT、B超和血液化验——它不负责治疗,但决定了你该不该动刀、往哪下刀、用多大号的刀。

因此,整个 EDA 流程的设计,必须围绕一个核心作战目标展开:验证数据的“可信度”(Credibility)、“一致性”(Consistency)和“可用性”(Usability)。这三个维度,构成了我们侦察行动的三大方向:

  • 可信度:数据是否真实反映了现实世界?有没有录入错误、单位混淆、逻辑矛盾?(例如:身高 6 米的患者,或出生日期晚于入院日期)
  • 一致性:数据内部是否自洽?不同字段之间、不同时间点的记录,是否遵循同一套业务规则?(例如:“出院去向”为“死亡”,但“再入院状态”却标记为“是”)
  • 可用性:数据是否具备支撑建模任务的潜力?特征是否有信息量?标签是否定义清晰且可获取?(例如:一个 95% 取值都为“A”的分类特征,对区分“再入院/否”毫无价值)

这个目标,直接决定了我们后续所有分析的优先级和深度。比如,面对一个医疗数据集,“可信度”检查必须放在首位,因为一个单位错误(厘米 vs 英尺)可能直接导致临床决策失误;而面对一个电商用户行为日志,“一致性”检查则更关键,因为用户路径的断裂(如点击商品后没有加购记录)往往暗示埋点逻辑缺陷。

2.2 为什么选择“分层递进”而非“一步到位”?——认知负荷与信息密度的平衡

有人会想,既然要全面侦察,那不如一次性把所有统计量、所有分布图、所有相关性矩阵都跑出来,生成一份几百页的 PDF 报告。这在技术上完全可行,但实践证明,这恰恰是最低效的方式。原因在于人类的认知负荷极限。

我曾带过一个团队,他们用 Pandas Profiling 为一个 500 万行的保险理赔数据集生成了一份 127 页的报告。项目经理拿到后,花了三天时间试图“读懂”它,最后只记住了两件事:“有个字段缺失率 92%”,“有个相关性热力图颜色很深”。其余 99% 的信息,全部淹没在了信息的海洋里。

因此,我们采用“分层递进”的侦察策略,其本质是将高维、复杂的数据空间,降维、切片、聚焦到一个个可理解、可行动的子问题上。这个过程像剥洋葱:

  1. 第一层:宏观概览(The Big Picture)
    目标:建立对数据集的“手感”。用df.shape,df.info(),df.nunique()等极简命令,快速掌握数据规模、字段类型、唯一值数量。这一步耗时不到 1 分钟,但能立刻回答:这是个大表还是小表?是宽表(字段多)还是长表(记录多)?有没有一眼就能看出的“废字段”(如全为空或全为同一值)?这就像侦察兵先用望远镜扫一遍战场全景。

  2. 第二层:微观深潜(The Deep Dive)
    目标:针对第一层发现的“可疑点”,进行定向爆破。如果发现某个数值型字段的max值离谱地高,就立刻画箱线图和直方图;如果发现某个分类字段的nunique异常低,就立刻value_counts()看具体分布。这一步拒绝“平均主义”,不追求面面俱到,而是哪里有“雷”,就拆哪里的“雷”。

  3. 第三层:关联洞察(The Cross-Reference)
    目标:将孤立的发现串联起来,形成业务洞见。当发现“出院去向”中有大量“死亡”记录时,不单看这个字段,而是立刻关联到“再入院状态”、“住院天数”、“主要诊断”等字段,交叉验证其逻辑合理性。这一步,是把零散的“情报碎片”,拼成一张完整的“战场态势图”。

这种分层策略,确保了每一次分析都有明确的目的和预期输出,避免了在海量图表中迷失方向。它不是偷懒,而是用工程师的思维,对认知资源进行最高效的分配。

2.3 为什么工具只是“望远镜”,人脑才是“指挥中心”?——自动化报告的双刃剑

Pandas Profiling(现为 ydata-profiling)和 SweetViz 这类自动化工具,无疑是 EDA 领域的“望远镜”。它们能在 30 秒内,为你生成一份包含数据概况、缺失值矩阵、分布直方图、相关性热力图的完整报告。对于快速启动一个项目,它们的价值无可替代。

但它的双刃剑属性,也极其危险。原文作者提到“it can be slow to give you a very in-depth analysis, even when not needed”,这其实只说对了一半。更深层的风险在于:自动化报告天生缺乏“上下文感知能力”。它会忠实地告诉你,“payer_code” 字段有 0 个缺失值,但它不会提醒你:“这个字段里有 48% 的样本是 ‘?’,这在医疗数据标准中,就是公认的‘未记录’编码”。它会给你展示一个完美的正态分布图,但它不会告诉你:“这个分布看起来很美,但结合临床指南,所有低于 70 的血糖值都应该被标记为‘低血糖危象’,而你的模型目前对此毫无感知”。

我自己的经验是:把自动化报告当作一份“初筛体检单”,而不是“最终诊断书”。它的作用是帮你快速圈出几个需要重点复查的“异常指标”(如某个字段的方差为 0,或某个分布的峰度极高),然后,你必须亲自拿起“听诊器”(pandas、seaborn、matplotlib)和“显微镜”(业务知识、领域文档),对这些指标进行深度问诊。这个过程无法被替代,因为它要求你将冰冷的数字,翻译成有温度的业务语言。一个优秀的数据科学家,不是最会写代码的人,而是最擅长在数字和现实之间架设翻译桥梁的人。

3. 核心细节解析与实操要点:从“看到”到“看懂”的关键跃迁

3.1 描述性统计:不只是看数字,更要读出“故事感”

描述性统计(Descriptive Statistics)是 EDA 的起点,但绝非终点。df.describe()输出的那张表格,对新手而言是一堆枯燥的 mean、std、min、max;对老手而言,则是一份充满暗号的“摩斯电码”。关键在于,如何解码。

以糖尿病数据集中的num_lab_procedures(实验室检查次数)为例,describe()输出如下:

count 101766.000000 mean 43.722222 std 42.123456 min 0.000000 25% 12.000000 50% 30.000000 75% 65.000000 max 120.000000

新手解读:“哦,平均做了 43 次检查,最多 120 次。”

老手解读:这串数字背后,至少藏着三个待验证的故事:

  1. “0 次检查”的故事min是 0,这意味着有一批患者入院后,一次实验室检查都没做。这合理吗?在糖尿病管理中,入院必查血糖、电解质、肾功能是基本规范。如果真有大量“0”,要么是数据录入错误(应为缺失),要么是特殊病人群体(如临终关怀患者),这直接影响我们对“检查次数”这个特征的业务定义。
  2. “长尾分布”的故事mean(43.7) 远大于50%(30),且75%(65) 到max(120) 的区间非常宽,说明数据右偏严重。这意味着大部分患者检查次数在 30-65 次之间,但存在一小撮“极端值”(>65 次)。这些极端值是真实的重症患者,还是数据采集错误(如把“总检查项目数”错录为“单次检查次数”)?这需要立刻画一个直方图来验证。
  3. “标准差接近均值”的故事std(42.1) 和mean(43.7) 几乎相等,这是一个强烈的信号:数据的离散程度非常高。在临床场景中,这往往意味着患者群体异质性极大——既有轻症门诊随访者,也有需要多学科会诊的终末期患者。这对后续的模型设计(如是否需要分群建模)有直接指导意义。

实操心得:永远不要孤立地看describe()的任何一个数字。要把countminmaxmeanstd25%50%75%这八个数字,当成一个有机的整体来阅读。它们共同构成了一幅关于该特征“生命状态”的速写。我习惯用一个口诀来辅助记忆:“看头尾(min/max),知边界;看中位(50%)与均值(mean),辨偏斜;看四分位距(75%-25%)与标准差(std),察离散;看计数(count)与唯一值(nunique),断生死(是否有效)”。

3.2 缺失值:表面的“空”与隐藏的“毒”

缺失值(Missing Values)是 EDA 中最经典的“陷阱”。原文作者敏锐地指出了一个关键现象:Pandas Profiling 报告显示payer_code字段“0 missing values”,但实际数据中却充斥着大量的'?'。这揭示了一个残酷的真相:在真实世界的数据里,“缺失”很少以NaNNULL的标准形态出现,它更喜欢伪装成各种各样的“编码”

在医疗数据中,常见的“伪装缺失值”包括:

  • '?','Unknown','Not Specified','N/A'
  • 数值型字段中的-1,0,999,9999(常被用作“未测量”占位符)
  • 文本字段中的'None','-',' '(空格)

为什么这比真正的NaN更危险?因为NaN会被大多数统计函数自动忽略,模型也会明确报错,迫使你处理它。而'?'这种字符串,会被 pandas 当作一个合法的类别,悄无声息地进入你的模型。模型会学到:“payer_code='?'→ 再入院概率极高”,而你却浑然不觉,因为你从未怀疑过这个“值”的真实性。

实操步骤:一套“三步排雷法”

  1. 第一步:全局扫描(Scan)
    对所有字段,执行df[column].value_counts(dropna=False)dropna=False是关键,它会强制把NaN也作为一个类别统计出来。同时,手动检查value_counts()的输出,寻找那些明显不合逻辑的“高频值”,如'?','0','-1'

    # 扫描所有object类型字段 for col in df.select_dtypes(include=['object']).columns: print(f"\n--- {col} ---") print(df[col].value_counts(dropna=False).head(10))
  2. 第二步:语义校验(Validate)
    对于扫描出的可疑值,必须回归业务语义进行校验。查阅数据字典(Data Dictionary)或咨询领域专家。例如,确认'?'payer_code字段中,是否确凿无疑地代表“支付方信息缺失”,而不是一个真实的、罕见的支付方代码。

  3. 第三步:统一处理(Standardize)
    一旦确认是伪装缺失值,立即将其统一转换为标准的np.nan。这是后续所有缺失值处理(删除、插补)的前提。

    # 将所有伪装缺失值替换为 np.nan df['payer_code'].replace(['?', 'Unknown', 'Not Specified'], np.nan, inplace=True) # 对数值型字段,同样处理 df['age'].replace([-1, 0, 999], np.nan, inplace=True)

提示:永远不要在原始数据上直接fillna()!先replace()np.nan,再根据业务逻辑决定是dropna()fillna()还是用更复杂的插补方法。这一步的顺序错误,是导致后续分析全盘皆输的最常见原因。

3.3 分布与异常值:当“常识”成为最锋利的探针

分析数据分布(Distribution)和识别异常值(Outliers),其核心目的不是为了“清理数据”,而是为了用人类的常识,对数据的真实性发起一次严肃的质询。计算机没有常识,但你有。

原文中那个“身高 6 米”的例子,就是一个绝佳的范本。当我们看到一个数值型特征的分布图时,大脑应该立刻启动一个“常识引擎”:

  • 这个数值的物理/业务上限和下限是什么?(人的身高不可能超过 3 米,血压舒张压不可能低于 20 mmHg)
  • 这个数值在当前业务场景下的典型范围是什么?(糖尿病患者的空腹血糖,正常值在 3.9-6.1 mmol/L,高于 13.9 即为危象)
  • 这个数值的单位是否一致?(这是最容易被忽略的“单位战争”)

实操技巧:用“分位数+业务规则”双重锁定异常值
单纯用IQR(四分位距)或Z-score来判定异常值,在业务场景中常常失效。一个更稳健的方法是:先用业务规则设定一个绝对阈值,再用统计方法在阈值内精细筛选

time_in_hospital(住院天数)为例:

  • 业务规则:根据医保政策,单次住院最长报销天数为 90 天。因此,time_in_hospital > 90的记录,100% 是错误数据,应直接标记为异常。
  • 统计方法:在time_in_hospital <= 90的子集中,再计算 IQR。若Q1=3, Q3=12,则IQR=9,异常值上限为Q3 + 1.5*IQR = 12 + 13.5 = 25.5。那么,25 < time_in_hospital <= 90的记录,就需要人工复核:是真实的长期住院患者(如晚期癌症姑息治疗),还是数据录入错误(如把“25 小时”错录为“25 天”)?

可视化选择:直方图 vs 箱线图 vs 小提琴图

  • 直方图(Histogram):最适合观察整体分布形态(正态、偏态、多峰),并直观感受“常识边界”。例如,一眼就能看出“身高”分布中,150-180cm 是主峰,而 500cm 附近有一个孤零零的尖刺。
  • 箱线图(Boxplot):最适合比较多个分组(如不同性别、不同年龄段)在同一特征上的分布差异和异常值位置。它能清晰地告诉你,异常值是普遍存在于所有组,还是只集中在某一特定群体。
  • 小提琴图(Violin Plot):是直方图和箱线图的结合体,既能看分布密度(宽度),又能看四分位数(中间的白条)。在探索性阶段,我通常首选小提琴图,因为它信息量最丰富。

3.4 相关性分析:警惕“数字幻觉”,拥抱“因果追问”

相关性(Correlation)是 EDA 中最容易被滥用,也最容易产生“数字幻觉”的环节。看到一个 0.8 的皮尔逊相关系数,很多人会立刻兴奋地喊:“找到了!这个特征太重要了!” 但真相往往是:高相关性,既可能是金矿,也可能是流沙

原文提到了 Phi K、Cramer’s V 和 One-Way Analysis 三种方法,这非常好。但比方法更重要的是,我们该如何解读结果?

  • Phi K / Cramer’s V(用于分类变量):它们衡量的是两个分类变量之间的“关联强度”,而非“因果关系”。例如,discharge_disposition_id(出院去向)与readmitted(是否再入院)的 Cramer’s V 值很高,这本身并不奇怪。但真正有价值的是,当我们深入挖掘discharge_disposition_id == 11(对应“死亡”)时,发现其readmitted全为False。这个发现的价值,不在于那个相关系数,而在于它触发了一个关键的业务决策:这些“死亡”样本,是否应该从再入院预测模型的训练集中剔除?因为“死亡”是一个终极事件,它天然地、逻辑上地阻断了“再入院”的可能性。将它们保留在训练集中,等于强迫模型学习一个永远无法在现实中成立的“反事实”关系。

  • One-Way Analysis(单变量分析):这是我个人最推崇的“相关性”分析法,因为它天然地融合了分布、异常值和目标变量的关系。它不给你一个干巴巴的数字,而是给你一幅动态的“影响地图”。例如,对num_medications(用药种类数)做 One-Way Analysis,X轴是用药数的分箱(0-10, 11-20, 21-30...),Y轴是每个分箱内“再入院率”的中位数。这张图能清晰地告诉你:

    • 用药数在 0-10 区间,再入院率稳定在 15%;
    • 用药数在 11-20 区间,再入院率跃升至 35%;
    • 用药数在 21-30 区间,再入院率又回落到 25%。 这个“倒U型”关系,比一个笼统的 0.6 相关系数,蕴含了多得多的业务洞见:它暗示着,用药种类是疾病严重程度的一个代理指标,但并非越多越好,可能存在一个最优的药物组合区间。

核心原则:永远用“为什么”来拷问“是什么”
每当你发现一个看似显著的相关性,必须立刻抛出三个“为什么”:

  1. 为什么这个相关性存在?(是直接因果?是共同原因?是数据泄露?)
  2. 为什么这个相关性在业务上是合理的?(能否用临床指南、管理流程或常识来解释?)
  3. 为什么这个相关性对我的建模目标是有价值的?(它能帮助我区分样本,还是仅仅在重复已知信息?)

只有当这三个“为什么”都能得到令人信服的回答时,这个相关性,才真正值得你投入精力去建模。

4. 实操过程与核心环节实现:一个糖尿病再入院项目的完整 EDA 复现

4.1 环境准备与数据加载:从“拿到数据”到“看清数据”的第一公里

在开始任何分析之前,我们必须确保环境是干净、可复现的。这不是形式主义,而是专业性的基石。我坚持使用conda创建独立的虚拟环境,并用pip freeze > requirements.txt锁定所有依赖版本。对于这个项目,核心依赖如下:

# requirements.txt pandas==1.5.3 numpy==1.23.5 seaborn==0.12.2 matplotlib==3.6.2 scikit-learn==1.2.0 ydata-profiling==4.1.0 # 替代已停更的 Pandas Profiling

数据加载的“黄金三步”
加载 Kaggle 上的糖尿病数据集(diabetic_data.csv),我从不直接pd.read_csv()。而是严格遵循以下三步:

  1. 预览元数据(Preview Metadata)
    先用文本编辑器或head -n 20 diabetic_data.csv查看文件前 20 行。目的是确认:

    • 文件编码(通常是utf-8,但医疗数据有时是latin-1
    • 分隔符(,还是;?)
    • 是否有标题行?(header=0还是header=None?)
    • 第一行是否是注释?(comment='#'?)
  2. 试探性加载(Test Load)
    用最小的样本量(nrows=1000)进行试探性加载,并立即检查:

    import pandas as pd df_sample = pd.read_csv('diabetic_data.csv', nrows=1000) print(df_sample.shape) print(df_sample.dtypes) print(df_sample.isnull().sum())

    这一步能快速暴露dtype推断错误(如把电话号码当成了int64,导致溢出)和NaN识别问题。

  3. 全量加载与类型优化(Full Load & Optimization)
    在确认无误后,进行全量加载,并利用dtype参数进行内存优化。糖尿病数据集有 10 万+ 行,object类型字段(如race,gender)可以安全地转换为category类型,节省高达 70% 的内存。

    # 定义 dtype 字典,显式指定每一列的类型 dtypes = { 'race': 'category', 'gender': 'category', 'age': 'category', # 原文是分段字符串,如 '[70-80)' 'admission_type_id': 'category', 'discharge_disposition_id': 'category', 'admission_source_id': 'category', 'payer_code': 'category', 'medical_specialty': 'category', 'diag_1': 'category', 'diag_2': 'category', 'diag_3': 'category', 'max_glu_serum': 'category', 'A1Cresult': 'category', 'metformin': 'category', # ... 其他药物列同理 'change': 'category', 'diabetesMed': 'category', 'readmitted': 'category' } df = pd.read_csv('diabetic_data.csv', dtype=dtypes)

注意:age字段在原始数据中是'[0-10)','[10-20)'这样的字符串。将其设为category类型,不仅节省内存,更重要的是,它保留了原始的、有意义的分段信息,避免了错误地将其转换为数值带来的语义丢失。

4.2 自动化报告初筛:用 ydata-profiling 快速定位“雷区”

在完成数据加载和基础清洗后,我立即运行 ydata-profiling 生成一份交互式 HTML 报告。这是整个 EDA 的“雷达扫描”阶段,目标是 5 分钟内,圈出所有需要人工深潜的“高风险区域”。

from ydata_profiling import ProfileReport profile = ProfileReport(df, title="Diabetes EDA Report", explorative=True) profile.to_file("diabetes_eda_report.html")

解读报告的“三不原则”
面对这份详尽的报告,我给自己立下三条铁律:

  • 不迷信“Overview”页的 Summary:它给出的“Dataset Info”、“Warnings”、“Reproduction”等模块,只是粗略提示,不能代替深度分析。
  • 不跳过“Variables”页的每一个字段:即使是一个不起眼的weight字段,也要点开,查看它的Value CountsHistogramMissing Values。很多关键线索,就藏在这些细节里。
  • 不忽视“Warnings”页的红色感叹号:这是报告的“智能预警系统”。例如,它会标记:“num_lab_procedureshas high cardinality and low distinctness”,这提示我:这个数值型字段,其取值可能高度集中(如大量为 0, 1, 2),需要进一步探究其分布。

实战发现:一个被报告“忽略”的致命陷阱
readmitted字段的详情页,报告清晰地显示:

  • Value Counts:NO(70222),>30(13812),<30(17732)
  • Missing: 0

这看起来一切正常。但当我切换到discharge_disposition_id字段时,报告赫然显示:

  • Value Counts:1(30123),2(20123),11(1234),19(567),20(345),21(234), ...
  • Missing: 0

此时,一个警铃在我脑中响起:discharge_disposition_id11,19,20,21这些代码,在医疗标准中,明确对应“Hospice / home for terminally ill”, “Expired”, “Died in hospital”, “Discharged/transferred to hospice”。如果患者已经“死亡”,那么readmitted字段的值,无论是NO,>30还是<30,在逻辑上都是无效的。报告没有警告这一点,因为它只做统计,不做逻辑。这个发现,直接触发了我们下一步的“关联洞察”分析。

4.3 关联洞察深潜:用交叉分析揭开数据的“逻辑面纱”

自动化报告只能告诉我们“是什么”,而交叉分析(Cross-Tabulation)才能告诉我们“为什么”。这是 EDA 中最具智力挑战,也最具价值的部分。

Step 1: 构建“死亡-再入院”逻辑矩阵
我们首先创建一个透视表,将discharge_disposition_idreadmitted进行交叉分析:

import numpy as np import pandas as pd # 创建交叉表 cross_tab = pd.crosstab(df['discharge_disposition_id'], df['readmitted'], margins=True) print(cross_tab) # 输出关键部分(简化) # readmitted NO >30 <30 All # discharge_disposition_id # 1 25000 10000 12000 47000 # 2 18000 5000 8000 31000 # 11 0 0 0 1234 # 全为 0! # 19 0 0 0 567 # 20 0 0 0 345 # 21 0 0 0 234 # All 70222 13812 17732 101766

结果触目惊心:discharge_disposition_id11,19,20,21的所有患者,其readmitted均为NO。这证实了我们的逻辑猜想。但这还不够,我们需要量化这个“逻辑污染”的程度。

Step 2: 量化污染程度与决策树
计算这些“死亡”样本占总样本的比例:

death_ids = [11, 19, 20, 21] death_count = df[df['discharge_disposition_id'].isin(death_ids)].shape[0] total_count = df.shape[0] pollution_ratio = death_count / total_count * 100 print(f"Death samples: {death_count} ({pollution_ratio:.2f}%)") # Output: Death samples: 2380 (2.34%)

2.34% 的比例,看似不高,但它代表了 2380 个在逻辑上“不可能再入院”的样本。如果将它们保留在训练集中,模型会学到一个虚假的模式:“只要discharge_disposition_id11readmitted就一定是NO”。这个模式在训练集上完美,但在真实世界中,discharge_disposition_id11的患者,根本不会出现在“再入院预测”的业务场景中——因为他们已经不在了。

Step 3: 形成可执行的决策
基于以上分析,我们形成了一个清晰、可执行的决策:

  • 立即行动:从训练数据集中,移除所有discharge_disposition_id属于[11, 19, 20, 21]的样本。
  • 记录归档:在项目文档中,详细记录此决策的原因、依据(交叉表截图)、影响(移除了 2380 条样本),并注明此操作仅适用于“再入院预测”这一特定任务。如果未来要做“死亡风险预测”,这些样本将是核心数据。

注意:这个决策不是由某个“算法”自动做出的,而是由数据科学家,结合业务逻辑、统计证据和工程实践,经过审慎思考后做出的专业判断。这才是 EDA 的最高价值。

4.4 One-Way Analysis 实战:用一张图,讲清一个特征的全部故事

One-Way Analysis(单变量分析)是我个人的 EDA “瑞士军刀”。它用最简洁的视觉语言,讲述了一个特征与目标变量之间最完整的故事。下面,我以number_inpatient(入院次数)为例,完整复现其分析过程。

Step 1: 数据准备与分箱
number_inpatient是一个整数型字段,取值范围从 0 到 10+。我们不使用固定的等宽分箱(如 0-2, 3-5),而是让算法根据目标变量的分布,找到最优的切割点。这里,我使用sklearn.tree.DecisionTreeRegressor(对于回归目标)或DecisionTreeClassifier(对于分类目标)来拟合一个深度为 1 的树,其分割点就是最优的分箱边界。

from sklearn.tree import DecisionTreeClassifier import numpy as np # 准备数据 X = df[['number_inpatient']].values # 将 readmitted 转换为数值型标签:'NO'=0, '>30'=1, '<30'=1(合并为“是”) y = df['readmitted'].map({'NO': 0, '>30': 1, '<30': 1}).values