中国研究生数学建模竞赛(华为杯)学习笔记——数据预处理全流程
中国研究生数学建模竞赛华为杯学习笔记——数据预处理全流程文章目录原始数据体检原始数据处理到可建模数据的基本顺序数据检查缺失与异常处理缺失值识别缺失值处理案例分析代码实现异常值识别代码实现异常值判断格式与重复四类字段的规范化要点代码实现AI辅助清洗实操第1轮:只检查第2轮:分步清洗第3轮:生成说明代码流水线函数化结构日志记录验证与论文表达可视化验证论文表达学习内容原始数据体检先把问题分清楚再选择处理方式。图中年份格式不对应城市名称不统一特殊符号缺失数量及异常单位混用重复记录。原始数据处理到可建模数据的基本顺序数据检查# 读取 Excel 文件保留原始数据副本df_rawpd.read_excel(城市年度数据.xlsx)# 读取原始数据不做任何改动dfdf_raw.copy()# 复制一份作为工作副本后续操作都在 df 上进行保护原始数据# ---------- 数据初步探查 ----------print(df.shape)# 查看数据维度(行数, 列数)df.head()# 查看前 5 行数据直观了解数据内容df.info()# 查看每列的名称、非空数量、数据类型以及内存占用# 可发现哪些列存在缺失、哪些列类型需要转换如数值被识别为 objectdf.describe(includeall)# 生成统计描述# 数值列 → 计数、均值、标准差、最值、四分位数# 分类列 → 唯一值数、最高频值及其出现次数# includeall 表示同时包含数值列和分类列df.isna().sum()# 统计每列的缺失值NaN数量用于确定后续清洗策略df.duplicated().sum()# 统计完全重复的行数判断是否需要去重缺失与异常处理缺失值识别统一转成NaN便于后续处理。特殊符号不处理缺失率会算低;后面的中位数、插值也可能报错。# ---------- 1. 缺失值符号规范化 ----------# 数据中可能用各种符号表示缺失先统一收集起来missing_symbols[--,未知,缺失,NA,]# 常见的非标准缺失标记# 将这些符号统一替换为标准的 NaN便于后续 pandas 缺失值处理函数识别dfdf.replace(missing_symbols,np.nan)# ---------- 2. 数值列类型转换 ----------# 读取 Excel 时因存在 -- 等符号数值列常被识别为 object字符串类型# errorscoerce无法转换为数值的内容如残留的非数字字符串→ 强制转为 NaN而不是报错forcolin[GDP,人口,能耗,污染排放]:df[col]pd.to_numeric(df[col],errorscoerce)# ---------- 3. 计算各列缺失率 ----------# isna().mean()每列缺失值的比例0~1乘以 100 转为百分比# 用于评估数据质量决定后续是填充还是删除missing_ratedf.isna().mean()*100缺失值处理在数据清洗中处理缺失值是最常见的任务之一。很多人的第一反应是用均值填补但这种做法并不总是合理。是否选用均值取决于数据本身的分布和结构当数据分布近似对称时均值确实能代表典型水平但当数据存在偏态或极端值时均值会被拉偏此时用中位数更稳妥。此外还有两种更贴合业务场景的策略——当不同群体比如不同城市的量级差异明显时应该分组填充避免把一线城市的数值和中小城市混在一起平均而当数据是连续的时间序列时最合理的做法是线性插值利用前后时间点的趋势来估算中间的缺失值。总结成一句话对称分布用均值偏态分布用中位数群体差异大用分组填充时间序列用插值。案例分析假设我们有北京 GDP 的三年片段2021 年为 38136 亿元2022 年缺失2023 年为 43761 亿元。如果此时用全表均值来填补 2022 年显然不合理——因为缺的是时间序列中间的一年它前后两年的数值就是最有信息量的参考。正确做法是取前后两年的平均值即 (38136 43761) ÷ 2 40948.5 亿元。这个例子也说明填补策略应当利用数据内在的先后关系而不是简单地“抹平”所有缺失。但这个案例里还藏着一个更隐蔽的坑三行数据的城市名分别是“北京”、“北京市”、“Beijing”。如果不先统一名称pandas 会把它们当成三个不同的城市按城市分组插值就会失效——每个“城市”名下只剩一两个有效数据点根本无从插值。所以正确的流程是先做城市名称归一化再按年份排序、分组插值。代码实现# 第一步统一名称把 北京 / 北京市 / Beijing 合并为同一个城市# df[城市] df[城市].replace({北京市: 北京, Beijing: 北京})# 第二步按城市和年份排序保证每个城市内部年份递增dfdf.sort_values([城市,年份])# 第三步按城市分组组内对 GDP 做线性插值并写回原列gdf.groupby(城市)[GDP]df[GDP]g.transform(lambdas:s.interpolate()# 用前后年份的值线性估算缺失年份)异常值识别数据清洗中的另一大任务是异常值检测。面对一列数值很多人靠“看谁不顺眼就删谁”这种做法既容易误删真实数据也容易漏掉隐藏的错误。更可靠的做法是用统计方法划定一个“正常范围”把超出范围的记录先标记出来再做人工判断。其中最常用的就是 IQR四分位距法。它的原理很简单把数据从小到大排序后取第 25 百分位数Q₁和第 75 百分位数Q₃两者之差就是 IQR代表数据中间 50% 的分布宽度。以这个宽度为基准向两侧各扩展 1.5 倍就得到了下界Q₁ − 1.5 × IQR和上界Q₃ 1.5 × IQR。落在界外的记录就是需要重点检查的候选异常值。这个方法的好处是稳健——上下界由四分位数决定极端值本身几乎不影响它们的位置不像均值和标准差那样会被异常值“带跑”。代码实现# 第一步计算第 25 和第 75 百分位数Q1df[人口].quantile(0.25)Q3df[人口].quantile(0.75)IQRQ3-Q1# 四分位距中间 50% 数据的跨度# 第二步划定正常范围的上下界lowerQ1-1.5*IQR upperQ31.5*IQR# 第三步筛选出超出范围的记录候选异常值outliersdf[(df[人口]lower)|(df[人口]upper)]在本案例的数据集城市年度数据中运行上述代码后筛出了一条记录城市为 Beijing时间是 2022 年人口为 219000——远高于其余城市。结合常识判断这很可能是录入时多写了两个 0真实值可能是 2190 左右的量级具体单位要看数据说明。这是一个典型的“假异常”数值上极端但成因是录入错误而非真实的城市人口突变。异常值判断检测只是第一步真正的难题在于拿到一条离群记录后该拿它怎么办 数据处理的常见误区就在这里很多人看到数值离群第一反应是直接删除图个干净省事。这种做法的风险在于——如果那条“异常”其实是真实数据比如超大城市的真实人口删掉它就等于亲手扭曲了数据集而如果它是录入错误却没被处理它又会持续污染后续的统计结果。所以异常值处理的核心原则是先判断成因再对症下药绝不因为“离群”就删除。异常值按成因大致可以分为三类每一类对应不同的处理方式。第一种是录入错误。 数据在采集或录入过程中出了差错。这类错误的正确处理方式是核对原始来源后修正如果原始来源也无法确认正确值就记为缺失值交给后续的空值处理流程均值、中位数或插值去补。第二种是真实极端值。 数值虽然离群但它是真实发生的——比如一线城市的人口相对中小城市就是天然极端。这类数据必须保留并在分析报告中说明保留原因。真实的极端值往往正是分析中最有信息量的部分删掉它们等于丢掉了故事的主角。第三种是无法确认的情况。 既找不到原始来源佐证是笔误也判断不出它是否真实。对这类记录稳妥的做法是先做标记比如增加一列 is_outlier 标记然后分别在有它和无它的两种数据集上跑分析比较处理前后的结果。如果结果差异不大说明它影响有限如果差异显著就需要回到业务层面进一步追查。格式与重复原始数据往往来自不同来源、由不同人录入这就导致同一个信息有多种写法。比如年份可能是“2020”也可能是“2022/01”同一个城市可能写作“北京”、“北京市”或“Beijing”同一种能耗有的用“吨”有的用“万吨”更麻烦的是同一城市同一年可能出现了两条记录。如果这些问题不解决就急着建模后果很严重分组统计时同一个城市会被拆成三个实体导致统计虚高单位不统一会让数值比较毫无意义重复记录会成倍放大某些城市的权重扭曲相关分析和模型训练的结果。所以名称、日期、单位、主键这四类字段必须在建模前统一这是任何后续分析可靠性的前提。四类字段的规范化要点第一是日期字段统一。 原始数据里有时是完整的“2020年”有时是“2022/01”这种带月份的格式。建模和分组通常只需要年份所以需要提取四位数年份统一转成整数才能作为时间维度的可靠主键。第二是名称字段统一。 这是最容易出问题的一类。北京、北京市、Beijing 指的是同一个城市但如果不映射pandas 会当成三个不同的值。解决方法是建立一张名称映射表把各种写法统一映射到标准名称再对字段做替换。第三是单位字段统一。 能耗这类指标原始数据可能混合了“吨”和“万吨”两种单位。单位不统一数值就没有可比性。正确做法是统一换算成标准单位本例统一为“万吨”让所有记录处于同一度量纲之下。第四是主键去重。 城市年度数据里每一行的唯一标识应该是“城市 年份”的组合。如果同一城市同一年出现了两条记录就说明数据有重复需要用联合主键去重保证每个城市每年只有一行。代码实现# 1. 年份提取为四位整数处理 2020年 / 2022/01 等格式df[年份](df[年份].astype(str).str.extract(r(\d{4}))[0].astype(int))# 2. 城市名称映射把 北京/北京市/Beijing 统一为标准名称df[城市]df[城市].replace(city_map)# 3. 能耗统一为万吨把吨换算成万吨df[能耗]df[能耗].apply(to_万吨)# 4. 城市-年份应唯一按联合主键去重dfdf.drop_duplicates([城市,年份])规范化完成后需要回头验证是否达标。合格的数据应该满足四点年份都是整数城市只保留标准名称能耗统一为“万吨”重复记录数为 0本例中重复记录从 1 条降到了 0 条。这些检查点可以直接用 .dtypes、.unique() 和 .duplicated().sum() 等命令快速核验。AI辅助清洗实操在数据清洗的实操环节很多人和 AI 协作时习惯甩出一句“帮我把这张表清洗干净”然后期待一次到位。这种做法风险很大一方面AI 一次性改动过多会掩盖每一步的具体操作出了问题难以回溯另一方面字段类型、缺失、重复、异常这四类问题互相牵制顺序错了往往会“越洗越乱”比如还没统一城市名称就去做分组插值结果同一城市被拆散插值自然失效。更现实的是如果让 AI 直接动手改数据它可能在没摸清数据全貌的情况下就下了判断——比如把真实的极端值当成错误删掉。因此正确地做法是把清洗拆成“诊断—清洗—说明”三个轮次每轮只做一件事并且明确约束 AI 的行为。在空值处理环节我们知道了“按城市分组、组内插值”是处理城市 GDP 时间序列缺失的正确思路。但这里隐藏着一个 AI 常犯的典型错误它可能会直接写 df.groupby(“城市”)[“GDP”] 去做填补却忽略了城市名称本身尚未统一。在这个数据集里“北京”“北京市”“Beijing”其实是同一个城市但在未归一化之前pandas 会把它们当成三个完全不同的组。于是原本应该一条连续时间序列的北京被拆成了三截每一截都只剩零星的一两个数据点前后年份凑不齐插值也就无法生效——结果是分成了三个组缺失值依然填不上。这个案例很好地说明了AI 的思路大体正确但它往往不会主动意识到“字段值是否已规范”这个前置条件。要修正这个问题需要把操作顺序调整成四步。第一步映射城市名称——把“北京”“北京市”“Beijing”统一映射为标准名称这是所有分组操作的前提。第二步转换 GDP 为数值型——确保该列是数值而不是字符串否则插值会报错或得到错误结果。第三步按城市排序并插值——先按“城市 年份”排序保证时间顺序正确再分组做线性插值。第四步输出填补前后的记录核验——把填补前后的数据打印出来对比确认缺失值确实被合理填上没有出现“仍未填上”或“填出明显不合理数值”的情况。第1轮:只检查请生成数据质量检查代码不修改原始数据;输出字段类型、特殊缺失、重复和异常候选。第2轮:分步清洗先统一缺失符号与字段类型再处理名称、单位、缺失和异常;每一步返回检查结果。第3轮:生成说明根据清洗日志生成论文描述;只能使用已提供的数字不得编造处理数量。代码流水线真实的数据分析项目中真正的考验在于这些步骤能不能被复现、能不能被追溯。如果只是把一堆命令代码堆在一起跑一遍当时数据被改了哪些、删了多少条、填了什么值事后很难说清楚——尤其当论文需要如实描述处理过程、或团队其他人需要复现你的结果时这种“一次性代码”几乎没法交付。因此规范的做法是把清洗整理成一条有明确阶段、可反复运行、自动记录过程的流水线。这样既能保证结果可复现也能让每一步的操作有据可查。先看整体的处理管线它把清洗拆成六个边界清晰的环节。第一个环节 raw() 是源头负责建立原始数据的只读副本保证后续任何操作都不破坏最初的输入。第二个环节 audit() 做质量体检相当于给数据拍照存档——记录字段类型、缺失、重复、异常等初始状况。第三个环节 standardize() 负责格式与单位统一处理城市名映射、日期提取、单位换算这类“规范化”问题。第四个环节 clean() 处理缺失与异常是真正动手修正数据的部分。第五个环节 validate() 做验证并生成日志检查清洗结果是否达标。最后的 export() 把干净的数据导出为建模表。六个环节像流水线一样依次传输数据每一步只关心自己的职责。函数化结构为了让这条流水线真正可运行、可维护建议把每个阶段封装成独立函数再由一个主函数按顺序调用。下面是一个推荐的结构defclean_city_data(df_raw):dfdf_raw.copy()# 1. 建立原始数据只读副本log[]# 2. 初始化日志记录每一步操作dfnormalize_missing(df,log)# 3. 规范化缺失符号dfstandardize_fields(df,log)# 4. 统一格式与单位dfclean_values(df,log)# 5. 处理缺失与异常值dfdrop_key_duplicates(df,log)# 6. 按主键去重validate_clean_data(df)# 7. 验证清洗结果returndf,pd.DataFrame(log)# 8. 返回干净数据 清洗日志这个结构有两个优点。第一每个函数都接收 df 和 log——df 让数据在函数间依次传递log 让每个函数把“我做了什么”追加到日志里从而实现自动化的过程记录。第二主函数最后同时返回 df 和 pd.DataFrame(log)——返回值不只是清洗后的数据还包括一份结构化的清洗日志这两者合起来才构成完整、可追溯的处理记录。日志记录日志每一步至少应该包含五项内容处理前的记录数基线知道从多少行开始处理规则与涉及字段做了什么、针对哪些列修改、填补或删除的数量这是论文里最常引用的数字处理后的复查结果这一步是否生效、数据形态如何以及异常无法确认时的标记记录哪些记录存疑、为何未处理。有了这五项整个清洗过程就成了一本“账本”随时可以核对。一份合格的日志应该包含步骤操作序号、发现的问题针对什么、处理方式用了什么规则、数量处理了多少、结果处理后数据变成什么样。在第 X 步、针对 Y 问题、用了 Z 规则、处理了 N 条、结果变成了 M。步骤发现的问题处理方式数量结果1特殊缺失符号统一转为 NaN3个进入后续处理2城市名称不统一映射为标准名称5条3个标准城市3GDP/能耗/污染缺失分组插值或中位数3个缺失率降为 04人口数量级异常回查后修正1条219000→21845城市—年份重复联合主键去重1条16→15 条记录验证与论文表达可视化验证清洗完成后应把数据画成图做最后的可视化验收。以 GDP 序列为例填补前折线在 2020、2021 年正常上升但 2022 年因数据缺失出现断裂趋势被切断用前后年份线性插值填补后2020—2024 连成一条完整平滑的上升曲线缺失点被补在了“应该在的位置”。看图时主要检查三点插值后趋势是否自然、有没有产生新的跳变、清洗结果能否支持后续建模。这些图的作用是双重的——对内是质量检查的放大镜一眼看出趋势断裂、量级异常等问题对外则是论文里解释处理依据的直观证据一张填补前后对比图往往比文字说明更有说服力。论文表达论文方法部分的写作要诀是把问题、方法、理由和结果写具体而不是一句“进行了适当处理”带过。示例写法先交代原始规模16 条记录再依次说明特殊符号转缺失值、字段数值化、按城市分组结合相邻年份线性插值并给出理由——不同城市经济量级不同、异常值回查修正、名称与单位统一、按“城市—年份”联合主键删除 1 条重复记录最后用结果收尾保留 15 条、缺失率降为 0。提交前按五项清单自查字段名来自原表、数量与日志一致、单位已统一、异常值有依据、清洗后已验证——核心底线是所有数字和原因必须来自清洗日志这样方法部分才经得起复现和追问。论文写法示例“原始数据共包含 16 条城市年度记录。首先将 --、未知 等特殊符号统一转换为缺失值并对 GDP、人口、能耗和污染排放字段进行数值化处理。考虑到不同城市的经济量级存在差异对 GDP 等少量缺失值按城市分组并结合相邻年份进行线性插值对人口字段中识别出的数量级异常值回查原始记录后进行修正。随后统一年份、城市名称及能耗单位并以‘城市—年份’为联合主键删除 1 条重复记录。预处理后共保留 15 条有效记录各建模字段缺失率降为 0可用于后续相关分析与预测建模。”学习内容【数模国赛数据预处理全流程从 “脏数据” 到可建模数据缺失值 / 异常值 / AI 清洗一次讲透助力国赛模型跑出好结果】https://www.bilibili.com/video/BV1bg8E6VEmN/?share_sourcecopy_webvd_source9bd30904a7520995fabec0d90dd26e50