news 2026/9/2 20:36:35

用Python分析乒乓球比赛数据:从CSV清洗到可视化实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Python分析乒乓球比赛数据:从CSV清洗到可视化实战

一场乒乓球比赛的结果,最终在媒体上可能只剩一行标题:张本美和 4:2 力克陈幸同,夺得横滨冠军赛女单冠军。但作为开发人员,如果接到“把比赛结果变成数据报表”的需求,看到的就不只是比分数值,而是一整条从数据采集、清洗、统计到可视化输出的链路。这个链路在很多体育数据项目里都会反复出现,值得用一篇完整文章把它讲透。

我会以这场“4:2”的比赛数据作为切入点,设计一个最小可运行的 Python 数据分析项目。读者可以按文章顺序准备好环境、构造示例数据,然后通过 pandas 完成读取、清洗和统计,再用 Matplotlib 画出选手胜场对比图和局分分布图。最后还会补充常见坑和生产环境落地时的差异,方便你直接迁移到自己的赛事数据处理项目中。

1. 先理解体育赛事数据分析要解决什么问题

1.1 原始比分记录离可分析数据还差几步

标题中的“4:2”是一句自然语言,但程序并不能直接理解它。程序需要的是结构化字段:选手 A 是谁、选手 B 是谁、两人各胜几局、比赛在哪一天、属于哪个赛事、最终获胜者是谁。只有把这些信息整理成表格,才能用代码做统计。

所以在实际项目中,第一步不是写统计函数,而是把零散比赛结果整理成规整的数据集。常见做法包括:

  • 赛事运营团队手工维护一份 CSV 表格;
  • 数据供应商提供 JSON 接口;
  • 内部系统把比赛记录写入数据库。

无论来源是什么,最终都要落成统一的表结构。数据字段越规范,后续统计和可视化越省事。反之,如果一开始就堆很多自由文本,后面每一步都会遇到清洗问题。

1.2 不同数据来源的格式差异和选型

不同来源的数据质量差异很大,这里用一个表格来对比常见方案的适用场景:

数据来源优点缺点适用场景
手工维护 CSV易于修改,方便理解人工录入容易出错,缺少约束小规模项目、爬坑学习
数据商 API字段完整,实时性好需要鉴权,调用量有限制生产环境,数据源稳定
内部数据库查询灵活,便于关联需要建表和权限管理已有业务系统的团队
网页公开数据信息丰富格式不固定,存在版权和合规风险仅用于验证思路,不能随意抓取

在本文的最小示例中,我选择 CSV 作为数据载体。原因是 CSV 能用文本编辑器直接查看,也便于在文章中展示。真实项目中如果接 API,只需要把请求返回的 JSON 转成同样的 DataFrame,后处理逻辑可以保持不变。

需要注意的是,生产环境接入数据源之前,先确认数据授权和接口使用规范。公开数据可以用于学习和开发验证,但不要绕过登录限制或高频抓取。

1.3 技术选型为什么是 Python + pandas + Matplotlib

Python 生态里做表格数据处理,最直接的选择是 pandas。pandas 的 DataFrame 天然适合存储比赛记录,可以方便地做筛选、分组、聚合。Matplotlib 则负责把统计结果画成图,便于直接交给运营人员查看。

这个组合的优势不是“性能最强”,而是迭代快。体育赛事数据通常不会大到需要 Spark 或大数据框架,单机 pandas 足够处理几万条比赛记录。先把逻辑在本地跑通,再考虑是否迁移到数据库或实时管道,符合大多数体育数据项目的成长路径。

2. 准备 Python 开发环境,并设计最小示例数据

2.1 环境要求和依赖安装

本文章使用 Python 3.9 以上版本。推荐先创建一个虚拟环境,再安装依赖,避免把全局 Python 环境弄乱。需要安装的依赖如下:

依赖库用途
pandas数据读取、清洗、分组统计
matplotlib绘制统计图表

