用Python数据分析验证大满贯种子爆冷:pandas清洗与分组统计实战
发布时间:2026/9/3 21:53:17 作者:尧图编辑部 阅读量:1,286

最近刷到一条挺有意思的网球赛事话题某一年大满贯前几轮打下来ATP 排名前十的种子选手几乎全被淘汰最后只剩一人撑场面甚至有球迷调侃“冠军和亚军都没鸟”。这种“爆冷”信息在社交平台上传播很快但如果你真想去验证它手动翻赛程、查每轮比分、再对照种子排名工作量非常大。我的做法是直接把整件事变成一个数据分析任务用 Python 拉取公开的 ATP 大满贯赛事数据写脚本统计各轮过后“前十种子还剩几个”顺带把冠军、亚军是不是前十种子也查一遍。这样既能快速验证新闻里的说法又能练一遍 pandas 数据清洗和分组统计。下面就把这套分析过程完整拆出来代码可以直接复制运行。1. 分析目标与整体思路1.1 什么是“前十种子”网球赛事里的“种子”是赛会根据参赛选手的排名和过往战绩设定的签位保护机制。大满贯一般设 32 个种子种子排名越靠前签位分布越分散。我们常说的“前十种子”通常指赛会种子编号 1 到 10 的选手。注意这里的“前十”有两个口径赛会种子排名tournament seed。实时 ATP 世界排名ATP Ranking。两者大部分时间一致但偶尔会有差异比如有人因伤病排名下滑却靠着去年成绩保住种子席位。在本文分析中我们以数据集中自带的winner_seed和loser_seed字段为准也就是赛会种子编号。1.2 数据集选择网球比赛数据方面比较常用的开源数据集是 Jeff Sackmann 维护的tennis_atp项目。它按年份拆分成多个 CSV 文件例如atp_matches_2020.csv atp_matches_2021.csv atp_matches_2022.csv atp_matches_2023.csv每个文件的一行就是一场比赛包含赛事名称、场地类型、轮次、胜者、败者、双方种子等字段。用它来分析“前十种子存活情况”非常合适。1.3 整体分析流程整个验证过程分为四步读取并合并指定年份的比赛数据。筛选出澳网、法网、温网、美网四个大满贯赛事。按轮次统计“种子编号小于等于 10 的获胜者人数”得到每轮结束后前十种子的幸存数量。单独提取决赛轮查看冠军和亚军是不是前十种子。写成流程图就是下面这个样子不需要复杂架构简单直观即可读取CSV → 合并年份 → 筛选大满贯 → 清洗种子字段 → 分组统计 → 输出表格 → 可视化2. 环境准备与数据集说明2.1 环境依赖本次分析使用 Python 3.8 以上版本。核心依赖如下pandas数据处理。numpy数值计算。matplotlib绘制柱状图。seaborn图表美化可选。建议使用虚拟环境安装python -m venv venv source venv/bin/activateWindows 下激活命令是venv\Scripts\activate然后安装依赖pip install pandas numpy matplotlib seaborn2.2 数据文件准备从tennis_atp仓库下载需要年份的 CSV 文件放到项目目录下的data/文件夹中。project/ ├── data/ │ ├── atp_matches_2020.csv │ ├── atp_matches_2021.csv │ ├── atp_matches_2022.csv │ └── atp_matches_2023.csv ├── analysis.py └── output/如果你只想分析某一年那就只放对应年份的文件。后面代码里会支持批量读取所以多放几年也没问题。2.3 关键字段说明tennis_atp数据集里和本次分析相关的字段主要有字段名类型说明tourney_name字符串赛事名称surface字符串场地类型如 Hard、Clay、Grassround字符串轮次如 R128、R64、R32、R16、QF、SF、Fwinner_name字符串胜者姓名winner_seed字符串/浮点胜者种子编号非种子选手为 NaNloser_name字符串败者姓名loser_seed字符串/浮点败者种子编号非种子选手为 NaNscore字符串比赛比分需要特别注意的是winner_seed在原始 CSV 里读进来可能是字符串类型并且非种子选手对应的值是NaN所以要先做类型转换。3. 数据清洗与预处理拿到原始数据之后不能马上统计因为种子字段、赛事名称、轮次命名都可能存在细微差异。下面先处理几个关键点。3.1 将种子字段转成数值种子编号在 CSV 中可能被读成字符串比如1。直接比较大小会出问题所以用pd.to_numeric统一转成浮点数转换失败的值变成NaN。import pandas as pd import numpy as np df pd.read_csv(data/atp_matches_2023.csv) df[winner_seed_num] pd.to_numeric(df[winner_seed], errorscoerce) df[loser_seed_num] pd.to_numeric(df[loser_seed], errorscoerce)转换后种子编号 1 到 10 就对应“前十种子”。3.2 筛选大满贯赛事大满贯在数据集里的名称不一定完全统一。常见的叫法有澳网Australian Open法网Roland Garros温网Wimbledon美网US Open为了避免漏选可以用isin做筛选。majors [Australian Open, Roland Garros, Wimbledon, US Open] df_major df[df[tourney_name].isin(majors)].copy()如果你只看澳网、法网、温网就把条件改一下。原始标题中提到的是三个赛事实际分析时也可以灵活调整赛事范围。3.3 检查轮次字段不同年份数据集的round字段取值一般是大写的轮次缩写R128, R64, R32, R16, QF, SF, F其中QF是四分之一决赛SF是半决赛F是决赛。建议先打印唯一值确认一下print(df_major[round].value_counts())如果某个文件里出现RR小组循环赛之类的取值说明该赛事可能不是标准淘汰赛制需要额外处理。3.4 合并多个年份文件如果要分析“连续几年的大满贯”可以用glob批量读取并合并。import glob files sorted(glob.glob(data/atp_matches_*.csv)) df_list [pd.read_csv(f) for f in files] df_all pd.concat(df_list, ignore_indexTrue)合并后先做去重避免重复文件重复统计。df_all df_all.drop_duplicates().reset_index(dropTrue)到这里数据清洗基本完成可以进行核心统计了。4. 核心代码实现4.1 统计各轮结束后“前十种子幸存人数”统计逻辑是这样的某一场比赛结束后如果胜者的种子编号在 1 到 10 之间就说明这位前十种子成功晋级下一轮。那么某一轮所有满足条件的胜者数量就是该轮结束后的前十种子幸存人数。注意如果某位前十种子在前一轮被淘汰他就不应该出现在后一轮的比赛记录里所以后一轮的胜者统计天然会少掉这个人。用“胜者种子编号 10”这一条规则就能准确反映每一轮结束后仍留在签表中的前十种子数量。def count_top10_survivors(df_major): # 构造标记列胜者是否为前十种子 df_major df_major.copy() df_major[is_top10_winner] df_major[winner_seed_num].between(1, 10, inclusiveboth) # 按赛事和轮次统计 result ( df_major[df_major[is_top10_winner]] .groupby([tourney_name, round]) .size() .reset_index(nametop10_alive) ) # 将轮次转为有序类别便于排序 round_order [R128, R64, R32, R16, QF, SF, F] result[round] pd.Categorical(result[round], categoriesround_order, orderedTrue) result result.sort_values([tourney_name, round]) return result调用方式df_major df_all[df_all[tourney_name].isin(majors)].copy() df_major[winner_seed_num] pd.to_numeric(df_major[winner_seed], errorscoerce) df_major[loser_seed_num] pd.to_numeric(df_major[loser_seed], errorscoerce) survivors count_top10_survivors(df_major) print(survivors)输出效果大致如下具体数值取决于数据分析的年份和数据集tourney_name round top10_alive Australian Open R128 8 Australian Open R64 6 Australian Open R32 4 Australian Open R16 3 Australian Open QF 2 Australian Open SF 1 Australian Open F 0 Roland Garros R128 10 ...如果某一行的top10_alive是 0说明该轮结束后前十种子已经全部出局。4.2 检查冠军和亚军是否属于前十种子要验证“冠军和亚军一个都没鸟”需要从决赛轮里提取胜者和败者的种子编号。finals df_major[df_major[round] F].copy() finals[champion_is_top10] finals[winner_seed_num].between(1, 10, inclusiveboth) finals[runnerup_is_top10] finals[loser_seed_num].between(1, 10, inclusiveboth) check finals[ [tourney_name, winner_name, winner_seed_num, loser_name, loser_seed_num, champion_is_top10, runnerup_is_top10] ] print(check)这段代码的输出会列出每站大满贯的冠军、亚军姓名以及他们各自的种子编号。如果冠军和亚军都是非种子选手那么两列布尔值都是False说明新闻里“冠军亚军都没前十种子”的说法是成立的。4.3 统计前十种子被谁淘汰如果只想看“冷门制造者”也就是击败前十种子的对手是谁可以用下面这段逻辑。upsets df_major[ df_major[loser_seed_num].between(1, 10, inclusiveboth) ].copy() upsets[winner_is_top10] upsets[winner_seed_num].between(1, 10, inclusiveboth) # 非前十种子击败前十种子 real_upsets upsets[~upsets[winner_is_top10]] real_upsets real_upsets[ [tourney_name, round, loser_name, loser_seed_num, winner_name, winner_seed_num] ] print(real_upsets)这里real_upsets中的每一行都代表一位前十种子选手输给了一位非前十种子选手。聚在一起之后你就能看出“冷门都发生在哪一轮、谁制造了冷门”。4.4 可视化展示为了让结果更直观可以用柱状图展示各轮前十种子的幸存数量变化。import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False sns.set_style(whitegrid) plt.figure(figsize(10, 6)) sns.barplot( datasurvivors, xround, ytop10_alive, huetourney_name, ) plt.title(前十种子在各轮结束后的幸存人数) plt.xlabel(轮次) plt.ylabel(幸存人数) plt.legend(title赛事) plt.tight_layout() plt.savefig(output/top10_survivors.png, dpi150) plt.show()如果 CSV 数据里的赛事名称不是英文中文字体设置也正常那么图表会非常直观柱子的高度一路下降到 0 或者 1正是“只剩一人”的视觉体现。4.5 完整脚本汇总把上面的代码整合成一个完整可运行脚本方便直接跑。import glob import pandas as pd import numpy as np MAJORS [Australian Open, Roland Garros, Wimbledon, US Open] ROUND_ORDER [R128, R64, R32, R16, QF, SF, F] def load_data(patterndata/atp_matches_*.csv): files sorted(glob.glob(pattern)) if not files: raise FileNotFoundError(未找到数据文件请检查 data 目录) df_list [pd.read_csv(f) for f in files] df pd.concat(df_list, ignore_indexTrue) df df.drop_duplicates().reset_index(dropTrue) return df def prepare(df): df df[df[tourney_name].isin(MAJORS)].copy() df[winner_seed_num] pd.to_numeric(df[winner_seed], errorscoerce) df[loser_seed_num] pd.to_numeric(df[loser_seed], errorscoerce) return df def count_top10_survivors(df_major): df_major df_major.copy() df_major[is_top10_winner] df_major[winner_seed_num].between(1, 10, inclusiveboth) result ( df_major[df_major[is_top10_winner]] .groupby([tourney_name, round]) .size() .reset_index(nametop10_alive) ) result[round] pd.Categorical(result[round], categoriesROUND_ORDER, orderedTrue) result result.sort_values([tourney_name, round]) return result if __name__ __main__: df_all load_data() df_major prepare(df_all) survivors count_top10_survivors(df_major) print(survivors) finals df_major[df_major[round] F].copy() finals[champion_is_top10] finals[winner_seed_num].between(1, 10, inclusiveboth) finals[runnerup_is_top10] finals[loser_seed_num].between(1, 10, inclusiveboth) print(finals[[tourney_name, winner_name, winner_seed_num, loser_name, loser_seed_num, champion_is_top10, runnerup_is_top10]])运行命令python analysis.py如果数据放的位置不对脚本会明确提示找不到文件。先把数据目录结构建好再运行就顺利了。5. 运行结果解读5.1 从“幸存人数表”看整体走势假设你分析的是某几年的大满贯输出表的每一行代表“某一站大满贯的某一轮结束后还有几名前十种子留在签表里”。判断思路如果R128第一轮结束后是 8说明首轮就有 2 位前十种子被淘汰。如果到QF后已经变成 1说明八强之后只剩一人。如果到F后是 0说明前十种子连决赛都没进。这种表格比文字描述更直观也能看清是“某一轮集中爆冷”还是“每轮持续消耗”。5.2 从“冠军亚军表”看结论finals的输出表里每一行对应一站大满贯的决赛。如果冠军和亚军的winner_seed_num、loser_seed_num都大于 10 或者是NaN说明决赛双方都不是前十种子。这样“冠军亚军一个都没前十”的说法就被数据证实了。这里需要提醒一点非种子选手的种子编号是NaN不是 0。判断时要注意空值处理。5.3 从“冷门制造者表”看爆冷来源看real_upsets输出表时重点观察两个信息败者是谁被淘汰的前十种子。胜者是谁击败他的非前十种子。如果很多冷门都集中在某一位“黑马”身上那这位选手大概率就是当届赛事最大搅局者。6. 常见问题与排查思路在实际运行过程中可能会遇到下面几个问题。问题现象常见原因解决思路FileNotFoundErrorCSV 文件路径不对检查data/目录和文件名报错No columns to parse from fileCSV 文件为空或下载不完整重新下载数据文件种子字段全是NaN字段名可能不同打印df.columns查看实际字段名round字段出现乱码或不标准数据版本不同用value_counts()查看轮次取值统计结果人数偏多多个年份文件重复合并合并后执行drop_duplicates()图表中文乱码系统没有中文字体把SimHei换成Arial Unicode MS或直接用英文标签种子编号被读取成字符串CSV 中带引号或空格用pd.to_numeric(..., errorscoerce)转换还有一些逻辑层面的坑单独说一下。6.1 “前十种子幸存者”不是简单求和如果某位前十种子在第二轮输球他虽然参加过第二轮但不该算进“第二轮结束后幸存者”。所以只能统计“赢得该轮比赛的种子选手人数”不能统计“该轮参赛的前十种子人数”。6.2 轮次排序要固定round是字符串直接排序会按字母序排F会跑到前面。用pd.Categorical指定顺序是最稳妥的做法。6.3 大满贯名称不统一有的年份可能用简称比如Australian Open写成Aus Open。如果筛选后结果为空先打印赛事名称的去重值。print(df_all[tourney_name].unique())根据实际名称调整MAJORS列表即可。7. 最佳实践与工程建议7.1 明确统计口径分析“前十种子”之前先想清楚口径是赛会种子还是 ATP 实时排名。如果你用的是tennis_atp数据集默认口径是赛会种子。如果你想看 ATP 排名前 10 的选手需要额外引入排名数据不能直接用种子字段替代。7.2 数据版本要记录公开数据集会持续更新CSV 的字段和内容可能有差异。建议在项目目录下放一个README.md记录数据下载日期、数据版本、使用的起始年份。这样后续复现结果或排查异常时能更快定位。7.3 统计结果避免过度解读如果你的分析结论是“前十种子全军覆没”要注意数据覆盖范围。如果只看某一年、某一站比赛结论只能描述该范围不能推广到“所有年份的普遍规律”。在写分析报告时建议把代码、数据范围、统计口径一起贴出来方便读者验证。7.4 性能优化如果分析范围是 2000 年至今的全部大满贯数据合并后的 DataFrame 会比较大。这个时候可以在筛选赛事之后再处理种子字段减少不必要的计算量。还有一种情况是只需要统计冠军和亚军那可以直接读取 CSV 后只保留决赛行避免全量数据占内存。df_finals_only df_all[df_all[round] F]7.5 可视化输出建议图表输出到本地时建议设置dpi150以上方便在博客或报告中直接使用。如果项目的output/目录不存在需要先创建目录否则savefig会报错。import os os.makedirs(output, exist_okTrue)7.6 安全与合规提醒如果你后续想把数据发布到公网或集成到 Web 应用里需要注意版权和数据集协议。个人学习场景下使用公开数据集没有问题但商用前要确认数据集的 License。8. 总结与后续扩展方向通过这套脚本你就能用数据验证“前十种子全军覆没”这类热点说法了。核心流程无非是读取 CSV → 筛选大满贯 → 清洗种子字段 → 按轮次统计幸存人数 → 提取决赛信息。整个分析不依赖复杂框架一个 pandas 脚本足够完成。如果还想继续深入可以从下面几个方向扩展统计 2000 年以来每年澳网、法网、温网、美网的前十种子淘汰趋势看看“爆冷”是否逐年加剧。加入 ATP 实时排名数据比较“种子前十”和“排名前十”两种口径的差异。分析“冷门制造者”在击败前十种子后的后续成绩看黑马到底能走多远。把结果输出成 HTML 或 Excel 报表结合定时任务做成每周自动更新的数据看板。引入机器学习模型用首轮赔率、场地类型、近期胜率等特征预测种子选手的出局概率。最后提醒一下跑这个分析前记得把数据集先下载完整确认字段名称与脚本一致如果数据文件年份不同字段有小差异优先根据实际输出调整列名。动手跑一遍比看十遍文章印象深得多遇到报错就顺着表格里的字段去查很快就能定位问题。希望这篇教程能帮你在“吃瓜看比赛”和“动手写代码”之间找到一个结合点。下次再刷到各种夸张的赛事热点不妨自己拉数据验证一下既学了 pandas又能看清事件的真实全貌。