2026/9/2 12:48:07

用Python量化乒乓球赛事:从张本智和连冠看体育数据分析

用Python量化乒乓球赛事:从张本智和连冠看体育数据分析 2026年WTT冠军赛横滨站刚刚落下帷幕张本智和不仅连续两届在这项赛事中夺得男单冠军成为外协球员中第一位做到这一点的选手还在同一个比赛日见证了妹妹张本美和拿下女单冠军。兄妹俩同场登顶创造了日本乒乓球历史上的新纪录。作为一个常年和数据打交道的技术人这类消息传到我这里时第一反应不是去争论某个关键球是否擦边而是立刻想到另一个问题这些“历史性突破”到底意味着什么能不能用数据量化如果让我从零搭建一套乒乓球赛事数据分析流程应该怎么设计这篇文章就围绕这件事展开。我不会去复述比赛过程而是从工程和数据角度完整演示如何用 Python 对乒乓球赛事进行数据建模、指标计算和可视化分析。哪怕你不是乒乓球爱好者这套流程也能迁移到羽毛球、网球甚至电子竞技选手分析中。1. 从横滨连冠看体育数据分析正在改变什么1.1 什么是赛事数据分析赛事数据分析英文叫 Sports Analytics是指通过采集运动员在训练和比赛中的结构化数据用统计方法、数据可视化乃至机器学习模型帮助教练团队做战术决策、帮助媒体做深度报道、帮助球迷更客观地理解比赛。过去我们看乒乓球比赛主要靠眼睛和经验。某个选手发球好某个选手相持能力强某个选手关键时刻容易手软这些判断往往来自解说员的主观评价。但同样一场比赛不同的解说员可能会给出截然不同的说法。数据化之后这些问题可以被拆解成具体的指标发球直接得分率、接发球失误率、相持段得分率、关键分把握率等。赛事数据分析并不是要取代人的判断而是给判断一个更可靠的依据。它可以把“感觉”“印象”转换成“数字”“趋势”“概率”这对训练指导和赛前备战很有价值。1.2 乒乓球项目的数据分析能解决什么问题乒乓球比赛数据可以分成几个层次。第一层是基础比分数据包括总局数、小分、每局比分、胜负关系。这类数据用来描述比赛结果。第二层是技术统计包括发球得分、接发球得分、台内挑打成功率、相持段正手得分率等。这类数据主要回答“为什么赢”或“为什么输”。第三层是轨迹数据需要借助高速摄像机或传感器采集比如球的旋转速度、落点分布、移动距离。这类数据门槛较高通常只有专业队或科研机构才能拿到。我在本文中演示的是第一层和第二层数据的组合分析。对于一个普通开发者或数据分析初学者来说这类数据最容易获取也最能讲清楚分析思路。1.3 外协第一人背后的可量化指标回到张本智和这次横滨夺冠。报道中说他是“外协第一人”这里的“外协”指的是除中国乒协之外的外国协会。在国际乒联的赛事体系中中国队长期占据统治地位因此外协选手能在一站WTT冠军赛中连续两届夺冠确实需要很高的稳定性和临场发挥。从数据角度看“连冠”这个成就有两层含义。第一层是绝对实力即他能击败不同风格、不同排名的对手第二层是稳定性即连续两届赛事都能走到最后。前者可以用冠军数、胜率来衡量后者可以用连续夺冠的概率模型来分析。后面我会用模拟数据演示如果一位选手的真实夺冠概率是 15%那么他连续两站夺冠的概率是多少如果概率提升到 25%连续夺冠的概率又是什么水平这种量化思路可以帮助我们更理性地看待“突破”二字。2. 环境准备与项目结构2.1 版本选择本文使用的技术栈如下Python 3.8 及以上版本pandas 1.5 及以上版本numpy 1.23 及以上版本matplotlib 3.6 及以上版本JupyterLab 或 VSCode 作为开发环境如果你使用的是 Anaconda 发行版通常已经内置了 pandas、numpy 和 matplotlib。建议在开始前先确认本机环境python --version pip list | findstr pandas pip list | findstr matplotlib如果你的环境中没有 pandas 或 matplotlib安装命令如下pip install pandas numpy matplotlib如果你希望用 Jupyter Notebook 逐段运行代码还可以安装 JupyterLabpip install jupyterlab jupyter lab2.2 项目目录设计为了便于阅读和复用我建议用下面的目录结构组织这个分析项目pingpong_analytics/ ├── data/ │ ├── matches.csv │ └── players.csv ├── notebooks/ │ └── analysis.ipynb ├── scripts/ │ ├── generate_data.py │ ├── analysis.py │ └── visualization.py └── output/ └── charts/data目录存放原始数据scripts目录存放分析脚本output/charts目录存放生成的可视化图表。如果你只是在本地试跑也可以直接把所有代码写在一个 Python 文件或 Jupyter Notebook 中不影响结果。3. 构建乒乓球赛事数据模型3.1 字段设计思路在开始写代码之前首先要设计数据表的结构。乒乓球比赛的最小记录单位是“一场比赛”但为了让分析更有深度我建议把每场比赛拆分成更细的记录。我设计的matches.csv包含以下字段字段名类型说明match_idstring比赛唯一标识player_idstring选手IDplayer_namestring选手姓名tournamentstring赛事名称seasonint赛季roundstring轮次如 R32、R16、QF、SF、Fopponentstring对手姓名opponent_countrystring对手协会resultstring胜或负scorestring大比分如 4:2total_points_wonint本场比赛总得分total_points_lostint本场比赛总失分serve_wonint发球得分serve_lostint发球失分receive_wonint接发球得分receive_lostint接发球失分forehand_winnersint正手制胜分backhand_winnersint反手制胜分unforced_errorsint非受迫性失误这些字段覆盖了结果数据和技术统计足够完成后续的指标计算和分析。3.2 生成示例数据由于 2026 横滨冠军赛的完整技术统计尚未公开下面用一个生成脚本构造一份“模拟数据”。这里的重点不是数据本身而是数据结构和分析流程。在scripts/generate_data.py中写入import csv import random random.seed(42) players [ {id: P001, name: 张本智和, country: 日本}, {id: P002, name: 张本美和, country: 日本}, {id: P003, name: 林诗栋, country: 中国}, {id: P004, name: 王楚钦, country: 中国}, {id: P005, name: 林昀儒, country: 中国台北}, {id: P006, name: 费利克斯·勒布伦, country: 法国}, {id: P007, name: 莫雷加德, country: 瑞典}, {id: P008, name: 奥恰洛夫, country: 德国}, {id: P009, name: 早田希娜, country: 日本}, {id: P010, name: 申裕斌, country: 韩国}, ] rounds [R32, R16, QF, SF, F] rows [] mid 1 for player in players: for season in [2024, 2025, 2026]: for rnd in rounds: win_prob 0.7 if player[country] 中国 else 0.55 result W if random.random() win_prob else L total_won random.randint(35, 55) total_lost random.randint(25, 50) serve_won random.randint(15, 28) serve_lost random.randint(8, 18) receive_won random.randint(10, 22) receive_lost random.randint(10, 20) forehand_winners random.randint(3, 12) backhand_winners random.randint(2, 10) unforced_errors random.randint(6, 18) row { match_id: fM{mid:04d}, player_id: player[id], player_name: player[name], tournament: WTT Champions Yokohama, season: season, round: rnd, opponent: f对手{mid}, opponent_country: random.choice([中国, 日本, 韩国, 德国, 法国, 瑞典]), result: result, score: f4:{random.randint(0, 3)} if result W else f{random.randint(0, 3)}:4, total_points_won: total_won, total_points_lost: total_lost, serve_won: serve_won, serve_lost: serve_lost, receive_won: receive_won, receive_lost: receive_lost, forehand_winners: forehand_winners, backhand_winners: backhand_winners, unforced_errors: unforced_errors, } rows.append(row) mid 1 with open(data/matches.csv, w, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnamesrows[0].keys()) writer.writeheader() writer.writerows(rows) print(模拟数据已生成共, len(rows), 条记录)运行这个脚本python scripts/generate_data.py3.3 数据说明与免责声明需要强调上面生成的是模拟数据不是 2026 横滨冠军赛的真实统计。真实比赛数据需要从赛事官网、国际乒联官网或专业数据平台获取。之所以使用模拟数据是为了让分析流程完整跑通避免因为数据获取门槛影响读者理解。如果你手头有真实数据只需要把 CSV 文件替换为实际字段结构即可。分析代码的核心逻辑不会受影响。4. 核心指标计算与选手对比4.1 数据读取与概览首先读取数据查看数据规模、字段类型和缺失值情况。import pandas as pd df pd.read_csv(data/matches.csv) print(df.shape) print(df.dtypes) print(df.isnull().sum())输出结果应该显示 270 条记录各字段无缺失值。4.2 发球得分率与接发球得分率乒乓球比赛中有两个非常基础的技术指标发球得分率和接发球得分率。发球得分率 发球得分 / 发球总球数接发球得分率 接发球得分 / 接发球总球数。在数据表中发球总球数等于serve_won serve_lost接发球总球数等于receive_won receive_lost。df[serve_total] df[serve_won] df[serve_lost] df[receive_total] df[receive_won] df[receive_lost] df[serve_win_rate] df[serve_won] / df[serve_total] df[receive_win_rate] df[receive_won] / df[receive_total] print(df[[player_name, round, serve_win_rate, receive_win_rate]].head(10))这里要特别注意如果某场比赛的serve_total为 0除零会产生 NaN。在实际项目中应该先做数据清洗过滤掉这类异常记录。4.3 关键分把握能力乒乓球比赛中关键分通常指局末 10 平之后或决胜局 8 平之后的分数。这类分数虽然数量少但直接决定一局或一场比赛的归属。在缺少逐球数据的情况下可以用“胜负与总得分关系”来近似衡量选手把握关键分的能力。思路是如果一位选手全场总得分更高却输掉比赛说明他的得分分布不够集中可能在关键分上处理不够好。df[points_diff] df[total_points_won] - df[total_points_lost] df[unexpected_loss] (df[points_diff] 0) (df[result] L) unexpected_loss_count ( df.groupby(player_name) .agg( total_matches(result, count), unexpected_loss(unexpected_loss, sum) ) .reset_index() ) print(unexpected_loss_count)在模拟数据中每个选手都会出现少量“总分领先却输掉比赛”的情况。如果真实数据也出现类似现象通常说明选手的关键分执行力还有提升空间。4.4 外协球员冠军数对比“外协第一人”这个概念在数据上可以转化为外协选手在指定赛事中连续夺冠的次数。我们可以在模拟数据中计算每个选手在每站赛事中的夺冠次数。champion_df ( df.groupby([player_name, season]) .apply( lambda x: Y if (x[round] F) (x[result] W).any() else N ) .reset_index(nameis_champion) ) champion_count ( champion_df[champion_df[is_champion] Y] .groupby(player_name) .size() .reset_index(namechampion_times) .sort_values(champion_times, ascendingFalse) ) print(champion_count)这段代码展示了如何从比赛记录中筛选出冠军次数。真实项目中还需要结合赛季字段判断是否为“连冠”而不是简单累加。5. 数据可视化让成绩趋势一目了然5.1 外协球员冠军数 TOP10 柱状图数据可视化是数据分析中最直观的呈现方式。下面用 matplotlib 绘制外协球员冠军数 TOP10 柱状图。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False top10 champion_count.head(10) plt.figure(figsize(10, 6)) plt.bar(top10[player_name], top10[champion_times], color#4C72B0) plt.title(外协球员WTT冠军赛冠军数TOP10模拟数据) plt.xlabel(球员) plt.ylabel(冠军次数) plt.xticks(rotation45) plt.tight_layout() plt.savefig(output/charts/champion_top10.png, dpi150) plt.show()需要注意的是plt.rcParams[font.sans-serif] [SimHei]这一行是在 Windows 系统下使用中文字体的常见配置。如果你的系统没有 SimHei 字体可以改成系统自带的字体名称否则图表中的中文会显示为方框。5.2 张本智和近几届冠军赛成绩走势折线图适合展示时间序列。下面绘制张本智和从 2024 到 2026 年在横滨冠军赛中的平均发球得分率走势。zhang_df df[df[player_name] 张本智和] season_serve_rate ( zhang_df.groupby(season)[serve_win_rate] .mean() .reset_index() ) plt.figure(figsize(8, 5)) plt.plot(season_serve_rate[season], season_serve_rate[serve_win_rate], markero, linewidth2) plt.title(张本智和横滨冠军赛平均发球得分率走势模拟数据) plt.xlabel(赛季) plt.ylabel(平均发球得分率) plt.ylim(0, 1) plt.grid(True, linestyle--, alpha0.6) plt.tight_layout() plt.savefig(output/charts/zhang_serve_trend.png, dpi150) plt.show()从这个图可以直观看到发球得分率的变化趋势。如果真实数据中该指标逐年上升说明选手的训练方向和技改策略可能是有效的。5.3 兄妹同届夺冠时间线兄妹俩同时夺冠是这次比赛的重要看点之一。如果用时间轴展示两个人的晋级路径可以很清楚地看到他们在各自半区的推进过程。在缺少每个比赛日具体时间的情况下可以用轮次作为横坐标用散点图展示两人在各轮次是否获胜。siblings df[df[player_name].isin([张本智和, 张本美和]) (df[season] 2026)] pivot siblings.pivot_table(indexround, columnsplayer_name, valuesresult, aggfunclast) round_order [R32, R16, QF, SF, F] pivot pivot.reindex(round_order) fig, ax plt.subplots(figsize(8, 5)) for player in pivot.columns: wins pivot[pivot[player] W].index ax.scatter(wins, [player] * len(wins), s200, labelplayer) ax.set_xlabel(轮次) ax.set_title(张本兄妹2026横滨冠军赛晋级轨迹模拟数据) ax.legend() plt.tight_layout() plt.savefig(output/charts/siblings_timeline.png, dpi150) plt.show()这个图虽然简单但能够表达“兄妹俩齐头并进、最终同场登顶”的过程。5.4 中文乱码问题很多刚接触 matplotlib 的读者都会遇到中文显示乱码的问题。这里强调一遍matplotlib 默认字体不支持中文必须手动指定中文字体。三种常见解决方式在代码中指定字体如plt.rcParams[font.sans-serif] [SimHei]。使用系统支持的其他中文字体例如Microsoft YaHei、Noto Sans CJK SC。在代码中提前注册字体文件适合离线环境。如果你是 Mac 或 Linux 用户可以把字段改为plt.rcParams[font.sans-serif] [WenQuanYi Zen Hei]不同环境下字体名称不同建议先用代码查看本机字体列表再选择可用字体。6. 进阶分析连冠的难度有多大6.1 用二项分布计算夺冠概率标题中提到了“连冠”。从概率角度看连续两届夺冠的概率可以建模为如果选手每站夺冠概率为 p在假设各站比赛独立的前提下连续两站夺冠概率为 p²。这里“独立”是一个强假设。实际上选手的体能、状态、签运都会影响结果但作为简化分析这个假设可以接受。我们可以模拟一个基础场景import numpy as np p 0.15 # 假设选手每站夺冠概率为15% n_sim 100000 sim_results np.random.rand(n_sim, 2) p prob_two sim_results.all(axis1).mean() print(f当每站夺冠概率为 {p:.0%} 时连续两站夺冠的模拟概率约为 {prob_two:.4%})6.2 连冠概率模拟为了更直观地理解“连冠有多难”可以绘制不同单站夺冠概率对应连冠概率的曲线。p_list np.linspace(0.05, 0.5, 100) prob_list p_list ** 2 plt.figure(figsize(8, 5)) plt.plot(p_list, prob_list, color#C44E52, linewidth2) plt.xlabel(单站夺冠概率 p) plt.ylabel(连续两站夺冠概率 p²) plt.title(连冠难度与夺冠概率的关系) plt.grid(True, linestyle--, alpha0.6) plt.tight_layout() plt.savefig(output/charts/back_to_back_prob.png, dpi150) plt.show()从图中可以看出如果单站夺冠概率是 20%连冠概率只有 4%即使单站夺冠概率提升到 40%连冠概率也只有 16%。这说明“连冠”确实是一个小概率事件。6.3 如何解读结果有了概率模型后我们可以更理性地评价张本智和的横滨连冠。首先连冠确实强因为即使你是顶尖选手单站夺冠概率也不会太高。其次概率模型只能说明“难度”不能直接说明“含金量”。含金量还要看签表质量、击败对手的排名、决赛是否打满等细节。在真实项目中如果你要做一个类似的“成绩突破分析”建议同时查看每次夺冠时的世界排名晋级路上击败的最高排名对手半决赛和决赛的局分同赛事其他外协选手的成绩这些维度组合在一起才能讲清楚“突破”的分量。7. 常见问题与排查思路7.1 常见报错清单在跑上面的代码时下面的报错频率最高问题现象常见原因解决思路UnicodeDecodeErrorCSV 文件编码不是 UTF-8读取时指定encodingutf-8或改为gbk图表中文显示为方框matplotlib 缺少中文字体配置设置plt.rcParams[font.sans-serif]KeyError: serve_total字段名拼写不一致用df.columns打印实际字段名核对分组统计结果为空按字符串匹配时数据格式不一致检查是否有空格或全半角字符差异除零警告总球数为 0添加if serve_total 0条件或使用np.whereread_csv 找不到文件路径错误或目录不对使用绝对路径或确认当前工作目录7.2 排查顺序建议遇到问题不要急着改代码按照下面顺序排查效率更高。第一步确认数据能正确读取。打印数据形状、字段列表和前几行。print(df.shape) print(df.columns.tolist()) print(df.head())第二步确认字段类型。很多异常来自字段被读成字符串而不是数值。print(df.dtypes)第三步确认关键字段的取值分布。print(df[result].value_counts()) print(df[round].unique())第四步单跑出错的语句把中间结果用print输出来检查。这种逐层定位的方法比反复重跑整个脚本要快得多。8. 赛事数据分析工程化建议8.1 数据采集真实场景中赛事数据通常来自官方数据接口或赛事统计网站。手动复制粘贴效率低且容易出错建议优先寻找 API 接口。如果没有 API可以把网页表格复制到本地再用 pandas 读取tables pd.read_html(matches.html) df tables[0]需要说明的是爬取公开数据时要注意网站的使用条款只抓取允许公开访问的非敏感数据并标注数据来源。8.2 数据存储与清洗当数据量变大后CSV 文件就不够用了。建议把结构化比赛数据存入数据库例如 SQLite 或 MySQL。清洗环节要关注几个点同一选手的不同名字写法例如“张本智和”和“Tomokazu Harimoto”需要建立映射表。轮次字段尽量统一为 R32、R16、QF、SF、F不要混用中文和英文。比分字段建议拆成局分和总比分两个字段方便后续计算。8.3 分析口径做体育数据分析最容易犯的错误是分析口径不一致。比如计算胜率时有人按场次算有人按局数算还有人按小分算。一套规范的分析流程应该在项目开始前定义好指标口径例如胜率 获胜场数 / 总场数发球得分率 发球得分 / 发球总球数关键分 局末 10 平后的得分率定义好口径后所有代码只围绕这套口径展开避免不同模块之间出现指标打架。8.4 可视化与报告在输出给他人查看时建议把图表和分析结论放在同一个报告中。可以使用 Jupyter Notebook 导出 HTML也可以把 matplotlib 生成的图片插入到 Markdown 文档中。需要注意的是不要过度美化图表。体育数据分析的图最重要的是准确传达信息。坐标轴范围、单位、数据来源都要清晰标注防止读者产生误解。9. 写在最后从张本智和与张本美和横滨同日夺冠这件事出发我整理了一套完整的乒乓球赛事数据分析流程。我们讨论了什么是赛事数据分析如何设计数据结构如何计算发球得分率、接发球得分率如何用柱状图、折线图、散点图展示趋势以及如何用二项分布量化“连冠”的难度。如果你看完这篇文章后也想对感兴趣的赛事做一次类似分析我的建议是从一场完整比赛的技术统计开始不要一开始就想着做大数据平台。把数据字段设计好用 pandas 跑通清洗和统计再用 matplotlib 输出第一张看板你就能建立起对体育数据分析的基本感知。体育赛事最有魅力的地方是它永远充满不确定性。而数据分析能做的是在不确定性到来之前尽量把每一份努力和每一个趋势看清楚。下次再看到某个选手刷新历史时不妨打开电脑把这套流程跑一遍。