数据分析八成时间花在清洗上

这个说法不是夸张。真实业务数据几乎从不干净:导出的 Excel 里日期是文本、金额带货币符号、同一个客户有三种写法、缺失值用横杠表示、还有几行是测试时留下的垃圾数据。

新手常犯的错是拿到数据直接建模,结果报错不断,或者更糟——不报错,但结果全错。所以清洗必须是一套固定流程,而不是遇到问题才临时处理。

第一步:先体检,不要急着动手

做任何处理前,先摸清数据的真实状况。四件事必做:

  1. 看行列规模和数据总量,确认没读错文件
  2. 看每列的非空数量与数据类型,类型不对的列要标记出来
  3. 看数值列的统计摘要,均值、最大最小值,异常立刻可见
  4. 抽样看前几行,直观感受数据长什么样

常用命令组合:

df.shape
df.info()
df.describe()
df.head(10)

第三步的统计摘要特别有用。如果某列年龄的最大值出现 999,或者销售额出现负值,你不用看具体数据就知道有问题。

特别检查:本该是数值的列是否显示为 object 类型。这通常意味着里面混入了非数字字符,是后续计算出错的根源。

第二步:处理缺失值,不要一律删除

先统计每列的缺失比例,然后按情况分别处理。粗暴地删掉所有含缺失的行,可能一次性损失 30% 的数据。

处理策略按缺失比例和字段重要性决定:

  • 缺失超过 60% 且非关键字段:直接删除该列
  • 缺失 5% 到 30% 的数值列:用中位数填充(比均值稳健,不受极端值影响)
  • 缺失 5% 到 30% 的分类列:填充为 未知 单独作为一类,往往缺失本身就有信息
  • 缺失少于 5%:直接删除这些行,影响很小
  • 时间序列数据:用前后值填充或插值,不要填中位数

第三条值得强调:用户没填收入,这个空缺本身可能就是有意义的信号。填成 未知 保留了这个信息,填成平均值则抹掉了它。

df.isnull().sum() / len(df)
df[‘age’].fillna(df[‘age’].median(), inplace=True)
df[‘city’].fillna(‘未知’, inplace=True)

另外注意:很多数据集的缺失不是 NaN,而是横杠、空字符串、N/A、NULL 等各种写法。处理前先把它们统一转为真正的空值。

第三步:去重,但要想清楚依据

去重的关键是定义什么算重复。通常有两种:

  • 完全重复:所有列都一样,直接删
  • 业务重复:关键标识列相同,例如同一订单号出现多次

df.drop_duplicates()
df.drop_duplicates(subset=[‘order_id’], keep=’last’)

第二个更有价值。keep 参数的取舍要看业务:保留最后一条通常意味着保留最新状态,保留第一条意味着保留原始记录。选错会导致结论相反。

业务重复不要盲目删。先看看重复行的其他字段是否一致,如果不一致,可能是数据同步问题,需要回到源头查。

第四步:修正数据类型

这是报错高发区。常见的转换:

  • 日期列:转成 datetime 类型,后续才能做时间筛选和区间计算
  • 带符号的数值:先去掉货币符号和千分位逗号,再转数值
  • 百分比:统一转成小数或统一转成带百分号的字符串,不要混用
  • 分类列:转成 category 类型,节省内存且加快分组运算

df[‘date’] = pd.to_datetime(df[‘date’], errors=’coerce’)
df[‘amount’] = df[‘amount’].str.replace(‘[¥,]’, ”, regex=True).astype(float)

errors 参数很重要。设为 coerce 会把无法解析的值变成 NaT 而不是直接报错中断,之后再统一处理这些异常值。处理真实数据时,让它先跑完再查问题,比中途崩溃高效得多。

第五步:清洗字符串列

文本列的问题通常是隐藏的:前后空格、大小写不一致、全角半角混用。这些会导致分组时出现同一个值的多个版本。

标准处理顺序:

  1. 去除首尾空格
  2. 统一大小写(英文场景)
  3. 替换全角字符为半角
  4. 检查该列的唯一值,发现异常写法手动映射

df[‘city’] = df[‘city’].str.strip().str.replace(‘ ’, ”)
df[‘city’].value_counts()

第四步不能省。跑一遍唯一值统计,你往往会发现 北京、北京市、北京 (带空格)三种写法。用字典映射统一它们。

这一列如果不清洗,后面所有按城市的分组统计都是错的,而且错得很隐蔽——数字看起来合理,只是被拆散了。

第六步:识别异常值,不要自动删除

异常值不等于错误值。一笔 50 万的订单可能是测试数据,也可能是真的大客户。处理原则是识别出来,但由业务判断。

两种识别方法:

  • 统计法:基于分位数或标准差,找出偏离中心很远的数值
  • 业务规则法:直接定义合理区间,例如年龄在 0 到 120 之间

q1 = df[‘amount’].quantile(0.25)
q3 = df[‘amount’].quantile(0.75)
iqr = q3 – q1
df[(df[‘amount’] < q1 - 1.5*iqr) | (df['amount'] > q3 + 1.5*iqr)]

先把异常行筛出来看一遍,再决定是删除、修正还是保留。看到具体数据之后,判断往往很明确:明显是录入错误的多按了几个零,或者真实存在但需单独分析的大客户。

第七步:把流程封装成函数

如果每个月都要处理结构相同的数据,把上面的步骤封装成一个清洗函数。下个月直接调用,出问题时只改一处。

函数里建议保留检查点:每步处理前后打印数据规模,这样哪一步意外删掉了大量数据能立刻发现。静默地丢掉一半数据,是最难排查的事故。

常见问题与误区

  • 问:转换类型时报错怎么办?用 errors 参数容错,转换后检查产生了多少空值,再针对性处理。
  • 误区:缺失值一律填 0。这会严重扭曲均值和分布。金额缺失去填 0,等于凭空制造了一批零元订单。
  • 问:数据量很大跑得慢?指定合适的 dtype 读取,用 category 类型处理低基数字符串列,必要时分块读取。
  • 误区:清洗完就直接分析。清洗后要重新做一次体检,确认行列数、类型、缺失情况符合预期。
  • 问:链式赋值不生效?这是 pandas 的经典陷阱。避免链式赋值,用明确的赋值方式,必要时复制数据副本。

效率数据与实测结论

以一份 20 万行、18 列的销售导出数据为例:手工在表格中清洗约需 2.5 小时且容易遗漏;写成 pandas 脚本首次约 50 分钟,之后每月重跑约 1 分钟。按每月处理一次计算,第二个月起每次节省约 2.4 小时。

更重要的是可复现性。手工清洗的步骤无法追溯,别人接手时会得到不同结果;脚本化的清洗流程,任何人跑出来的数据完全一致——这在需要对外汇报的场景里,价值远超节省的时间。