ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CSV数据打散与拆分全指南:从pandas到sklearn的可靠数据集制作

CSV数据打散与拆分全指南:从pandas到sklearn的可靠数据集制作 简介面向Python数据分析与机器学习初学者资源包聚焦CSV数据集在建模前的关键预处理环节随机打散与训练集/测试集拆分。它演示了如何借助pandas读取结构化表格、检查数据质量再通过随机化操作打乱数据顺序避免因原始数据分布偏差而影响模型训练与评估之后按比例切分出训练集和测试集为后续特征工程与模型评估提供规范数据基础。压缩包共4个文件包含1个Python脚本和3个CSV数据文件整体仅7KB体量小巧、便于快速下载。脚本覆盖从原始CSV加载、打散到按比例拆分并输出结果的全过程附带原始数据及拆分后的训练/测试文件可直接对照运行结果理解拆分逻辑与数据分布一致性要求。已有109人学习/下载适合希望用最短时间掌握数据打散与切分标准写法的学习者。1. 打散与拆分CSV数据为什么这步决定后续实验还准不准当你从网上下载一份公开数据集或者从业务系统导出CSV日志用 Python 的 pandas 读进来后第一眼看到的往往是前几千行是一个品类中间几千行是另一个品类再往后时间戳突然跳了半年。这种顺序不是巧合而是数据在采集端天然按时间或类别落盘。如果跳过打散直接按 8:2 切分训练集学到的分布和验证集看到的分布完全不一致后面的调参都像在对着错误的地图开车。标题里说的“打散与拆分”就是把这份有序 CSV 重新洗牌再按比例或按条件切成几份作为模型训练、数据分析、YOLOv8 等模型训练前数据准备工作的前置步骤。这套操作成本极低却能挡住一批隐蔽的数据泄漏问题适合刚接触数据集预处理的新手也适合被“验证集分数很好、一上线就崩”折磨的从业者。2. 用pandas打散CSVsample与shuffle的取舍和三处必调参数2.1 打散的本质打破顺序偏差先厘清你在打破什么CSV 数据并不天然随机它常常带有采集侧的三类顺序特征。第一类是时间顺序。传感器日志、交易流水、接口调用记录都是按时间戳一条一条追加写入的相邻两条数据在时间上强相关。第二类是类别聚集。很多导出工具会按类别字段排序同一个品类的样本紧挨在一起写导致整段 CSV 里同一类别的数据扎堆。第三类是主键顺序。数据库表导出时按自增 ID 排序ID 相邻的样本特征相似度往往更高。如果这些顺序数据不做任何处理直接切分模型会在训练过程中把“顺序”本身当作可用信号。一个最典型的翻车场景是按行号前 70% 做训练、后 30% 做验证结果训练集里几乎全是 A 类样本验证集里全是 B 类样本。模型从头到尾没见过 B 类的分布验证集分数自然惨不忍睹。这不是调参能解决的问题而是拆分这一步就埋下了偏差。打散的本质是对行索引做一次随机重排让行与行之间的原始顺序依赖消失。这里要分清三个概念打散是重排行位置行数和内容都不变抽样是按比例取出一部分行排序是按某字段重排。三者不能混用。在数据准备流程里打散通常发生在切分之前而且是针对全量数据先打散、再切开不是在各个子集内部各自打散。2.2 最小可用的打散命令sample(frac1)的一行代码与必调参数在 pandas 里有一个常见误区很多人以为 DataFrame 有shuffle()方法实际并没有。真正的打散要用sample(frac1.0)。import pandas as pd # 读取原始 CSV中文内容建议显式指定编码 df pd.read_csv(raw_dataset.csv, encodingutf-8) # 打散frac1 表示取全部行random_state 固定随机种子 shuffled df.sample(frac1.0, random_state42) # 重置索引避免原先的行号留在数据里 shuffled shuffled.reset_index(dropTrue) # 写出indexFalse 防止多出一列索引utf-8-sig 兼容 Excel shuffled.to_csv(raw_dataset_shuffled.csv, indexFalse, encodingutf-8-sig)逻辑说明sample(frac1.0)在 pandas 内部实现为对行索引做随机排列然后按新索引取数。frac1.0代表抽取比例为 100%也就是打散后行数不变如果改成frac0.8就会随机抽取出 80% 的行这在做下采样时也常用。random_state42是随机种子固定之后无论运行多少次打散后的行顺序都一样保证实验可复现。参数说明里有三处最容易踩坑的地方indexFalse如果你不写这个参数to_csv默认会把 pandas 的索引写成文件里的第一列名字叫Unnamed: 0。读回来的时候这列会被当成普通特征模型训练时等于多了一个和时间强相关的泄漏列。reset_index(dropTrue)sample(frac1.0)打散后原先行号仍然保留在索引中。如果后续有代码按索引取数拿到的还是原来的顺序。重置索引后新顺序被固化下来后续操作才真正按打散后的排列进行。encodingutf-8-sig写出的 CSV 如果要交给 Windows 下的 Excel 打开或者与其他工具交换建议带 BOM 的utf-8-sig否则中文表头会出现乱码。sample 有几个相关参数也值得分清参数作用备注frac1.0抽取比例1.0 打散全量0.8 随机抽 80%random_state42随机种子固定后可复现replaceFalse无放回抽样默认值打散时保持行不重复replaceTrue有放回抽样用于 bootstrap打散不要开ignore_indexTrue重置索引等价于后面接 reset_index(dropTrue)2.3 内存装不下几个GB的CSV磁盘级打散的流式写法pandas 全量读入再打散在文件达到几个 GB 时非常容易内存溢出。8GB 内存的机器读 4GB 的 CSV仅仅read_csv就可能把内存吃满更别说后面做 sample 时还会产生一份副本。常见做法是行号打散先扫描一遍文件记录每一行的字节偏移位置只对偏移量做随机排列再按新顺序把对应行写入新文件。整个过程内存里只保存一份整数偏移数组不加载实际数据内容。import random input_path big_dataset.csv output_path big_dataset_shuffled.csv # 第一遍扫描记录每行的字节偏移 offsets [] with open(input_path, r, encodingutf-8) as f: header f.readline() # 表头单独保存不参与打散 offsets.append(f.tell()) # 第一行数据起始位置 for line in f: offsets.append(f.tell()) # 每次循环后记录下一行的偏移 # 生成行号并打散行号从 0 开始对应 offsets 中的下标 line_indices list(range(len(offsets) - 1)) random.Random(42).shuffle(line_indices) # 第二遍扫描按打散后的行号顺序写新文件 with open(input_path, r, encodingutf-8) as f_src, \ open(output_path, w, encodingutf-8-sig, newline) as f_dst: f_dst.write(header) for idx in line_indices: f_src.seek(offsets[idx]) f_dst.write(f_src.readline())这段代码的逻辑是第一遍用f.tell()记录每行起始位置第二遍通过seek跳到指定行的偏移处读一行。文件的读取次数是两次完整扫描时间成本是固定的内存成本只有偏移列表和单行内容。这个写法有一个前提每行末尾必须一致不能出现字段内换行。如果你的 CSV 里某个字段值本身包含换行符比如描述文本那偏移量和行号会错位。遇到这种文件要么先做一次清洗把字段内换行替换掉要么放弃这个方案改用 chunksize 分块处理。当文件极小、几百 MB 以内时这个流式方案反而更慢因为两次磁盘扫描和大量 seek 操作的开销比read_csv全量打散大。我一般以 2GB 作为分界线2GB 以下直接 pandas2GB 以上才考虑行号打散。3. 把CSV拆成训练/验证/测试集三种拆分思路与代码落地3.1 先看手动切片错在哪顺序偏差的典型后果很多人拿到 CSV 后的第一反应是按行号切import pandas as pd df pd.read_csv(dataset.csv, encodingutf-8) train df.iloc[:8000] # 前 8000 行做训练 test df.iloc[8000:10000] # 后 2000 行做测试这段代码本身没有语法错误但它隐含了一个前提原始 CSV 的行必须是随机排列的。实际情况恰恰相反绝大多数生产环境导出的 CSV 都不是随机排列。数据库按主键导出、日志按时间追加、报表按部门分组这些顺序会原样保留在文件里。如果在这种顺序下直接iloc切分典型后果是训练集和测试集的类别分布差异巨大或者时间跨度完全不重叠。举个具体的例子一个商品销售数据集按日期排序前 8000 行是上半年的数据后 2000 行是下半年的数据。季节因素、促销策略都变了模型拿上半年学到的规律去预测下半年预测误差自然大得离谱。3.2 按比例随机拆分train_test_split 的参数与常见用法用 scikit-learn 的train_test_split是做随机拆分最省事的方案from sklearn.model_selection import train_test_split import pandas as pd df pd.read_csv(dataset.csv, encodingutf-8) # 先打散再拆分的写法shuffleTrue 会先随机重排 train, test train_test_split( df, test_size0.2, random_state42, shuffleTrue, ) train.to_csv(train.csv, indexFalse, encodingutf-8-sig) test.to_csv(test.csv, indexFalse, encodingutf-8-sig)这里test_size0.2表示测试集占 20%也可以写成固定行数比如test_size2000。shuffleTrue是默认值它保证了拆分前先打乱顺序。注意如果只写train_test_split(df, test_size0.2)而不指定shuffle实际上默认也是 True不会按原顺序切这经常被误解。train_test_split 的局限在与它做的是简单随机抽样。当数据里存在类别不平衡时比如标签 A 占 90%、标签 B 占 10%随机抽样有可能让测试集里的 B 类样本只有寥寥几个甚至完全缺失。这种情况就不能用简单随机拆分需要用到分层拆分。3.3 分层拆分保分布类别不均衡时用 stratify分类任务里标签分布很重要。全量数据里 A 类 70%、B 类 30%切完之后训练集和测试集也应该保持这个比例而不是让某个子集里某一类占比失衡。train, test train_test_split( df, test_size0.2, random_state42, stratifydf[label], # 按 label 列分层 )stratifydf[label]的意思是拆分时保持 label 字段在每个子集中的分布与全量一致。全量数据里 A 类 70%、B 类 30%那么训练集和测试集里都会是 A 类约 70%、B 类约 30%。用 stratify 时要检查两件事一是标签列不能有缺失值否则 sklearn 会直接报错二是样本量极小的类别要注意如果某个类别只有 5 个样本而测试集比例要切出 20%那这个类别的样本在测试集里最多只能分到 1 个sklearn 会提示样本量不足并抛异常。常规处理是把过少出现的类别先归并成一个“其他”类再分层拆分。还要特别提示当训练集还要继续拆出验证集时要分层拆两次保持每一层的分布一致。不要只对整体拆一次训练测试再在训练集内部随机切一刀那样验证集的分布会漂移。3.4 时间序列数据不能用随机打散按时间切是更稳妥的方向时间序列场景有一个特殊约束不能用随机打散。假设你要预测明天的销量训练数据只能是今天之前的所有记录不能混入明天以后的数据否则就构成典型的时间泄漏。正确做法是按时间列排序然后按时间点切分df pd.read_csv(timeseries.csv, parse_dates[timestamp]) df df.sort_values(timestamp) split_date 2024-06-01 train df[df[timestamp] split_date] test df[df[timestamp] split_date] train.to_csv(train.csv, indexFalse) test.to_csv(test.csv, indexFalse)切分之后如果还要在训练集内部做交叉验证不能用 sklearn 默认的 KFold因为它默认打散数据。应该用TimeSeriesSplit它保证每一折的训练数据永远早于验证数据。这个场景里第 2 章讲的打散操作完全不能用。这也说明了“打散”和“拆分”这两个动作并不是绑定关系随机拆分前需要打散时间切分前需要排序方向完全相反。4. 串成一条可复用的数据流水线拆分脚本的设计与回读验证4.1 设计函数时先想清楚入参和出参把打散和拆分写成一次性脚本最大的问题是不可复现。三个月后回来看这批文件你可能完全不记得当时是按什么比例、用什么种子切出来的。所以我更建议封装成一个独立函数放进项目的工具目录里。函数设计先想清楚入参和出参输入原始 CSV 路径、输出目录、三个集合的占比、随机种子。可选输入分层列名分类任务、时间列名时序任务。输出三个 CSV 文件外加一份 JSON 格式的拆分报告。报告里记录输入路径、随机种子、比例、每个文件的行数、分层分布。这份报告的价值是你不需要记住任何过程细节。以后任何一次实验结果异常都能从报告里看到这批数据是怎么来的能不能复现当时的环境。4.2 完整实现读CSV、打散、拆分、写出一次讲清下面这个函数可以直接粘进项目里改参数使用import json import os import pandas as pd from sklearn.model_selection import train_test_split def split_dataset( input_csv: str, output_dir: str, train_ratio: float 0.7, val_ratio: float 0.15, test_ratio: float 0.15, random_state: int 42, stratify_col: str None, time_col: str None, ): 打散并拆分CSV为train/val/test三份同时输出拆分报告。 参数 input_csv: 原始CSV路径 output_dir: 输出目录 train_ratio/val_ratio/test_ratio: 三个集合占比求和应为1 random_state: 随机种子固定后可复现 stratify_col: 分层列名分类任务建议传入 time_col: 时间列名传入时按时间顺序切分不随机打散 df pd.read_csv(input_csv, encodingutf-8) # 比例校验 total_ratio train_ratio val_ratio test_ratio assert abs(total_ratio - 1.0) 1e-6, 三个比例之和必须为1 # 分支一随机打散后按比例切分默认 if time_col is None: df df.sample(frac1.0, random_staterandom_state) df df.reset_index(dropTrue) n len(df) n_train int(n * train_ratio) n_val int(n * val_ratio) train df.iloc[:n_train] val df.iloc[n_train:n_train n_val] test df.iloc[n_train n_val:] # 分支二按时间列排序后切分时间序列 else: df df.sort_values(time_col) n len(df) n_train int(n * train_ratio) n_val int(n * val_ratio) train df.iloc[:n_train] val df.iloc[n_train:n_train n_val] test df.iloc[n_train n_val:] # 写出文件 os.makedirs(output_dir, exist_okTrue) train.to_csv(os.path.join(output_dir, train.csv), indexFalse, encodingutf-8-sig) val.to_csv(os.path.join(output_dir, val.csv), indexFalse, encodingutf-8-sig) test.to_csv(os.path.join(output_dir, test.csv), indexFalse, encodingutf-8-sig) # 写拆分报告 report { input_csv: input_csv, random_state: random_state, train_ratio: train_ratio, val_ratio: val_ratio, test_ratio: test_ratio, rows_train: len(train), rows_val: len(val), rows_test: len(test), } if stratify_col: report[label_distribution] { train: train[stratify_col].value_counts().to_dict(), val: val[stratify_col].value_counts().to_dict(), test: test[stratify_col].value_counts().to_dict(), } with open(os.path.join(output_dir, split_report.json), w, encodingutf-8) as f: json.dump(report, f, ensure_asciiFalse, indent2) if __name__ __main__: split_dataset( input_csvdataset.csv, output_dir./data_split, train_ratio0.7, val_ratio0.15, test_ratio0.15, random_state42, stratify_collabel, )这段代码的逻辑层次read_csv读入全量数据编码按 utf-8 处理。比例校验放在最前面三个比例之和差一点都会导致切出来的文件行数不对提前拦下这种低级错误。默认分支里先打散再切分这是随机拆分场景的标准顺序。先切再打的写法会造成子集组成不随机这一点在避坑章节里还会展开。时间序列分支不经过打散直接排序后按位置切保证时间顺序不被破坏。拆分报告记录了所有关键信息以后回看时不需要猜。如果要用分层拆分把默认分支的df.iloc[:n_train]替换成两次train_test_split调用# 分层拆分的替换写法 if stratify_col: train, temp train_test_split( df, test_sizeval_ratio test_ratio, random_staterandom_state, stratifydf[stratify_col], ) val, test train_test_split( temp, test_sizetest_ratio / (val_ratio test_ratio), random_staterandom_state, stratifytemp[stratify_col], )两次拆分的思路是第一次把全量拆成训练集和临时集第二次把临时集按剩余比例拆成验证集和测试集。每次拆分都用stratify保持标签分布一致这样三个子集的分布都贴近全量。4.3 拆分后的三个回读验证跳过这一步迟早后悔文件写完后不要直接拿去训练。我一般会顺手做三个检查每个都有具体的 Python 语句。第一个是行数校验。将三个文件的行数和原始 CSV 行数作对比差一行都不行train pd.read_csv(train.csv) val pd.read_csv(val.csv) test pd.read_csv(test.csv) total len(train) len(val) len(test) print(f拆分后总行数: {total}) with open(dataset.csv, r, encodingutf-8) as f: original_total sum(1 for _ in f) - 1 # 减掉表头 print(f原始行数: {original_total})导致行数对不上的原因通常是原始文件末尾有空行被读取时跳过或者字段内换行导致read_csv解析出多行还有可能是导出工具用了 CRLF 换行而读取端按 LF 解析。定位时可以对比差值是 1 还是更多差 1 优先怀疑末尾空行。第二个是表头校验。三个文件的列名应该完全一致cols_train train.columns.tolist() cols_val val.columns.tolist() cols_test test.columns.tolist() assert cols_train cols_val cols_test, 三个文件表头不一致 print(表头校验通过:, cols_train)表头不一致常见原因是写文件时某个子集没设indexFalse多出一列Unnamed: 0。第三个是交叉校验。如果数据里有唯一标识列比如用户 ID、订单号要检查训练集和验证集之间有没有重叠id_col user_id train_ids set(train[id_col]) test_ids set(test[id_col]) overlap train_ids test_ids print(ftrain 与 test 重叠样本数: {len(overlap)}) assert len(overlap) 0, 存在数据泄漏样本交叉出现在训练集和测试集抽样打散的场景下重叠几乎不会发生。但如果你按用户 ID 做过条件切分同一用户的多个订单很可能被切到两边这个检查能立刻暴露问题。5. 打散与拆分的常见问题与排查现象、原因、处理办法5.1 随机种子不固定同一份CSV两次拆分结果差很多现象同事跑同一份代码两遍第一遍精度 0.85第二遍变成 0.79查了很久才发现每次拆出来的训练集和验证集都不一样。原因train_test_split或sample里没有固定random_state程序每次运行时随机种子不同拆分结果自然不同。单人复现、多人协作、批量实验任何环节重新运行脚本都可能拿到另一份数据组合实验结论根本站不住。解决把随机种子固定为一个常量并在拆分报告里记录这个值。复用第 4 章的split_dataset函数时random_state已经写死在调用参数里报告文件里也留了记录可以保证同一份数据在任意时刻重跑都能得到完全一致的拆分。遇到需要对比不同种子影响的实验可以在循环里显式传入不同种子值而不是依赖默认随机。5.2 中文乱码与BOMCSV编码问题是动手第一步现象pd.read_csv(数据.csv)直接抛UnicodeDecodeError或者写出的 CSV 在 Excel 里打开中文全是乱码。原因CSV 文件本身没有声明编码的机制。Windows 旧版 Office 默认按 ANSI/GBK 解析而 pandas 默认按 utf-8 读取。两边不一致就会在读入时报错或者写出后乱码。另一个场景是文件带 BOM 头pandas 读进来第一列列名里多了几个不可见字符。解决读取时遇到报错就换编码重试比较稳妥的做法是先用工具确认文件编码再显式传参。写出时统一用utf-8-sig这个编码自带 BOM 标记Excel 能正确识别。另外文件名和路径尽量不用中文尤其在 Linux 服务器和 Windows 之间来回拷贝时避免路径编码引发的新型乱码。5.3 表头被吃或多出一列index与header的双重坑现象拆分后某个文件多出一列叫Unnamed: 0的数据或者训练时第一行数据被当成了表头。原因写文件时忘了indexFalsepandas 把原有索引写进文件列名就显示为Unnamed: 0。读文件时header0会把第一行当成列名如果原始 CSV 本来没有表头数据的第一行就被误解成字段名。解决写文件一律indexFalse。读文件时如果 CSV 本身没有表头显式传headerNone, names[col1, col2, ...]不要依赖 pandas 自动判断。打散操作之后记得先reset_index(dropTrue)再写出从源头避免索引列混入。5.4 拆完文件行数对不上CRLF换行和末尾空行的排查现象第 4 章的行数校验打印出来train val test 比原始 CSV 少了几行或者多出几行。原因Windows 下 CSV 使用 CRLF 换行pandas 读取时一般能正确处理。但某些导出工具会在文件末尾追加一个空行读取时这个空行会被忽略导致统计行数比实际少 1。反方向的问题也有某些字段值内包含换行符read_csv默认会把它解析成多行统计行数就会偏多。解决先确认差值是 1 还是更多。差 1 优先检查文件末尾有没有空行用 Vim 或十六进制工具看一眼最后几个字节。差得多的话检查字段内是否包含换行可以用 Pandas 读取时指定quotechar和quotingcsv.QUOTE_ALL或者提前用文本处理工具把字段内换行统一替换成空格。5.5 分层拆分传错了列分布记录是对的切出来却是错的现象拆完后统计训练集和测试集的标签占比发现和全量数据差异很大但代码里也没有报错。原因stratify参数传错了列比如把 ID 列传进去了或者标签列里存在大量缺失值sklearn 在底层无法按缺失值分层。另一个常见情况是某个类别在测试集里占比太小sklearn 自动放弃了分层逻辑。解决拆分前先打印df[label].value_counts(normalizeTrue)看各类别占比确认目标列无误。拆分后再打印一遍三个子集的分布和全量对比。如果某个类别样本过少先合并成“其他”类再做分层。这类问题不会报错只能靠前后对比来发现。5.6 先打散再拆分 vs 先拆分再打散顺序反了效果差一截现象网上能看到两种写法的代码有人先train_test_split再在各子集内部打散也有人先打散再切。前者套用到生产环境里训练时发现数据顺序仍然有规律。原因如果先把 CSV 拆成训练集和测试集再各自在子集内打散打散只改变了子集内部的行的排列子集本身的组成没有变化。也就是说训练集仍然可能全是前半段时间的数据测试集仍然是后半段的子集内打散完全解决不了顺序偏差。解决统一按“全量读取 - 打散 - 切分”的顺序执行。打散发生在拆分之前切分操作本身不依赖顺序。如果一个项目已经用了先拆再打的流程建议重新生成一遍数据并且把顺序固定在后来的脚本里避免版本混乱。6. 把“能不能复原”当作拆分的及格线一个更稳的验证习惯我在项目里养成了一个习惯每次拆分完不急着进训练先验证一下能不能拼回去。这个验证动作比单独检查行数、表头或重叠都更全面因为它一次性把打散和拆分过程中的所有顺序问题都暴露出来。具体做法是在拆分函数里给每一行添加一个原始行号字段写出三个文件时保留这个字段验证时把三个文件拼接回来按原始行号排序再和原始 CSV 逐行对比import pandas as pd # 拆分前给数据加原始行号 df[_orig_row] range(len(df)) # 拆分和写出时都保留 _orig_row 字段 # 三个子集分别写入 train.csv / val.csv / test.csv # 验证拼接并排序 train pd.read_csv(train.csv) val pd.read_csv(val.csv) test pd.read_csv(test.csv) merged pd.concat([train, val, test], ignore_indexTrue) merged merged.sort_values(_orig_row).reset_index(dropTrue) original pd.read_csv(dataset.csv) original original.reset_index(dropTrue) # 逐行对比去掉辅助列 _orig_row assert (merged.drop(columns[_orig_row]).values original.values).all(), 拼接后与原始数据不一致 print(拼接校验通过数据完整且顺序可复原)这段代码的关键在于如果打散时丢了行、拆分时出现重复、切分后某个文件有顺序偏移拼接后和原始数据对比时一定有一行对不上。它能同时验证“行数一致”“无重叠”“无遗漏”“顺序可恢复”四个性质比单独做任何一项检查都高效。另一个配套习惯是把拆分报告当作实验记录的一部分和模型权重一起归档。每次实验记录里都写上训练数据的原始路径、随机种子、拆分比例跑完实验后即使换了机器也能按报告里的配置完整复现。数据集拆分看似是数据准备里最不起眼的一步但它埋下的错误会顺着后续每一个模型偷偷溜进实验结果里。固定种子、写报告、回读拼接这三件事坚持做下来大部分“验证集挺好、上线就崩”的问题都能在数据源头被拦住。至少我的经验里这类问题最终定位到数据拆分环节的远比定位到模型算法的多。希望帮到你。本文还有配套的精品资源点击获取
返回列表