在终端执行以下命令:

python -m venv .venv source .venv/bin/activate pip install pandas matplotlib

安装完成后,可以用一段极短代码确认版本:

python -c "import pandas as pd; print(pd.__version__)" python -c "import matplotlib; print(matplotlib.__version__)"

不同版本之间接口差异不大,但如果你在旧版本上运行报错,优先检查 pandas 和 matplotlib 是否太低。本文示例代码基于 pandas 2.x 和 matplotlib 3.x 编写,其他版本需要微调。

2.2 设计比赛记录表结构

为了让示例数据既能体现“张本美和 4:2 陈幸同”这个结果,又不编造更多真实比赛细节,我会构造一个匹配需求的最小数据集。字段设计如下:

字段名示例值说明
match_idM001比赛唯一标识
match_date2025-04-12比赛日期
tournament横滨冠军赛赛事名称
player_a张本美和左侧选手
player_b陈幸同右侧选手
score_str4:2双方各自胜局数
winner张本美和最终胜者

这里把总比分保存为字符串4:2,是为了演示“解析字符串”这一常见工程处理。真实数据源里,比分可能来自 JSON,也可能来自数据库的两个整数字段,但解析思路是一致的。

创建matches.csv文件,内容如下:

match_id,match_date,tournament,player_a,player_b,score_str,winner M001,2025-04-12,横滨冠军赛,张本美和,陈幸同,4:2,张本美和 M002,2025-04-12,横滨冠军赛,孙颖莎,王曼昱,4:3,孙颖莎 M003,2025-04-11,横滨冠军赛,陈幸同,早田希娜,4:1,陈幸同 M004,2025-04-11,横滨冠军赛,张本美和,孙颖莎,2:4,孙颖莎 M005,2025-04-10,横滨冠军赛,王曼昱,伊藤美诚,4:0,王曼昱 M006,2025-04-10,横滨冠军赛,张本美和,申裕斌,4:2,张本美和 M007,2025-04-09,横滨冠军赛,陈幸同,平野美宇,4:3,陈幸同 M008,2025-04-09,横滨冠军赛,孙颖莎,伊藤美诚,4:1,孙颖莎

这些记录用于演示处理逻辑,不是官方统计。实际项目中,数据需要以正式比赛记录为准。

2.3 项目目录结构

项目文件可以按下面的结构组织:

table_tennis_analysis/ ├── matches.csv ├── analysis.py └── output/

analysis.py是主脚本,output/用来存放生成的图表和统计结果。先创建目录:

mkdir -p table_tennis_analysis/output cd table_tennis_analysis

将上面的 CSV 保存到项目根目录,然后开始编写分析脚本。

3. 读取比赛数据,并解析比分字段

3.1 用 pandas 读取 CSV

analysis.py中先导入依赖并读取 CSV:

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("matches.csv") print("数据条数:", len(df)) print("\n前 3 行数据:") print(df.head(3))

运行脚本:

python analysis.py

正常情况下可以看到前 3 行数据。这里要注意,pandas 默认会把中文字段读取为字符串,match_date在读入时可能还是object类型。所以第二步需要确认字段类型,避免后续日期处理出错。

print("\n字段类型:") print(df.dtypes)

如果match_date显示为object,说明它还是字符串,需要专门处理成日期类型。

3.2 从 score_str 解析出双方胜局数

4:2这种字符串不能直接参与数学运算,需要拆成两个整数。可以用一个函数统一处理:

def parse_score(score_str): left, right = score_str.split(":") return int(left), int(right) df["score_a"] = df["score_str"].apply(lambda s: parse_score(s)[0]) df["score_b"] = df["score_str"].apply(lambda s: parse_score(s)[1])

也可以一次性生成两列,效率更高:

score_parts = df["score_str"].str.split(":", expand=True) df["score_a"] = score_parts[0].astype(int) df["score_b"] = score_parts[1].astype(int)

拆分之后,可以计算单场比赛的总局数和分差:

