☰
机器学习数据预处理:CSV打散与训练测试集拆分实战指南
2026/9/24 18:32:11 网站建设 项目流程

简介:面向Python数据分析与机器学习初学者,这份资源聚焦CSV数据集的打散与拆分,帮助解决建模前训练集、测试集划分不随机、分布不一致等常见问题。资源包为zip压缩格式,共4个文件、约7KB,包含1个Python处理脚本及3份csv样例数据;脚本覆盖pandas读取、数据质量检查、随机打散、按比例切片生成训练集与测试集等完整过程,样例数据便于反复验证不同拆分比例下结果的变化。目前已有109人学习下载。通过案例可掌握DataFrame的sample和iloc等关键用法,理解随机因子对模型评估可信度的影响,并建立一套可复用的建模前数据预处理路径,帮助后续特征工程、模型训练与评估环节更顺畅。

1. 打散与拆分:机器学习流水线里最低调的两个环节

Python对CSV数据集的处理流程里,打散和拆分是我见过最容易被跳过、又最直接影响模型评估可信度的两步。很多人拿到CSV后直接调用train_test_split,从不关心原始文件的行顺序是不是按类别、按来源或者按时间排好的,结果训练集和测试集的分布严重不一致,线下指标虚高,模型一上线就翻车。这份资源带的processdata.py示例和data0数据组,就是针对这个场景的标准解法:先把整个数据集随机打散,再按7:3切成训练集和测试集。适合刚入门机器学习、用CSV自己组装数据集的同学,也适合被数据顺序坑过、想把预处理流程规范化的从业者。

2. 读入 CSV 先做三道检查:read_csv 参数、缺失值与原始副本

CSV 文件在磁盘上就是一个文本文件,读进来的第一眼决定后面所有步骤是否靠谱。这一步的核心不是“把数据读进来”,而是“确认读进来的数据跟源头一致”,包括列类型、缺失值标记和行数。我习惯在读入之后先打印 shape、dtypes 和 head 三件套,再决定后续处理策略。很多人跳过这一步直接进打散,遇到乱码和类型问题再回头查,反而更浪费时间。

2.1 用 read_csv 读入:先对齐分隔符、编码和列类型

import pandas as pd df = pd.read_csv( "data0.csv", encoding="utf-8", na_values=["", "NA", "null"], keep_default_na=True, ) print("shape:", df.shape) print("columns:", df.columns.tolist()) print(df.dtypes) print(df.head())

逻辑说明:pd.read_csv 是 pandas 读取 CSV 的统一入口,返回 DataFrame。这里显式传了 encoding 和 na_values 两个参数,目的是把文件中可能出现的空字符串、NA、null 统一识别为缺失值,避免它们被当成普通字符串混进数值列。shape 和 dtypes 能快速发现两类问题:行数比预期少说明文件里有换行转义问题,列类型为 object 说明数值列被读成了字符串。

参数说明:read_csv 默认分隔符是英文逗号,制表符文件需要手动传 sep="\t";encoding 在 Windows 上生成的 CSV 经常是 gbk,如果 utf-8 报 UnicodeDecodeError,改成 encoding="gbk" 再试。na_values 是额外缺失值标记列表,keep_default_na=True 表示保留 NaN、None 等默认标记,两者叠加使用。Excel 导出的 CSV 偶尔带 UTF-8 BOM,读进来后第一列列名会带 \ufeff 前缀,打印 columns 一眼就能看出来,这时候把 encoding 换成 "utf-8-sig" 就能解决。

read_csv 参数作用常见取值场景
sep指定分隔符默认逗号,制表符文件用 "\t"
encoding文件编码中文文件常见 utf-8 / gbk / utf-8-sig
na_values追加缺失值标记把空串、NA、null 统一收进来
dtype指定列类型读取时就把类别列限成 category

2.2 缺失值和重复值:先查清楚再决定怎么清

print(df.isnull().sum()) print(df.duplicated().sum())

逻辑说明:isnull().sum() 逐列统计缺失值数量,duplicated().sum() 统计完全重复的行数。这一步的作用是给清洗策略提供依据:缺失值占比高到一定程度的列,要么填充要么直接丢弃;重复行可以直接删除。重点在于,清洗动作必须在拆分之前完成,否则训练集和测试集的缺失值比例、总行数都会各自偏掉。

注意:dropna 默认删除包含任何缺失值的整行,如果先拆分后清洗,训练集和测试集各自丢掉的行数不一样,后续模型训练和评估的数据规模就不可比。我一般先用 isnull 看分布,缺失集中在某一列且占比低于 5% 时直接 dropna,高于 20% 时考虑把这个列放进数据分析报告里单独处理,而不是盲目删除。重复行的处理类似,先打印数量再决定是否调用 drop_duplicates,不要想当然。

