☰
比赛预测数据清洗实战:从脏数据到干净数据集的完整指南
2026/10/17 1:09:53 网站建设 项目流程

1. 为什么数据清洗是比赛预测里最容易被低估的一环

做比赛预测做到第四个阶段,很多人会有一个错觉:模型选得好、特征工程做得花哨,预测准确率就能上去。但真正跑过完整流程的人都知道,原始数据的脏乱程度往往超出想象,而清洗质量直接决定了后面所有环节的天花板。我见过不少项目,模型调参调了半天,最后发现是数据里混进了重复场次、时间戳错位、选手位置标注混乱,导致整个训练集都是噪声。

这一篇就专门聊比赛预测项目里的数据清洗。所谓数据清洗,指的是把从各种渠道采集来的原始比赛记录,经过缺失值处理、异常值识别、格式统一、去重、字段对齐等一系列操作,变成一份可以直接喂给特征工程和模型的干净数据集。它听起来不性感,但它是整个预测流水线里最不能偷懒的环节。

适合谁看?如果你正在做电竞赛事的数据分析、想搭建自己的预测模型,或者只是好奇一场比赛背后有多少数据需要整理,这篇内容都能给你一套可以直接抄作业的思路。我会尽量把每一步的“为什么”讲清楚,而不是只丢一堆代码让你照抄。毕竟清洗逻辑是跟数据源强相关的,理解原理比记住代码重要得多。

2. 先搞清楚你手里到底有哪些脏数据

2.1 比赛数据的常见来源与各自的坑

做预测之前,数据来源通常有这么几类:官方赛事接口、第三方数据平台、社区维护的开源数据集、以及自己爬取的比赛记录。每一类的脏法都不一样。

官方接口的数据结构通常最规范,但字段往往偏“赛事运营”视角,比如只给你比分和胜负,不给你选手级别的经济曲线。第三方平台的数据维度丰富,但不同平台对同一场比赛的记录可能不一致,比如比赛时长一个记的是游戏内时间,一个记的是实际耗时。社区数据集质量参差不齐,有的连字段命名都不统一,今天叫team_a,明天叫blue_side。自己爬的数据则要面对反爬导致的字段缺失、编码错乱、时间格式五花八门等问题。

我一般会先做一件事:把所有来源的数据各抽十条样本,人工看一遍。这一步不能省,因为你对数据的第一印象决定了后面清洗策略的方向。很多人一上来就写代码跑describe(),结果被一堆统计量绕晕,反而没注意到某个字段里混着中英文两种取值。

2.2 用一张表盘点字段的“健康度”

在动手清洗前,我会先给每个字段做一次体检。下面这张表是我常用的字段健康度盘点模板,你可以直接套用:

字段名来源缺失率取值示例潜在问题
match_id接口A0%1001, 1002不同来源ID规则不同
match_date接口A2%2023-01-01, 01/02/2023格式不统一
team_blue接口B5%T1, SKT队名历史变更
duration接口B0%32:15, 1935单位混用
winner爬取8%1, 0, 蓝, 红取值编码混乱

这张表看起来简单,但它能帮你快速定位哪些字段需要重点处理。缺失率高的字段要考虑是丢弃还是填充,取值混乱的字段要建立映射规则,单位不统一的字段要统一量纲。我个人的经验是,先把这张表填完,再写清洗代码,效率比边写边想要高得多。

2.3 缺失值不是都要填,先分清三种类型

缺失值处理是清洗里最容易一刀切的地方。很多人看到NaN就无脑填均值或者直接删行,结果把有用的样本也删没了。我的做法是先给缺失值分类:

  • 完全随机缺失:比如某场比赛的观众人数没记录,跟比赛本身没关系,这种可以考虑填充或忽略。
  • 随机缺失:比如某些场次的经济数据因为接口超时没抓到,这种要看缺失比例,比例低可以删,比例高要想办法补。
  • 非随机缺失:比如低级别联赛的选手数据本来就少,这种缺失本身携带信息,不能随便填。

举个例子,如果duration字段缺失,而缺失的场次恰好都是某支队伍的比赛,那这个缺失可能跟队伍打法有关,直接填平均值会引入偏差。这时候更稳妥的做法是标记一个duration_missing的布尔字段,让模型自己去学这个缺失模式。

3. 去重这件事,比你想的要复杂

3.1 完全重复和“伪重复”要分开处理

去重听起来简单,drop_duplicates()一行代码的事。但比赛数据里的重复往往不是完全一样的行,而是“伪重复”——同一场比赛被不同来源记录了两次,字段值有细微差异。

比如接口A记录的比赛时长是32:15,接口B记录的是1935秒,两条记录看起来完全不同,但其实是同一场。如果只用整行去重,这两条都会留下,导致模型在训练时对这场比赛“看了两遍”,权重被放大。