df["total_games"] = df["score_a"] + df["score_b"] df["score_diff"] = abs(df["score_a"] - df["score_b"])

这里score_diff表示局分差,能反映比赛是接近还是悬殊。比如 4:3 的分差是 1,4:0 的分差是 4。

3.3 数据清洗:缺失值、日期和统一字段

真实数据里,最常见的清洗任务包括:

  • 字段名不统一,比如有时候叫player_a,有时候叫player_a_name
  • 日期格式不一致,比如2025-04-122025/04/12混用;
  • 某个选手胜负字段为空;
  • 比分字符串里有不可见字符,比如4 : 2

先用代码检查缺失值:

print("\n缺失值统计:") print(df.isna().sum())

处理缺失值的策略取决于业务,不要一刀切删除。如果winner为空,可以根据双方比分重新计算;如果match_date为空,可以考虑从比赛序号推导或者标记为未知。

日期字段建议统一为 pandas 的 datetime 类型:

df["match_date"] = pd.to_datetime(df["match_date"])

如果出现解析异常,通常会提示类似DateParseErrorValueError。可以先检查异常格式:

bad_dates = df[pd.to_datetime(df["match_date"], errors="coerce").isna()] print("解析失败的日期:", bad_dates)

不要忽略这一步,因为后续按周、按月统计时,日期类型必须正确。

4. 选手战绩与局分统计

4.1 校验胜者字段是否和比分一致

有了score_ascore_b后,可以按比分重新计算胜者,并和 CSV 中的winner字段对照:

df["actual_winner"] = df.apply( lambda row: row["player_a"] if row["score_a"] > row["score_b"] else row["player_b"], axis=1 ) diff_count = (df["actual_winner"] != df["winner"]).sum() print("胜者字段不一致的记录数:", diff_count)

如果 CSV 里胜者与比分冲突,说明数据录入有误。此时应该回到数据源确认,而不是盲目信任某一个字段。

4.2 计算每位选手的胜场、负场和胜率

统计选手战绩时,需要同时考虑选手出现在player_aplayer_b的情况。一个简洁的做法是构造“选手维度”和“是否获胜”的长表:

records = [] for _, row in df.iterrows(): records.append({ "player": row["player_a"], "opponent": row["player_b"], "is_winner": row["actual_winner"] == row["player_a"], "score_self": row["score_a"], "score_opponent": row["score_b"], }) records.append({ "player": row["player_b"], "opponent": row["player_a"], "is_winner": row["actual_winner"] == row["player_b"], "score_self": row["score_b"], "score_opponent": row["score_a"], }) long_df = pd.DataFrame(records)

然后按选手聚合:

stats = long_df.groupby("player").agg( wins=("is_winner", "sum"), matches=("is_winner", "count"), ).reset_index() stats["losses"] = stats["matches"] - stats["wins"] stats["win_rate"] = stats["wins"] / stats["matches"] print(stats.sort_values("win_rate", ascending=False))

这里win_rate会是一个小数,如果希望显示成百分比,可以再乘 100 并保留两位小数:

stats["win_rate_percent"] = (stats["win_rate"] * 100).round(2)

4.3 按对手和赛事维度继续下钻

除了整体胜率,项目里常需要看“某位选手对阵主要对手时的表现”。可以按playeropponent分组:

head_to_head = long_df.groupby(["player", "opponent"]).agg( wins=("is_winner", "sum"), matches=("is_winner", "count"), ).reset_index() head_to_head["losses"] = head_to_head["matches"] - head_to_head["wins"] print(head_to_head.sort_values(["player", "matches"], ascending=[True, False]))

如果想看不同赛事的统计,把tournament也加入分组即可。赛事维度的价值在于发现选手在不同赛事中的表现波动。

tournament_stats = df.groupby(["tournament", "actual_winner"]).size().reset_index(name="match_count") print(tournament_stats)

