ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

数学建模数据清洗实战:从脏数据到可建模数据集的完整流程

数学建模数据清洗实战:从脏数据到可建模数据集的完整流程 早些年我第一次带队打数学建模竞赛的时候犯过一个特别蠢的错误。拿到附件数据后看都没细看直接跑了一个线性回归结果 (R^2) 出来是负的当时差点以为模型写错了。后来排查了半天才发现附件里有十几行是文本说明还有几列的单位不统一、日期断档、重复行到处都是。从那以后我就记住了建模做得再花哨数据这关过不去后面全是白搭。这一篇是“数学建模算法案例精讲500篇”的第三篇主题是数据清洗。前两篇聊了算法选型和特征工程的基础思路这篇重点落在“数据从原始附件变成能直接进模型的样子”这条流水线上。数学建模里的数据清洗和你平时在公司里做数据治理、写SQL刷数不太一样。竞赛和科研场景有一个鲜明的特点时间极紧数据量通常不大但脏的程度往往出人意料。大多数情况下你面对的是一份Excel或CSV少则几百行多则几万行里面什么情况都有缺值、异常、重复、错位、编码混乱、单位混用。如果前期不用一套标准流程快速把数据“洗”干净后面做回归、分类、评价、仿真每一步都会被带偏。这篇文章适合三类人正在准备国赛、美赛或华为杯的队员做毕设或科研时需要独立处理数据的学生以及工作中经常要跟“别人给的破数据”打交道的工程师。我会把整个数据清洗拆成“探查—清洗—验证”三个阶段用贴近竞赛真题的场景做示范并把每一步的坑和判断依据都写出来。1. 为什么说数据清洗是建模的“胜负手”1.1 数据清洗在竞赛中到底占多重的分量很多新手队伍喜欢把精力全扑在算法上今天学粒子群明天调神经网络后天翻《数学建模算法与应用》。这些确实重要但一到实战就会发现真正卡住你的往往不是模型推不出来而是数据读进来之后根本没法用。我统计过自己带过的队伍在竞赛第一天上午的时间分配从拿到题目到第一版可用的训练集平均要花掉2到3个小时。这2到3个小时里有相当一部分是在处理“附件里的脏数据”。如果能在出发前就把清洗流程练熟这个时间能压缩到40分钟以内。省下来的时间足够把模型多调十几次或者把论文里那几张关键图表做得更漂亮。更重要的是数据清洗直接决定模型结果的上限。一个常识就是“垃圾进垃圾出”。你要是把一堆含缺失值的行丢给回归模型有的库会直接报错有的库会默认删除有的库会用均值填充——这些“默认行为”完全不受你控制等到结果出来不对再回头查反而更浪费时间。自己主动做清洗核心目的就是把数据的控制权握在自己手里。1.2 竞赛附件的“脏数据”通常长什么样结合我带队的经验竞赛附件里的脏数据主要集中在这几类缺失值整行空白、某些关键列只有NaN、用“-”“/”“null”字符串代替真实值。异常值个别数据偏离正常范围比如测距离的序列里突然冒出一个惊天大数或者数值出现负值但物理意义上根本不可能是负。重复数据同一行被复制多次或不同行内容完全相同。类型错乱本应是数值的列被读成了字符串本应是日期的列被读成了文本导致排序和绘图全乱。单位混用同一列里有的行是米有的行是厘米有的行是小时有的行是分钟。干扰行与说明行Excel里经常有“注数据来自...”之类的说明文字或“合计”“均值”之类的统计行混在数据中间。索引与时间不连续本来应该按时间等间隔采样的数据中间断了几条直接影响时序模型。说起来每一条都挺“低级”但组合在一起足以让一支队伍在第一晚崩溃。别问我怎么知道的都是血泪。2. 数据清洗的整体流程与设计思路2.1 先“看清数据”再做清洗探查五步法拿到附件后的第一件事绝对不是写清洗代码而是把数据完整地“看”一遍。我用pandas做探查的时候固定跑五步import pandas as pd df pd.read_excel(附件.xlsx, sheet_namesheet1) # 1. 看前几行了解字段名和数据长什么样 df.head(10) # 2. 看整体规模和类型 df.info() # 3. 看统计量注意min/max/mean是否离谱 df.describe() # 4. 看缺失值占比 df.isnull().sum() # 5. 看唯一值数量判断字段类型 df.nunique()这五步跑完我对数据的“体检报告”基本就出来了。很多人会跳过head()直接看describe()这是不对的。head()的价值在于能让你直观看到脏数据的“脏法”是文本干扰是单位混用还是列错位。有时候光看前几行就能发现这个表根本不是标准的宽表而是“摘要明细”混排的人肉报表。nunique()这个函数我尤其推荐。它能快速告诉你某个字段是不是“伪数值”。比如一个叫“编号”的列如果nunique()等于行数那它就是唯一标识如果只有十几个值那它八成是分类变量或者你读错了字段。2.2 清洗流水线的搭建思路与优先级数据清洗不是“一招鲜”更推荐的做法是把它当成一条可重复执行的流水线。我的标准顺序是这样的备份原始数据。永远保留一份原始附件清洗过程中只在副本上操作否则改错了没有后悔药。统一格式与类型。先把所有列读成正确类型把文本列里的数字提出来把日期列解析好。处理干扰行。删掉说明行、小计行、空行把范围框定在真正的数据区。处理重复值。根据主键去重并且判断哪些重复是“真的重复”哪些是“同一个对象的不同记录”。处理缺失值。区分随机缺失和非随机缺失按任务场景决定删除还是填补。处理异常值。先识别再判断“是不是真实存在的极值”最后才决定是否处理。保存清洗日志。每做一步变换就用一个变量记录删了多少行、填了多少值后面写论文的“数据预处理”部分会非常方便。这套流水线的核心思想是“从低风险操作到高风险操作”。删掉说明行基本不会出错但填补缺失值、剔除异常值就需要谨慎判断因为每一步都可能改变数据集的统计特性。把低风险操作放前面先让数据集“成型”后面做判断时面对的数据量更小、干扰更少效率更高。3. 核心细节解析与实操要点3.1 缺失值填不填、怎么填要看任务类型缺失值处理是数据清洗里最需要“讲道理”的一步。我见过不少同学一看到NaN就条件反射删行结果把原本有效的信息也删掉了。我的判断逻辑分成三步第一步看缺失比例。比例低于5%的列可以酌情删除对应行或做填充超过20%的列就要考虑这个字段到底还值不值得保留在30%以上且不是关键字段直接放弃这一列更省事。第二步看缺失机制。如果缺失完全随机比如仪器偶尔抽风少记了一行那删除或均值填充都行。但如果缺失和某些因素相关比如“大于一定数值的测量结果才缺失”就有系统性问题此时简单填充会带来偏差。第三步选具体方法。有几种实用选择行删除数据量大、缺失行少时使用最安全。均值/中位数填充适合数值型且分布相对对称的情况。分布偏斜时用中位数不用均值。前向/后向填充适合时间序列用上一时刻的值补缺失。线性插值适合近似线性的数据pandas里的df.interpolate()很好用。简单模型预测用其他列训练一个小模型来预测缺失值理论上更精细但竞赛中时间有限一般用在最关键的一两列上。举一个实际例子。有一年处理“风速-发电功率”数据风速列有个别缺失我直接用整列均值填充结果把功率拟合曲线硬生生拉偏了。后来改成用该时刻前后的风速做插值效果立刻正常。时序数据里的缺失优先用“时间相邻”的信息不要用全局统计量。3.2 异常值别一棍子打死先问“这个数可能真实吗”异常值是最容易引发争论的地方。教材里教的通常是“3σ原则”或“箱线图IQR”实操中这些工具只能帮你圈出候选值真正的判断还是要靠业务逻辑。我的习惯是三步第一步画出分布。用箱线图或散点图把所有“看起来不对劲”的值可视化出来。有时候所谓异常值其实只是数据存在多个模式比如工作日和周末的用电量差异极大整体画出来就像有异常点。第二步用规则辅助判断。3σ原则适用于近似正态分布的数据IQR法更鲁棒适合偏斜分布。但这两个方法都只管“统计离群”不管“物理合理”。比如你测一个桌子长度数值出现-3.5统计方法可能觉得它是离群点实际上它压根违反物理常识直接删。第三步保留还是修正要回归问题本身。如果异常值是真实发生的极端事件比如台风天的风速极值那它恰恰是建模的关键不能删。如果是录入错误比如小数点位置错了就需要修正。如果是超出测量范围的值才优先删除。这里有一个真实的经验某次做“城市共享单车调度”题目附件里有一天的骑行时长出现了800分钟大多数人直接当异常删了。后来看原始材料才发现那一天系统维护有一批订单的结束时间没记录用默认值补齐了。这种“异常”恰恰提醒你去检查数据背后的系统逻辑而不是简单把数删掉。3.3 重复值与格式问题先统一再合并竞赛数据里重复值常常来自多个文件拼接或多人协作采集。处理重复数据没那么复杂核心就两件事定义“重复”的标准然后按标准去重。如果数据有一个唯一ID列重复ID大概率意味着录入错误保留第一条即可。如果没有唯一ID就需要选一组关键字段来判断比如“时间地点数值”都相同才算重复。还有一种“近似重复”比如同一个人在不同表格里名字写法不同“张三”和“张 三”这种要先做格式化再判断。格式问题也一样。最常见的坑是Excel里把数值列的一部分存成了文本。因为有些人会在单元格里敲一个空格或者加了千分位符。读入之后这一列就变成了object类型。解决方式是用pd.to_numeric(errorscoerce)它会尝试转成数值转不了的变成NaN然后再按缺失值处理。df[value] pd.to_numeric(df[value], errorscoerce)这个函数我几乎每场竞赛都会用到值不值得谁用谁知道。3.4 时间与索引规范化别让“时间格式”坑了你的时序模型时间字段是另一个重灾区。常见情况包括日期格式不统一有的行是“2024/05/01”有的是“2024-05-01”甚至有的是“20240501”。时间是字符串没法直接排序。明明应该按固定频率采样但时间序列里有很多断点。处理方式很简单统一转成pandas的datetime类型df[time] pd.to_datetime(df[time], format%Y-%m-%d %H:%M:%S) df df.sort_values(time)转成datetime类型之后你可以直接做重采样、差值、取时间特征。对于时间序列建模的题目这一步能直接决定模型能不能跑起来。还有一个细节建立索引不等于把时间列删掉。很多人做set_index(time)之后就把原列丢了后面想画图、做展示时又要重新拼接。建议set_index之后保留一个copy()或者用reset_index()按需求切换灵活一点。4. 实操记录一个贴近竞赛出题风格的完整清洗案例4.1 案例背景几何运动观测数据的“脏法”有多夸张讲完方法论我们进入实战。假设你拿到一道和2024年国赛A题类似的题目某个装置在平面内做运动附件里给了若干时刻的位置观测数据。这类题目的数据清洗第一批问题几乎出在Excel本身。打开原始附件你可能会看到这样的结构第1行是标题“某装置运动观测数据”第2行说明“数据来自第几次测试”第3行是表头从第4行到第500行是数据但中间混着“平均”“最大”这两行统计结果另外“时刻”列里有的填“0:00”有的填“0:05”有的直接空着坐标列里还有几行是文本“未捕获”。如果是第一次参赛看到这种表格真的会满脸问号。但你一旦习惯了清洗流程就明白这属于“常见基础操作”。4.2 从原始附件到可建模数据集的五个步骤下面我把这个案例的完整清洗过程分步写出来每一步都附上代码和判断理由。第1步读取并裁掉干扰区域import pandas as pd raw pd.read_excel(附件.xlsx, headerNone, skiprows2) raw.head(10)先用headerNone读进来不管原始表头保留数据原本的样子。skiprows2跳过前两行标题和说明。打印前10行之后找到“平均”“最大”所在的行号把它们过滤掉。# 假设统计行出现在索引 496 和 497 raw raw.drop(index[496, 497])第2步规整列名并转换类型把第一行有效数据变成列名然后统一列名风格。接着用to_numeric把坐标列转为数值转换失败的会变成NaN正好进入缺失值处理环节。raw.columns [time, x, y] raw[x] pd.to_numeric(raw[x], errorscoerce) raw[y] pd.to_numeric(raw[y], errorscoerce)这里有一点要注意如果“未捕获”之类文本被coerce成NaN它就不再是文本干扰而变成了缺失值统一归口处理逻辑会很清爽。第3步处理时间字段和缺失值先统一时间格式再按时间排序。空隙不大的用插值空隙大的直接删除或分段处理。raw[time] pd.to_datetime(raw[time], format%H:%M:%S) raw raw.sort_values(time).reset_index(dropTrue) raw[[x, y]] raw[[x, y]].interpolate(methodlinear, limit_directionboth)如果缺失集中在开头结尾limit_directionboth会做前后向填充。这样处理的逻辑是运动轨迹本身是连续的线性插值比均值填充更能保持轨迹形状。第4步去重和格式再确认raw raw.drop_duplicates(subset[time], keepfirst) raw raw.reset_index(dropTrue)如果同一时刻有多条记录保留第一条。竞赛中同一时刻重复出现往往意味着是文档复制的产物直接去重不会有风险。第5步输出清洗日志print(原始行数:, len(raw_origin)) print(清洗后行数:, len(raw)) print(缺失值数量:, raw.isnull().sum().sum())这个日志不是给别人看的是给你自己看的。后面论文里“数据预处理”小节需要写明做了哪些操作有据可查。4.3 清洗效果量化从“跑不动”到“能出图”清洗完的数据第一步验证就是画散点图。如果轨迹是连续曲线图上一眼就能看出是否有断点、跳变。我见过一个队伍原始数据画出来是一堆乱点清洗后画出来是一条漂亮的光滑曲线。反差之大直接让组员发出“卧槽原来数据长这样”的感叹。更关键的是清洗后的数据能直接喂给模型了。用几何运动类题目举例你要计算速度和加速度必须先得到平滑的时间序列。如果有缺失值没处理差分时就会冒出无穷大如果有文本残留numerical类型都过不了。每一步清洗都是在为后续算法扫除地雷。5. pandas在数据清洗中的高阶用法与效率技巧5.1 必会函数清单与常见搭配这几个函数掌握好基本能应对竞赛里九成以上的清洗场景pd.read_excel()/pd.read_csv()读数据注意skiprows、sheet_name、encoding三个参数。df.rename()改列名统一命名风格。df.drop()/df.dropna()删行删列优先用df.drop因为可以指定axis和inplace。df.fillna()/df.interpolate()填充缺失值interpolate最适合连续性数据。df.duplicated()/df.drop_duplicates()查重和去重注意keep参数。pd.to_numeric()/pd.to_datetime()类型转换配errorscoerce是万能钥匙。df.groupby()df.transform()分组填充比如按城市分组后用各组中位数填充缺失值。df.apply()自定义函数处理适合复杂的格式清洗逻辑但大批量数据慎用。组合技巧里我比较常用的是“分组填充”。比如处理多站点的气象数据不同站点量纲一致但分布不同全局均值填充会抹掉站点差异用groupby(站点)[温度].transform(lambda x: x.fillna(x.median()))更稳妥。5.2 大文件读入分清“要全量还是只要关键列”竞赛里很少遇到超大文件但偶尔也会碰到“附件大到Excel都打不开”的情况。这时切忌硬扛要分情况处理。如果只需要其中几列可以只读关键列df pd.read_csv(big_data.csv, usecols[time, x, y])如果文件是真的大比如几个GB就别用pandas死磕了先用chunksize分块读入边读边清洗再汇总chunks pd.read_csv(big_data.csv, chunksize100000) clean_chunks [process_chunk(c) for c in chunks] df pd.concat(clean_chunks, ignore_indexTrue)还有一个实用的小技巧df.memory_usage(deepTrue)可以查每列占用内存。如果某一列是字符串但只有几个固定取值把它转成category类型内存会小很多。不过竞赛中通常用不上这一步了解即可。5.3 保存中间结果让清洗过程可回退、可复盘我有一个坚持了很久的习惯每完成一个清洗阶段就保存一份中间结果。df_clean1.to_csv(data_clean_stage1.csv, indexFalse, encodingutf-8-sig)为什么用utf-8-sig因为Excel打开CSV时如果文件没有BOM中文字段名会乱码。这个小细节救过我好几次。保存中间结果的价值在于一旦发现后面某一步做错了可以快速回退到上一个阶段而不是从头再跑一遍。多人协作时中途换人接手也方便。6. 常见问题与排查技巧实录6.1 典型报错与解决办法速查表实操中你能遇到的大概率就是下面这些情况我把它们整理成一张速查表报错/问题出现原因解决思路ValueError: could not convert string to float文本混在数值列用pd.to_numeric(errorscoerce)先转NaN再处理日期列排序错乱字符串格式不统一pd.to_datetime()统一转换读CSV中文乱码编码问题读入时指定encodinggbk或encodingutf-8输出用utf-8-sigduplicated()查不到重复存在不可见字符或首尾空格先df[col]df[col].str.strip()再查重删除后索引乱没有重置索引用reset_index(dropTrue)均值填充后精度下降字段含少量异常值拉偏均值换中位数填充或先处理异常值再填充数据量超大内存不足一次性读入过多用chunksize分块读入这张表是长期经验的浓缩遇到问题先对号入座不要慌。6.2 我踩过的几个“非典型”坑有些坑属于字面上看不出来、只有亲手踩过才知道的。我写三个比较典型的第一个坑merge时两列类型不一致。有一次把两个表按ID合并左边的ID是字符串“001”右边的ID是整数1结果merge后大量空值。排查了很久才发现类型不同导致匹配不上。现在我的习惯是任何表合并之前先print(df1[id].dtype, df2[id].dtype)保证类型一致。第二个坑Excel里的“表面空值”其实不是空值。有些单元格是空字符串不是NaN。isnull()检测不出来却会干扰后续计算。解决办法是数据探查阶段就把空字符串替换成NaNdf df.replace(r^\s*$, pd.NA, regexTrue)第三个坑对“连续性”字段做全局排序时误伤分组结构。如果数据里有“地区”这样的分类字段排序得先按分类排再按时间排。很多人直接按时间全局排序把不同地区的数据交错在一起后面做分组分析时全乱套。正确做法是df.sort_values([region, time])。这些小坑常规教材里不会写但对比赛来说每一个都可能导致几个小时的无效工作。6.3 数据清洗的“最后一公里”验证与交接清洗做完别急着建模。花五分钟做三件小事重新跑一次df.info()和df.describe()确认类型正确、统计量在合理范围。画几张关键变量的分布图或时间序列图肉眼确认没有明显异常。把清洗前后的数据集对比保存并把清洗逻辑讲给队友听。最后这一步经常被忽略但它特别重要。数学建模是团队作战负责建模的队友如果不理解你为什么删掉某些行、为什么用插值而不是均值后面一旦遇到结果异常整个团队都会没有头绪。数据清洗不是一个人的活清洗结果必须能让全队达成共识。我个人在实际操作中还有一个习惯把每一步清洗代码封装成函数形成自己的“清洗工具箱”。这样遇到类似数据时直接复用代码不用每次重写。连续打几场比赛之后工具箱里的函数会越来越全清洗效率会非常可观。数据清洗这件事乍看琐碎实则是整个建模流程里最值得投入时间的环节。很多队伍把三天时间压到最后一天赶论文却从不肯在第一天上午好好看数据。等你多打几次比赛就会明白能快速把一份脏数据变成可信数据集的队伍往往比只会套高级算法的队伍走得更远。后续在这个系列里我会继续拆解特征工程的实战思路以及常见算法在不同赛题里的适用场景下一篇见。
返回列表