中国研究生数学建模竞赛(华为杯)学习笔记——数据预处理全流程

中国研究生数学建模竞赛(华为杯)学习笔记------数据预处理全流程

文章目录


原始数据体检

先把问题分清楚,再选择处理方式。

图中年份格式不对应,城市名称不统一,特殊符号缺失,数量及异常,单位混用,重复记录。

原始数据处理到可建模数据的基本顺序

  1. 读取数据
  2. 字段体检
  3. 识别缺失
  4. 统一格式
  5. 识别异常
  6. 处理缺失
  7. 删除重复(检查联合主键)
  8. 可视验证(前后对比)
  9. 导出留痕(数据+日志)

数据检查

python 复制代码
# 读取 Excel 文件,保留原始数据副本
df_raw = pd.read_excel("城市年度数据.xlsx")  # 读取原始数据,不做任何改动
df = df_raw.copy()                          # 复制一份作为工作副本,后续操作都在 df 上进行,保护原始数据

# ---------- 数据初步探查 ----------

print(df.shape)                 # 查看数据维度:(行数, 列数)

df.head()                       # 查看前 5 行数据,直观了解数据内容

df.info()                       # 查看每列的名称、非空数量、数据类型,以及内存占用
                                # 可发现哪些列存在缺失、哪些列类型需要转换(如数值被识别为 object)

df.describe(include="all")      # 生成统计描述:
                                # 数值列 → 计数、均值、标准差、最值、四分位数
                                # 分类列 → 唯一值数、最高频值及其出现次数
                                # include="all" 表示同时包含数值列和分类列

df.isna().sum()                 # 统计每列的缺失值(NaN)数量,用于确定后续清洗策略

df.duplicated().sum()           # 统计完全重复的行数,判断是否需要去重

缺失与异常处理

缺失值识别

统一转成NaN便于后续处理。特殊符号不处理,缺失率会算低;后面的中位数、插值也可能报错。

python 复制代码
# ---------- 1. 缺失值符号规范化 ----------
# 数据中可能用各种符号表示"缺失",先统一收集起来
missing_symbols = ["--", "未知", "缺失", "NA", ""]  # 常见的非标准缺失标记

# 将这些符号统一替换为标准的 NaN,便于后续 pandas 缺失值处理函数识别
df = df.replace(missing_symbols, np.nan)

# ---------- 2. 数值列类型转换 ----------
# 读取 Excel 时,因存在 "--" 等符号,数值列常被识别为 object(字符串)类型
# errors="coerce":无法转换为数值的内容(如残留的非数字字符串)→ 强制转为 NaN,而不是报错
for col in ["GDP", "人口", "能耗", "污染排放"]:
    df[col] = pd.to_numeric(df[col], errors="coerce")

# ---------- 3. 计算各列缺失率 ----------
# isna().mean():每列缺失值的比例(0~1),乘以 100 转为百分比
# 用于评估数据质量,决定后续是"填充"还是"删除"
missing_rate = df.isna().mean() * 100

缺失值处理

在数据清洗中,处理缺失值是最常见的任务之一。很多人的第一反应是用均值填补,但这种做法并不总是合理。是否选用均值,取决于数据本身的分布和结构:当数据分布近似对称时,均值确实能代表典型水平;但当数据存在偏态或极端值时,均值会被拉偏,此时用中位数更稳妥。此外还有两种更贴合业务场景的策略------当不同群体(比如不同城市)的量级差异明显时,应该分组填充,避免把一线城市的数值和中小城市混在一起平均;而当数据是连续的时间序列时,最合理的做法是线性插值,利用前后时间点的趋势来估算中间的缺失值。

总结成一句话:对称分布用均值,偏态分布用中位数,群体差异大用分组填充,时间序列用插值。

案例分析

假设我们有北京 GDP 的三年片段:2021 年为 38136 亿元,2022 年缺失,2023 年为 43761 亿元。如果此时用全表均值来填补 2022 年,显然不合理------因为缺的是时间序列中间的一年,它前后两年的数值就是最有信息量的参考。正确做法是取前后两年的平均值,即 (38136 + 43761) ÷ 2 = 40948.5 亿元。这个例子也说明:填补策略应当利用数据内在的先后关系,而不是简单地"抹平"所有缺失。

但这个案例里还藏着一个更隐蔽的坑:三行数据的城市名分别是"北京"、"北京市"、"Beijing"。如果不先统一名称,pandas 会把它们当成三个不同的城市,按城市分组插值就会失效------每个"城市"名下只剩一两个有效数据点,根本无从插值。所以正确的流程是先做城市名称归一化,再按年份排序、分组插值。

代码实现