这种写法可以快速回答“哪个赛事里哪些选手赢过比赛”之类的问题。

5. 把统计结果画成图表

5.1 先解决 Matplotlib 中文显示问题

Matplotlib 默认字体不含中文字体,直接画中文标题,会出现方框。最简单的解决方式是明确指定系统中可用的中文字体。

import matplotlib import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"] # Windows # Linux 可以改用 ["Noto Sans CJK SC"] 或 ["WenQuanYi Zen Hei"] plt.rcParams["axes.unicode_minus"] = False

设置之后,负号也能正常显示。不同系统的字体名不同,如果设置了仍无效,可以先查看当前可用字体:

from matplotlib import font_manager fonts = [f.name for f in font_manager.fontManager.ttflist if "Hei" in f.name or "CJK" in f.name] print(fonts)

5.2 绘制选手胜场对比条形图

用前面得到的stats表绘制条形图:

fig, ax = plt.subplots(figsize=(8, 5)) plot_df = stats.sort_values("wins", ascending=True) ax.barh(plot_df["player"], plot_df["wins"], label="胜场") ax.barh(plot_df["player"], plot_df["losses"], left=plot_df["wins"], label="负场") ax.set_xlabel("比赛场次") ax.set_title("选手胜负场分布") ax.legend() plt.tight_layout() plt.savefig("output/wins_losses.png", dpi=150)

这里用了一个左右堆叠的条形图,能同时展示胜场和负场。left=plot_df["wins"]是关键参数,它让负场柱状图从胜场柱状图的末端开始画,形成堆叠效果。

5.3 绘制局分分布图

局分差可以反映比赛激烈程度。绘制直方图:

fig, ax = plt.subplots(figsize=(8, 5)) ax.hist(df["score_diff"], bins=4, edgecolor="white") ax.set_xlabel("局分差") ax.set_ylabel("比赛数量") ax.set_title("局分差分布") plt.tight_layout() plt.savefig("output/score_diff_hist.png", dpi=150)

如果统计双方胜局数 4:3 的比赛最多,说明赛事整体竞争激烈;如果大量 4:0,说明强弱分明。这个分布图在赛后总结中很实用。

5.4 把汇总结果导出为文件

统计结果不能只存在控制台里,建议导出成 CSV 或 Excel 供后续使用:

stats.to_csv("output/player_stats.csv", index=False, encoding="utf-8-sig") head_to_head.to_csv("output/head_to_head.csv", index=False, encoding="utf-8-sig")

保存 CSV 时,utf-8-sig编码可以避免 Excel 打开中文乱码。如果是 Linux 环境且只用程序读取,直接utf-8也可以。

6. 运行验证与结果检查

6.1 完整脚本的预期输出

将上面的代码组合到analysis.py,运行后会在终端看到:

数据条数: 8 胜者字段不一致的记录数: 0 选手统计: player wins matches losses win_rate win_rate_percent 0 孙颖莎 3 4 1 0.75 75.0 1 张本美和 2 3 1 0.67 66.7 2 陈幸同 2 3 1 0.67 66.7 3 王曼昱 1 2 1 0.50 50.0 4 伊藤美诚 0 2 2 0.00 0.0

同时在output/目录下生成wins_losses.pngscore_diff_hist.png两张图。

6.2 用交叉验证确认统计口径

统计结果是否正确,不能只看脚本没报错。建议用两种方式交叉验证:

  • 手动数一遍 CSV 中每位选手出现的场次和获胜场次;
  • 用透视表再算一次胜场数,和groupby结果对比。
pivot_check = df.apply( lambda row: pd.Series({ "winner": row["actual_winner"], "loser": row["player_b"] if row["actual_winner"] == row["player_a"] else row["player_a"], }), axis=1 ) winner_counts = pivot_check["winner"].value_counts() print(winner_counts)

如果winner_countsstats["wins"]对不上,说明长表展开或胜负判断逻辑有误。

6.3 检查图表是否合理

