ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

机器学习数据预处理:用Pandas完成清洗与标准化实战

机器学习数据预处理:用Pandas完成清洗与标准化实战 1. 为什么数据预处理是训练模型的“门票”做机器学习这几年我最大的体会是模型结构再先进、调参再熟练遇上脏数据照样白搭。GIGO这条铁律在训练数据集上体现得淋漓尽致——垃圾进垃圾出。很多初学者习惯把数据下载下来直接扔进模型结果损失函数震荡、精度死活上不去回头排查半天才发现是原始数据里一堆缺失值、重复记录、错误格式在捣鬼。这篇文章要聊的就是训练数据集在喂给模型之前必须经过的清洗与标准化步骤核心工具是Python生态里最常用的Pandas。Pandas之于数据科学家相当于手术刀之于外科医生——你可以徒手撕数据但用对了工具效率和精度完全不在一个量级。这里会覆盖从读取原始文件、处理缺失值与重复值、修正数据类型到标准化特征、划分训练集的全流程。适合谁看如果你正在做机器学习项目或者准备参加Kaggle之类的竞赛或者手里有一份杂乱的真实业务数据正发愁怎么处理这篇内容就是给你准备的。我会把实操中的坑和取舍逻辑一并写出来不是那种只甩几个API的文档式教程而是真正踩过坑之后沉淀下来的经验。2. 数据清洗先搞清楚你的数据到底有多“脏”2.1 第一步读取数据先别急先摸清底细拿到任何一份数据我的习惯动作是三步head()看长什么样、info()看类型和非空情况、describe()看数值分布。这三板斧打完数据的大致健康状况就有数了。import pandas as pd df pd.read_csv(train_data.csv, encodingutf-8) print(df.head()) # 前5行确认列名和内容 print(df.info()) # 每列的非空计数与dtype一眼看出缺失和类型问题 print(df.describe()) # 数值列的均值、标准差、min/max、四分位数这里有个容易被忽视的细节read_csv的encoding参数。中文数据经常是GBK编码直接读取会报UnicodeDecodeError。我一般在读取时带上encodinggbk或encodinggb18030再不行就用errorsignore兜底。还有一种情况是分隔符不是逗号比如制表符\t分割的文件记得加sep\t。这些看似小的问题在实际项目中能卡住你半小时。info()输出里object类型的列值得特别注意。如果一列数值数据在Pandas里显示为object通常意味着里面混入了字符串比如 “1,234” 这种带千分位分隔符的值或者干脆有 “N/A”“Unknown” 这类占位文本。这种情况下后续的数值计算全部会失效必须做类型转换后面会细说。2.2 缺失值处理dropna与fillna的取舍缺失值是训练数据集里最常见的“脏东西”。处理缺失值有两个方向删除和填充选择哪种取决于缺失比例和数据性质。先看缺失比例和分布missing_ratio df.isnull().sum() / len(df) print(missing_ratio[missing_ratio 0])如果某列缺失比例超过70%这列基本可以放弃留着只会引入噪声。缺失比例在5%以内且行数充足时直接删除这些行也问题不大df_clean df.dropna(subset[age, income]) # 只对关键列删行但有几个场景不能无脑删。一是时间序列数据删除行会破坏时间连续性二是缺失集中在某个关键特征上而样本数本身就有限删完可能就没数据了。这时候要用填充策略# 数值列用中位数填充比均值更稳健不受极端值影响 df[income] df[income].fillna(df[income].median()) # 类别列用众数填充 df[category] df[category].fillna(df[category].mode()[0]) # 时间序列用前向填充 df[sales] df[sales].ffill()数值列我强烈建议用中位数而不是均值。原因很简单均值容易被极端值拉偏比如一组收入数据里有个千万富翁均值会明显上移用这种偏掉的中心值填充缺失值相当于给模型注入了错误先验。中位数在存在异常值的情况下依然能代表“大多数人的水平”。还有一个进阶技巧缺失值本身可能携带信息。比如银行风控数据里有些客户“收入缺失”本身就是一种风险信号。这时候与其填充一个估计值不如保留一个income_missing标记列让模型自己去学这个模式的含义。2.3 重复值处理drop_duplicates的隐藏参数重复值比缺失值隐蔽得多。drop_duplicates()默认是整行所有列完全一致才算重复但实际业务场景里往往只有部分列应该作为判重依据。比如用户行为数据里同一用户在同一时刻的重复点击记录判定重复的条件是“用户ID 时间戳”而不是整行相同。# 整行完全重复 df_clean df.drop_duplicates() # 指定列判重保留第一次出现的记录 df_clean df.drop_duplicates(subset[user_id, timestamp], keepfirst) # 保留最后一次出现的记录适合处理“最终状态”类数据 df_clean df.drop_duplicates(subset[order_id], keeplast)这里keep参数值得细讲。默认keepfirst保留第一条但如果你在处理一个订单状态流转的数据集每条记录代表一次状态变更那你想要的是每个订单的最终状态这时候应该用keeplast。判断用哪个核心是问自己这条记录的“有效性”是随时间递增还是递减。还有一种情况是“看似重复实则不重复”比如两个用户特征完全一样但确实是两个人。如果业务上没有明确的唯一键别轻易删。我的经验是判重列要结合业务含义来确定宁可少删不可误删。2.4 数据类型转换astype、to_numeric、to_datetime三大金刚类型转换是数据清洗里最磨人但收益最大的一步。前面提过object类型的数值列处理方式是用pd.to_numeric# 强制转换无法解析的变为NaN df[price] pd.to_numeric(df[price], errorscoerce) # 转换后再处理这些新产生的NaN df[price] df[price].fillna(df[price].median())errorscoerce是这里的灵魂参数。它会尝试把每个值转成数值失败的变成NaN这样你能定位到哪些原始值是垃圾文本。我喜欢在转换前先跑一次# 找出无法转换的值长什么样 bad_values pd.to_numeric(df[price], errorscoerce).isnull() df[price].notnull() print(df.loc[bad_values, price].head())排查出这些坏值后你就能理解问题了可能是“1,200”这种千分位格式、可能是“100元”这种带单位的、也可能是Excel导出时数字变成了科学计数法文本。针对不同病因对症下药才高效。日期时间列的转换同样常用。真实的训练数据集里时间戳往往是字符串格式五花八门df[date] pd.to_datetime(df[date], format%Y-%m-%d, errorscoerce)用format参数明确告诉Pandas输入格式能显著加快解析速度。如果你不确定格式可以先用Pandas自动推断但大数据量下自动推断慢得让人抓狂。解析完日期之后你还经常需要继续衍生新特征——提取“星期几”“是否为节假日”“距今天数”等这些在后续特征工程里都是常用输入。astype则适合那些明确且安全的转换浮点转整型、int64转int32省内存。但我必须提醒一句astype如果遇到无法转换的值会直接抛异常没有errors参数可救。所以我的经验是——涉及数据清洗场景优先用to_numeric和to_datetime这两个函数天生就是为“不干净的数据”设计的。3. 标准化与特征缩放为什么模型这么在意量纲3.1 两种主流方法Z-score与Min-Max数据清洗干净之后下一步是标准化。先解释一个最常见的困惑为什么需要标准化你看一个例子某数据集有两列年龄20-80和年收入5万-200万。如果用基于距离的算法KNN、SVM、K-Means计算欧氏距离时收入这一维的差异会完全碾压年龄维度——年龄差30的贡献可能还没有收入差1000的贡献大。模型还没开始学权重就先被量纲绑架了。Z-score标准化是应用最广的公式也不复杂减去均值除以标准差。标准化后数据均值为0标准差为1符合标准正态分布。Pandas里一行搞定df[age_scaled] (df[age] - df[age].mean()) / df[age].std()Min-Max归一化则是把数据压缩到[0,1]区间减去最小值除以极差。它对数据的分布不做“正态化”假设只是纯粹的线性缩放。df[age_scaled] (df[age] - df[age].min()) / (df[age].max() - df[age].min())3.2 具体场景怎么选没有银弹只有适合选择哪种方法我的判断依据是这几条第一看下游算法。树模型随机森林、XGBoost、LightGBM对特征量纲不敏感因为这些模型本质上是做分裂阈值搜索量纲不影响分裂点的选择。线性模型、逻辑回归、神经网络、SVM、KNN、K-Means这些基于距离或梯度的算法对量纲高度敏感必须做标准化。如果你用的算法不确定或者想图省事一律标准化不会错。第二看数据本身的分布。如果特征存在极端异常值Z-score会被均值和标准差严重干扰这时候要么先做异常值处理再标准化要么用更稳健的标准化方法比如用中位数和四分位距代替均值和标准差。Min-Max对异常值更敏感因为一个极端值就能把其他所有值压缩到贴地皮。第三看是否需要保留稀疏性。如果你的数据包含大量0值比如推荐场景的用户-物品评分矩阵Min-Max会把0映射成一个非零值破坏稀疏结构。这时候Z-score也未必好因为均值不再是0附近。这种情况我一般先做异常值处理再按业务决定。3.3 训练集/测试集的标准化陷阱这个是区分新手和老手的分水岭。很多人会把整个数据集合并起来做标准化然后再切训练集和测试集。这在小样本演示时看着没问题但放到真实生产环境就是隐患测试集的信息泄漏进了训练过程。正确的做法是先切分再用训练集的统计量去转换测试集。用代码说清楚from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) scaler StandardScaler() # 只用训练集拟合 scaler.fit(X_train) # 用训练集的均值和标准差转换两边 X_train_scaled scaler.transform(X_train) X_test_scaled scaler.transform(X_test)fit时求得的均值和标准差来自训练集。测试集只是被“应用”了这套参数绝对不能参与fit。否则相当于模型在考试时偷偷看到了答案分布测试集上的评估结果全部失真。生产环境还有一个更隐蔽的坑训练时用了全量的均值标准差线上推理时发现新数据的特征分布已经漂移。这不是Pandas能解决的但你应该从一开始就把“标准化器持久化”养成习惯——用joblib.dump(scaler, scaler.pkl)保存训练好的scaler对象线上加载后直接用于转换实时数据。4. 异常值与噪声处理别让几颗老鼠屎坏了一锅汤4.1 异常值为什么危险异常值对训练数据集的危害比缺失值更隐蔽也更致命。缺失值只是“信息缺失”而异常值是在“积极误导”。一个极端值可以把线性回归的斜率硬生生拽偏几十度可以让神经网络在训练前期疯狂震荡可以让标准化后的数据分布彻底变形。我很难忘的一次经历用某平台公开的房价数据训练模型describe()里看面积这一列最大值居然有9999平方米。查了原始数据才发现这其实是录入时的一个占位符原始值为空系统自动填了9999。如果不处理这是一个相当于几十个标准差的异常值模型会被它严重带偏。4.2 Z-score和IQR两种检测方法的Pandas实现最基础的异常值检测方法是三倍标准差原则Z-score。假设数据服从正态分布约99.7%的数据落在均值±3倍标准差范围内超出这个范围的视为异常z_scores (df[area] - df[area].mean()) / df[area].std() df[is_outlier] z_scores.abs() 3这个方法简单但对数据分布要求高。如果数据本身偏态严重或者存在多个极端值导致均值和标准差本身被污染Z-score方法的检测效果就会打折扣。更稳健的替代方案是IQR四分位距法。IQR Q3 - Q1也就是第75百分位数与第25百分位数之差。通常把低于 Q1 - 1.5×IQR 或高于 Q3 1.5×IQR 的值视为异常Q1 df[area].quantile(0.25) Q3 df[area].quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR df[is_outlier] (df[area] lower_bound) | (df[area] upper_bound)IQR方法基于分位数对极端值不敏感尤其在非正态分布数据上明显更可靠。实际项目中我经常先用IQR快速圈出候选异常值再人工查看因为有些“统计异常”在业务上其实是正常值——比如“凌晨3点的下单量”可能确实很低但那是规律性的低谷不该删。异常值的处理方式也要分情况如果确认是录入错误面积9999平方米删除或修正如果是真实存在的极端事件金融危机期间的股票波动保留反而能增强模型鲁棒性如果异常值是业务规则禁入的比如年龄为负直接过滤。5. 完整实操一个端到端的清洗与标准化流水线5.1 预处理不是一次性动作而是可复用的管线很多人都犯过一个错误数据清洗的代码是东一榔头西一棒子写一次就扔。到了下个项目、下一份数据又从头开始写。真正省力的做法是把清洗流程封装成一个可复用的函数或流水线。我一般会把流程分成四个阶段加载与初检 → 清洗缺失值、重复值、类型转换 → 特征处理异常值、标准化 → 切分与保存。每个阶段之间用清晰的函数边界隔离既方便单步调试也方便复用。5.2 实战完整的数据预处理代码假设我是从零开始处理一份用户画像数据集包含年龄、收入、所在城市、注册日期和最近30天消费金额。看看完整的预处理流程怎么组织import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler def load_raw_data(file_path): 阶段一加载与初检 df pd.read_csv(file_path, encodingutf-8-sig) print(f原始数据: {df.shape}) print(df.info()) return df def clean_data(df): 阶段二清洗 # 1. 类型转换 df[income] pd.to_numeric(df[income], errorscoerce) df[last_30d_spend] pd.to_numeric(df[last_30d_spend], errorscoerce) df[reg_date] pd.to_datetime(df[reg_date], errorscoerce) # 2. 处理缺失值 df df.dropna(subset[age, income]) # 关键列缺失删行 df[last_30d_spend] df[last_30d_spend].fillna(df[last_30d_spend].median()) # 3. 去重 df df.drop_duplicates(subset[user_id], keeplast) # 4. 异常值过滤IQR方法 for col in [income, last_30d_spend]: Q1, Q3 df[col].quantile(0.25), df[col].quantile(0.75) IQR Q3 - Q1 lower, upper Q1 - 1.5 * IQR, Q3 1.5 * IQR df df[(df[col] lower) (df[col] upper)] return df def engineer_features(df): 阶段三特征衍生与标准化 # 特征衍生注册时长天 df[reg_days] (pd.Timestamp(2024-06-01) - df[reg_date]).dt.days # 类别编码城市转为数值 df[city_code] pd.Categorical(df[city]).codes # 选择建模特征并标准化 feature_cols [age, income, last_30d_spend, reg_days] X df[feature_cols].copy() y df[is_high_value] # 假设有目标标签 return X, y def split_and_scale(X, y): 阶段四切分与标准化 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) scaler StandardScaler() scaler.fit(X_train) X_train_scaled scaler.transform(X_train) X_test_scaled scaler.transform(X_test) return X_train_scaled, X_test_scaled, y_train, y_test # 流水线执行 df_raw load_raw_data(user_profile.csv) df_clean clean_data(df_raw) X, y engineer_features(df_clean) X_train, X_test, y_train, y_test split_and_scale(X, y) print(f清洗后数据: {df_clean.shape}) print(f训练集: {X_train.shape}, 测试集: {X_test.shape})5.3 这段代码背后的几个关键选择注意几个细节。一是train_test_split里用了stratifyy这是分类任务里保持训练集和测试集类别比例一致的重要手段。如果你的标签分布本身不均衡比如正样本只占5%随机切分可能让测试集里一个正样本都没有评估直接失去意义。用stratify后两边正负样本比例会保持一致。二是切分时固定了random_state42。这不是迷信而是可复现性要求——每次运行代码得到同样的切分结果试验之间才有可比性。如果你想刻意做一个“数据版本管理”可以在不同模型迭代时换不同的random_state但一定要记录清楚。三是标准化放在特征衍生之后。reg_days是从日期衍生出来的数值特征它同样需要标准化如果先标准化再做特征衍生新特征就失去了统一量纲的机会。顺序很重要。6. 常见问题与排查技巧实录6.1 高频问题速查表问题现象可能原因解决方案read_csv报编码错误文件是GBK/GB18030编码改用encodinggbk或用encodingutf-8-sig处理带BOM的文件info()显示数值列是object类型列中存在文本脏值千分位、单位、占位符用pd.to_numeric(..., errorscoerce)转换并定位坏值缺失值填充后分布明显偏移填充策略不合理如用均值填充高偏态数据换用中位数或增加缺失标记列标准化后数据仍有极端值Z-score被异常值污染先用IQR过滤异常值再做标准化训练精度高但测试精度崩盘测试集参与了标准化拟合先切分再fit测试集只能用transform切分后训练集和测试集标签比例失衡样本不均衡且未分层抽样train_test_split(..., stratifyy)日期解析结果全是NaT日期格式不符合默认解析规则使用format参数明确指定输入格式内存不够用了读取时未限制数据类型用dtype参数指定列类型或只读需要的列usecols6.2 三个我踩过坑后的独家心得第一先做异常值处理再做标准化。这是个天然的先后顺序问题但很多人忽略。如果先标准化再筛异常值异常值已经通过均值和标准差渗透进了整个变换过程筛完之后的标准结果仍然是“被污染的标准”。反过来先清洗干净再标准化得到的特征分布才是数据真实状态的反映。第二预处理代码必须写成函数并且配日志。你无法预测v2版本的数据源会不会突然多出一列或者某个字段的格式说变就变。每次运行清洗流程时打印出关键统计值——清洗前多少行、删除多少重复行、填充了多少缺失值、过滤了多少异常值——形成一份数据质量报告。这样模型效果变差时你能第一时间判断是数据源头变了还是模型本身的问题。第三验证清洗效果的最快方式看一眼标准化后的分布。处理完之后我习惯跑一眼X_scaled.describe()理想情况下每列均值接近0、标准差接近1。如果发现某列标准差偏离1太多回头排查是不是漏了异常值或者转换方式选错了。这条小习惯帮我抓出过不少隐性问题。最后多提一句关于训练数据集规模与清洗成本之间的权衡。有时数据量足够大比如几十万条而脏数据比例不到1%直接过滤可能是成本最低的行为但数据量本身就少时每一行都很宝贵清洗就更需要“精细修复”而非“一删了之”。经验是不要迷信某一招要基于你的数据规模、业务含义和下游模型来综合决策。这套Pandas清洗与标准化的方法论我用在了不止一个项目上。从结构化表格数据到文本分类的训练集构造从几万条的小样本到数十万条的业务数据核心思路从来没变过先摸底、再清洗、后转换、终切分。把这套流程固化成自己的标准操作你会发现在项目里最枯燥、最耗时的数据准备阶段反而成了效率最高的环节。
返回列表