2.3 留一份原始副本:打散拆分前的后悔药

raw_df = df.copy()

逻辑说明:copy() 是对当前 DataFrame 的深拷贝,后续打散、删除行、修改 dtype 都不会影响 raw_df。数据预处理的流程一旦展开,清洗动作是不可逆的,留一份原始副本可以让每一步都回退对比,排查问题时也能明确“是清洗改坏了,还是模型调参出了问题”。

参数说明:copy(deep=True) 是默认的深拷贝行为,会重新申请内存复制数据,数据量大时内存占用翻一倍。如果机器内存紧张,也可以用浅拷贝先顶着,但浅拷贝和原对象共享部分内存,修改时互相影响。踩过这个坑之后,我对于这种小规模 CSV 数据集一律用深拷贝,省得给自己挖坑。

3. 打散不是在凑随机感:sample(frac=1) 的机制与参数细节

打散的目的是打破原始文件里可能存在的顺序规律。CSV 文件的行顺序往往不是随机的,要么按采集时间排列,要么按来源分组,甚至可能是某个 SQL 查询结果直接导出,前 300 行是 A 类、后 300 行是 B 类。如果不打散就直接按比例切,切出来的训练集和测试集分布会非常难看。

3.1 数据顺序里藏着的偏见:一个具体的反面案例

假设 data0.csv 有 600 行,前 300 行都是类别 A,后 300 行都是类别 B。直接按 7:3 切,训练集就是前 420 行,包含 300 个 A 和 120 个 B;测试集是后 180 行,全部是 B。模型在训练集里看到的 B 样本偏少,测试时面对的又是清一色 B,评估指标完全没有参考价值。这个例子有点极端,但真实场景里按时间段、按设备、按地区排序的情况非常普遍,危害是一样的。

数据顺序的问题在于它不会显式报错。模型训练流程能正常跑完,损失值能正常下降,但评估结果不可信。很多从业者遇到线上效果和线下对不上,第一反应是调模型,很少有人会回头检查数据拆分的顺序。所以打散不仅是流程需要,更是一个让评估结果具备参考价值的前置条件。

3.2 用 sample(frac=1) 打散:全量无放回抽样

shuffled_df = df.sample(frac=1, random_state=42)

逻辑说明:DataFrame.sample 是从原表中随机抽取行的函数,frac=1 表示抽取比例是 100%,也就是全量抽一遍。由于抽样结果保留了原表的所有行,但顺序是随机生成的,所以等价于一次打散(shuffle)。抽样的本质是无放回抽样,每一行最多被抽中一次,不会有重复行混进来。

参数说明:random_state 是随机数种子,固定成任意整数(比如 42)之后,每次运行打散结果完全一致,这是一种“可复现的随机”。如果不传这个参数,每次跑出来的顺序都不同,模型指标的波动会让你分不清是数据问题还是模型问题。随机数种子的取值范围就是整数,没有其他限制,团队协作时统一约定一个固定值即可。

sample 内部走的是 numpy 的随机数机制,所以用 numpy.random.seed 全局设种子也能影响它。但直接传 random_state 更清晰,作用范围只限定在这一次调用里,不会污染其他随机过程。如果你习惯 numpy 风格,也可以写成 df.loc[numpy.random.permutation(df.index)],效果类似,但要多处理一次索引对齐,pandas 的 sample 一行搞定,我更推荐后者。

这里补充一个边界:frac 可以小于 1,比如 frac=0.8 表示抽取 80% 的行,这在超大数据集里可以做降采样。但打散这个场景必须用 frac=1,否则会少行,后面拆分时行数对不上,问题就很难排查了。

3.3 打散后立刻重置索引:别让旧行号坑到你

shuffled_df = shuffled_df.reset_index(drop=True)

逻辑说明:sample 返回的 DataFrame 行索引还是原始 CSV 里的行号,比如原表第 300 行在被抽到后仍然带着索引 300。如果不重置,后面用 iloc 按位置切片时不受影响,但一旦用 loc 按索引取值、或者做 merge 操作,就会发现取出来的行是“跳着”的,跟打印结果对不上。reset_index(drop=True) 把索引重新变成从 0 开始的连续整数,同时丢弃旧索引列。

参数说明:drop=True 的意思是旧索引直接扔掉,而不是作为一列保留。如果漏掉这个参数,旧索引会被塞进一个叫 index 的新列,后面拆分和导出 CSV 时这个列会混进数据,属于很隐蔽的脏数据来源。这个问题在数据量大时尤其难发现,因为打印前面几行时 index 列看起来就像正常的数据列。