打开图片后,重点检查三处:

  • 标题、坐标轴文字是否出现中文;
  • 柱状图图例是否和图块顺序一致;
  • 直方图的横轴范围是否符合分数差取值范围。

如果中文出现方框,回到 5.1 修改字体配置;如果两张图空白,检查是否调用了plt.show()之后再savefig(),这会导致保存的图片没有内容。

7. 常见问题与排查路径

7.1 Matplotlib 中文乱码

现象:图表标题和坐标轴文字显示为方框。

原因:Matplotlib 默认字体不包含中文字形,或系统缺少中文字体。

排查:

fc-list :lang=zh

在 Linux 上可以查看系统安装的中文字体。如果返回为空,需要安装字体包。解决方案:

plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]

或者改用系统中已有的中文字体名称。设置后最好重启 Python 进程,再重新执行绘图代码。

7.2 pandas 读取日期报错

现象:pd.to_datetime抛出ValueErrorDateParseError

原因:日期列中存在多种格式,或包含无法识别的文本。

排查:

bad = df[pd.to_datetime(df["match_date"], errors="coerce").isna()] print(bad)

如果异常记录较少,可以直接修正;如果很多,说明数据源格式不统一,需要统一清洗规则。

7.3 胜率出现除零错误

现象:某位选手没有比赛记录,计算wins / matches时出现ZeroDivisionError,或者统计表里出现 NaN。

原因:数据源缺失某位选手的比赛记录,或者长表展开时没有覆盖所有选手。

解决方案:

stats["win_rate"] = stats["wins"] / stats["matches"].replace(0, pd.NA)

更合理的做法是过滤掉matches == 0的行,因为零场比赛的胜率没有业务意义。建议在统计前先检查:

print(stats[stats["matches"] == 0])

7.4 CSV 字段错位或编码问题

现象:读取 CSV 后列名错位,或者中文字段显示为乱码。

原因:CSV 文件头和数据行的分隔符不一致,常见于表格软件导出的逗号分隔文件与中文逗号、分号混用;或者是文件编码不是 UTF-8。

排查:

head -3 matches.csv | cat -A

如果没有中文乱码,head -3能看到每一列的分隔符。读取时指定编码:

df = pd.read_csv("matches.csv", encoding="utf-8-sig")

如果文件本来就是 GBK 编码,就改成encoding="gbk"

7.5 图表保存后是空白

现象:脚本运行正常,savefig也有输出,但生成的图片是纯白背景。

原因:在plt.show()之后调用plt.savefig(),图像窗口关闭导致画布内容被清空。

解决方案:先savefig(),再show(),或者直接注释掉show()。建议在脚本模式中只使用savefig(),在 Jupyter 环境再显示绘图。

8. 从学习环境到生产环境的落地建议

8.1 学习环境怎么快速跑通

学习环境的目标是理解逻辑,不需要一开始就做得很重。推荐顺序是:

  1. 使用本文的 CSV 示例,跑通读取、清洗、统计、画图全流程;
  2. 把示例数据改成自己的赛事数据,观察哪些字段会变化;
  3. 把统计口径写清楚,再逐步加入自动化脚本和参数配置。

不要在学习阶段引入数据库、消息队列、分布式任务,这些会分散对核心数据处理逻辑的注意力。

8.2 生产环境还需要补充的模块

如果要把这套逻辑放到团队或业务系统里,至少还要补上以下内容:

模块需要处理的问题
数据源接入从 API 或数据库读取,而不是读 CSV
配置外置数据库连接串、赛事 ID 等放到环境变量或配置中心
日志监控记录每日处理条数、异常记录数、图表生成状态
权限安全控制数据修改权限,防止误改原始比赛记录
调度使用定时任务每天更新战绩
回滚每次跑批前备份上一版本报表,失败时能恢复
# 示例:环境变量读取数据库连接 import os db_url = os.getenv("DATABASE_URL") if not db_url: raise RuntimeError("缺少 DATABASE_URL 环境变量")