python 复制代码
# 第一步:统一名称(把 北京 / 北京市 / Beijing 合并为同一个城市)
# df['城市'] = df['城市'].replace({'北京市': '北京', 'Beijing': '北京'})

# 第二步:按城市和年份排序,保证每个城市内部年份递增
df = df.sort_values(['城市', '年份'])

# 第三步:按城市分组,组内对 GDP 做线性插值并写回原列
g = df.groupby('城市')['GDP']
df['GDP'] = g.transform(
    lambda s: s.interpolate()  # 用前后年份的值线性估算缺失年份
)

异常值识别

数据清洗中的另一大任务是异常值检测。面对一列数值,很多人靠"看谁不顺眼就删谁",这种做法既容易误删真实数据,也容易漏掉隐藏的错误。更可靠的做法是用统计方法划定一个"正常范围",把超出范围的记录先标记出来,再做人工判断。其中最常用的就是 IQR(四分位距)法。

它的原理很简单:把数据从小到大排序后,取第 25 百分位数(Q₁)和第 75 百分位数(Q₃),两者之差就是 IQR,代表数据中间 50% 的分布宽度。以这个宽度为基准,向两侧各扩展 1.5 倍,就得到了下界(Q₁ − 1.5 × IQR)和上界(Q₃ + 1.5 × IQR)。落在界外的记录,就是需要重点检查的候选异常值。这个方法的好处是稳健------上下界由四分位数决定,极端值本身几乎不影响它们的位置,不像均值和标准差那样会被异常值"带跑"。

代码实现

python 复制代码
# 第一步:计算第 25 和第 75 百分位数
Q1 = df['人口'].quantile(0.25)
Q3 = df['人口'].quantile(0.75)
IQR = Q3 - Q1  # 四分位距:中间 50% 数据的跨度

# 第二步:划定正常范围的上下界
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR

# 第三步:筛选出超出范围的记录(候选异常值)
outliers = df[(df['人口'] < lower) |
              (df['人口'] > upper)]

在本案例的数据集(城市年度数据)中,运行上述代码后筛出了一条记录:城市为 Beijing,时间是 2022 年,人口为 219000------远高于其余城市。结合常识判断,这很可能是录入时多写了两个 0(真实值可能是 2190 左右的量级,具体单位要看数据说明)。这是一个典型的"假异常":数值上极端,但成因是录入错误,而非真实的城市人口突变。

异常值判断

检测只是第一步,真正的难题在于:拿到一条离群记录后,该拿它怎么办? 数据处理的常见误区就在这里:很多人看到数值离群,第一反应是直接删除,图个干净省事。这种做法的风险在于------如果那条"异常"其实是真实数据(比如超大城市的真实人口),删掉它就等于亲手扭曲了数据集;而如果它是录入错误却没被处理,它又会持续污染后续的统计结果。所以,异常值处理的核心原则是:先判断成因,再对症下药,绝不因为"离群"就删除。

异常值按成因大致可以分为三类,每一类对应不同的处理方式。

第一种是录入错误。 数据在采集或录入过程中出了差错。这类错误的正确处理方式是核对原始来源后修正;如果原始来源也无法确认正确值,就记为缺失值,交给后续的空值处理流程(均值、中位数或插值)去补。

第二种是真实极端值。 数值虽然离群,但它是真实发生的------比如一线城市的人口相对中小城市就是天然极端。这类数据必须保留,并在分析报告中说明保留原因。真实的极端值往往正是分析中最有信息量的部分,删掉它们等于丢掉了故事的主角。

第三种是无法确认的情况。 既找不到原始来源佐证是笔误,也判断不出它是否真实。对这类记录,稳妥的做法是先做标记(比如增加一列 is_outlier 标记),然后分别在有它和无它的两种数据集上跑分析,比较处理前后的结果。如果结果差异不大,说明它影响有限;如果差异显著,就需要回到业务层面进一步追查。

格式与重复

原始数据往往来自不同来源、由不同人录入,这就导致同一个信息有多种写法。比如年份可能是"2020"也可能是"2022/01";同一个城市可能写作"北京"、"北京市"或"Beijing";同一种能耗,有的用"吨"有的用"万吨";更麻烦的是,同一城市同一年可能出现了两条记录。如果这些问题不解决就急着建模,后果很严重:分组统计时同一个城市会被拆成三个实体,导致统计虚高;单位不统一会让数值比较毫无意义;重复记录会成倍放大某些城市的权重,扭曲相关分析和模型训练的结果。所以,名称、日期、单位、主键这四类字段,必须在建模前统一,这是任何后续分析可靠性的前提。

四类字段的规范化要点

第一是日期字段统一。 原始数据里,有时是完整的"2020年",有时是"2022/01"这种带月份的格式。建模和分组通常只需要年份,所以需要提取四位数年份,统一转成整数,才能作为时间维度的可靠主键。

