第三章:数据清洗——别让脏数据毁了你的模型
数据清洗这事儿,说实话,比建模本身更磨人。我做了这么多年量化分析,至少一半的时间都耗在数据清洗上。艺术品拍卖数据尤其脏——你想想看,几百年来的交易记录,手工录入的、不同拍卖行格式各异的、还有那些明显离谱的「天价」……不洗干净,模型跑出来就是个笑话。
这一章,咱们就聊聊怎么处理缺失值、揪出异常值、以及做标准化和归一化。嗯,都是基本功,但基本功往往最要命。
3.1 缺失值处理:别随便填个0就完事
艺术品数据里缺失值太常见了。比如某幅画的「尺寸」字段是空的,或者「艺术家出生年份」查不到。我个人习惯,先分三类情况处理:
- 随机缺失:比如某次拍卖记录员漏填了「材质」,这种可以插补
- 系统缺失:比如早期拍卖根本不记录「 provenance( provenance )」,这种得标记出来
- 完全随机缺失:纯属运气不好,删掉也无妨
我在项目中遇到过最头疼的情况——某位重要画家的作品,尺寸字段缺失率高达40%。直接删掉?太浪费了。后来我用「同类画作中位数插补」,效果还行。但要注意,千万别用均值插补,艺术品尺寸分布往往是偏态的,均值会被几幅巨幅画作带偏。
核心原则:缺失值处理没有银弹。先理解数据为什么缺失,再决定怎么处理。
代码示例(Python):
import pandas as pd
import numpy as np
# 加载数据
df = pd.read_csv('auction_data.csv')
# 查看缺失情况
print(df.isnull().sum())
# 数值型字段:用中位数填充
df['尺寸_cm2'] = df['尺寸_cm2'].fillna(df['尺寸_cm2'].median())
# 分类型字段:用众数填充
df['材质'] = df['材质'].fillna(df['材质'].mode()[0])
# 或者干脆标记缺失
df['尺寸缺失标记'] = df['尺寸_cm2'].isnull().astype(int)
小技巧:对于时间序列数据(比如同一画家的历年成交价),用前向填充或后向填充往往比全局中位数更合理。
3.2 异常值检测:天价拍品是宝藏还是毒药?
达芬奇的《救世主》拍了4.5亿美元,毕加索的《阿尔及尔的女人》1.79亿美元……这些「天价拍品」在数据里就是典型的异常值。但问题是,它们真的是「异常」吗?
我个人认为,艺术品市场的异常值要分两种:
- 数据录入错误:比如把100万录成了1000万,小数点错位
- 真实极端值:比如顶级艺术家的代表作,确实值那么多钱
我曾经犯过一个错——用3σ原则一刀切,结果把梵高的《星空》给剔除了。后来想想,这种顶级作品本身就是市场风向标,删掉反而丢失了重要信息。
所以我的建议是:先用IQR(四分位距)法初步筛选,然后人工复核。具体做法:
# IQR法检测异常值
Q1 = df['成交价_USD'].quantile(0.25)
Q3 = df['成交价_USD'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# 标记异常值
df['is_outlier'] = (df['成交价_USD'] < lower_bound) | (df['成交价_USD'] > upper_bound)
# 查看异常值
print(df[df['is_outlier']][['艺术家', '作品名', '成交价_USD']])
注意:艺术品价格分布是典型的幂律分布(长尾)。用正态分布假设去检测异常值,会把很多真实的高价作品误判为异常。我建议用对数变换后再做检测。
对于天价拍品,我的处理策略是:
- 如果数量极少(比如前1%),单独建一个「顶级市场」子模型
- 如果数量适中,用Winsorization(缩尾处理)把极端值拉到99%分位数
- 如果是为了预测普通拍品,直接剔除极端值反而更干净
3.3 数据标准化与归一化:让不同量纲的数据能对话
你想想看,成交价是几百万美元,尺寸是几百平方厘米,艺术家年龄是几十岁……这些变量放在一起,如果不做标准化,模型会天然偏向数值大的变量。说白了,就是「以大欺小」。
常用的方法就两种:
| 方法 | 公式 | 适用场景 |
|---|---|---|
| Z-score标准化 | (x - μ) / σ | 数据近似正态分布,或使用线性模型时 |
| Min-Max归一化 | (x - min) / (max - min) | 数据有明确边界,或使用神经网络时 |
我个人习惯:如果后续要用PCA或聚类,优先用Z-score;如果要用深度学习,Min-Max更稳定。但要注意,Min-Max对异常值非常敏感——一个天价拍品就能把整个归一化范围拉宽,导致大部分数据挤在0.01到0.05之间。
避坑指南:我曾经在项目里先做了Min-Max归一化,然后才做异常值检测。结果异常值把归一化范围拉得极宽,正常数据全挤在一起,模型完全学不到东西。正确的顺序是:先处理缺失值和异常值,再做标准化/归一化。
代码示例:
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# Z-score标准化
scaler_std = StandardScaler()
df['成交价_标准化'] = scaler_std.fit_transform(df[['成交价_USD']])
# Min-Max归一化
scaler_mm = MinMaxScaler()
df['尺寸_归一化'] = scaler_mm.fit_transform(df[['尺寸_cm2']])
3.4 知识体系总览
说了这么多,咱们用一张图把整个数据清洗的流程串起来。我习惯在做任何清洗之前,先画个流程图——这样心里有数,不会漏掉步骤。
这张图看起来简单,但每一步都有坑。我见过太多人跳过缺失值分析直接插补,或者用默认参数跑异常值检测。记住一句话:数据清洗不是流水线,每一步都要回头看。
我的个人习惯:每次清洗完数据,我都会做一次「清洗前后分布对比图」。如果分布形状变了太多,说明清洗方法可能有问题。比如,归一化后数据集中在0.01-0.02之间,那大概率是异常值没处理好。
好了,数据清洗这部分就聊到这儿。下一章咱们会进入特征工程——怎么从这些清洗好的数据里,挖出真正能预测价格的特征。嗯,那才是真正有意思的部分。
公众号:蓝海数据掘金营,微信deep3321