这段代码说明了为什么生产环境要把敏感配置外置:避免把连接串写死在代码仓库里。

8.3 可复用交付清单

当一个赛事数据统计脚本准备交付时,可以对照下面这个清单逐项检查:

[ ] 输入文件路径是否为绝对路径或通过配置指定 [ ] 日期字段是否已经转成 datetime,且没有解析失败记录 [ ] 胜者字段是否和比分交叉验证一致 [ ] 胜率除零问题是否处理 [ ] 中文图表是否正常显示 [ ] 统计结果是否导出为 CSV,且编码适合目标系统 [ ] 是否记录本次处理的数据条数和异常条数 [ ] 是否在数据源变更时能快速定位字段映射问题 [ ] 是否明确区分示例数据和真实数据

这个清单不需要覆盖所有业务,但能避免最常见的数据质量事故。

回到开头那场“4:2”的比赛。单纯看比分,只能知道胜负;把它放进结构化的比赛数据里,就会有选手战绩、局分差、胜率、对抗关系、赛事分布等更多维度。对于开发人员来说,真正有价值的不是写出某一段统计代码,而是能识别数据问题、设计可靠的结构、并在出错时快速定位。把这套流程练熟,再去对接真实赛事数据源或构建数据看板,就会顺畅很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 20:35:03

CS2 Insight Agent:基于demo回放的伪实战录制测试框架

demo 录制程序通常承担两类工作:一类是把游戏回放录制成视频素材,另一类是提取回放中的结构化数据供分析使用。CS2 Insight Agent 这个项目名称里,Insight 强调数据洞察,Agent 强调自动调度;放到录制测试场景里&#x…

作者头像 李华
网站建设 2026/9/2 20:33:04

PCB上游材料全解析:从覆铜板、铜箔到树脂的关键选型指南

PCB 行业的竞争,很大程度是上游材料的竞争。这个判断放在消费电子时代可能还有点模糊,但放在 AI 服务器、高多层板、高频高速板集中爆发的今天,已经变成一条明线:一块 PCB 的信号完整性、热可靠性、加工良率、最终成本&#xff0c…

作者头像 李华
网站建设 2026/9/2 20:30:22

地铁视频拍摄全流程:机位参数、FFmpeg批量转码与后期处理

这次要记录的题材不是某个开源模型,也不是本地部署项目,而是一段具体的城市轨交影像:郑州地铁 4 号线列车在中央商务区站进站、关门、出站,方向为老鸦陈。放在 CSDN 上写,是因为这类画面看似随手一拍,实际要…

作者头像 李华
网站建设 2026/9/2 20:29:49

AssetStudio多语言版完全指南:Unity游戏资源提取与解包实战

简介:AssetStudio(简称AS)多语言版是一款专门用于Unity游戏资源提取的开源免费工具,面向游戏开发学习者、美术素材研究者、独立游戏爱好者以及需要逆向分析游戏资源的用户。它能够解包大多数未加密的Unity资源文件,最常…

作者头像 李华
网站建设 2026/9/2 20:29:16

Unity接入微信与支付宝SDK全流程指南

简介:Unity 项目中集成微信登录、微信分享、微信支付与支付宝支付的 SDK 整合资源包,面向需要快速接入第三方能力的 Unity 开发者,解决多平台 SDK 导入、配置与调用的重复劳动问题。压缩包共 7322 个文件,包含 3000 余个 C# 脚本、…

作者头像 李华
网站建设 2026/9/2 20:26:39

旅客列车编组实战:从车钩匹配到动态验证的完整流程

“东拼西凑又是一列旅客列车”这句话看起来像自嘲,实际上正好说中了铁路爱好者和仿真玩家拼编组最真实的日常:手里不一定凑得出同一厂家、同一时期、同一涂装、同一型号的一整套车辆,但你仍然可以按正确的规则,把不同来源的机车和…

作者头像 李华