第二是名称字段统一。 这是最容易出问题的一类。北京、北京市、Beijing 指的是同一个城市,但如果不映射,pandas 会当成三个不同的值。解决方法是建立一张名称映射表,把各种写法统一映射到标准名称,再对字段做替换。

第三是单位字段统一。 能耗这类指标,原始数据可能混合了"吨"和"万吨"两种单位。单位不统一,数值就没有可比性。正确做法是统一换算成标准单位(本例统一为"万吨"),让所有记录处于同一度量纲之下。

第四是主键去重。 城市年度数据里,每一行的唯一标识应该是"城市 + 年份"的组合。如果同一城市同一年出现了两条记录,就说明数据有重复,需要用联合主键去重,保证每个城市每年只有一行。

代码实现

python 复制代码
# 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. 城市-年份应唯一,按联合主键去重
df = df.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() 把干净的数据导出为建模表。六个环节像流水线一样依次传输数据,每一步只关心自己的职责。

函数化结构

为了让这条流水线真正可运行、可维护,建议把每个阶段封装成独立函数,再由一个主函数按顺序调用。下面是一个推荐的结构:

python 复制代码
def clean_city_data(df_raw):
    df = df_raw.copy()          # 1. 建立原始数据只读副本
    log = []                    # 2. 初始化日志,记录每一步操作
    df = normalize_missing(df, log)      # 3. 规范化缺失符号
    df = standardize_fields(df, log)     # 4. 统一格式与单位
    df = clean_values(df, log)           # 5. 处理缺失与异常值
    df = drop_key_duplicates(df, log)    # 6. 按主键去重
    validate_clean_data(df)              # 7. 验证清洗结果
    return df, pd.DataFrame(log)         # 8. 返回干净数据 + 清洗日志

这个结构有两个优点。第一,每个函数都接收 df 和 log------df 让数据在函数间依次传递,log 让每个函数把"我做了什么"追加到日志里,从而实现自动化的过程记录。第二,主函数最后同时返回 df 和 pd.DataFrame(log)------返回值不只是清洗后的数据,还包括一份结构化的清洗日志,这两者合起来才构成完整、可追溯的处理记录。

日志记录

日志每一步至少应该包含五项内容:处理前的记录数(基线,知道从多少行开始);处理规则与涉及字段(做了什么、针对哪些列);修改、填补或删除的数量(这是论文里最常引用的数字);处理后的复查结果(这一步是否生效、数据形态如何);以及异常无法确认时的标记(记录哪些记录存疑、为何未处理)。有了这五项,整个清洗过程就成了一本"账本",随时可以核对。

一份合格的日志应该包含步骤(操作序号)、发现的问题(针对什么)、处理方式(用了什么规则)、数量(处理了多少)、结果(处理后数据变成什么样)。

在第 X 步、针对 Y 问题、用了 Z 规则、处理了 N 条、结果变成了 M。

步骤 发现的问题 处理方式 数量 结果
1 特殊缺失符号 统一转为 NaN 3个 进入后续处理
2 城市名称不统一 映射为标准名称 5条 3个标准城市
3 GDP/能耗/污染缺失 分组插值或中位数 3个 缺失率降为 0
4 人口数量级异常 回查后修正 1条 219000→2184
5 城市---年份重复 联合主键去重 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_source=copy_web\&vd_source=9bd30904a7520995fabec0d90dd26e50

相关推荐
醍醐实验室1 小时前
基准测试的自动化报告生成:从 JSON 指标到交互式 HTML 图表
人工智能
坚定信念,勇往无前1 小时前
数字人直播技术方案
ai
小飞象—木兮1 小时前
BCG波士顿矩阵深度解析及应用指南:核心逻辑、落地路径、常见误区、案例
大数据·人工智能·矩阵·数据分析·用户运营
Raas1001 小时前
AI网关有哪些功能?MAI Gateway(魔芋企业级AI网关)实战能力深度解读
大数据·人工智能·网关·gateway·ai网关·mai gateway·企业级产品
新知图书1 小时前
第 14 章 后训练算法GRPO详解与实战
人工智能·算法
zzzzzz3102 小时前
anthropics/skills:高关注度“官方技能”项目,应该怎样读
人工智能·开源·github
moonsims2 小时前
Voliro 无人机-Aerial Mobile Robot(空中移动机器人):把无人机从“飞过去拍摄”,升级成“飞过去并与目标物理接触、测量甚至操作”
前端·人工智能·安全·无人机·量子计算
职场的momo3 小时前
用户讨论:算法开发Offer决赛:大疆纯后端Agent与网易游戏测试,对内AI跳槽难?
人工智能·游戏·跳槽