4. 拆分训练集和测试集:手动 iloc 切片与 train_test_split 怎么选

数据打散之后,拆分的核心逻辑就只剩“按位置切一刀”。但这一刀怎么切、要不要分层、用 pandas 还是用 sklearn,决定了后续评估的可信度。我的判断标准很简单:数据能随机打散就用 train_test_split,数据必须保持顺序(比如时间序列)就手动切片。

4.1 手动切片:先算整数边界再 iloc

split_idx = int(len(shuffled_df) * 0.7) train_df = shuffled_df.iloc[:split_idx].copy() test_df = shuffled_df.iloc[split_idx:].copy() print(f"train: {len(train_df)} rows, test: {len(test_df)} rows")

逻辑说明:先乘比例再取整,得到训练集的边界下标。iloc[:split_idx] 取前 70% 行,iloc[split_idx:] 取剩下的 30%,两者加起来正好等于原始行数,不会重叠也不会漏行。copy() 在这里不是多余的:iloc 切片返回的是原对象的视图,如果后续对测试集做原地修改,比如填充缺失值,可能连带影响原 DataFrame,加上 copy() 之后两个集合就彻底独立了。

参数说明:train_ratio=0.7 只是一个约定俗成的起点,样本量小时可以提到 0.8,样本量很大时用 0.9 甚至更高都常见,但测试集至少要保证 500 行以上,否则评估指标的置信区间太宽。比例的选择取决于数据规模和问题难度,没有绝对最优值。切完之后顺手打印两个集合的行数,确认 train 加 test 等于原始行数,这个对账动作能提前发现边界错误。

手动切片最典型的适用场景是时间序列。比如 data0.csv 是某个传感器按小时采集的数据,必须先按时间升序排序,再直接按位置切分,保证训练集里的时间点全部在测试集之前。如果这种数据先打散再切,未来数据混进训练集,模型评估结果会乐观到失真,上线后真实表现断崖式下跌。

4.2 换成 train_test_split:自带随机和分层

from sklearn.model_selection import train_test_split train_df, test_df = train_test_split( shuffled_df, test_size=0.3, random_state=42, stratify=shuffled_df["label"] if "label" in shuffled_df.columns else None, )

逻辑说明:sklearn 的 train_test_split 内部会先做一次随机打散,再做切分,所以这里传入已经打过散的 DataFrame 也没问题,只是多一次无谓的随机操作,结果不受影响。stratify 参数是它比手动切片最大的优势:传入类别列之后,函数会按该类别的比例分配训练集和测试集,保证两边各类别占比基本一致。

参数说明:test_size=0.3 与手动切片的 30% 等价;random_state 同样是固定种子。当数据类别不平衡,比如正样本只占 5% 时,普通随机切分很容易把测试集里的正样本丢得只剩几个,stratify 能稳妥解决这个问题。

对比维度手动 iloc 切片train_test_split
适用场景时间序列、保持顺序常规随机拆分、类别不平衡
随机打散需要自己先 sample内部自带 shuffle
分层支持不支持,需自行实现stratify 参数直接传类别列
依赖pandas 自带需要安装 scikit-learn

5. 避坑:打散拆分阶段最容易翻车的五个地方

这一章是实际跑数据时最容易踩的坑,覆盖面主要是顺序、种子、索引、分层和编码这五个维度。每条按“现象、原因、解决”的结构来写,都是我踩过或者帮别人排查过的真实问题。

5.1 缺失值处理顺序不一致,训练和测试各缺一截

现象:拆分前没统计缺失值,拆分后分别对训练集和测试集 dropna,结果训练集丢掉 30 行、测试集丢掉 18 行,两边数据量不同,模型评估时训练集和测试集的行数比例变成了 70:28 而不是 70:30。

原因:缺失值分布并不是按行均匀铺开的,先拆分再清洗等于让两个集合各自独立丢失样本,破坏了原始分布,而且两个集合清洗后的行数无法对齐。

解决:在 read_csv 之后、sample 之前完成所有清洗动作,把 dropna 或 fillna 的结果记录下来,确认删除行数、剩余行数后再进入打散拆分。这样一来训练集和测试集只是原始清洗后数据的两个切片,行数比例严格受控。

5.2 random_state 不固定,指标对不上号

现象:同一份数据、同一个模型,今天跑 AUC 是 0.85,明天跑变成 0.83,你以为是模型问题,其实是数据拆分的随机种子变了,训练集和测试集的内容已经完全不同。

原因:sample 和 train_test_split 默认都是随机过程,不固定种子,每次打散顺序都不同,拆分结果就不同,下游所有指标都跟着抖。