我的处理方式是分两步:先按业务主键去重,再按相似度去重。业务主键通常是match_id加match_date的组合,如果两个来源的ID体系不同,就用“日期+双方队伍+比分”作为联合主键。相似度去重则是对那些主键对不上的记录,计算字段间的相似度,超过阈值就判定为同一场。

3.2 用联合主键识别同一场比赛

联合主键的构造需要一点领域知识。以比赛预测为例,一场比赛的核心标识是:比赛日期、蓝方队伍、红方队伍、最终比分。这四个字段组合起来,基本能唯一确定一场比赛。

但这里有个坑:队伍名称在不同数据源里可能不一样。比如同一支队伍,有的源写全称,有的源写缩写,有的源用历史队名。所以构造联合主键之前,必须先做队名标准化。我一般会维护一张队名映射表,把所有别名映射到统一的标准名,这张表需要人工维护,但一次建好后面都能用。

# 队名标准化映射示例 team_name_map = { "SKT": "T1", "SK Telecom T1": "T1", "T1": "T1", "SSG": "GEN", "Samsung Galaxy": "GEN", "GEN": "GEN", } def normalize_team(name): return team_name_map.get(name.strip(), name.strip())

这段代码看起来简单,但映射表的质量直接决定了去重和后续分析的准确性。我建议把映射表单独存成一个配置文件,方便随时补充新出现的别名。

3.3 时间戳对齐:别让时区毁了你的数据

时间戳问题在跨区域赛事里特别常见。同一场比赛,不同来源可能用不同时区记录,有的用UTC,有的用当地时间,有的甚至用比赛服务器所在时区。如果不统一,去重时会认为这是两场不同的比赛,特征工程里按时间排序也会乱套。

我的做法是:所有时间戳统一转成UTC存储,同时在数据集里保留一个original_timezone字段,方便追溯。转换的时候要注意夏令时问题,虽然电竞赛事跨夏令时的情况不多,但一旦遇到,差一小时就可能导致日期错位。

提示:时间戳转换后一定要做一次抽样验证,随机抽几条记录,手动换算一下,确认转换逻辑没问题。我踩过一次坑,某平台的“比赛日期”其实是比赛结束日期,而另一个平台是开始日期,跨零点的比赛直接对不上。

4. 异常值识别:哪些数据该扔,哪些该留

4.1 业务规则先于统计规则

异常值检测有很多统计方法,比如Z-score、IQR、孤立森林。但在比赛数据里,我永远先用业务规则筛一遍,再用统计方法兜底。原因很简单:统计方法会把一些“罕见但真实”的情况误判为异常。

比如一场比赛时长只有15分钟,统计上可能是异常值,但如果是某方投降或者重开,这就是真实数据,不该扔。反过来,一场比赛时长显示为999分钟,这明显是数据错误,业务规则一眼就能识别。

我常用的业务规则包括:

  • 比赛时长必须在10分钟到120分钟之间(极端情况可放宽,但要标记)
  • 双方经济差不能超过某个合理上限
  • 选手击杀数不能为负数
  • 比赛日期不能晚于当前日期

这些规则写起来不复杂,但能过滤掉大部分明显的脏数据。

4.2 用分位数和箱线图定位可疑记录

业务规则筛完之后,剩下的数据用统计方法做二次检查。我习惯先看分位数,再看箱线图。

import pandas as pd # 查看关键字段的分位数 cols = ["duration", "blue_gold", "red_gold", "blue_kills", "red_kills"] print(df[cols].quantile([0.01, 0.25, 0.5, 0.75, 0.99]))

分位数能帮你快速发现极端值。比如duration的1%分位数是8分钟,99%分位数是45分钟,那超出这个范围的就值得怀疑。箱线图则更直观,能看出哪些点落在须线之外。

但要注意,分位数和箱线图都是基于当前数据分布的,如果数据本身已经严重偏斜,这些方法会失灵。所以我会结合多个字段一起看,比如时长异常的比赛,它的经济数据是不是也异常,如果多个字段同时异常,那这条记录大概率有问题。

4.3 异常值处理的三种策略及适用场景

识别出异常值之后,怎么处理?我一般分三种策略:

策略适用场景操作方式
删除确认是错误数据,且数量少直接drop
修正能推断出正确值按规则修正,如单位换算
标记不确定是否错误,或本身有信息量新增布尔字段标记

删除是最简单的,但也是最危险的,因为删多了会损失样本。修正需要你对数据有足够理解,比如发现时长字段有的用秒有的用分钟,那就统一换算成秒。标记则是最稳妥的,保留原始值,额外加一个is_outlier字段,让模型自己决定怎么用。

我个人的偏好是:能标记就不删除,能修正就不标记。因为比赛数据本身样本量就不算大,每一条记录都可能有价值。

5. 字段标准化:让不同来源的数据说同一种语言

