数据探索与预处理:数据清洗、缺失值处理、异常值检测、描述性统计分析
各位同学,欢迎来到第四章。说实话,这一章可能是整个课程里最「脏」、最累,但也最关键的环节。我见过太多人,模型建得花里胡哨,结果一上线就崩,为什么?因为数据没洗干净。你想想看,垃圾进,垃圾出,这是铁律。
我个人习惯,拿到任何数据集,第一件事不是跑模型,而是先「摸」一遍数据。怎么摸?就是本章要讲的:清洗、补缺、揪异常、做统计。这四步走完,你才算真正认识你的数据。
核心观点:数据预处理占整个建模工作量的60%-80%。别嫌烦,这一步省下的时间,后面会加倍还回来。
4.1 数据清洗:把「脏东西」筛出去
数据清洗,说白了就是处理那些格式不对、内容乱码、重复记录的问题。我在项目中遇到过最离谱的一次,客户给的报价表里,价格字段居然混着「电话号」和「备注」,因为Excel单元格合并导致数据错位了。
常见的脏数据有这几类:
- 格式不一致:比如日期有的是"2024-01-01",有的是"01/01/2024",还有的是"2024年1月1日"。
- 重复记录:同一个客户、同一个产品、同一个时间点,出现了两条一模一样的报价。
- 无效字符:数字字段里混入了字母、空格、换行符。
- 逻辑错误:比如折扣率大于100%,或者成本价高于售价。
嗯,这里要注意:清洗不是删除所有「看起来不对」的数据,而是要基于业务逻辑判断。我曾经因为手滑删了一批「异常折扣」的数据,结果那是大客户的战略合作价,差点出事故。
# 一个简单的清洗示例(Python伪代码)
import pandas as pd
df = pd.read_csv('quote_data.csv')
# 1. 去除重复行
df = df.drop_duplicates()
# 2. 统一日期格式
df['quote_date'] = pd.to_datetime(df['quote_date'], errors='coerce')
# 3. 去除价格字段中的非数字字符
df['price'] = df['price'].str.replace(r'[^0-9.]', '', regex=True)
df['price'] = pd.to_numeric(df['price'], errors='coerce')
# 4. 过滤逻辑错误:折扣率不能大于1
df = df[df['discount_rate'] <= 1.0]
4.2 缺失值处理:别急着删,先问问为什么
缺失值,是数据预处理里最让人头疼的问题。很多人一上来就用均值填充,或者直接删掉有缺失的行。我建议你先停一下,想想:这个数据为什么缺失?
缺失的原因通常有三种:
- 随机缺失:比如客户忘记填某个字段,纯属偶然。这种可以用均值、中位数或众数填充。
- 非随机缺失:比如高价值订单的「折扣率」字段经常为空,因为销售故意不填。这种填充会引入偏差,需要单独建模处理。
- 结构性缺失:比如新产品没有历史报价数据。这种不能填充,只能标记为「无历史」。
我的经验:对于报价数据,如果缺失率超过30%,我一般会把这个字段单独拎出来,作为一个「是否缺失」的特征加入模型。有时候缺失本身就是一个信号。
# 缺失值处理示例
# 查看缺失率
print(df.isnull().mean())
# 数值型字段:用中位数填充(抗异常值)
df['price'].fillna(df['price'].median(), inplace=True)
# 分类型字段:用众数填充
df['product_category'].fillna(df['product_category'].mode()[0], inplace=True)
# 时间序列字段:用前向填充
df['last_quote_price'].fillna(method='ffill', inplace=True)
4.3 异常值检测:揪出那些「离谱」的数据点
异常值,就是那些明显偏离正常范围的数据点。比如一个普通产品的报价是100元,突然冒出来一个100000元,这大概率是录入错误。
检测异常值的方法有很多,我常用的有三种:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Z-Score(3σ原则) | 数据近似正态分布 | 简单直观 | 对非正态分布效果差 |
| IQR(四分位距法) | 偏态分布、有离群点 | 不受极端值影响 | 阈值选择较主观 |
| 孤立森林 | 高维数据、非线性关系 | 无需假设分布 | 计算量较大 |
我个人习惯先用IQR法快速扫一遍,再用孤立森林做二次确认。为什么?因为IQR法在报价数据上表现稳定,不容易误伤。
# IQR法检测异常值
Q1 = df['price'].quantile(0.25)
Q3 = df['price'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df[(df['price'] < lower_bound) | (df['price'] > upper_bound)]
print(f"检测到 {len(outliers)} 个异常值")
警告:异常值不一定是错误!它可能是真实的「黑天鹅事件」,比如大促期间的超低价。处理前一定要跟业务方确认。我曾经把一次「清仓甩卖」的数据当异常删了,结果模型完全预测不出促销效果。
4.4 描述性统计分析:给你的数据「画像」
做完清洗、补缺、揪异常,接下来就是描述性统计了。这一步的目的是让你对数据有一个全局的、量化的认识。
我通常会看这几个维度:
- 集中趋势:均值、中位数、众数。报价数据我特别关注中位数,因为它不受极端值影响。
- 离散程度:标准差、方差、极差、四分位距。标准差大,说明报价策略差异大,可能需要细分市场。
- 分布形态:偏度、峰度。偏度为正说明大部分报价偏低,少数偏高;峰度高说明报价集中在某个区间。
- 频数统计:分类字段的分布,比如哪个产品类别报价最多,哪个客户询价最频繁。
# 描述性统计示例
# 数值型字段概览
print(df.describe())
# 分类字段频数统计
print(df['product_category'].value_counts())
# 偏度与峰度
print(f"价格偏度: {df['price'].skew():.2f}")
print(f"价格峰度: {df['price'].kurtosis():.2f}")
你看,做完这一步,你对数据的「画像」就清晰了。比如你发现价格的中位数是500,但均值是800,说明有少量高价订单在拉高平均价。这时候你就要想:这些高价订单是正常的VIP客户,还是异常值?
知识体系总览
下面这张图,是我自己总结的数据预处理全流程。你可以把它当作一个检查清单,每次做项目时对照着来。
好了,这一章的内容就到这里。数据预处理没有太多花哨的技巧,核心就是「细心」和「业务理解」。你只要把这四步走扎实了,后面的建模工作会顺畅很多。