解决:把 random_state=42 写死在处理脚本里,或作为函数参数传入,保证每一次运行拿到相同的训练集和测试集。这个习惯在排查模型问题时能省掉很多无谓的怀疑,不然你会花大量时间调参,最后发现是数据每次都不同。

5.3 打散后没有重置索引,loc 取值“跳行”

现象:shuffled_df 打印出来顺序是正常的,但用 loc[100:200] 取出来是乱七八糟的行,甚至报 KeyError。

原因:打散后索引还是原始行号,loc 是按索引标签取值的,它根本不知道你期望的是“第 100 到第 200 行”,它拿标签 100 到 200 去匹配,匹配到哪一行算哪一行。

解决:打散后紧跟一行 reset_index(drop=True),让索引重新连续。如果已经拆完了才发现,那就回退到打散步骤重新走一遍,这种情况没有捷径,只能从头跑。

5.4 类别不平衡时忽略分层,小类样本在测试集里“失踪”

现象:数据里正样本占 3%,随机切分后测试集 5 万行里只有几十个正样本,评估曲线抖得没法看,精确率召回率每次跑都差很多。

原因:随机切分不保证类别比例,少数类样本量本来就小,随机分完两边数量波动很大,测试集里的小类样本少到无法支撑稳定评估。

解决:用 train_test_split 的 stratify 参数,按类别列分层抽样,确保训练集和测试集的每个类别占比都和全量数据一致。如果坚持用手动切片,那就得按类别分组、各组内部按比例切、最后再合并,工作量大且容易出错。

5.5 中文 CSV 乱码与路径报错

现象:read_csv 读取时抛 UnicodeDecodeError,或者 CSV 用 Excel 打开正常但 pandas 读出来全是乱码。

原因:文件本身是 gbk 或 gb2312 编码,pandas 默认按 utf-8 解码,解码对不上就报错或出现乱码。

解决:读入时指定 encoding="gbk",或者读取前先用文本编辑器把文件另存为 utf-8 编码,一劳永逸。文件路径尽量用英文,Windows 下中文路径配合 pandas 老版本有时会莫名报错,把数据文件放到纯英文目录下能少折腾几次。这类问题通常在换机器、换数据源后随机出现,写代码时养成显式传 encoding 的习惯能规避大部分。

6. 把流程封装成 process_csv:一致性检查与批量导出

把前面的步骤收拢成一个函数,日常处理新数据集时只需要改文件路径和比例,就能稳定复现整个过程。封装的意义在于把顺序固定下来,减少手工操作带来的偏差,也能让团队其他人拿到脚本后跑出完全一致的结果。

6.1 封装 function:打散、拆分、检查、导出一次完成

import pandas as pd def process_csv(input_path, train_path, test_path, train_ratio=0.7, random_state=42): df = pd.read_csv(input_path, encoding="utf-8") df = df.drop_duplicates().reset_index(drop=True) df = df.sample(frac=1, random_state=random_state) df = df.reset_index(drop=True) split_idx = int(len(df) * train_ratio) train_df = df.iloc[:split_idx].copy() test_df = df.iloc[split_idx:].copy() numeric_cols = df.select_dtypes(include="number").columns if len(numeric_cols) > 0: diff = train_df[numeric_cols].mean() - test_df[numeric_cols].mean() print("均值差最大值:", diff.abs().max()) train_df.to_csv(train_path, index=False, encoding="utf-8") test_df.to_csv(test_path, index=False, encoding="utf-8") return train_df, test_df

逻辑说明:函数先把重复行清掉并重置索引,再打散,再按比例切片,最后用数值列均值差做一次快速一致性检查。均值差绝对值越小,说明训练集和测试集的数值分布越接近;如果这个值明显偏大,优先怀疑数据没打散,或者数据本身就存在按某个字段分组的强规律。导出的 CSV 都加了 index=False,避免把行号写进文件。

比例和编码这类参数都成了函数入参,新数据集换一下路径就能复用。数据量大时,在函数里把均值检查换成目标列分布对比,比如用 value_counts 计算训练集和测试集的类别占比差,判断逻辑是一样的。这一步检查虽然只有几行代码,但它能把分布异常在训练前暴露出来,比模型跑完再回头查数据要快得多。

我有一次因为偷懒跳过了这个均值检查,测试集 F1 比线上高了十几个点,从特征工程一路排查到数据拆分,最后发现是 CSV 按机房来源排序,没打散就切了。从那以后我每次拆完数据都强制走一遍分布对比,确认训练集和测试集的差距在合理范围内才继续下一步。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询