5.1 枚举值映射:把“蓝/红”“1/0”统一起来

不同数据源对同一概念的编码方式千奇百怪。胜负字段有的用1/0,有的用win/lose,有的用蓝/红。选手位置有的用top/jungle/mid/adc/support,有的用上单/打野/中单/射手/辅助。

这些不统一,后面做特征工程时就会出问题。我的做法是建一个枚举映射字典,把所有可能的取值映射到统一编码。

winner_map = { "1": 1, "0": 0, "win": 1, "lose": 0, "蓝": 1, "红": 0, "blue": 1, "red": 0, } def normalize_winner(val): return winner_map.get(str(val).strip().lower(), None)

映射的时候要注意大小写和空格,str(val).strip().lower()这三连操作能解决大部分格式问题。映射不到的取值返回None,后面统一处理。

5.2 数值字段的量纲统一

数值字段的量纲问题在比赛数据里很常见。经济数据有的用“千”为单位,有的用原始值;时长有的用秒,有的用“分:秒”字符串;百分比有的用0.45,有的用45。

统一量纲的原则是:选一个最常用的单位作为标准,其他全部换算过去。我一般把经济统一成原始值,时长统一成秒,百分比统一成小数。

def parse_duration(val): """把各种格式的时长统一成秒""" if isinstance(val, (int, float)): return int(val) if ":" in str(val): parts = str(val).split(":") return int(parts[0]) * 60 + int(parts[1]) return None

这个函数能处理1935、32:15两种格式。实际项目中可能还有32分15秒这种中文格式,那就再加一个正则匹配分支。关键是每遇到一种新格式,就补充一条解析规则,慢慢就能覆盖所有情况。

5.3 类别字段的编码一致性

类别字段比如队伍名、选手名、英雄名,编码一致性直接影响后续的聚合和分组。我遇到过最头疼的情况是同一个选手在不同场次里名字拼写不一样,有的是全拼,有的是缩写,有的中间多了个空格。

处理这类问题,除了建映射表,还可以用模糊匹配辅助。比如用编辑距离找出相似度高的名字,人工确认后合并。但模糊匹配只能辅助,最终还是要人工确认,因为有些选手ID本身就很像,机器容易误判。

注意:类别字段的映射表一定要版本化管理。每次补充新映射,都记录一下修改时间和原因。否则过几个月回头看,你自己都不记得为什么某个值被映射成了另一个。

6. 清洗流水线的工程化组织

6.1 把清洗步骤拆成可复用的函数

数据清洗最怕的是一次性脚本——跑完就扔,下次换一批数据又要重写。我的做法是把每个清洗步骤封装成独立函数,每个函数只做一件事,输入输出都是DataFrame。

def remove_duplicates(df): """按联合主键去重""" df = df.drop_duplicates(subset=["match_date", "team_blue", "team_red"]) return df def fill_missing_duration(df): """填充缺失时长,并标记""" df["duration_missing"] = df["duration"].isna() df["duration"] = df["duration"].fillna(df["duration"].median()) return df def normalize_teams(df): """队名标准化""" df["team_blue"] = df["team_blue"].apply(normalize_team) df["team_red"] = df["team_red"].apply(normalize_team) return df

这样组织的好处是,每个函数可以单独测试,出问题容易定位。而且清洗流程可以像搭积木一样组合,换数据源时只需要替换或调整部分函数。

6.2 用配置文件管理映射规则

映射规则(队名、枚举值、量纲换算)不要硬编码在代码里,抽出来放到配置文件。我一般用YAML或JSON,结构清晰,非技术人员也能改。

team_name_map: SKT: T1 SK Telecom T1: T1 SSG: GEN winner_map: "1": 1 "0": 0 win: 1 lose: 0

配置文件的好处是,规则变更不需要改代码,也不会因为改代码引入新bug。而且配置文件可以版本控制,每次变更都有记录。

6.3 清洗日志:记录每一步删了什么、改了什么

清洗过程中删了多少行、改了多少值,这些信息一定要记录下来。我习惯在每一步清洗后打印一行日志:

def log_step(df, step_name, before_count): after_count = len(df) print(f"[{step_name}] {before_count} -> {after_count}, 删除 {before_count - after_count} 行") return df

这些日志看起来不起眼,但当你发现最终数据集比预期少了很多行时,日志能帮你快速定位是哪一步删多了。我还会把日志存成文件,方便后续复查。

7. 清洗完之后,怎么验证数据真的干净了

7.1 用断言做自动化检查

清洗完不要急着进入下一步,先写几个断言做自动化检查。断言的内容包括:关键字段没有缺失、数值字段在合理范围内、类别字段取值都在预期集合内。

assert df["match_id"].notna().all(), "match_id 存在缺失" assert df["duration"].between(600, 7200).all(), "duration 存在异常值" assert set(df["winner"].unique()).issubset({0, 1}), "winner 取值异常"

