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

资讯详情

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

用pandas解析BO3赛果:从嵌套JSON到自动战报标题生成

用pandas解析BO3赛果:从嵌套JSON到自动战报标题生成 赛后运营群弹出一行赛果BFX 2:1 拿下 KRX可惜了今天看不到跳舞了。这句话作为社交平台战报很自然但如果赛事平台要自动完成“赛果统计—数据对比—战报标题生成”这一整套流程程序员面对的问题会更具体赛制怎么理解数据字段怎么设计JSON 怎么压平成表格统计口径怎么定标题模板怎么生成。以 BFX 与 KRX 两支示例队伍、BO3 赛制、最终比分 2:1 为例下面会从一条样例赛果开始走完数据加载、统计、可视化、标题生成的完整流程。整个过程不依赖真实赛事系统只需要本地 Python 环境就可以跑通。适合正在做电竞数据运营、赛事后台开发或者想通过一个具体场景练习 pandas 与数据可视化的人阅读。文中所有队伍名、比赛编号和局内数据都用于演示不代表任何真实赛事结果。1. 先理解比赛结果背后的数据形态1.1 赛制与比分2:1 到底意味着什么BFX 2:1 KRX 表示这是一场 BO3 比赛即 Best of 3三局两胜。BFX 赢下两局KRX 赢下一局最终 BFX 获胜。这里要先区分“比赛”和“局”两个层级比赛是一次完整的 BO3 对抗局是比赛内的单张地图或单场对局。不同赛制直接影响数据模型的层数。如果数据表只记录总比分那么 BO3 和 BO5 的区别就不容易体现出来如果记录到局则可以回答“哪支队伍赢下了第一局”“第三局打了多久”这类细节问题。赛制全称胜出条件常见场景BO1Best of 1一局定胜负小组赛、快速赛BO3Best of 3三局两胜常规淘汰赛、双败赛BO5Best of 5五局三胜总决赛、胜者组决赛在设计数据结构时建议把bo作为比赛层字段保存而不是从局数反推。因为有的比赛可能因为弃权、判负、网络问题中断局数不完整直接根据games数组长度推断赛制会出错。1.2 一场比赛最少需要记录哪些字段最小可用的赛果数据结构至少分三层比赛层、队伍层、局层。比赛层记录这场比赛是谁和谁打、什么赛制、最终谁赢。队伍层记录队伍短名、所属赛区等稳定信息。局层记录每一局的地图、时长、双方击杀、死亡、经济以及这一局的实际胜者。字段建议这样设计字段层级类型示例说明match_id比赛层stringdemo_20240516_bfx_krx比赛唯一标识competition比赛层string示例联赛赛事名称bo比赛层int3赛制局数teams比赛层objectBFX / KRX队伍信息映射result比赛层objectwinner / score最终赛果games局层array三局数据局数据列表game_id局层int1局序号map局层string示例地图A地图或场地duration_seconds局层int2150本局时长winner局层stringBFX本局胜者kills局层-队伍int18队伍总击杀deaths局层-队伍int12队伍总死亡economy局层-队伍int152000队伍总经济这里的winner字段必须来自赛事系统或人工确认的结果不建议通过击杀数、推塔数反推。电竞比赛中击杀领先不一定等于获胜最终胜者以比赛目标推掉基地、占领目标点等为准。1.3 为什么不能只统计总比分2:1 只能说明最终谁赢了但看不出比赛过程。同样是 2:1可能是三局都胶着也可能是第一局碾压、第二局被翻盘、第三局艰难取胜。这些过程差异在战报标题里会体现为完全不同的表达“轻松拿下”“完成逆转”“苦战险胜”。在做数据分析时如果只统计总比分产出的报告说服力很弱。局内数据才能回答几个关键问题每局净击杀是多少哪支队伍在前中期更主动。经济领先是否转化为胜势。决胜局是否出现明显的击杀和经济差距。输掉的那局是彻底崩盘还是后期被翻盘。所以合理的做法是先把嵌套 JSON 压平成表格再基于 DataFrame 做窗口内统计。这样既保留全部原始信息又能灵活计算派生指标。2. 准备 Python 环境并构造样例赛事数据2.1 环境准备与依赖安装这个案例只需要 Python 3.9 以上版本以及 pandas、matplotlib、jupyter 三个核心库。pandas 负责表格处理和统计matplotlib 负责可视化jupyter 在探索阶段可以快速看到每步输出。建议先创建虚拟环境避免依赖污染系统 Python。python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate激活虚拟环境后安装依赖pip install pandas matplotlib jupyter安装完成后可以验证版本python -c import pandas; print(pandas.__version__)如果是在学习环境直接使用 jupyter notebook 或 VSCode 的 Interactive Window 都可以。如果要在服务器上批处理运行建议把逻辑写成.py脚本用python scripts/xxx.py执行。2.2 项目目录结构建议按数据、脚本、输出三层组织目录方便后续扩展。match-analysis/ ├── data/ │ └── bfx_vs_krx.json ├── scripts/ │ ├── load_data.py │ ├── statistics.py │ ├── visualize.py │ └── generate_title.py └── output/ └── 该目录用于存放图片和结果文件这样划分的目的是把“输入数据”“处理逻辑”“产出结果”分离。生产环境中data 层可能换成数据库或对象存储output 层可能换成报表服务scripts 层负责核心计算逻辑不需要跟着改。2.3 构造样例 JSON 数据下面创建data/bfx_vs_krx.json模拟一场 BO3 比赛BFX 以 2:1 击败 KRX。{ match_id: demo_20240516_bfx_krx, competition: 示例联赛, bo: 3, teams: { BFX: {short_name: BFX, region: 示例赛区}, KRX: {short_name: KRX, region: 示例赛区} }, result: { winner: BFX, score: {BFX: 2, KRX: 1} }, games: [ { game_id: 1, map: 示例地图A, duration_seconds: 2150, winner: BFX, teams: { BFX: {kills: 18, deaths: 12, economy: 152000}, KRX: {kills: 12, deaths: 18, economy: 128000} } }, { game_id: 2, map: 示例地图B, duration_seconds: 2380, winner: KRX, teams: { BFX: {kills: 9, deaths: 16, economy: 112000}, KRX: {kills: 16, deaths: 9, economy: 151000} } }, { game_id: 3, map: 示例地图C, duration_seconds: 2620, winner: BFX, teams: { BFX: {kills: 21, deaths: 14, economy: 165000}, KRX: {kills: 14, deaths: 21, economy: 130000} } } ] }选择 JSON 而不是直接把数据写进 Python 字典是为了贴近真实接口返回。实际赛事系统的数据往往来自 HTTP API 或数据管道JSON 是最常见的传递格式。后续替换成真实数据时只需要调整加载层不需要重写统计逻辑。2.4 用 pandas 加载并检查数据接下来把嵌套 JSON 压平成适合分析的表格。核心思路是遍历games数组再把每一局中的两个队伍拆成两行。创建scripts/load_data.pyimport json import pandas as pd with open(data/bfx_vs_krx.json, r, encodingutf-8) as f: match_data json.load(f) print(比赛编号:, match_data[match_id]) print(赛果:, match_data[result]) games [] for game in match_data[games]: for team_name, stat in game[teams].items(): games.append({ game_id: game[game_id], map: game[map], team: team_name, kills: stat[kills], deaths: stat[deaths], economy: stat[economy], game_winner: game[winner] }) df pd.DataFrame(games) print(df.head()) print(df.dtypes)运行后应该看到 6 行数据每局对应两个队伍。由于 JSON 中数字字段本身就是 intdf.dtypes检查时kills、deaths、economy都应该是 int64。这一步很重要如果字段是字符串后续sum()、mean()会报错或得到错误结果。3. 用 DataFrame 完成比分和局内指标统计3.1 从 result 对象解析总比分和胜者比分已经存在match_data[result][score]中但要注意字典的键顺序不能作为队伍顺序依据。正确的做法是根据winner字段判断谁是胜者再从 score 字典中取出对应比分。score match_data[result][score] winner match_data[result][winner] loser [t for t in score.keys() if t ! winner][0] print(f{winner} 以 {score[winner]}:{score[loser]} 击败 {loser})这里不推荐写成list(score.keys())[0]直接取第一个队伍。如果数据源中队伍顺序变化或者以后改成三队以上的娱乐赛这种写法就会出错。3.2 按局计算净击杀与击杀死亡比在每行都代表一个队伍一局的数据结构下可以先增加两个派生字段。df[kill_diff] df[kills] - df[deaths] df[kd_ratio] df[kills] / df[deaths]kill_diff表示净击杀数。正数说明该队本局击杀多于死亡负数说明处于劣势。kd_ratio是击杀死亡比用来衡量整体对抗强度。需要注意如果deaths为 0除零会报错。真实数据可能出现零死亡局建议先做保护比如统一替换为 0 或使用 numpy 的 divide 函数。查看每局数据print(df[[game_id, team, kills, deaths, kill_diff, economy]])从样例数据可以看到BFX 赢下的第一局和第三局净击杀都为正KRX 赢下的第二局净击杀为正。这说明净击杀和胜负方向基本一致但不能把这个规律当成硬性规则比赛仍以game_winner字段为准。3.3 汇总队伍层面的指标队伍汇总可以回答“整场比赛谁的整体表现更强”。用groupby按队伍分组再通过agg同时计算多个指标。team_summary df.groupby(team).agg( total_kills(kills, sum), total_deaths(deaths, sum), avg_economy(economy, mean), max_duration(duration_seconds, max) ).reset_index() print(team_summary)这段代码把三局数据聚合到队伍维度。total_kills是整场总击杀avg_economy是平均每局经济max_duration是该队参与局的最大时长。这里注意groupby后reset_index()让team重新变成普通列而不是索引。如果后续要算队伍总经济直接换成sum即可。聚合函数的选择取决于要看“总量”还是“均值”。3.4 找出关键局和最大差距局“关键局”可以从两个角度判断一是最终逆转的起点局二是双方差距最大的局。先查看每局双方的净击杀差距。当前 DataFrame 是长格式每局两行数据要对比双方需要先做一次透视。pivot df.pivot(indexgame_id, columnsteam, values[kills, economy]) pivot.columns [_.join(col) for col in pivot.columns] pivot pivot.reset_index() pivot[kill_gap] abs(pivot[kills_BFX] - pivot[kills_KRX]) pivot[economy_gap] abs(pivot[economy_BFX] - pivot[economy_KRX]) max_kill_gap_game pivot.loc[pivot[kill_gap].idxmax(), game_id] max_economy_gap_game pivot.loc[pivot[economy_gap].idxmax(), game_id] print(净击杀差距最大的局:, max_kill_gap_game) print(经济差距最大的局:, max_economy_gap_game)这样能快速定位“最悬殊的一局”。在战报标题里如果这一局是胜者赢下可能会用“碾压”如果败者赢下则可能是“关键追分”。这些规则在第 5 节生成标题时会用到。4. 用可视化呈现队伍对抗节奏4.1 每局击杀对比柱状图只输出数字不够直观。把每局击杀做成并列柱状图能一眼看出 BFX 和 KRX 在每一局的对抗强度。创建scripts/visualize.pyimport matplotlib.pyplot as plt import pandas as pd # 复用 load_data 中的加载逻辑 # 此处假设 df 已经存在 plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei, PingFang SC] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(8, 4)) team_colors {BFX: #3b82f6, KRX: #ef4444} for team in [BFX, KRX]: team_df df[df[team] team] offset -0.2 if team BFX else 0.2 ax.bar( team_df[game_id] offset, team_df[kills], width0.4, labelteam, colorteam_colors[team] ) ax.set_xticks([1, 2, 3]) ax.set_xticklabels([第1局, 第2局, 第3局]) ax.set_ylabel(击杀数) ax.set_title(BFX vs KRX 每局击杀对比) ax.legend() plt.tight_layout() plt.savefig(output/kills_by_game.png, dpi150)offset的作用是让同属一局的两个柱子并排显示而不是重叠。如果不偏移两个柱子会完全叠在同一个 x 坐标上最后只能看到其中一支队伍的数据。4.2 每局经济趋势折线图经济能反映队伍在中后期的资源积累能力。样例数据里只有每局总经济所以用折线图展示趋势是合理的。如果真实数据是逐分钟采样建议把数据结构改成[game_id, timestamp, team, economy]然后直接画时间序列。fig, ax plt.subplots(figsize(8, 4)) for team in [BFX, KRX]: team_df df[df[team] team].sort_values(game_id) ax.plot( team_df[game_id], team_df[economy], markero, labelteam, colorteam_colors[team] ) ax.set_xticks([1, 2, 3]) ax.set_xticklabels([第1局, 第2局, 第3局]) ax.set_ylabel(经济) ax.set_title(BFX vs KRX 每局经济趋势) ax.legend() plt.tight_layout() plt.savefig(output/economy_trend.png, dpi150)从样例数据看BFX 赢下的两局经济都高于 KRXKRX 赢下的第二局经济反超。这符合赛果但数据分析时仍要注意经济领先和获胜之间的因果关系需要结合比赛时间点不能只凭终局经济下结论。4.3 可视化检查点运行完脚本后检查两件事output目录下是否生成了两张 PNG 图片。图片中文是否正常显示如果出现方块说明系统中文字体缺失。如果是在 Linux 服务器上跑需要先安装中文字体或把字体配置换成服务器存在的字体。这个坑在第 6 节会详细说明。5. 把“可惜看不到跳舞”变成自动战报标题5.1 标题生成的基本规则回到最初那句“BFX 2:1 拿下 KRX可惜了今天看不到跳舞了”。这句话可以拆成两部分比赛结果部分和娱乐事件部分。比赛结果部分依赖比分和胜负BFX 以 2:1 击败 KRX。娱乐事件部分则来自额外信息赛后舞蹈环节是否正常进行。生产环境中这类信息往往不是结构化数据库字段可能来自运营配置、现场执行表或审核状态。比较好的做法是把娱乐事件作为一个布尔值传入标题生成函数而不是硬编码在统计代码里。5.2 写一个最小可用的标题生成函数创建scripts/generate_title.pydef generate_title(match_data: dict, celebrate_event: bool True) - str: score match_data[result][score] winner match_data[result][winner] loser [t for t in score.keys() if t ! winner][0] winner_score score[winner] loser_score score[loser] games match_data[games] first_game_winner games[0][winner] if winner_score 1 and loser_score 0: main_title f{winner} 横扫 {loser} elif first_game_winner ! winner: main_title f{winner} 完成逆转以 {winner_score}:{loser_score} 击败 {loser} else: main_title f{winner} 以 {winner_score}:{loser_score} 拿下 {loser} if celebrate_event: return main_title 赛后舞蹈环节照常进行 return main_title 可惜了今天看不到跳舞了执行测试print(generate_title(match_data, celebrate_eventFalse)) print(generate_title(match_data, celebrate_eventTrue))输出BFX 以 2:1 拿下 KRX可惜了今天看不到跳舞了 BFX 以 2:1 拿下 KRX赛后舞蹈环节照常进行这里的核心逻辑是模板变量替换。标题不是字符串拼接的“一次性代码”而是把winner、loser、winner_score、loser_score、比赛过程特征分别提取出来再组合成不同句式。以后要调整措辞只需要改模板不需要重写统计模块。5.3 让模板支持配置化和扩展更规范的做法是把模板放到 YAML 配置中业务人员可以直接修改文案不需要改代码。下面是一个简化示例。config/templates.yamlnormal: {winner} 以 {winner_score}:{loser_score} 拿下 {loser} sweep: {winner} 横扫 {loser} comeback: {winner} 完成逆转以 {winner_score}:{loser_score} 击败 {loser}Python 中使用时可以按条件选择对应模板再用format填充变量。场景判断条件示例标题常规胜局最终胜者赢下第一局且不是横扫BFX 以 2:1 拿下 KRX横扫胜者比分大于 1败者比分为 0BFX 横扫 KRX逆转第一局胜者不是最终胜者BFX 完成逆转以 2:1 击败 KRX娱乐事件缺失celebrate_eventFalse追加“可惜了今天看不到跳舞了”要注意模板随机化要谨慎。如果从“拿下”“击败”“战胜”多个词中随机选择同一个赛果每次生成标题可能不同。测试环境可以使用固定random.seed或干脆不随机避免回归测试不稳定。6. 常见问题排查6.1 排查链路先输入再类型再逻辑最后样式遇到赛果分析结果不对时不要急着改统计代码。按下面顺序排查输入数据是否正确JSON 里队伍名、比分、局数是否完整。字段类型是否正确数字字段是不是被解析成了字符串。加载逻辑是否正确嵌套 JSON 是否漏掉某个队伍。统计逻辑是否正确胜者判断是否依赖了击杀数而不是winner字段。展示逻辑是否正确中文字体、图片输出目录是否存在。6.2 常见问题速查表问题现象常见原因检查方式处理建议读取 JSON 后中文乱码open未指定 UTF-8 编码打印df.head()前几行统一使用encodingutf-8groupby 后求和报错数字字段是字符串类型执行df.dtypes用astype(int)转换总比分只输出了一个队伍score 字典键顺序被依赖打印score全量内容用winner字段反推败者柱状图只显示一根柱子两组柱子的 x 坐标重叠检查 width 和 offset 参数同一局内使用正负偏移图片中文显示为方块系统中文字体缺失查看plt.rcParams[font.sans-serif]安装中文字体或更换字体路径标题中出现 None模板变量存在缺失字段打印模板变量字典用match_data.get(...)提供默认值局数统计和赛制不一致只根据 games 数组长度推断 BO检查match_data[bo]字段以 bo 字段为准games 只作为明细6.3 一条完整排查示例场景读取 JSON 后df只有 4 行期望是 6 行。检查第一步用df[game_id].value_counts()看每局有多少行。print(df[game_id].value_counts())如果发现某个 game_id 只有一行说明该局teams里缺少一个队伍。回看 JSON可能是 KRX 的节点被漏写或者队伍名大小写不一致导致加载时覆盖。解决方式是在加载时增加断言保证每局都有两个队伍for game in match_data[games]: assert set(game[teams].keys()) set(match_data[teams].keys()), game[game_id]这样数据源一旦多队或缺队会在加载阶段直接报错而不是等统计结果异常后再返查。7. 最佳实践与扩展方向7.1 数据结构设计要前置从这个小案例可以看出数据字段设计直接影响后面所有代码。建议在项目开始时统一约定队伍标识统一用小写或大写不要混用BFX、Bfx、bfx。比赛 ID 使用稳定唯一值例如{赛事}_{日期}_{主队}_{客队}。score字典不要依赖键顺序始终通过winner判断胜者。result.winner和每一局的winner都来自官方结果不要从击杀数推导。如果这些约定不明确后期接入真实数据时每个接口都可能带来一批数据质量问题。7.2 学习环境与生产环境的区别本地跑通 JSON 样例只完成了 20%生产环境还需要考虑这些点数据源接入必须合规。优先使用赛事官方提供的开放接口或授权数据不要通过逆向、破解、绕过访问限制的方式获取数据。原始数据要留档。建议把每场比赛的原始 JSON 存入对象存储或数据库分析结果单独输出便于赛后追溯。数据质量检查要自动化。比分为 0:0、局数大于赛制局数、队伍名缺失、winner缺失等都属于异常情况需要触发告警。标题生成不要直接发布。增加人工审核或敏感词检查避免赛果表达引发争议。可视化结果需要归档。比赛日报、周报经常需要复用历史图表保证output目录按日期归档更稳妥。7.3 发布前检查清单每次输出赛果分析前至少检查以下项目[ ] JSON 能正常加载编码为 UTF-8。[ ] 所有数字字段类型正确。[ ] 总比分和最终胜者与官方赛果一致。[ ] 每局winner与总局比分逻辑一致。[ ] 每局都有两个队伍无重复、无缺失。[ ] 标题生成覆盖常规胜局、横扫、逆转三种场景。[ ] 娱乐事件字段缺失时不会返回 None。[ ] 图片输出路径存在中文字体正常。[ ] 原始 JSON 和结果文件已归档。这份清单可以直接作为自动化测试用例去实现。尤其是“总比分与官方赛果一致”这一条是对数据源真实性最重要的一道校验。7.4 可以继续扩展的方向从“BFX 2:1 拿下 KRX”到完整赛事分析系统后续扩展空间很大接入完整的赛程数据自动生成每日赛事日报。引入 Elo 评分或胜率预测模型用历史对局数据计算队伍实力曲线。对比赛弹幕、评论做情感分析识别“看不到跳舞了”这类娱乐情绪点。把统计结果输出成前端看板展示比分、经济曲线和关键事件。对标题做归因分析统计不同措辞下的阅读数据反推运营偏好。这些方向都建立在同一个基础上先把赛果数据稳定、准确地转换成结构化表格再让规则引擎和算法基于结构化数据工作。回到最初那句话。“BFX 2:1 拿下 KRX可惜了今天看不到跳舞了”是一句有情绪的战报但数据系统能做的是稳定输出准确比分、可解释的统计指标和可配置的标题模板。先把 JSON 到 DataFrame 再到标题生成的闭环跑通再逐步接入真实数据源这个方向在实际项目中不会走偏。
返回列表