简介:本资源面向Python初学者与数据科学入门者,聚焦CSV数据集预处理中的核心环节——随机打散与训练/测试集拆分,解决机器学习建模前数据准备不规范、分布偏差大等常见问题。压缩包共4个文件(3个CSV示例数据集+1个完整可运行的processdata.py脚本),总大小仅7KB,轻量易用:其中data0.csv为原始数据源,train/test两个CSV为打散后按比例拆分的标准化结果,py脚本则封装了pandas读取、缺失值检查、sample随机重排、iloc切片拆分等全流程逻辑,代码注释清晰,适合作为教学范例或项目脚手架。目前已有109人学习下载,读者可直接复现数据打散与7:3拆分操作,掌握保证数据分布一致性的关键实践,并快速迁移到自己的CSV数据处理任务中。
1. 这不是简单的train_test_split:一份能直接跑通的 CSV 打散拆分实战包,专治「训练集全是猫、测试集全是狗」的玄学翻车
你有没有遇到过:模型在训练集上准确率 98%,一到测试集就掉到 62%?调参调到凌晨三点,最后发现——原始 CSV 文件里前 500 行全是正样本,后 500 行全是负样本,压根没打散就直接切了 7:3?这种「数据分布偏移」导致的泛化失败,在真实项目里比算法选错更常见、更隐蔽、更让人后悔药都买不到。这份名为《Python对CSV数据集的处理-打散与拆分》的资源包,就是为解决这个具体痛点而生:它不讲 sklearn 的train_test_split原理,而是给你一个开箱即用的processdata.py脚本 + 三份配套 CSV(data0.csv原始数据、data0-train.csv和data0-test.csv拆分结果),所有逻辑封装成可读、可改、可 debug 的纯 Python 实现。适合刚学完 pandas 但还没在真实数据上踩过坑的新手,也适合需要快速验证数据预处理链路、拒绝黑匣子函数的熟手。它不承诺“一键炼丹”,但保证你执行完每一步,都能清楚看到数据在内存里怎么被 shuffle、怎么被切片、哪一行去了 train、哪一行进了 test。
2. 从data0.csv到train/test:四步落地流程与核心代码逐行拆解
2.1 加载与探查:先看清数据长什么样,再动手
拿到任何 CSV,第一件事不是切分,而是“看”。processdata.py开头就强制做这一步:
import pandas as pd import numpy as np # 步骤1:加载原始数据 df = pd.read_csv('data0.csv') print(f"原始数据形状: {df.shape}") print(f"列名: {list(df.columns)}") print("\n前5行预览:") print(df.head()) print("\n基础统计:") print(df.describe(include='all'))提示:这段代码不是摆设。
df.shape告诉你总行数,决定后续切分比例是否合理;df.columns检查列名是否含空格或特殊字符(比如"label "多了个空格会导致后续df['label']报错);df.head()看前五行,确认数据是否真按预期加载(曾有同事把 Excel 导出 CSV 时用了分号分隔,read_csv默认逗号,结果整张表变成一列);describe(include='all')同时显示数值列和非数值列的统计,一眼揪出object类型列里的空值、唯一值数量异常(比如某分类标签列本该有 3 类,却显示unique: 1,说明全列是空字符串)。
2.2 清洗与校验:跳过这步,后面全白干
摘要里提到“检查缺失值、异常值、重复记录”,processdata.py把它固化为可配置的校验环节:
# 步骤2:基础清洗与校验(可选启用) def validate_and_clean(df, drop_duplicates=True, handle_na='drop'): """ df: 输入DataFrame drop_duplicates: 是否去重(默认True) handle_na: 如何处理缺失值,'drop'/'fill_zero'/'fill_mode' """ print(f"\n=== 数据质量校验 ===") print(f"缺失值统计:\n{df.isnull().sum()}") if drop_duplicates: original_len = len(df) df = df.drop_duplicates() print(f"去重前 {original_len} 行,去重后 {len(df)} 行,删除 {original_len - len(df)} 行重复项") if handle_na == 'drop': df = df.dropna() print(f"删除含缺失值的行,剩余 {len(df)} 行") elif handle_na == 'fill_zero': df = df.fillna(0) print("缺失值已填充为0") elif handle_na == 'fill_mode': for col in df.columns: if df[col].dtype == 'object': df[col] = df[col].fillna(df[col].mode()[0] if not df[col].mode().empty else 'unknown') else: df[col] = df[col].fillna(df[col].median()) print("缺失值按类型填充:分类列填众数,数值列填中位数") return df # 调用校验(实际使用时取消注释) # df = validate_and_clean(df, drop_duplicates=True, handle_na='drop')参数说明:
drop_duplicates防止同一份样本被多次采样进 train/test;handle_na提供三种策略,'drop'最保守(适合小数据集),'fill_mode'更贴近业务(比如用户性别缺失,填“未知”比填0合理)。注意:此函数返回新 DataFrame,原df不变,避免副作用。
2.3 打散:为什么sample(frac=1)比shuffle=True更可控
摘要说“用sample(frac=1)打散”,这是关键。很多人直接用np.random.shuffle(df.values),但这是危险操作——它会原地修改df.values,但df.index不变,导致索引与数据错位。processdata.py采用安全方案:
# 步骤3:安全打散(推荐!) np.random.seed(42) # 固定随机种子,保证可复现 df_shuffled = df.sample(frac=1, random_state=42).reset_index(drop=True) print(f"打散后前3行索引: {list(df_shuffled.index[:3])}") print(f"打散后前3行label值: {list(df_shuffled['label'][:3])}")逻辑说明:
frac=1表示抽取 100% 行数,等价于全量重排;random_state=42确保每次运行结果一致,方便调试;reset_index(drop=True)重置索引为 0,1,2...,避免后续切片时因旧索引跳跃出错。对比np.random.shuffle:后者直接改df.values,若df有 MultiIndex 或自定义索引,极易引发KeyError;而sample是 pandas 原生方法,自动维护索引一致性。
2.4 拆分:按比例切片,同时支持 stratify(分层抽样)
核心拆分逻辑如下,支持两种模式:
# 步骤4:拆分训练集与测试集 def split_dataset(df, train_ratio=0.7, stratify_col=None): """ df: 已打散的DataFrame train_ratio: 训练集占比(0~1之间) stratify_col: 分层抽样列名(如'label'),确保train/test中各类别比例一致 """ n_total = len(df) n_train = int(n_total * train_ratio) if stratify_col is None: # 简单切片 train_df = df.iloc[:n_train].copy() test_df = df.iloc[n_train:].copy() print(f"简单切片:train {n_train} 行,test {n_total - n_train} 行") else: # 分层抽样(需scikit-learn) from sklearn.model_selection import train_test_split train_df, test_df = train_test_split( df, train_size=train_ratio, stratify=df[stratify_col], random_state=42 ) print(f"分层抽样:train {len(train_df)} 行,test {len(test_df)} 行") print(f"分层校验 - train中各类别数量:\n{train_df[stratify_col].value_counts().sort_index()}") print(f"分层校验 - test中各类别数量:\n{test_df[stratify_col].value_counts().sort_index()}") return train_df, test_df # 执行拆分(示例:按'label'列分层) train_df, test_df = split_dataset(df_shuffled, train_ratio=0.7, stratify_col='label')参数说明:
train_ratio=0.7即 70% 训练集;stratify_col='label'是救命功能——当你的数据类别极度不均衡(如 95% 正常样本、5% 异常样本),简单切片可能导致 test 里一个异常样本都没有,评估完全失效。分层抽样强制让 train/test 中正常/异常比例都接近 95:5。注意:分层模式需额外导入sklearn,但processdata.py中已用if包裹,不启用时无依赖。
3. 避坑指南:五个血泪经验总结,专治“明明代码没错,结果死活不对”
3.1 现象:train_df和test_df行数加起来不等于原始数据行数
原因:int(n_total * train_ratio)向下取整,比如 1001 行 × 0.7 = 700.7 →int()变成 700,test 得 301 行,总和 1001 没问题;但若n_total=1000,0.7得 700,test 300,总和 1000。看似合理,但若你手动写df.iloc[0:700]和df.iloc[700:],当n_total不是 10 的倍数时,700可能超出索引范围(如n_total=699)。
解决:永远用n_train = int(len(df) * train_ratio)计算,然后df.iloc[:n_train]和df.iloc[n_train:],后者自动处理边界(iloc[700:]对 699 行 DataFrame 返回空 DataFrame,不会报错)。
3.2 现象:train_df['label'].value_counts()和test_df['label'].value_counts()比例严重失衡,即使启用了stratify_col
原因:stratify要求df[stratify_col]中每个类别的样本数 ≥ 2(train_test_split内部要求最小类别至少有 2 个样本才能分层)。如果某类别只有 1 个样本,stratify会静默退化为普通切片,且不报错!
解决:拆分前强制检查df['label'].value_counts(),若存在count < 2的类别,必须先处理(如合并小类别、或用SMOTE过采样——但这已超出本包范围,脚本里加了预警):
if stratify_col and df[stratify_col].value_counts().min() < 2: print(f"警告:列 '{stratify_col}' 中存在少于2个样本的类别,分层抽样将失效!") print(f"建议:先处理稀有类别,或改用简单切片。") # 此处可抛出异常:raise ValueError("Stratify failed: min class count < 2")3.3 现象:processdata.py运行后生成的data0-train.csv里,中文列名变成乱码(如æ ‡ç¾)
原因:Windows 系统默认编码是gbk,而pandas.read_csv默认用utf-8解码。若原始 CSV 是 Excel 保存的,很可能用gbk编码,read_csv用utf-8读就会乱码。
解决:在read_csv中显式指定encoding:
# 尝试gbk编码(中文Windows常见) try: df = pd.read_csv('data0.csv', encoding='gbk') except UnicodeDecodeError: # 备用utf-8 df = pd.read_csv('data0.csv', encoding='utf-8')血泪经验:我一般会在脚本开头加一个
encoding_probe函数,用chardet库自动检测,但为减少依赖,本包默认用gbk试探,失败再切utf-8。
3.4 现象:data0-test.csv文件生成了,但用 Excel 打开全是乱码,用记事本打开却正常
原因:Excel 默认用系统编码(gbk)打开 CSV,但pandas.to_csv默认用utf-8保存。utf-8编码的文件,Excel 在无 BOM 的情况下会误判为ANSI(即gbk),导致乱码。
解决:保存时加 BOM(Byte Order Mark),让 Excel 正确识别utf-8:
train_df.to_csv('data0-train.csv', index=False, encoding='utf-8-sig') test_df.to_csv('data0-test.csv', index=False, encoding='utf-8-sig')注意:
'utf-8-sig'是 pandas 特有编码名,等价于utf-8+ BOM。不要用'utf-8',否则 Excel 打开必乱码。
3.5 现象:processdata.py第一次运行成功,第二次运行报错FileNotFoundError: [Errno 2] No such file or directory: 'data0-train.csv'
原因:脚本里写了train_df.to_csv('data0-train.csv'),但没加if os.path.exists('data0-train.csv'): os.remove('data0-train.csv')。第一次运行生成文件,第二次运行时to_csv会覆盖,但若文件被 Excel 占用(Windows 下 Excel 会锁住正在编辑的 CSV),to_csv就会因权限不足报错。
解决:加文件占用检测和清理:
import os def safe_save_csv(df, filename): if os.path.exists(filename): try: os.remove(filename) # 尝试删除 except PermissionError: print(f"警告:无法删除 {filename},可能被其他程序占用,请关闭相关软件后重试") return False df.to_csv(filename, index=False, encoding='utf-8-sig') print(f"已保存 {filename}") return True safe_save_csv(train_df, 'data0-train.csv') safe_save_csv(test_df, 'data0-test.csv')4. 进阶技巧:如何验证拆分结果真的“随机”且“公平”?
4.1 用统计检验验证随机性:Kolmogorov-Smirnov 检验
打散是否真随机?不能只信sample(frac=1)。对数值型特征(如age,price),我们可以用 KS 检验比较 train/test 的分布是否一致。processdata.py末尾附带验证函数:
from scipy.stats import ks_2samp def validate_distribution(train_df, test_df, numeric_cols): """ 对指定数值列,用KS检验验证train/test分布是否一致(p>0.05表示无显著差异) """ print("\n=== 分布一致性KS检验(p>0.05表示分布无显著差异)===") for col in numeric_cols: if col in train_df.columns and col in test_df.columns: stat, p_value = ks_2samp(train_df[col], test_df[col]) status = "✅ 一致" if p_value > 0.05 else "❌ 差异显著" print(f"{col:12}: KS统计量={stat:.4f}, p={p_value:.4f} {status}") # 示例:验证'age'和'salary'列 validate_distribution(train_df, test_df, ['age', 'salary'])原理:KS 检验衡量两个样本的经验分布函数之间的最大距离。
p>0.05意味着我们没有足够证据拒绝“两分布相同”的原假设,即 train/test 在该特征上分布相似。这是比肉眼观察describe()更严格的验证。
4.2 可视化对比:用 Seaborn 画分布直方图
光看数字不够直观,加个可视化:
import seaborn as sns import matplotlib.pyplot as plt def plot_feature_distribution(train_df, test_df, col, bins=30): """绘制单特征train/test分布对比图""" plt.figure(figsize=(8, 4)) sns.histplot(train_df[col], bins=bins, kde=True, label='Train', alpha=0.6) sns.histplot(test_df[col], bins=bins, kde=True, label='Test', alpha=0.6) plt.title(f"'{col}' 分布对比 (Train vs Test)") plt.xlabel(col) plt.ylabel('频数') plt.legend() plt.grid(True, alpha=0.3) plt.show() # 示例:画'age'分布 plot_feature_distribution(train_df, test_df, 'age')技巧:
kde=True叠加核密度估计曲线,比纯直方图更能看出分布形状;alpha=0.6让重叠区域颜色加深,一眼看出差异点。若 train 的峰值在 25 岁,test 的峰值在 45 岁,说明打散没做好,得回去检查sample步骤。
4.3 分层抽样深度验证:混淆矩阵视角
stratify_col保证了 label 比例一致,但能否进一步验证“每个 label 下的子特征分布也一致”?比如label=1(欺诈交易)中,amount的均值在 train/test 是否接近?我们可以按 label 分组再检验:
def validate_stratify_deep(train_df, test_df, stratify_col, numeric_col): """验证分层列下,某数值列的分布一致性""" print(f"\n=== 按 '{stratify_col}' 分层验证 '{numeric_col}' 分布 ===") for label_val in sorted(train_df[stratify_col].unique()): train_sub = train_df[train_df[stratify_col] == label_val][numeric_col] test_sub = test_df[test_df[stratify_col] == label_val][numeric_col] if len(train_sub) > 10 and len(test_sub) > 10: # 样本足够才检验 stat, p = ks_2samp(train_sub, test_sub) print(f"Label {label_val}: train均值={train_sub.mean():.2f}, test均值={test_sub.mean():.2f}, p={p:.4f}") validate_stratify_deep(train_df, test_df, 'label', 'amount')价值:这步能发现“表面分层成功,实则内部偏移”的隐患。例如
label=1在 train 中平均交易额 5000 元,test 中却是 15000 元,模型在 test 上的欺诈识别逻辑可能完全失效。
4.4 自动化报告:生成 HTML 验证报告
把以上检验打包成一键报告,省去手动截图:
def generate_validation_report(train_df, test_df, output_html='validation_report.html'): """生成包含统计、图表、检验结果的HTML报告""" from io import BytesIO import base64 # 生成图表 fig, axes = plt.subplots(1, 2, figsize=(12, 4)) train_df['label'].value_counts().plot(kind='bar', ax=axes[0], title='Train Label Distribution') test_df['label'].value_counts().plot(kind='bar', ax=axes[1], title='Test Label Distribution') plt.tight_layout() # 保存图表为base64 buf = BytesIO() plt.savefig(buf, format='png', dpi=100, bbox_inches='tight') buf.seek(0) img_base64 = base64.b64encode(buf.read()).decode() # 构建HTML html = f""" <html><body> <h2>数据集拆分验证报告</h2> <h3>Label分布对比</h3> <img src="data:image/png;base64,{img_base64}" alt="Label分布"> <h3>KS检验结果</h3> <pre>{ks_results_text}</pre> </body></html> """ with open(output_html, 'w', encoding='utf-8') as f: f.write(html) print(f"验证报告已生成:{output_html}") # 使用时调用 generate_validation_report(train_df, test_df)提示:这个函数需要
matplotlib和seaborn,但它是可选的。我一般只在项目交付前跑一次,把validation_report.html作为数据质量凭证发给导师或客户。
5. 从那以后,我每次处理新数据集都强制走一遍“三查一存”流程
所谓“三查一存”,是我踩了太多坑后固化下来的肌肉记忆:
- 一查原始编码:用
file命令(Linux/Mac)或chardet库(Windows)先看data0.csv真实编码,绝不依赖 Excel 的“另存为”描述。曾经一个项目,甲方给的 CSV 明明标着 UTF-8,实测是gb18030,read_csv不指定编码直接崩。 - 二查索引连续性:
df.index.is_monotonic_increasing and df.index.equals(pd.RangeIndex(len(df))),如果不是,立刻df.reset_index(drop=True)。因为iloc切片依赖物理位置,索引跳跃会导致iloc[100:]漏掉中间行。 - 三查 label 分布稳定性:运行
split_dataset后,立刻print(train_df['label'].value_counts(normalize=True))和print(test_df['label'].value_counts(normalize=True)),两列数字必须小数点后两位都一致(如0.70和0.70),差0.01都要警惕——可能是int()取整误差,改用round()或np.floor()更稳妥。 - 一存可复现快照:
np.random.seed(42)是底线,但更保险的是把打散后的df_shuffled也存成data0-shuffled.csv。这样下次调试不用重跑sample,直接pd.read_csv('data0-shuffled.csv'),时间省一半,心态稳三分。
这个包里的processdata.py就是按这四步写的:查编码(encoding试探)、查索引(reset_index)、查分布(value_counts输出)、存快照(可选to_csv打散版)。它不炫技,不堆库,就做一件事:让你在数据进入模型前,心里有底。希望帮到你。
本文还有配套的精品资源,点击获取