很多初学者拿到一份数据集后的第一反应,是赶紧把模型跑起来。这个反应本身没有错,但往往也是项目翻车的起点。最近在跟着 CampusX 的 100 天机器学习计划推进,第 19 天的主题正是“理解你的数据”。这一阶段不讲算法公式,也不碰模型调参,只做一件事:在动手训练之前,先把数据本身读明白。
从实际项目看,绝大多数第一次做机器学习作业或比赛的人,最后悔的不是模型选得不好,而是没有在数据阶段发现该处理的缺失值、异常分布和目标变量不平衡。本文会围绕数据理解这件事,讲清楚它到底包含哪些步骤,为什么值得在建模前认真做一遍,然后给出一套可以直接照着写代码的流程,用一份经典数据集跑通整个过程。无论你是刚入门机器学习,还是已经能跑通一些模型但总感觉效果不稳定,这篇文章都值得收藏备用。
1. 拿到数据就训练,是机器学习入门最大的坑
先看一个很常见的场景。很多人在学习机器学习时,拿到一份数据集,比如泰坦尼克号生存预测、房价预测或者垃圾邮件分类,第一件事就是导入 RandomForest、SVM 或者 XGBoost。本地调参折腾了好几天,训练集上的准确率看起来不错,一提交或者换到新数据上,效果立刻打回原形,甚至不如一个简单的规则基线。
问题往往不在模型本身,而在数据理解不足。
模型只是机器学习流程中的一环。一个典型的监督学习项目,完整链路是:数据采集、数据理解、数据清洗、特征工程、模型训练、评估、迭代部署。如果把大量时间花在最后几步,而跳过数据理解,相当于盖楼时没有认真看地基的地质报告。地基里有空洞、有软土,你却在上面选择用什么牌子的水泥,选得再好也没有意义。
数据理解在机器学习工程里通常被叫做 EDA(Exploratory Data Analysis,探索性数据分析)。它的核心不是“画几张图看看”,而是系统性地回答:这份数据到底长什么样、质量如何、有哪些规律、有哪些坑。CampusX 的 100 天计划把“理解你的数据”单独列为第 19 天的主题,也是在提醒学习者:数据理解不是可有可无的前奏,而是需要有意识、有方法、有产出物的一项硬功夫。
这里可以给一个明确的判断:对于入门阶段的项目,把数据理解做扎实,比盲目调参对模型效果的提升更明显。因为数据理解决定了特征工程的方向,而特征工程决定了模型能达到的上限。你后面用线性回归、决策树还是神经网络,都是在逼近这个上限。上限本身,早在你理解数据的那一刻就基本定下来了。
2. 理解你的数据,到底在理解什么
有人会觉得,理解数据不就是用 df.head() 看几行、用 df.describe() 看几个统计量吗?这只是表面动作。真正的数据理解,是在回答一组非常具体的问题。
一份数据集通常包含多个字段,每个字段背后都有对应的业务含义。理解数据,就是要弄清楚这些问题:
- 数据长什么样:有多少行、多少列、每个字段的含义是什么。
- 每个字段是什么类型:数值型、分类型、时间型,还是文本型。
- 数据有多完整:是否存在缺失值、重复值、异常值,占比如何。
- 目标变量长什么样:如果是分类问题,类别是否均衡;如果是回归问题,分布是否偏斜。
- 特征之间存在什么关系:是否有高度相关的特征,是否存在冗余。
- 哪些特征与目标变量相关:哪些字段可能真正影响预测结果。
这几个问题不是一次性看完,而是层层递进。先看整体结构,再看单特征分布,最后看特征与特征、特征与目标之间的关系。每一层都在为下一步做准备。
为什么这一步决定了模型上限?最简单的解释是:垃圾进,垃圾出。如果某个特征缺失了 80%,你直接把它喂给模型,模型只能学到大量噪声;如果某个分类特征有 50 个离散取值,但大部分样本落在其中 3 个取值上,不做处理会让模型过拟合;如果目标变量严重不平衡,模型的预测结果会偏向多数类,准确率很高但实际没有价值。
更重要的是,数据理解能帮你避免数据泄漏。比如在做时间序列预测时,不小心把未来信息混进了训练集;或者在做数据预处理时,用全量数据的均值和标准差去做标准化,再切分训练集和测试集。这些错误都在数据理解阶段容易被发现,一旦到了模型阶段,排查成本会高很多。
所以,数据理解并不是一个“走流程”的步骤,它的产出质量,直接决定了后续每一步是事半功倍还是事倍功半。
3. 环境准备与数据集选择
3.1 Python 环境与依赖库
做数据理解最常用的语言是 Python,核心库是 pandas、numpy、matplotlib、seaborn。pandas 负责表格数据处理,numpy 负责数值计算,matplotlib 和 seaborn 负责可视化。建议使用 Jupyter Notebook 做交互式探索,因为每一步输出都能直接看到,方便边分析边记录结论。如果你更习惯 VS Code 或 PyCharm,也完全可以,只是需要把代码写成脚本运行。
创建虚拟环境并安装依赖的命令如下:
# 建议先创建虚拟环境 python -m venv ml_data_env # Windows 激活 ml_data_env\Scripts\activate # macOS / Linux 激活 # source ml_data_env/bin/activate # 安装依赖 pip install pandas numpy matplotlib seaborn版本不需要刻意追求最新。pandas、seaborn 的 API 在近几个版本里保持稳定,本文示例代码只使用通用接口,你本地的版本只要不是特别旧,都能正常运行。如果是在校学生或公司内网环境,离线安装时优先选择 Python 3.8 以上版本,并且注意安装顺序:先 numpy,再 pandas,最后 seaborn 和 matplotlib。
3.2 数据集说明
本文选用的示例数据集是 Titanic(泰坦尼克号乘客生存预测),它几乎是机器学习入门最经典的数据集之一。选择它有三个原因:字段类型丰富,同时包含数值型特征和分类型特征;存在明显的缺失值,可以用来讨论数据质量问题;目标变量 survived 是二分类,便于观察类别分布和特征关系。
获取方式有两种。第一种是直接使用 seaborn 内置的数据集,代码简单,适合快速演示:
import seaborn as sns df = sns.load_dataset("titanic")第二种是去 Kaggle 下载原始的 train.csv,然后用 pandas 读取。seaborn 内置版本做了部分整理,字段与 Kaggle 版本略有差异,但核心字段基本一致。如果你本机无法访问外网,可以直接用本地 csv 文件。下面示例以 seaborn 内置数据集为主,如果你使用本地文件,只需要把加载方式改成:
import pandas as pd df = pd.read_csv("train.csv")后续所有操作与文件来源无关,分析思路完全一致。
4. 数据理解流程拆解:从结构到目标变量
数据理解的核心方法论是:由粗到细,由整体到局部。不要一开始就钻进某一个字段的细节,而是先建立全局视图,再逐层深入。下面按步骤拆解。
4.1 查看数据形状与字段清单
拿到数据后,第一件事是确认数据的规模。df.shape 返回 (行数, 列数),行数代表样本量,列数代表特征数量。样本量决定了你后续可以选择的模型复杂度,特征数量则决定了是否需要做降维或特征筛选。
# 查看数据形状和字段 print("数据形状:", df.shape) print("字段列表:", df.columns.tolist()) # 查看前几行 df.head()这一步输出的价值在于,让你知道接下来要面对多大的数据范围。Titanic 数据集大概有 800 到 900 行,字段在 10 到 15 个之间。如果是几万行高维表格,后续分析策略就要优先考虑效率和下游存储问题,比如采样或分布式处理。
4.2 识别特征类型
用 df.info() 可以一次性查看每个字段的非空数量和数据类型。这一步非常重要,因为特征类型直接决定了你后续采用哪套处理方案。
df.info()在输出中,int64 和 float64 是数值型,object 通常代表文本或分类值。比如 Titanic 数据集中,age 是 float64,survived 和 pclass 被存成了 int64,sex 和 embarked 是 object。真实项目里这个输出还可能包含 datetime64 类型,代表时间特征,以及 category 类型,代表已经规整好的分类型特征。
这里有个容易踩的坑:数值型不一定是真正的数值特征。比如 pclass 用 1、2、3 表示舱位等级,它虽然是整数,但本质上是序数分类特征。如果你只看类型就把它当成连续特征处理,后续可能会做出错误的标准化。相反,一些看起来是文本的字段,比如身份证号、手机号,虽然被解析成 object,但显然不应该被当成分类特征。所以,理解字段的业务含义,比机械地看数据类型更重要。
4.3 目标变量
对监督学习来说,必须先把目标变量弄清楚。目标变量是分类还是回归,决定了评价指标的选择和模型的类型。比如 survived 是 0/1 二分类,那就不能直接用均方误差去评价模型,而要考虑准确率、精确率、召回率和 AUC。
# 目标变量分布 print(df["survived"].value_counts()) print(df["survived"].value_counts(normalize=True))value_counts(normalize=True) 会输出每个类别的占比。如果发现存在比较严重的类别不平衡,比如 0 占 90%、1 占 10%,后续建模就需要注意采样策略或评价指标的选择,而不是单纯追求准确率。
5. 统计描述与缺失值:读懂数据质量
5.1 数值特征的统计描述
df.describe() 是对数值型特征做快速统计的利器。它输出 count、mean、std、min、25%、50%、75%、max 等指标。count 是有效值数量,从这里你就能初略看出哪些字段存在缺失;mean 和 50% 分位数如果差距较大,说明分布可能存在偏斜;max 和 min 的极值如果偏离正常范围,可能存在异常值。
df.describe()举个例子,Titanic 数据集中 fare 的 max 如果远大于 75% 分位数,说明少数极高价舱位把均值拉高了。这样的分布对线性模型不太友好,后续可以考虑取对数或做分箱。age 的 count 如果小于总行数,说明年龄字段有缺失,你需要决定如何处理。
5.2 分类特征的频次分析
分类特征的价值在于观察类别分布是否合理。如果某个类别只出现几次甚至一次,模型很难从中学到可靠规律,需要考虑低频类别合并。
for col in ["sex", "pclass", "embarked"]: print(df[col].value_counts(dropna=False))dropna=False 可以同时显式地看到缺失值数量,避免遗漏。embarked 可能有两个以上的类别,且存在少量缺失;sex 是二分类,非常干净。频次分析的意义是帮你发现哪些类别可能需要特殊处理,比如将低频类别合并为 other。
5.3 缺失值分析
缺失值是数据理解中最常见的问题。判断缺失值严重程度,不能只看个数,要看比例。
missing = df.isnull().sum() missing_percent = (missing / len(df) * 100).round(2) print(pd.DataFrame({"缺失个数": missing, "缺失比例": missing_percent}))一般来说,缺失比例低于 5%,可以忽略或简单填充;5% 到 20%,需要结合业务决定填充策略,是均值、中位数、众数还是模型预测;超过 20%,这个特征是否还有保留价值就要打一个问号。处理缺失值的思路差距很大,不是简单 dropna 或 fillna 就能解决的,关键是在理解缺失原因之后再决定方案。
6. 相关性与分组分析:找到关键线索
6.1 数值特征相关性矩阵
相关性分析是理解特征与目标关系最直接的手段。对数值型特征计算相关系数,能得到一个矩阵,其中每个值代表两个字段之间的线性相关性。值越接近 1 或 -1,相关性越强;越接近 0,说明线性关系越弱。
numeric_cols = df.select_dtypes(include=["number"]).columns corr = df[numeric_cols].corr() print(corr["survived"].sort_values(ascending=False))从输出能看出,fare 与 survived 的正相关性可能较高,pclass 与 survived 呈负相关。这意味着票价越高、舱位等级数字越小,生存概率越大。这些结论可以直接指导后续特征工程,比如把 pclass 当作有序分类特征处理,或者构造 fare 的分箱特征。
需要注意,相关系数只衡量线性关系,不能捕捉非线性关系。比如某个特征在阈值两侧对目标影响完全不同,相关性可能接近 0,但实际很有用。所以相关性矩阵是一个起点,不是终点。
6.2 分组聚合对比
分组分析比相关性更灵活,它能直接对比不同类别下的目标变量差异。对 Titanic 数据来说,最经典的分析是看性别和舱位等级对生存率的影响。
print(df.groupby("sex")["survived"].mean()) print(df.groupby("pclass")["survived"].mean())输出会让你看到不同群体之间的均值差异。如果差异明显,说明这个特征对目标有较强的区分能力,建模时应该保留并充分利用。如果某个特征分组后的目标均值几乎没有变化,那这个特征很可能对预测没有帮助,可以考虑丢弃。
年龄这种连续特征也可以先分箱,再看每个年龄段下的生存率。分组分析的优点是可以结合业务视角提出问题,比如“什么类型的人更容易获救”,然后用数据回答。这一步走完,你基本就对各个特征与目标之间的关系有了一个整体判断。
6.3 把发现整理成数据理解报告
很多人探索完数据就结束,画一堆图,但没有任何结论沉淀。这等于白做。数据理解的结果应该整理成一份简短报告,内容包括:数据概况、质量问题清单、特征观察、下一步处理计划。这份报告可以作为特征工程和数据清洗的输入,也可以方便回顾和团队协作。
7. 完整代码示例:Titanic 数据理解全流程
7.1 完整代码
下面是一段可以完整运行的 Python 脚本,包含前面讲到的核心步骤。代码里的中文注释已经标明每一步在做什么。
# 文件路径:eda_titanic.py import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # 设置图表样式 sns.set_style("whitegrid") plt.rcParams["figure.figsize"] = (12, 6) # 如果系统安装了中文字体,可取消下面注释,避免绘图中文乱码 # plt.rcParams["font.sans-serif"] = ["SimHei", "Arial Unicode MS"] # plt.rcParams["axes.unicode_minus"] = False # 加载数据集 df = sns.load_dataset("titanic") print("数据形状:", df.shape) print("字段列表:", df.columns.tolist()) # 1. 查看前几行 print("\n前5行数据:") print(df.head()) # 2. 信息概览 print("\n数据集信息:") df.info() # 3. 数值特征统计描述 print("\n数值特征统计:") print(df.describe()) # 4. 缺失值统计 print("\n缺失值统计:") print(df.isnull().sum()) print("\n缺失比例:") print((df.isnull().sum() / len(df) * 100).round(2)) # 5. 目标变量分布 print("\n目标变量 survived 分布:") print(df["survived"].value_counts(normalize=True).round(4)) # 6. 分类特征频次 for col in ["sex", "pclass", "embarked"]: print(f"\n分类特征 {col} 频次:") print(df[col].value_counts(dropna=False)) # 7. 数值特征相关性 numeric_cols = df.select_dtypes(include=[np.number]).columns corr = df[numeric_cols].corr() print("\n各数值特征与 survived 的相关系数:") print(corr["survived"].sort_values(ascending=False)) # 8. 可视化 fig, axes = plt.subplots(2, 2, figsize=(14, 10)) sns.countplot(x="survived", data=df, ax=axes[0][0]) axes[0][0].set_title("Survived Count") sns.histplot(df["age"].dropna(), bins=30, kde=True, ax=axes[0][1]) axes[0][1].set_title("Age Distribution") sns.boxplot(x="pclass", y="fare", data=df, ax=axes[1][0]) axes[1][0].set_title("Fare by Pclass") sns.heatmap(corr, annot=True, cmap="coolwarm", fmt=".2f", ax=axes[1][1]) axes[1][1].set_title("Correlation Heatmap") plt.tight_layout() plt.savefig("eda_titanic.png", dpi=150) plt.show()7.2 运行结果与验证方法
如果代码正常运行,你会依次看到表格形式的数据形状、字段列表、前 5 行数据、info 输出、数值统计、缺失值统计、目标变量分布等多个输出块,最后会打开一张包含 4 个子图的画布,同时在工作目录生成 eda_titanic.png 图片。
判断你的数据理解是否有效的标准很简单:你是否能在看完输出后用 3 到 5 句话概括这份数据。比如:总样本约 891 行,age 和 embarked 存在缺失,survived 分布基本均衡约 38% 存活,sex 与 pclass 与生存率高度相关。如果还不能写出这样的结论,说明还有字段没有看透。
如果脚本报错,先看报错发生在哪一步。最常见的是 sns.load_dataset 联网失败,那就在确认网络可用后重启内核,或者改用 pd.read_csv 读取本地文件;如果图里中文显示成方块,把中文字体配置取消注释,并确认当前系统确实安装了相应字体。这些内容在下一节会做出排查整理。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| pd.read_csv 报 UnicodeDecodeError | 文件编码不是 utf-8 | 查看文件编码格式 | 用 encoding="gbk" 或 encoding="utf-8" 重新读取 |
| df.info() 显示某列是 object,但实际是数值 | 数据中混入文本或空字符串 | 执行 df[col].unique() 查看取值 | 先清洗文本,再 astype(float) |
| 缺失值统计为 0,但实际有空值 | 空字符串或 NA 文本未被解析 | 用 value_counts(dropna=False) 检查取值 | 读取时加 na_values=["", "NA"] |
| sns.load_dataset("titanic") 失败 | seaborn 未安装或网络不可用 | 检查依赖安装与网络 | 改用本地 CSV 文件 |
| 相关性矩阵报错或全为 NaN | 所选列全部是 object,没有数值列 | 检查 select_dtypes 的结果 | 先筛选数值列,再计算相关性 |
| 热力图中文乱码 | matplotlib 默认字体不支持中文 | 查看绘图时是否有字体警告 | 设置中文字体并启动内核对配置生效 |
| groupby 聚合结果出现 FutureWarning | pandas 版本较新,类别型分组行为变化 | 阅读 warning 信息 | 分组列确保不是 category 类型,或升级查看官方文档 |
遇到报错时不要急着搜索完整错误,先看它发生在加载、统计、计算还是绘图阶段,再对照上表定位。数据理解阶段出现的错误大多不复杂,但养成读日志、拆解问题步骤的排查习惯,对后续建模阶段有很大帮助。
9. 最佳实践与后续学习方向
数据理解是一项可以刻意练习的技能。这里给出几条在真实项目中比较实用的建议。
第一,先写数据字典。拿到数据后,第一件事不是写代码,而是把每个字段的含义、类型、取值范围、可能缺失的原因记录下来。可以放在项目的 README 里,也可以单独写一个 markdown 文件。数据字典能帮你避免分析到一半忘记字段含义的尴尬。
第二,用脚本保存探索过程。不要只在一个临时 notebook 里胡乱尝试。把探索步骤整理成 eda.py 或 eda.ipynb,输出内容尽量统一,方便复现和分享。你过两周再回来看,会发现这份脚本比自己脑子里的记忆可靠得多。
第三,每个发现都要落到下一步动作。比如发现 age 缺失 20%,下一步动作是决定采用中位数填充还是建立模型预测缺失值;发现 fare 分布偏斜,下一步动作是尝试 log1p 变换。数据理解是否到位,可以看你能把多少发现转化成具体的特征工程决策。
第四,注意数据泄漏。在分组统计、缺失值填充、标准化这些操作中,尽量只在训练集上计算统计量,再应用到测试集。尤其是比赛项目,用全量数据做预处理后切分,会导致验证结果虚高,这一点必须警惕。
关于后续学习方向,有几条路径可以继续深入。一是系统学习 pandas 的数据处理操作,比如 groupby、apply、merge、pivot_table,这些是特征工程的高频工具。二是学习数据可视化进阶,包括分布图、箱线图、小提琴图、热力图的使用场景和读图方法。三是逐步接触缺失值插补、异常值检测、特征编码等方法,把这些内容从理解数据延伸到数据清洗和特征工程阶段。
如果你刚做完今天这一步,可以立刻做一件事:不要训练任何模型,只对任选的一份数据集完成数据理解,并写出一份包含结论的报告。这个练习坚持下去,会让你后面的建模过程顺畅很多。下一次你可以接着学习数据清洗和特征工程,那是在“理解数据”之后最应该掌握的下一环。