这些断言跑一遍,几秒钟的事,但能拦住大部分低级错误。我建议把断言写成一个独立的检查函数,每次清洗完都跑一遍。

7.2 抽样人工复核

自动化检查只能覆盖你想到的问题,想不到的问题还得靠人工。我一般会随机抽20到30条记录,逐字段看一遍,确认没有明显的脏数据。

抽样的时候要注意分层,不能只抽一种类型的比赛。比如按联赛级别分层,按时间段分层,确保各个子群体都被覆盖到。人工复核虽然慢,但它是最后一道防线,不能省。

7.3 和原始数据做对比统计

最后一个验证方法是把清洗后的数据和原始数据做对比统计。比如原始数据有10000行,清洗后剩9500行,那500行是怎么没的,要能解释清楚。关键字段的分布有没有发生大的偏移,比如清洗前胜率是50%,清洗后变成55%,那就要查是不是去重或异常值处理引入了偏差。

这个对比不需要很复杂,几个关键统计量就够了。但它能帮你发现一些隐蔽的问题,比如某个队伍的记录被误删了,导致整体分布偏移。

8. 几个我踩过的坑和对应的解法

8.1 队名映射表更新不及时导致去重失败

有一次做跨赛季数据清洗,某支队伍在赛季中改了名字,我的映射表没更新,结果同一支队伍被当成两支,去重没去掉,特征工程里队伍统计全乱了。后来我养成了一个习惯:每次拿到新数据,先跑一遍队名唯一值列表,和映射表对比,发现新名字就及时补充。

8.2 时间戳格式混用导致排序错乱

还有一次,两个数据源的时间戳一个用YYYY-MM-DD,一个用DD/MM/YYYY。我一开始没注意,直接按字符串排序,结果日期全乱了。后来统一用pd.to_datetime解析,并且指定format参数,避免自动推断出错。对于格式不统一的,用errors="coerce"把解析失败的标记出来,人工处理。

8.3 缺失值填充引入的隐性偏差

最隐蔽的一个坑是缺失值填充。我曾经用均值填充比赛时长,后来发现缺失时长的比赛大多是某支队伍的,而这支队伍打法偏激进,比赛时长本来就短。用全局均值填充,等于人为拉长了这支队伍的比赛时长,模型学到的模式就被扭曲了。后来我改成按队伍分组填充,或者直接标记缺失,效果好很多。

8.4 清洗顺序不对导致重复劳动

清洗步骤的顺序也很重要。我一开始先做异常值处理,再去重,结果发现有些异常值其实是重复记录导致的,先去重的话这些异常值根本不存在。后来调整成:先去重,再处理缺失值,再处理异常值,最后做标准化。这个顺序不一定适用于所有场景,但大原则是:先做那些能减少数据量的操作,再做那些需要全量数据的操作。

9. 清洗后的数据怎么衔接特征工程

数据清洗不是终点,它的输出要能直接对接特征工程。所以清洗的时候就要考虑后面要用哪些特征。比如你要做“队伍近期胜率”这个特征,那清洗后的数据必须保证每场比赛的日期是准确的、队伍名是统一的、胜负是明确的。如果这些字段还有脏数据,特征工程就会跟着出错。

我一般会在清洗的最后一步,按照特征工程的需求做一次字段对齐检查。列出特征工程需要的所有字段,确认清洗后的数据集里都有,且格式正确。这个检查能避免很多返工。

另外,清洗后的数据集最好存成一份“干净基线”,后续所有特征工程都基于这份基线做。这样如果特征工程出了问题,可以快速回滚到基线,重新开始,而不用重新跑一遍清洗。

10. 关于工具选择的一点个人看法

清洗工具方面,pandas是绕不开的,它的DataFrame操作足够灵活,社区资源也多。数据量特别大的时候可以考虑polars或者Dask,但大多数比赛预测项目的数据量,pandas完全够用。

我不建议一上来就用太重的工具,比如Spark。清洗逻辑还没跑通的时候,用重工具只会增加调试成本。先把逻辑用pandas跑通,确认没问题了,再考虑要不要迁移到更高效的工具上。

还有一点,清洗代码尽量写得“笨”一点,可读性优先。我见过一些清洗脚本,用了一堆链式操作和lambda,写的时候很爽,过两周自己都看不懂。清洗逻辑是要反复调整的,可读性比炫技重要得多。

最后分享一个小技巧:每次清洗完,把清洗后的数据集和清洗脚本一起存档,标注好版本和日期。比赛数据会不断更新,清洗规则也会不断调整,有了版本记录,你才能追溯每一次预测结果背后的数据基础是什么。这个习惯看起来麻烦,但当你需要复现几个月前的一次预测时,会感谢自己当初留了档。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询