数据清洗这件事,我在数学建模竞赛里栽过一次狠的:花了大把时间调算法,结果在数据预处理阶段漏掉了一批异常值,最终的模型预测结果被带偏,国奖直接没了下文。从那以后我形成了一个习惯——拿到赛题数据的第一件事,永远不是建模,而是先做数据清洗。
很多参赛队把数据清洗当成"填空"和"删行"的体力活,其实它的本质是数据审计。你得在动手之前搞清楚:这份数据是怎么产生的?采集过程中可能引入什么噪声?哪些字段是模型真正需要的?哪些值是"肉眼看着正常、但统计上一眼假"的?这些问题想清楚了,后续的模型效果、论文说服力、甚至赛题得分上限,都会完全不一样。
这篇博客是"数学建模算法案例精讲"系列里的基础篇,聚焦数学建模中最容易被低估的环节——数据清洗。我会用真实竞赛中常见的脏数据情况作为例子,带你走一遍从数据审查到缺失值处理、异常值判定、类型修正、文本和时间特征整理的完整流程,每一段都会给出可以直接复制运行的Pandas代码,并解释这一步背后的统计学和建模逻辑,而不是光给命令。适合正在准备国赛、美赛、华数杯等竞赛的同学参考。
1. 数据清洗在建模竞赛里的真实分量:为什么它是策略不是杂活
1.1 一个反直觉的现象:同一道题,两组算法相似,成绩差一档
参加过一次全国大学生数学建模竞赛的评审模拟后,我发现一个规律:评审专家看论文时,最先翻的往往是"数据预处理"那一节。如果预处理草草两行字带过,评委的第一印象就是"这个队对数据不敏感";反过来说,如果预处理部分有理有据——为什么删掉这5个样本、为什么对销售额取对数、为什么缺失值用中位数而不是均值填充——评委会在心里加分,甚至在你模型不完美的时候愿意给一个更好的评价。
这不是玄学。竞赛数据通常是从实际系统里采集的:传感器读数、销售流水、气象站观测、用户行为日志。这些数据的真实分布里必然有噪声、缺失、重复、单位混杂,不可能像教科书数据集那样干净。建模竞赛的核心能力之一,就是在不完美数据里提取出可以建模的信号。数据清洗的好坏,直接决定后续算法能跑到的上限——再好的XGBoost、再精美的LSTM,喂进去一堆脏数据,输出也是垃圾。
1.2 从评审角度倒推:数据清洗要留下"可审计"的痕迹
我在帮学弟学妹改参赛论文时,最常见的问题不是算法不会写,而是清洗过程"黑箱化":用了一堆dropna()、replace(),但论文里说不清为什么这么处理。评委看到的结果是——你到底删了多少行?为什么删?填充了哪些列?用了什么方法?如果你连这些都说不上来,评委会怀疑你的模型结果是否可靠。
所以我在自己的建模流程里,给数据清洗定了三条硬规矩:
- 每一步清洗操作都有"原因记录":比如"删除购买金额为负的样本,原因是业务上不可能出现负消费,且只有3条,占0.02%,不影响样本量"。
- 清洗前后各保存一份数据快照:方便回溯,也方便在论文里写"清洗前后对比表"。
- 每类字段单独审视,而不是全局一把梭:连续字段、离散字段、时间字段、文本字段,问题类型完全不同。
这三条规矩看起来费时间,实际上能帮你避免"洗过头"的灾难——我有一次把城市名的空格去掉后,发现"北京 "和"北京"不再重复,但实际上这两类如果在业务里代表同一个城市,应该合并成同一个编码,而不是当作两个类别放进模型。如果当时没有记录清洗逻辑,这个问题会一直藏到建模结束才发现。
1.3 数据清洗的常见误区:先把"万能模板"扔掉
现在网上一搜数据清洗,铺天盖地都是"十行代码搞定缺失值"这类速成模板。但竞赛数据不是Kaggle上的Titanic,不会那么规整地只缺Age和Cabin。真实赛题里经常会给你这样一些"惊喜":
- 一张表里日期格式五花八门,有
2023/1/1、2023-01-01、20230101、还有01/01/23。 - 明明应该是数值的列,读进来是
object类型,里面有"1,200"、"unknown"、"-"。 - 同一行数据在不同字段中出现了互相矛盾的值,比如"年龄=25"但"工龄=30"。
- 数据总量看起来很多,但去掉缺失、去掉异常、去掉重复之后,有效样本只剩一半。
这种时候套模板只会让问题更糟。数据清洗的第一原则不是"用哪种方法",而是"先搞清楚每列到底是什么,再决定怎么处理"。这也是接下来这一整篇的主线思路:先审查,再决策,最后执行。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 拿到脏数据的第一件事:结构审查与数据画像
2.1 不要急着一行代码都不写,先把数据结构"看"明白
我见过太多同学拿到赛题附件,第一件事就是pd.read_csv()然后画个相关性热力图。但相关性热力图是建模阶段看的,清洗阶段的第一件事是搞清楚这份数据的"体质"——它有多少行多少列、每列的类型是什么、缺了多少、取值范围是什么、分布是否畸形。
这一段我会用一个模拟的数据框来演示完整的"数据画像"流程。假设我们拿到一份销售数据,字段包括:订单ID、日期、城市、商品类别、销售额、客户年龄、客户评分。这份数据是从业务系统导出的,必然带着各种问题。
python复制import pandas as pd
import numpy as np
# 模拟一份包含脏数据的销售记录
df = pd.DataFrame({
'订单ID': ['A001', 'A002', 'A003', 'A004', 'A005', 'A006', 'A007', 'A001'],
'日期': ['2023/1/1', '2023-01-02', '20230103', '2023/01/04', '2023-01-05', '2023-01-06', '2023-01-07', '2023/1/1'],
'城市': ['北京', '上海', '广州', ' 北京', '深圳', '上海', '广州', '北京'],
'商品类别': ['手机', '电脑', '手机', '平板', '电脑', '手机', '平板', '手机'],
'销售额': ['1,200', '8999', '4500', '6000', '300', '25000', '99999', '1,200'],
'客户年龄': [25, 32, np.nan, 45, 29, 200, 35, 25],
'客户评分': [4.5, 3.2, 5.0, 2.1, 4.8, 4.0, '优', 4.5],
})
看到这份数据的第一眼,普通人会觉得"好像没什么问题"。但如果你先执行基础画像,问题立刻就会浮出水面:
python复制# 维度信息:行数、列数、每列非空值数量、数据类型
df.info()
code复制<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8 entries, 0 to 7
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 订单ID 8 non-null object
1 日期 8 non-null object
2 城市 8 non-null object
3 商品类别 8 non-null object
4 销售额 8 non-null object
5 客户年龄 7 non-null float64
6 客户评分 7 non-nul
