十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python乒乓球赛数据分析:JSON读取、比分统计与可视化实战

Python乒乓球赛数据分析:JSON读取、比分统计与可视化实战 上一场横滨冠军赛张本智和 3:0 横扫向鹏赛后球迷讨论最热烈的不是最终比分而是过程“第二局是不是差一点就翻盘了”“三局下来向鹏到底输在哪些分”只看比分播报很难回答这些问题但只要把比赛数据整理成结构化表格结论就会清晰很多。本文不讨论比赛争议只从技术角度切入教大家用 Python 完成一次完整的乒乓球比赛数据分析包括 JSON 数据读取、比分统计、关键分挖掘和可视化展示。无论你是第一次接触体育数据分析还是想在自己的项目中加入赛事数据模块都可以按这套流程直接复用。1. 从一场比赛说起为什么用数据拆解乒乓球乒乓球比赛看起来只是“谁先到 11 分谁赢一局”但真正落到数据分析层面可挖掘的信息远比比分牌丰富。比如本场 3:0 的比分背后可能隐藏着发球得分率、接发球稳定性、关键分把握能力、每局得分波动等多个维度。如果只盯着“3:0”这个结果很容易忽略过程信息。体育数据分析的核心就是把比赛过程中产生的离散事件变成结构化数据。对乒乓球来说每分球至少包含发球方、接发球方、得分方、得分方式、回合数等字段每一局又包含局分、是否打到 10:10、谁先拿到局点等信息。这些数据一旦被记录和加工就能回答很多主观感受无法回答的问题某位选手的得分是否集中在发球轮第二局 10:10 之后关键分处理能力谁更强所谓“横扫”到底是每局都大比分领先还是多局打到关键分才险胜本场比赛 3:0 的结果给球迷的印象是“实力差距明显”但如果我们把每局小分拿出来看或许会发现第二局是 12:10 这种接近比分这说明比赛并非完全没有悬念。数据分析的意义就在这里用客观数据修正主观印象同时为教练组后续训练提供参考。从工程角度看这类数据分析项目非常适合作为 Python 入门到进阶的练习。它不涉及复杂的分布式系统但涵盖了文件读取、数据清洗、统计计算、可视化、异常处理等常用技能而且对初学者来说比较有成就感跑完代码立刻能看到和比赛相关的图表。2. 环境准备与版本说明本文示例基于 Python 3.8 以上版本依赖库只需要 pandas、matplotlib 和 json。json 是 Python 内置模块不需要额外安装pandas 和 matplotlib 如果本地没有可以用 pip 安装。pip install pandas matplotlib如果使用的是 Anaconda 环境通常已经内置 pandas 和 matplotlib不需要重复安装。版本方面本文示例对版本不敏感pandas 1.3、matplotlib 3.5 均可运行。实际开发时请以你电脑上的环境为准如果安装失败可以先检查 pip 源是否可用或者用虚拟环境隔离依赖。推荐的项目目录结构如下pingpong-analysis/ ├── data/ │ └── pingpong_match.json ├── src/ │ ├── parser.py │ └── analyzer.py ├── output/ │ └── score_chart.png └── main.py为了便于测试本文使用本地 JSON 文件作为数据源。真实项目中你可以把这里的 JSON 换成赛事官方提供的公开数据文件也可以换成自己手动记录的比赛统计表代码核心逻辑不变。需要提醒的是体育数据通常涉及版权和平台使用条款本文只演示本地样例数据的分析方法不提供任何爬取绕过方案。如果你要采集真实比赛数据请先确认目标数据源的授权范围不要对未公开数据做自动化抓取。3. 乒乓球比分数据的核心字段在看代码之前我们先设计比赛数据的结构。乒乓球比赛数据可以从三个层级来描述比赛层、局层、分球层。比赛层数据包括赛事名称、轮次、比赛日期、选手信息、总比分、赛制类型。这一层解决的是“这场比赛是谁和谁打什么赛事最终结果如何”的问题。局层数据包括局号、该局双方得分、该局胜者。这一层是比分统计的基础。比如第二局 12:10表示这一局双方打到 10 平之后又继续争夺最终一方领先 2 分获胜。分球层数据是最细粒度的记录每个球一条记录至少包含当前局号、当前双方累计得分、发球方、得分方、得分方式。只有拿到分球层数据才能做发球得分率、关键分表现、相持球能力等深度分析。本文为了演示先用局层数据做主流程分析再扩展展示分球层数据能做什么。样例 JSON 文件data/pingpong_match.json内容如下{ tournament: WTT Champions Yokohama, round: Quarterfinal, date: 2025-04-04, best_of: 5, players: { home: { player_id: P001, name: Harimoto Tomokazu, country: Japan }, away: { player_id: P002, name: Xiang Peng, country: China } }, result: { home_games: 3, away_games: 0 }, games: [ { game_index: 1, home_score: 11, away_score: 8, winner: home }, { game_index: 2, home_score: 12, away_score: 10, winner: home }, { game_index: 3, home_score: 11, away_score: 7, winner: home } ], point_details: [ {game_index: 1, home_points: 0, away_points: 0, server: home, scorer: home, rally_count: 4}, {game_index: 1, home_points: 1, away_points: 0, server: home, scorer: home, rally_count: 7}, {game_index: 1, home_points: 1, away_points: 1, server: away, scorer: away, rally_count: 5} ] }说明一下上面 JSON 是教学模拟数据不是真实比赛逐分记录。第二局设置为 12:10是为了演示“关键分”分析场景。实际项目中请使用你自己获取到的真实数据数据结构可以保持一致。字段含义如下tournament赛事名称。round轮次。date比赛日期。best_of赛制5 表示五局三胜制。WTT 不同赛事的赛制可能有差异具体以赛事手册为准。players.home / players.away主客双方选手信息这里的 home 和 away 只是数据标记不代表主客场实际含义。result总局比分。games每一局的明细比分。point_details逐分记录本文示例只列了几条实际分析时可扩展到全场。4. 实战用 Python 读取并分析比分这一节从零开始搭建分析脚本。先写一个最小可运行版本再逐步完善功能和封装。4.1 读取 JSON 文件打开main.py先完成数据读取import json JSON_PATH data/pingpong_match.json with open(JSON_PATH, r, encodingutf-8) as f: data json.load(f) print(赛事:, data[tournament]) print(轮次:, data[round]) print(比赛日期:, data[date]) home data[players][home][name] away data[players][away][name] print(f对阵: {home} vs {away}) result data[result] print(f总比分: {result[home_games]} : {result[away_games]})运行后预期输出赛事: WTT Champions Yokohama 轮次: Quarterfinal 比赛日期: 2025-04-04 对阵: Harimoto Tomokazu vs Xiang Peng 总比分: 3 : 0这里要注意open函数必须指定encodingutf-8否则在 Windows 中文环境下读取包含中文的 JSON 可能报UnicodeDecodeError。如果 JSON 文件最外层不是对象而是数组json.load一样能解析后续取字段时按实际结构调整即可。4.2 用 pandas 整理局比分直接操作嵌套字典比较繁琐推荐用 pandas 把games列表转成 DataFrame。这样后续求均值、累计和、过滤都更方便。import pandas as pd games data[games] df pd.DataFrame(games) print(df)预期输出是一个包含四列的表格game_index home_score away_score winner 0 1 11 8 home 1 2 12 10 home 2 3 11 7 home接下来计算双方总得分、每局得分差、平均每局得分差等指标df[score_diff] df[home_score] - df[away_score] total_home_score df[home_score].sum() total_away_score df[away_score].sum() print(张本智和home总得分:, total_home_score) print(向鹏away总得分:, total_away_score) print(每局净胜分:) print(df[[game_index, score_diff]])计算逻辑很简单但这一步能反映很多信息虽然总比分是 3:0但总得分是 34:25说明向鹏单看局分差距并不像 3:0 那么大。尤其是第二局 12:10胜负就在两个关键分之间。如果后续还要统计发球得分率可以在point_details基础上继续加工point_df pd.DataFrame(data[point_details]) # 统计每位选手的得分次数 score_count point_df[scorer].value_counts() print(score_count) # 统计不同回合数下的得分情况 print(point_df.groupby(rally_count)[scorer].value_counts())这个扩展点很有价值。真实比赛中得分方式分为发球抢攻、接发球抢攻、相持得分、对手失误送分等类型如果数据源提供了这些字段你就可以很轻松地做出“得分来源地图”分析选手的得分结构。4.3 判断关键局与关键分关键分没有一个绝对统一的定义但实践中通常把单局打到 10:10 之后的分叫关键分因为此时每分都直接影响局点归属。我们可以写一个函数从局比分里挑出所有进入过 10:10 的局。def find_clutch_games(df, threshold10): result [] for _, row in df.iterrows(): home_score row[home_score] away_score row[away_score] if home_score threshold and away_score threshold: result.append( { game_index: row[game_index], final_score: f{home_score}:{away_score}, winner: row[winner], } ) return result clutch_games find_clutch_games(df) print(关键局统计:) for item in clutch_games: print(item)在样例数据中第二局 12:10 会被识别为关键局。这就实现了从“3:0 横扫”到“第二局存在关键分博弈”的量化判断。如果你的比赛数据是全场的逐分记录还可以进一步统计 10:10 之后双方的胜负分比率这里就不再展开。4.4 封装分析类初学者先把脚本写出来理解流程但在工程项目中建议把逻辑封装成类或模块。下面给出一个更结构化的写法目录中新建src/analyzer.pyimport json import pandas as pd class PingPongMatchAnalyzer: def __init__(self, json_path: str): self.json_path json_path self.data self._load_json() self.games_df pd.DataFrame(self.data[games]) def _load_json(self): with open(self.json_path, r, encodingutf-8) as f: return json.load(f) def get_result(self): result self.data[result] home self.data[players][home][name] away self.data[players][away][name] return f{home} {result[home_games]} : {result[away_games]} {away} def get_score_summary(self): self.games_df[score_diff] ( self.games_df[home_score] - self.games_df[away_score] ) return self.games_df def find_clutch_games(self, threshold10): result [] for _, row in self.games_df.iterrows(): if row[home_score] threshold and row[away_score] threshold: result.append( { game_index: row[game_index], final_score: f{row[home_score]}:{row[away_score]}, winner: row[winner], } ) return result在main.py中调用from src.analyzer import PingPongMatchAnalyzer analyzer PingPongMatchAnalyzer(data/pingpong_match.json) print(analyzer.get_result()) print(analyzer.get_score_summary()) print(关键局:) print(analyzer.find_clutch_games())封装的好处是后续接 API、接数据库只需要改_load_json一个方法上层调用逻辑基本不用变。这也符合单一职责原则每个类只负责比赛分析这一件事。5. 数据可视化把 3:0 变成图表数字只能给人冷冰冰的感觉图表能更直观地呈现比赛走势。这一节我们把比分数据绘制成三张图每局得分对比柱状图、每局净胜分柱状图、累计得分走势图。如果数据源包含逐分数据还可以把累计分曲线画得更细腻。5.1 每局得分对比图新建src/visualize.pyimport matplotlib.pyplot as plt import pandas as pd plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, Arial Unicode MS, sans-serif] plt.rcParams[axes.unicode_minus] False def plot_game_scores(df: pd.DataFrame, home_name: str, away_name: str, save_path: str): fig, ax plt.subplots(figsize(8, 5)) x df[game_index] width 0.35 ax.bar(x - width / 2, df[home_score], width, labelhome_name, alpha0.85) ax.bar(x width / 2, df[away_score], width, labelaway_name, alpha0.85) for xi, hs, aws in zip(x, df[home_score], df[away_score]): ax.text(xi - width / 2, hs 0.2, str(hs), hacenter, vabottom) ax.text(xi width / 2, aws 0.2, str(aws), hacenter, vabottom) ax.set_xticks(x) ax.set_xlabel(局数) ax.set_ylabel(单局得分) ax.set_title(每局得分对比) ax.legend() fig.tight_layout() plt.savefig(save_path, dpi150) plt.close(fig)调用import pandas as pd from src.visualize import plot_game_scores df pd.DataFrame(data[games]) plot_game_scores(df, Harimoto, Xiang, output/score_compare.png)这张图能直接看出每局的分数差距。第二局双方得分最接近视觉上柱子高度差也最小和关键局识别结果相互印证。5.2 累计得分走势图累计得分曲线适合观察比赛节奏。如果一方始终领先曲线会一直压在另一方上方如果有局次胶着曲线会反复交叉。由于我们只有局末比分可以用每局累计得分来模拟走势。def plot_cumulative_scores(df: pd.DataFrame, home_name: str, away_name: str, save_path: str): x df[game_index] cum_home df[home_score].cumsum() cum_away df[away_score].cumsum() fig, ax plt.subplots(figsize(8, 5)) ax.plot(x, cum_home, markero, labelhome_name) ax.plot(x, cum_away, markers, labelaway_name) ax.set_xticks(x) ax.set_xlabel(局数) ax.set_ylabel(累计得分) ax.set_title(累计得分走势) ax.legend() ax.grid(True, linestyle--, alpha0.5) fig.tight_layout() plt.savefig(save_path, dpi150) plt.close(fig)如果point_details包含每个球的实时比分你可以把上面的横坐标从“局数”替换成“总球数”用home_points和away_points记录逐分变化画出一条真正意义上的比赛进程曲线。这样的曲线在真实赛事分析系统里更常见。5.3 中文乱码问题matplotlib 默认字体不一定支持中文最容易出现的问题是图上的中文变成方框。上面的代码通过plt.rcParams[font.sans-serif]指定了优先字体方案Windows 下常见的是 SimHei 或 Microsoft YaHei。macOS 下可以用 Arial Unicode MS 或 PingFang SC。Linux 服务器上如果没有中文字体需要先安装字体或者在代码中指定字体文件路径。更稳妥的做法是使用系统的字体管理器动态查找中文字体但作为教程先按上面的静态配置即可。如果你只需要快速出图也可以把图内文字全部换成英文避免字体问题代价是阅读体验稍微弱一些。5.4 把图表组织成报告当图表数量变多时最好把绘制逻辑统一封装并集中到一个“生成报告”的入口函数中。例如在main.py中顺序调用多个绘图函数最后输出一个汇总文本和若干图片。以后接入定时任务就能每天自动输出图文报告这也是体育数据分析项目落地的常见形态。下面是可视化部分的最终调用示例def generate_report(): analyzer PingPongMatchAnalyzer(data/pingpong_match.json) df analyzer.get_score_summary() plot_game_scores(df, Harimoto, Xiang, output/score_compare.png) plot_cumulative_scores(df, Harimoto, Xiang, output/cumulative_score.png) print(图片已输出到 output 目录)6. 从样例到真实数据数据采集思路与合规边界前面几节使用的是本地样例数据但在实际工作中你很可能需要接入真实的赛事数据。这里提供一个合规且清晰的数据获取思路重点不是教大家绕过限制而是告诉大家如何以正确方式拿到可用的数据。真实比赛数据通常来自几个渠道赛事组织方公开的数据接口或官方数据下载页面。如果官方开放了竞赛数据优先使用这一渠道。新闻媒体和体育网站赛后发布的统计表。这类数据往往已经过加工适合做汇总分析但可能缺少逐分数据。团队内部采集系统。职业运动队通常有专门的视频标注工具和比赛统计软件数据质量最高但一般不对外公开。个人手工记录。对业余比赛或训练赛可以用 Excel 或在线表格手工记录整理成与样例 JSON 一样的数据结构。如果选择通过编程方式获取公开网页数据要特别注意几点遵守网站的robots.txt协议尊重网站的访问策略。控制请求频率不要并发请求压垮对方服务器。只采集已公开、允许下载的数据不碰付费内容、登录后可见内容或非公开接口。数据用途要明确不得用于商业牟利或二次传播原始数据。在代码中做好请求异常处理网络波动时要有重试和退避机制。下面给出一个简单的请求函数示例它只负责获取公开数据文本不诱导用户访问任何未经授权的资源import requests def fetch_public_data(url: str, timeout: int 10): try: resp requests.get(url, timeouttimeout) resp.raise_for_status() return resp.text except requests.RequestException as e: print(f请求失败: {e}) return None实际项目中拿到响应后还要结合具体页面结构做解析。解析 HTML 时推荐用 BeautifulSoup 或 lxml解析 JSON 接口则直接使用json.loads。需要强调的是不要把“能访问”等同于“有权采集”。很多网站虽然技术上没有做复杂反爬但服务条款明确禁止未经许可的自动化访问。如果你是在公司内部做体育数据分析建议先让法务或业务负责人确认数据来源合规性个人学习项目也尽量优先使用官方公开数据或自行生成模拟数据。7. 常见问题与排查清单在实际运行这段代码时新手可能会遇到下面这些问题。我把常见报错整理成表格方便直接对照排查。问题现象常见原因解决思路FileNotFoundError程序启动目录不在项目根目录或路径写错用绝对路径定位文件或在项目根目录下运行命令UnicodeDecodeError读取 JSON 时没有指定 UTF-8 编码open(JSON_PATH, r, encodingutf-8)JSONDecodeErrorJSON 文件内容格式错误有注释或多余逗号用 JSON 校验工具检查文件内容matplotlib 中文乱码系统缺少中文字体或未设置字体配置font.sans-serif或安装中文字体KeyError: home_scoreJSON 字段名与实际不一致打印data.keys()检查层级结构pandas 未安装当前 Python 环境缺少依赖库执行pip install pandas绘图没有弹出窗口脚本中未调用plt.show()或输出到文件后关闭图形需要交互查看时增加plt.show()排查逻辑其实有通用套路先看报错栈最后几行定位是文件层、数据层还是计算层的问题再用小步打印的方式确认每一步数据长什么样不要一次性写完整个脚本再调试建议边写边验证。项目中尤其容易踩坑的是 JSON 结构不规范。真实数据往往嵌套比较深字段命名也可能不统一建议在解析前写一个小的inspect_data函数打印每层 key帮助你快速了解数据结构。def inspect_data(obj, depth0): if isinstance(obj, dict): print( * depth keys:, list(obj.keys())[:10]) for v in obj.values(): inspect_data(v, depth 1) elif isinstance(obj, list): if obj: inspect_data(obj[0], depth 1)把它放到项目里遇到新数据源时先运行一遍能省不少时间。8. 最佳实践与工程建议到这里一个完整的乒乓球比赛数据分析流程已经跑通了。但如果你想把它真正应用到项目里还需要关注下面几个工程化问题。8.1 用 dataclass 管理比赛模型JSON 字典虽然灵活但类型不明确容易出现手误。更工程化的做法是定义数据模型把解析后的数据转成类型安全的对象。示例from dataclasses import dataclass dataclass class Player: player_id: str name: str country: str dataclass class Game: game_index: int home_score: int away_score: int winner: str dataclass class Match: tournament: str round: str date: str best_of: int home: Player away: Player games: list[Game]这样写的好处是 IDE 补全友好字段拼写错误能在运行前就被发现。缺点是解析代码多一些适合项目规模变大后引入。8.2 数据源版本化比赛数据会持续更新建议把每次比赛的数据文件按日期和赛事命名例如data/2025-04-04_wtt_champions_yokohama.json。如果以后分析结论出错需要回溯当时的数据版本化管理会非常有用。8.3 日志与异常处理分析脚本如果只打印print出现问题后很难定位。建议引入logging模块区分信息、警告和错误级别。特别是在批量处理多场比赛时日志能帮助我们快速判断哪场比赛解析失败、为什么失败。import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) logger.info(开始解析比赛文件) try: analyzer PingPongMatchAnalyzer(data/pingpong_match.json) except Exception: logger.exception(比赛文件解析失败)8.4 指标口径要统一不同数据源对“得分率”的定义可能不同。有人用“得分 / 总回合数”有人用“发球得分 / 发球回合数”。在做跨赛事、跨选手对比时一定要先确认指标口径否则结论没有可比性。建议在代码中把指标计算函数集中放在一个模块里并写清楚注释。8.5 性能考虑单场比赛的数据量很小完全不需要考虑性能优化。但如果你要分析一个赛季几百场比赛、几千名选手的数据建议用 pandas 的向量化操作代替for循环避免逐行迭代如果数据量达到千万级别再考虑使用 Polars 或数据库存储。8.6 安全与权限如果分析系统接入内部数据库或云端存储务必要遵循最小权限原则。只给服务账号分配读取所需数据的权限不要把数据库账号密码硬编码在代码里推荐使用环境变量或配置中心管理。任何涉及生产数据的变更都要先在测试环境验证并做好备份。9. 小结与下一步文章用一场“3:0 横扫”的乒乓球比赛作为案例完整演示了从 JSON 数据读取、pandas 比分统计、关键局识别到 matplotlib 可视化输出的全流程。虽然样例数据来自本地模拟但核心分析方法可以直接迁移到真实赛事数据上。如果你之前没有接触过体育数据分析建议按下面顺序继续深入先扩充point_details数据尝试计算发球得分率和接发球得分率。再把脚本改成支持多场比赛批量分析输出对比表格。接着尝试接入公开数据源做成定时更新的数据看板。最后可以考虑把分析结果自动生成 Markdown 报告方便教练组或团队查阅。比分数据只是体育数据的一部分选手的跑动距离、发球落点、接发球姿势、比赛节奏等过程数据在真实的高水平运动队中同样重要。从一场乒乓球比赛开始入门你会慢慢发现数据分析在体育领域有非常广阔的应用空间。先把今天这版代码运行起来看到第一张比分图表你就已经迈出第一步了。
返回列表