简介:这是与《Python数据预处理(微课版)》配套的源代码V2.0资源包,面向正在学习数据清洗、转换与特征工程的读者,以及需要按教材实操练习的高校学生与自学人群。压缩包共134个文件,以57个.ipynb笔记为主体,配合36个csv、21个xls、4个xlsx表格数据,以及4个json、6个txt说明文档、1个py脚本和3个rar压缩项,总大小仅1016KB,轻量易获取。ipynb文件按章节组织,附可运行代码与输出结果,csv/xls数据文件覆盖超市销售、用户、音乐等多种示例,支撑缺失值处理、重复值删除、标准化等典型任务。其中rar压缩项可能含额外数据集或补充文档,配合txt说明可快速还原实验环境。目前已有55人学习,适合作为教材配套速查与复现工具。
1. 一份能跑通的源码,才是数据预处理该有的样子
拿到一份“Python数据预处理(微课版)-源代码V2.0”,你真正想确认的事情只有一件:这份代码能不能在我自己的电脑上跑起来,并且让我看清每一个预处理步骤到底改了什么。数据预处理不是“把数据变干净”这么一句话,而是从原始表到建模输入之间的一整套确定性操作:类型转换、缺失值填充、重复值删除、异常值截断、特征编码、无量纲化、数据集划分。每一环做没做、做得对不对,直接决定后面模型的上限。
V2.0 这个版本号暗示的不只是“修正了上一版的 bug”,更意味着代码结构、函数封装和运行结果都有了可复现的标准。这篇文章就围绕这套源码展开,把 Pandas 和 Scikit-learn 在预处理链路里的落点、参数选择、调试手段讲透,既让你能照着跑通最小例子,也让你看完之后有能力改写成自己项目里的预处理模板。适合正在学 Python 机器学习的初学者,也适合要接手别人预处理代码的工程师。
2. 先把“数据预处理”拆清楚:V2.0 源码到底在管哪些环节
2.1 一个数据预处理流程的完整生命周期
数据预处理不是单一步骤,而是从数据接入到建模之前的整条流水线。你打开一份 V2.0 的源码文件,大概率会看到按顺序排列的函数,每个函数负责一个环节,最后用一个主流程把它们串起来。
我一般会把这条链路拆成六个阶段:
- 数据加载与概览,读入 CSV 或 Excel,先看 shape、dtypes、describe。
- 数据清洗,处理缺失值、重复行、异常值。
- 类型标准化,把时间字符串转成 datetime,把类别文本转成 category 类型。
- 特征工程,包括编码、分箱、衍生字段。
- 无量纲化,标准化或归一化,让不同量纲的特征可比。
- 数据集划分,按比例切分成训练集和测试集,保留分布一致性。
V2.0 源码的价值在于它把以上步骤封装成了函数。你不需要改逻辑,只需调用clean_data(df)、encode_features(df)这类接口,就能看到每一步的输入输出。读源码时,先不要一头扎进细节,而是找到主函数,顺着调用顺序看每个函数在做什么。
import pandas as pd from sklearn.model_selection import train_test_split df = pd.read_csv('raw_data.csv') def load_and_overview(data_path): df = pd.read_csv(data_path) print(df.shape) print(df.dtypes) print(df.describe()) return df df = load_and_overview('raw_data.csv')这段代码展示了预处理的第一步不是处理,而是“观察”。打印shape能让你立刻发现列数是否符合预期,dtypes能暴露类型问题,describe则会告诉你数值列的分布范围。如果你发现某列的 max 值比 75% 分位数大到离谱,那这一列大概率有异常值,后续处理才有据可依。
2.2 Pandas 和 Scikit-learn 在预处理中的分工边界
V2.0 源码里会出现两个核心库:Pandas 负责数据切分、筛选、聚合、类型转换,Scikit-learn 负责需要“拟合”的变换器。这个边界必须分清楚,否则会把代码写成一团浆糊。
Pandas 擅长的是按行按列操作。比如删除重复行用drop_duplicates(),缺失值填充用fillna(),数据类型转换用astype()。这些操作没有“学习”成分,只是对当前数据做变换。
df.drop_duplicates(subset=['user_id'], keep='first', inplace=True) df['age'] = df['age'].astype(int) df['signup_date'] = pd.to_datetime(df['signup_date'])drop_duplicates的subset参数指定判断重复的列,keep='first'保留第一条。astype(int)会把浮点或字符串形式的年龄强制转成整数,但如果列里有缺失值就会报错,因此要先处理缺失。pd.to_datetime是 Pandas 内置的时间解析函数,能自动识别2024-01-01或2024/01/01这类格式。
而 Scikit-learn 的StandardScaler、MinMaxScaler、OneHotEncoder都是“有状态”的变换器,它们在训练集上计算均值、方差或取值集合,然后用同样的参数去变换测试集。这正是数据预处理中最容易出错的点:如果你用整个数据集去 fit 再 transform,就等于让测试集的信息泄漏进了训练过程。
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)注意,测试集只调用transform,绝不调用fit。V2.0 源码如果遵循规范写法,一定会在划分数据之后再做标准化。这一点你在读代码时可以专门确认。
2.3 V2.0 相对于 V1.0 的典型改进方向
“源代码V2.0”不会只是换个文件名。根据数据预处理项目的常见演进路径,我从工程角度推测 V2.0 大概率改进了以下方面。
代码封装提升了可复用性。V1.0 可能是从头到尾的一段脚本,V2.0 则拆成了函数或类,每个环节独立可调。配置文件外置了参数。比如缺失值填充策略、标准化方式、测试集比例,不再硬编码在代码里,而是放进config.yaml或config.py。这让你改参数时不用翻代码。增加了数据校验逻辑。V2.0 会在每个函数入口检查输入数据是否为 DataFrame、是否有预期列名、是否有空值,出错时抛出明确的异常信息。
def validate_input(df, required_cols): if not isinstance(df, pd.DataFrame): raise TypeError('输入必须是 pandas DataFrame') missing_cols = set(required_cols) - set(df.columns) if missing_cols: raise ValueError(f'缺少必要的列: {missing_cols}') return True这段校验代码的返回值永远是True,它不改变数据,只是提前暴露问题。做数据预处理时最怕的不是数据脏,而是代码在某个隐蔽步骤崩溃却不知道原因。校验函数把错误前置,让问题在最早的时刻暴露。isinstance检查类型,set差集找出缺失列名,raise ValueError给出可读的错误信息。
3. 数据清洗实操:缺失值、重复值、异常值的标准处理代码
3.1 缺失值处理的三种策略和适用场景
缺失值是数据预处理里最常遇到的情况。V2.0 源码里通常会针对不同列的特征选择不同的填充策略,而不是对整个 DataFrame 统一填充。判断方法先看缺失比例,再看缺失是否随机。缺失比例低于 5% 的列可以直接删除该行;缺失比例高但有业务含义的列要单独分析;连续型特征一般用中位数或均值填充,离散型特征用众数填充。
def fill_missing_values(df): df = df.copy() numeric_cols = df.select_dtypes(include=['float64', 'int64']).columns for col in numeric_cols: missing_ratio = df[col].isnull().mean() if missing_ratio < 0.05: df.dropna(subset=[col], inplace=True) else: median_val = df[col].median() df[col].fillna(median_val, inplace=True) return df print('缺失值比例:') print(df.isnull().mean())先看isnull().mean()输出,它给出每一列的缺失比例,这是决策的基础。代码里select_dtypes(include=['float64', 'int64'])只选择数值列,避免尝试填充文本列。dropna(subset=[col])删除该列为空的行,inplace=True直接修改当前 DataFrame。fillna(median_val)用中位数填充,中位数比均值对异常值更稳健,比如收入列有极端大值时,用中位数填充不会扭曲整体分布。
3.2 重复值检测:从精确重复到近似重复
精确重复行很容易处理,drop_duplicates()一行搞定。真实场景里更麻烦的是“近似重复”,比如同一个用户因为系统 bug 生成了两条记录,但注册时间差了几秒,其他字段完全一样。V2.0 源码一般只处理精确重复,因为近似重复的判定需要业务规则介入。我建议的做法是先做精确去重,再人工抽查近似重复的情况。
def remove_duplicates(df, subset_cols): before = len(df) df = df.drop_duplicates(subset=subset_cols, keep='first') after = len(df) print(f'删除 {before - after} 行重复数据') return df df = remove_duplicates(df, subset_cols=['user_id', 'signup_date'])代码先记录去重前的行数before,去重后计算差值,这样你明确知道删了多少数据。subset_cols指定判断重复的列组合,比如['user_id', 'signup_date']表示这两列完全相同的行才视为重复。keep='first'保留第一次出现的行,如果你更信任最后更新的数据,可以改成keep='last'。这个参数按业务场景定,没有绝对正确的选项。
3.3 用 Z-score 和 IQR 识别异常值并做截断处理
异常值处理的常用方法有两种:Z-score 和四分位距(IQR)。Z-score 适合近似正态分布的数据,IQR 适合分布不了解或偏态明显的数据。V2.0 源码通常实现的是 IQR 方法,因为它不依赖分布假设。
def cap_outliers_iqr(df, column): Q1 = df[column].quantile(0.25) Q3 = df[column].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR df[column] = df[column].clip(lower=lower_bound, upper=upper_bound) return df df = cap_outliers_iqr(df, 'age')quantile(0.25)和quantile(0.75)分别计算第一和第三四分位数。IQR是两者之差,代表中间 50% 数据的跨度。上下界的1.5是标准系数,如果设置的异常阈值更严格,可以改成 3,这样只有极端值才会被截断。clip方法把超出边界的值强行改到边界值,这种方法叫“截断”或“缩尾”,比起直接删除异常值,它保留了样本量,同时限制了极端值的负面影响。
3.4 数据类型转换的坑:字符串数字与时间格式
数据类型转换是新手最容易踩坑的地方。你从 CSV 里读进来的一列,Pandas 可能把它识别成了 object 类型,但里面的值其实是数字或日期。此时不做转换,后面所有数值计算都会报错。V2.0 源码中通常有专门的类型校正函数。
df['price'] = pd.to_numeric(df['price'], errors='coerce') df['order_date'] = pd.to_datetime(df['order_date'], errors='coerce') df['is_vip'] = df['is_vip'].map({'是': 1, '否': 0})pd.to_numeric尝试把列转成数值类型,errors='coerce'表示转换失败时置为 NaN。这个参数特别重要,比如价格列里有"1,299"这种带逗号的字符串,直接转 numeric 会失败,置为 NaN 后可以再回填。pd.to_datetime同理,识别不了的日期置为 NaT。map方法做自定义映射,把中文的“是/否”转成 0/1 数值,这是机器学习模型能消费的格式。
转换完之后,建议再次检查df.dtypes确认转换生效,并检查df['price'].isnull().sum()看有多少值因转换失败变成了缺失。如果这个数字过大,说明原始数据格式不规整,需要回到清洗阶段处理。
4. 特征编码与标准化:从文本到数值的完整转换方案
4.1 标签编码和独热编码,什么时候用哪个
机器学习模型只能处理数值,所以类别特征必须编码。类别特征分为两种:有顺序关系的,比如“低/中/高”,用标签编码;没有顺序关系的,比如“北京/上海/广州”,用独热编码。V2.0 源码如果混用这两种编码,需要看它是否在代码注释里标明了理由。
from sklearn.preprocessing import LabelEncoder, OneHotEncoder df['education_level'] = LabelEncoder().fit_transform(df['education_level']) df = pd.get_dummies(df, columns=['city'], prefix='city')LabelEncoder直接把类别文本映射成 0、1、2 这样的整数。问题是这会引入大小关系,比如“小学”是 0、“博士”是 5,模型会认为 5 比 0“大”,实际上只是类别不同。所以只有顺序特征才适合用标签编码。pd.get_dummies是把类别列拆成多列,每列用 0/1 表示是否属于该类别。prefix='city'为新列添加前缀,生成city_北京、city_上海这样的列名,方便肉眼识别。
4.2 StandardScaler 与 MinMaxScaler 的适用范围和参数差异
数值特征做无量纲化,是许多算法的前置要求。两种最常用的方法有本质区别:StandardScaler把数据变成均值为 0、标准差为 1 的分布,适合数据本身近似正态或存在异常值的情况;MinMaxScaler把数据缩放到 [0, 1] 区间,适合有明确上下界的均匀分布数据。
from sklearn.preprocessing import StandardScaler, MinMaxScaler std_scaler = StandardScaler() minmax_scaler = MinMaxScaler() X_std = std_scaler.fit_transform(df[['income']]) X_mm = minmax_scaler.fit_transform(df[['income']])StandardScaler先计算均值和标准差,然后执行(x - mean) / std。变换之后,数据的单位被消除,数值变成了相对均值的“标准差偏移量”。MinMaxScaler计算最小值和最大值,然后执行(x - min) / (max - min)。注意,如果你的数据里有极端异常值,最小值和最大值会被异常值拉偏,导致绝大部分数据被压缩到接近 0 或接近 1 的狭窄区域。这时候MinMaxScaler反而是坏选择。
不涉及神经网络等对尺度敏感的模型,选哪个影响不大;涉及 SVM、K-Means、PCA 时,必须做标准化。V2.0 源码里如果同时提供两种 scaler,通常会有一个参数控制选择哪种。
4.3 特征分箱:把连续变量离散化的源码写法
有些时候不做标准化更好——把连续特征分箱成离散特征也有其价值。分箱有两种:等宽分箱按数值区间均匀切分,等频分箱按样本量均匀分配。pd.cut做等宽分箱,pd.qcut做等频分箱。
df['age_group'] = pd.cut(df['age'], bins=[0, 18, 35, 55, 100], labels=['少年', '青年', '中年', '老年']) df['income_rank'] = pd.qcut(df['income'], q=4, labels=['Q1', 'Q2', 'Q3', 'Q4'])pd.cut的bins参数传入区间边界,labels给每个区间命名。这里四个年龄段由五个边界值定义,边界值必须单调递增。pd.qcut的q=4表示按四分位数切分,每份包含约 25% 的数据。分箱的好处是让模型对异常值不敏感,代价是损失精度信息。比如 35 岁和 36 岁在模型中完全等价,这符合某些业务理解,但不适合所有场景。
4.4 把预处理封进 Pipeline,防止数据泄漏的可靠做法
数据泄漏是数据预处理里最隐蔽的错误。如果标准化或编码用的是全量数据的统计量,模型评估结果会虚高。V2.0 源码如果封得比较完善,应该会把预处理步骤封装到 Scikit-learn 的Pipeline里,这样训练和预测就自动保持一致的变换逻辑。
from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer preprocess_pipe = Pipeline(steps=[ ('imputer', SimpleImputer(strategy='median')), ('scaler', StandardScaler()) ]) X_train_processed = preprocess_pipe.fit_transform(X_train) X_test_processed = preprocess_pipe.transform(X_test)Pipeline接收steps列表,每个元素是(名称, 变换器)的元组。SimpleImputer的strategy='median'用中位数填充缺失值。fit_transform会依次对训练数据执行各个变换器;transform则用训练好的参数变换测试数据,不会重新计算统计量。这样做的好处在于,无论多长的预处理链路,都是一次 fit、多次 transform,变换器之间的顺序保证一致。
5. 数据划分与均衡处理:训练集、验证集、测试集的正确打开方式
5.1 train_test_split 的 stratify 参数和 random_state 设置
拿到特征和标签之后,划分数据集是建模前的最后一步。V2.0 源码里大概率使用train_test_split,但很多初学版本忽略了一个重要参数:stratify。不做分层采样时,如果原始数据里正负样本比例是 9:1,切分后训练集和测试集的比例可能变成 8:2 或 9.5:0.5,导致模型评估失真。
from sklearn.model_selection import train_test_split X = df.drop('target', axis=1) y = df['target'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) print(f'训练集样本数: {len(X_train)}, 测试集样本数: {len(X_test)}') print(f'训练集正类比例: {y_train.mean():.3f}, 测试集正类比例: {y_test.mean():.3f}')test_size=0.2表示 20% 的数据作为测试集。random_state=42固定随机种子,让每次运行得到的划分结果一致,这是可复现的基础。stratify=y按标签分布进行分层采样,保证训练集和测试集里正负比例和原始数据一致。两个mean()的值如果差不多,说明分层生效了。如果差异超过 0.02,就要检查原始数据的类别分布是否均衡。
5.2 类别不平衡时的分层采样和重置索引
训练时遇到类别不平衡,比如目标变量中 99% 是0、1% 是1,你还需要做进一步处理。V2.0 源码如果涉及分类问题,通常会用过采样或欠采样来平衡类别。
X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) X_train.reset_index(drop=True, inplace=True) y_train.reset_index(drop=True, inplace=True)注意train_test_split返回的索引是乱的:它随机抽选行,所以切分后的 DataFrame 索引不是从 0 开始的连续整数。这在后续concat或按位置操作时会引发意外。reset_index(drop=True)重建从 0 开始的整数索。inplace=True直接修改对象。这一步看似无关紧要,实际调试时能省掉大量困惑。
5.3 验证集:不要在测试集上调参
即使有了训练集和测试集,还有一个环节容易被忽略:验证集。常规做法是再从训练集切出一部分做验证,用来调整超参数,测试集只评估一次最终模型。V2.0 源码如果结构完整,会提供validation_split参数。
X_train, X_val, y_train, y_val = train_test_split( X_train, y_train, test_size=0.25, random_state=42, stratify=y_train ) print(f'最终训练集: {len(X_train)}, 验证集: {len(X_val)}, 测试集: {len(X_test)}')在原始数据 80/20 划分之后,再从训练集中切 25% 作为验证集,最终训练、验证、测试的比例是 60:20:20。验证集用来观察模型收敛情况和调超参,测试集只用于最终报告性能指标。这三者职责必须分离,测试集一旦反复使用,它就不再能真实反映模型的泛化能力。
6. 快速验证源码可用性的三个技巧:断言、抽样对比、版本管理
拿到任何一份数据预处理源代码,最想知道的不是“代码写得规不规整”,而是“能不能跑、结果对不对”。我建议用三个方法快速验证,整个过程不超过十分钟。
先看一个完整的可运行示例,把断言写在每个预处理函数之后。比如处理完缺失值后,断言df.isnull().sum().sum() == 0,这样一旦填充逻辑有遗漏,程序会立刻报错。
assert df.isnull().sum().sum() == 0, '仍有缺失值' assert df.duplicated().sum() == 0, '仍有重复行' assert df.shape[1] >= X.shape[1], '特征列数不应减少'第一个断言验证缺失值已清零,第二个验证重复行已删除,第三个防止特征编码时列数意外减少,导致信息丢失。断言放在每个处理步骤之后,可以精确定位到哪一步出错。
再做抽样对比。在预处理前后各取 100 行数据,人工检查关键列的变化是否符合预期。比如年龄列的 max 值在截断前是 150,截断后应该是 100 或某个边界值。抽样对比看代码逻辑是否和业务预期一致。这一步不用写代码,直接df_before.head(100)和df_after.head(100)对比即可。
最后是版本管理。V2.0 源代码值得放进 Git 仓库,每次改动都提交一次。重点记录两个文件:原始数据和处理后的数据,即使中间步骤调了参数,也能随时对比版本差异。
git init git add requirements.txt preprocess.py git commit -m "V2.0 初始提交:缺失值填充+异常值截断+标准编码" git tag v2.0git tag v2.0给当前版本打标签,之后如果改动出了问题,可以用git checkout v2.0回到这个稳定版本。我一般还会在requirements.txt里固定依赖版本,避免 Python 环境升级导致结果不可复现。验证一份代码,方法越无脑,用起来越省心。
本文还有配套的精品资源,点击获取