ARTICLE DETAIL

资讯详情

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

用Python分析乒乓球七局逆转:从逐分数据到可视化复盘

用Python分析乒乓球七局逆转:从逐分数据到可视化复盘 最近不少球友在讨论横滨冠军赛上松岛辉空和张禹珍那场七局大战标题里“毁天灭地”这四个字虽然夸张但确实把比赛最后时刻的戏剧性表达出来了。作为一名经常和数据打交道的开发者我在看这类比赛时总忍不住想七局逆转到底是怎么发生的是发球轮次的变化还是关键分的把握又或者是某一局的连续追分改变了节奏如果只靠记忆和情绪复盘往往只能停留在“他后来打得好”“关键分没顶住”这种模糊层面。更专业的做法是把每一分、每一局、每一次发球都结构化然后用 Python 做统计、分析和可视化让“逆转”从形容词变成可以量化的数据曲线。这篇文章就以松岛辉空与张禹珍的比赛为背景案例从零构建一份乒乓球比赛的逐分数据再完成全场分析和可视化复盘。文章会尽量把代码写完整适合有 Python 基础、想学习体育数据处理的读者。1. 从“七局逆转”说起为什么比赛需要数据视角乒乓球比赛的节奏很快一局十一分转瞬即逝。现场看球时观众很难记住每一分的具体过程更多是记住比分牌上的数字变化。七局大战之所以精彩是因为它把比赛的不确定性拉满了先落后、再追赶、最后逆转每一步都可能改变最终结果。那么“逆转”在数据上到底长什么样如果有一份逐分数据我们就能回答很多具体问题松岛辉空是从第几局开始追上比分的他在哪一局的发球得分率最高张禹珍在前半段是靠什么建立优势的双方在 10 平之后的关键分表现如何这些问题的答案比“心态崩了”“技术压制”这种说法更有说服力。本文的技术路线大致如下设计乒乓球比赛的逐分数据结构。用 Python 根据指定局比分构造一场 7 局比赛的完整逐分记录。基于逐分记录做统计分析和可视化。从数据角度还原“落后—追近—反超—晋级”的过程。读完这篇文章你会掌握一套可复用的体育比赛数据处理思路可以把它迁移到羽毛球、网球、电竞等场景。即使你不太了解乒乓球规则跟着代码和注释也能看明白。2. 环境准备与数据约定2.1 运行环境本文示例使用 Python 3.8核心依赖是 pandas 和 matplotlib。如果你使用 Jupyter Notebook建议把可视化代码写在同一个 Notebook 里方便中断后继续调试。相关版本可以根据你的实际环境调整下面是我的建议安装命令pip install pandas matplotlib如果后续想使用更美观的图表样式也可以安装 seabornpip install seaborn2.2 数据约定为了避免歧义先约定两个标识A 方松岛辉空B 方张禹珍整场比赛使用“逐分长表”保存每一行代表一分。字段定义如下字段名含义game局号第 1 局到第 7 局point当前局内的分序号server发球方A 或 Breceiver接发方A 或 Bwinner本分得分方A 或 Bscore_a本分结束后 A 方局内得分score_b本分结束后 B 方局内得分set_a本分结束后 A 方大比分set_b本分结束后 B 方大比分这里使用长表而不是把每一局变成一行宽表原因是后续分析需要按发球方、得分方、局号做聚合长表配合groupby会更自然画趋势图时也更容易按逐分顺序连接。3. 乒乓球计分规则与逐分数据建模3.1 七局四胜与 11 分制乒乓球正式比赛通常采用七局四胜制也就是谁先赢下 4 局谁获得胜利。单局采用 11 分制一方先得到 11 分且领先至少 2 分时该局结束。这里有一个常见误区10 比 10 并不代表再得一分就赢。10 平之后必须连续领先 2 分才能拿下本局所以会出现 12:10、13:11、14:12 这样的比分。这一点在代码里必须严格遵守否则生成的数据就是不合法的。发球规则上比赛中每位选手连续发 2 分之后交换发球权。也就是说第 1、2 分由 A 发球第 3、4 分由 B 发球依次轮换。每局结束后下一局的首先发球方会交换本文为了简化按“每局 first_server 交替”的方式处理。3.2 逐分路径的合法性如果只记录最终局分比如 11:6其实丢失了过程信息。为了让分析有意义我们需要一条“逐分路径”也就是从 0:0 到 11:6 的过程中每一分是谁得到的。构建合法路径时必须满足任意时刻当前比分不能提前触发局结束条件。局结束的那一分必须是制胜分。10 平后的延长阶段双方比分差不能超过 2直到最后。4. 用 Python 构建比赛数据生成器4.1 根据局分生成合法路径下面这个函数接收一局的最终比分返回一个合法的逐分字符串序列。序列中的每个元素是A或B表示本分的得分方。def build_game_path(final_a, final_b): 根据一局最终比分构造一条合法逐分路径。 参数 final_a: A 方松岛辉空本局最终得分 final_b: B 方张禹珍本局最终得分 返回 list例如 [B, A, B, A, ...] winner A if final_a final_b else B loser B if winner A else A loser_score min(final_a, final_b) # 常规局败方得分小于 10 # 双方先交替得分到 X:X然后胜者再连续得分拉开差距 if loser_score 10: seq [] for _ in range(loser_score): seq.append(loser) seq.append(winner) remain abs(final_a - final_b) seq [winner] * remain return seq # 延长局双方至少打到 10:10 # 先交替到 10:10 seq [] for _ in range(10): seq.append(loser) seq.append(winner) # 10 平之后双方继续交替追分 extra loser_score - 10 for _ in range(extra): seq.append(winner) seq.append(loser) # 最后胜者连续拿 2 分完成领先 2 分制胜 seq.append(winner) seq.append(winner) return seq这里解释一下逻辑。以 11:6 为例败方得分是 6小于 10属于常规局。代码先让双方交替得分到 6:6然后胜者连续得 5 分最终变成 11:6。过程中胜者到 10 分时因为还没有到 11 分所以局不会提前结束等胜者到 11 分时领先优势已经超过 2 分判定结束完全合法。再看 12:10 这种延长局。双方先交替得分到 10:10此时没人到 11 分不会结束。然后根据败方得分减 10 得到额外轮次。10:10 后如果败方最终是 10 分说明胜者只是连拿 2 分比分变成 12:10。如果最终比分是 13:11则双方先到 11:11胜者再连拿 2 分。4.2 把路径转换成逐分记录有了逐分路径后还需要补充发球方、接发方和比分变化才能形成完整的数据行。def game_to_records(game_no, seq, first_serverA): 把一局的逐分路径转换为明细记录列表。 参数 game_no: 局号 seq: build_game_path 生成的路径 first_server: 本局第一个发球方 rows [] score_a, score_b 0, 0 server first_server for i, point_winner in enumerate(seq, 1): if point_winner A: score_a 1 else: score_b 1 rows.append({ game: game_no, point: i, server: server, receiver: B if server A else A, winner: point_winner, score_a: score_a, score_b: score_b, }) # 每 2 分交换发球权 if i % 2 0: server B if server A else A return rows发球轮换这段代码比较关键。i % 2 0表示第 2、4、6……分结束后交换发球方这样保证每位选手连续发 2 分。4.3 构建整场比赛现在把七局比赛组合起来。为了让故事线更贴近“逆转”我设计了一组演示局分demo_scores [ (9, 11), # 第 1 局松岛辉空 9:11 张禹珍 (7, 11), # 第 2 局松岛辉空 7:11 张禹珍 (11, 8), # 第 3 局松岛辉空 11:8 张禹珍 (11, 5),
返回列表