足球比赛数据复盘实战:用Python解析中甲赛事技术统计与威胁指数 兄弟们周末守着中甲第18轮石家庄功夫队和陕西联合队的比赛除了看比分你有没有想过一场90分钟的比赛里藏着多少能“量化”的东西控球率、射门数、威胁传球、防守强度甚至哪段时间哪支球队踢得更“爷们儿”其实都可以用数据说清楚。这篇文章不聊战术板上的玄学而是用 Python 从公开比赛记录出发做一次完整的“足球比赛数据复盘”实战。文章会覆盖数据整理、统计对比、事件时间线分析和威胁指数可视化代码都是可以直接复制的适合 Python 刚入门、想用数据分析足球的朋友也适合做体育数据项目的开发者参考。1. 背景与核心概念为什么足球比赛需要数据复盘1.1 足球比赛的数据化观察足球比赛的魅力在于不可预测但比赛过程中的射门、传球、抢断、越位、任意球等事件都是可以被记录的离散数据。把这些事件按照时间轴和空间位置整理好就能还原一场比赛的大致脉络。石家庄功夫队与陕西联合队的比赛是一次很典型的中甲对抗。中甲球队的技战术风格和中超不同边路推进、快速反击、高位逼抢的频率都需要从数据中观察而不是只看赛后技术统计表上“控球率60%对40%”这种粗颗粒数字。数据复盘要做的事情是把比赛拆成四个层次层次分析内容用到的技术赛事基本盘比分、射门、角球、黄牌等pandas 统计球队整体表现控球率、传球数、威胁进攻次数DataFrame 聚合时间段走势每15分钟事件密度变化时间序列分析核心事件线索进球机会、红黄牌、换人与走势关系事件流可视化一句话总结数据复盘不是预测谁赢谁输而是把比赛事件变成可对比、可回放、可检索的结构化记录。1.2 常见应用场景除了赛后写复盘文章这类技术还能用在几个方向球迷个人赛后数据雷达图制作。校园足球或业余联赛的技术统计小程序。体育媒体做可视化战报。数据分析爱好者练习 pandas、matplotlib、requests 的综合项目。为球队训练提供基础的数据反馈比如统计某位球员被侵犯次数、跑动覆盖范围。所以这篇文章虽然以中甲第18轮石家庄功夫队与陕西联合队的比赛为场景但处理思路是通用的换任何一场比赛都能复用。2. 环境准备与版本说明2.1 运行环境本文示例代码以 Python 3.8 以上版本为例推荐使用 3.10 或更高版本。涉及的第三方库如下pandas1.5.0 matplotlib3.6.0 requests2.28.0 openpyxl3.1.0安装方式建议使用虚拟环境python -m venv venv source venv/bin/activateWindows 系统激活命令venv\Scripts\activate然后安装依赖pip install pandas matplotlib requests openpyxl版本不需要与上面完全一致但建议 pandas 不要低于 1.5否则部分 DataFrame 操作会有兼容性问题。2.2 项目目录结构为了便于维护我们先用一个清晰的项目目录football_analysis/ ├── data/ │ ├── raw/ │ │ ├── match_events.csv │ │ └── team_stats.json │ └── output/ ├── src/ │ ├── data_loader.py │ ├── team_stats_analysis.py │ ├── time_series_analysis.py │ └── visual_utils.py ├── main.py └── requirements.txt说明data/raw存放原始数据data/output存放图片和清洗后的结果。2.3 数据来源与合规说明本节演示使用一份模拟的比赛记录字段结构参考了公开的比赛技术统计方式。为什么不用真实比赛数据直接演示因为公共数据接口的可访问性和更新速度不稳定直接网络请求很容易 403 或拿到非结构化 HTML。用结构一致的模拟数据可以保证案例可复现。如果你要分析真实比赛请优先使用联赛官方或数据平台的授权数据或者自己从可信来源手工整理遵守网站的 robots 协议与版权要求。3. 比赛数据获取与数据字典设计3.1 数据应包含哪些字段在动手写代码前先设计好数据结构。足球比赛事件记录至少需要这些字段字段示例含义minute23事件发生分钟team石家庄功夫事件归属球队event_typeshot事件类型player10号球员球员标识detail禁区外射门补充说明x55进攻方向半场横向位置0-100y42进攻方向纵向位置0-100resultsaved事件结果事件类型包括pass传球、shot射门、foul犯规、yellow_card黄牌、red_card红牌、substitution_in换人上场、substitution_out换人下场等。3.2 加载事件数据我们把事件数据放进data/raw/match_events.csv下面是一段示例。minute,team,event_type,player,detail,x,y,result 3,石家庄功夫,shot,9号,禁区内头球,62,48,saved 5,陕西联合,pass,17号,中场长传,30,55,completed 8,陕西联合,shot,10号,禁区外远射,28,44,goal 12,石家庄功夫,foul,6号,中场拉拽,55,55,foul 16,陕西联合,yellow_card,5号,战术犯规,40,38,yellow 18,石家庄功夫,shot,11号,左路内切射门,65,38,off_target 22,石家庄功夫,pass,8号,直塞身后,70,48,completed 25,陕西联合,shot,10号,禁区弧顶推射,32,47,saved注意这里只是模拟片段。实际自己爬取或整理时数据量会大得多可能包含几百条事件。3.3 编写数据加载模块在src/data_loader.py中封装加载逻辑import pandas as pd from pathlib import Path def load_raw_events(path: str) - pd.DataFrame: 加载比赛事件原始表 events pd.read_csv(path, encodingutf-8-sig) required_cols {minute, team, event_type, player, detail, x, y, result} missing required_cols - set(events.columns) if missing: raise ValueError(f缺少字段: {missing}) # 把分钟统一成整数 events[minute] events[minute].astype(int) # 新增一个比赛阶段字段方便按阶段聚合 events[phase] pd.cut( events[minute], bins[0, 15, 30, 45, 60, 75, 90], labels[0-15, 15-30, 30-45, 45-60, 60-75, 75-90], rightTrue ) return events关键点解释encodingutf-8-sig是为了兼容 CSV 文件常见的 BOM 头。pd.cut把分钟数切到不同的15分钟区间方便画“比赛走势”。如果 CSV 结构变动这里可以提前暴露问题而不是到后面聚合时才报错。3.4 输出事件统计概览继续编写一个简单调用查看事件分布# main.py 开头可先这样测试 from src.data_loader import load_raw_events events load_raw_events(data/raw/match_events.csv) print(events.head()) print(\n事件类型统计) print(events[event_type].value_counts()) print(\n各队事件数量) print(events.groupby(team)[event_type].count())运行后预期输出类似minute team event_type player detail ... phase 0 3 石家庄功夫 shot 9号 禁区内头球 ... 0-15 1 5 陕西联合 pass 17号 中场长传 ... 0-15这就是后面所有分析的“底层数据底座”。4. 技术统计对比石家庄功夫 vs 陕西联合4.1 编写球队统计聚合函数拿到原始事件后最容易做的是按球队聚合技术统计。我们会统计射门、射正、犯规、黄牌、传球次数等维度并统一存进一个 DataFrame。import pandas as pd def get_team_stats(events: pd.DataFrame) - pd.DataFrame: stats { 射门: events[events[event_type] shot].groupby(team).size(), 射正: events[(events[event_type] shot) (events[result] ! off_target)].groupby(team).size(), 犯规: events[events[event_type] foul].groupby(team).size(), 黄牌: events[events[event_type] yellow_card].groupby(team).size(), } # 传球可能需要区分完成/失败这里统一计数 pass_events events[events[event_type] pass] stats[传球] pass_events.groupby(team).size() team_df pd.DataFrame(stats) team_df team_df.fillna(0).astype(int) # 增加射正率字段避免除零 team_df[射正率] team_df[射正] / team_df[射门].replace(0, 1) return team_df team_stats get_team_stats(events) print(team_stats)为什么要单独处理“射正率”因为如果某队射门为 0直接相除会出现 NaN 或无穷大这在后续绘图和报表中会埋坑。4.2 雷达图对比雷达图适合展示两只球队的多维度能力对比。用 matplotlib 实现import matplotlib.pyplot as plt import numpy as np plt.rcParams[font.sans-serif] [SimHei] # 中文显示 plt.rcParams[axes.unicode_minus] False def plot_team_radar(team_df: pd.DataFrame, save_path: str data/output/team_radar.png): teams list(team_df.index) labels [射门, 射正, 犯规, 黄牌, 传球] # 注意雷达图对量纲敏感这里先做归一化 norm_df team_df[labels].copy() for col in labels: max_val norm_df[col].max() if max_val 0: norm_df[col] 0 else: norm_df[col] norm_df[col] / max_val * 100 angles np.linspace(0, 2 * np.pi, len(labels), endpointFalse).tolist() angles angles[:1] fig, ax plt.subplots(figsize(7, 7), subplot_kwdict(polarTrue)) for idx, team in enumerate(teams): values norm_df.loc[team].tolist() values values[:1] ax.plot(angles, values, labelteam, linewidth2) ax.fill(angles, values, alpha0.15) ax.set_xticks(angles[:-1]) ax.set_xticklabels(labels) ax.set_ylim(0, 100) ax.set_title(石家庄功夫 vs 陕西联合 技术统计雷达图, pad20) ax.legend(locupper right, bbox_to_anchor(1.3, 1.0)) plt.tight_layout() plt.savefig(save_path, dpi200) plt.show() plot_team_radar(team_stats)这段代码有几个容易踩的坑雷达图本质是极坐标系折线图angles必须首尾闭合否则图形会缺一块。不同队伍数量变化时颜色、标签位置要留意。中文显示依赖系统字体Windows 一般可以用SimHeimacOS 可以改成Arial Unicode MS或PingFang SC。4.3 统计口径的局限技术统计对比最容易被质疑的点是“定义不一致”。比如“射正”到底算不算打在门框上后卫封堵算不算一次有效射门不同数据服务商可能不同。所以在输出表格时最好加上口径说明用注释写清楚统计说明射正 射门事件中result 不为 off_target 的记录 传球 传球事件总数包含成功与失败 黄牌数 0 表示该队本场没有被出示黄牌。这个习惯在真实项目中非常重要数据结论只有在口径一致时才有可比性。5. 赛事事件时间线从分钟粒度看比赛节奏5.1 为什么时间线比总数据更有效总数据只能说明“两队整体怎么样”但很难回答比赛前10分钟是谁在主导石家庄功夫队的进球机会主要出现在哪个时间段陕西联合队是不是在领先后主动收缩时间线可以帮你找出这些规律。5.2 统计每15分钟各队关键事件先用 pandas 做分组统计画出事件密度柱状图。def plot_phase_events(events: pd.DataFrame, save_path: str data/output/phase_events.png): # 按队伍与比赛阶段统计所有事件数 phase_table events.groupby([team, phase], observedFalse).size().unstack(fill_value0) ax phase_table.plot(kindbar, figsize(10, 6)) ax.set_title(各队每15分钟事件数量对比) ax.set_xlabel(球队) ax.set_ylabel(事件数量) ax.legend(title时间阶段) plt.xticks(rotation0) plt.tight_layout() plt.savefig(save_path, dpi200) plt.show()这里有一个细节groupby加上observedFalse是为了避免 pandas 对分类数据的警告尤其在后续 pandas 2.1 以上版本中这个参数能减少运行噪音。5.3 绘图并定位“节奏拐点”如果只看总事件判断可能失真。可以把射门、犯规、黄牌加权处理射门重要权重 3。黄牌说明防守压力大权重 2。犯规中性事件权重 1。传球基础事件权重 0.5。这样可以得到一个简化版的“比赛强度指数”。event_weight { shot: 3, yellow_card: 2, foul: 1, pass: 0.5, } def plot_intensity_curve(events: pd.DataFrame, save_path: str data/output/intensity_curve.png): # 计算每分钟加权事件强度 events[weight] events[event_type].map(event_weight).fillna(1) minute_weight events.groupby([team, minute])[weight].sum().reset_index() fig, ax plt.subplots(figsize(12, 5)) for team in minute_weight[team].unique(): team_data minute_weight[minute_weight[team] team] ax.plot(team_data[minute], team_data[weight], labelteam, linewidth2) ax.set_title(石家庄功夫 vs 陕西联合 比赛强度曲线) ax.set_xlabel(比赛分钟) ax.set_ylabel(加权事件强度) ax.legend() ax.grid(alpha0.3) plt.tight_layout() plt.savefig(save_path, dpi200) plt.show()之所以用“加权”是因为直接把所有事件当作同样重要会掩盖真正的进攻威胁。比如第 60 分钟河北队有两次禁区内射门陕西联合队只有一次中场横传如果只看事件数量可能是 1:1但威胁程度完全不同。运行之后你会看到某支球队在某个时间段明显上扬那个时间段通常就是进球或连续威胁发生的阶段。6. 进阶实战构建“威胁指数”并输出比赛简报6.1 威胁指数模型设计到目前为止我们已经能计算常规技术统计和事件密度。但这个粒度还不够。更进一步我们可以给每一次事件打分把射门区域、射门方式、结果组合成一个“威胁指数”。一个简化模型如下def threat_score(row): # 根据 x 坐标判断距离球门的远近x 越大说明越接近对方球门 x row.get(x, 50) if row[event_type] ! shot: return 0 score 10 # 靠近球门加分 if x 70: score 15 elif x 50: score 5 # 根据射门结果调整 if row.get(result) goal: score 30 elif row.get(result) saved: score 10 elif row.get(result) off_target: score - 10 return score注意这不是严格的期望进球值 xG 模型只是一个方便入门理解的“威胁指数”示例。真实 xG 需要大量历史射门数据训练得到位置和角度对应的进球概率。6.2 将威胁指数汇总到时间维度我们可以把每次射门的威胁分累加到球队和比赛阶段上。def threat_index_summary(events: pd.DataFrame) - pd.DataFrame: events events.copy() events[threat] events.apply(threat_score, axis1) summary events.groupby([team, phase], observedFalse)[threat].sum().unstack(fill_value0) summary[总计] summary.sum(axis1) return summary threat_df threat_index_summary(events) print(threat_df)输出结构大致如下phase 0-15 15-30 30-45 45-60 60-75 75-90 总计 team 石家庄功夫 30 40 15 20 80 35 220 陕西联合 20 15 60 30 25 20 170如果某个队在最后 15 分钟威胁值突然上涨说明比赛末段他们投入了更多进攻兵力。6.3 一键生成文本版比赛简报很多场景下直接给一个 DataFrame 不够友好。我们可以把它写成自然语言简报。def generate_report(events: pd.DataFrame) - str: team_stats get_team_stats(events) threat_df threat_index_summary(events) team_list list(team_stats.index) lines [] lines.append(中甲第18轮技术简报演示数据\n) for team in team_list: lines.append(f【{team}】) lines.append(f- 射门{team_stats.loc[team, 射门]}次射正 {team_stats.loc[team, 射正]} 次) lines.append(f- 射正率{team_stats.loc[team, 射正率] * 100:.1f}%) lines.append(f- 犯规{team_stats.loc[team, 犯规]}次黄牌 {team_stats.loc[team, 黄牌]} 张) lines.append(f- 总威胁指数{threat_df.loc[team, 总计]}) peak_phase threat_df.loc[team].drop(总计).idxmax() lines.append(f- 最大威胁时段{peak_phase}) lines.append() return \n.join(lines) report generate_report(events) print(report) # 保存到文本文件 with open(data/output/match_report.txt, w, encodingutf-8) as f: f.write(report)这段代码的价值在于把数据分析和内容生成打通。以后如果你要做成定时任务比赛数据更新后自动生成战报核心逻辑就是上面这一段。6.4 完整 main.py 汇总把整个流程串起来main.py 内容如下from src.data_loader import load_raw_events from src.team_stats_analysis import get_team_stats, plot_team_radar from src.time_series_analysis import plot_phase_events, plot_intensity_curve def main(): events load_raw_events(data/raw/match_events.csv) print( 事件数量预览 ) print(events.shape) team_stats get_team_stats(events) print(\n 队伍技术统计 ) print(team_stats) plot_team_radar(team_stats) plot_phase_events(events) plot_intensity_curve(events) # 威胁指数与报告 threat_df threat_index_summary(events) print(\n 威胁指数 ) print(threat_df) report generate_report(events) print(\n 文本简报 ) print(report) if __name__ __main__: main()到这里已经从零完成了一套“足球比赛数据记录 → 数据清洗 → 技术统计 → 时间线分析 → 威胁指数 → 自动战报”的闭环流程。7. 常见问题与排查思路7.1 表格速查问题现象常见原因解决思路图表中文显示为方块系统没有安装对应中文字体设置plt.rcParams[font.sans-serif]为本机已有字体或下载中文字体groupby后出现 NaN有些组不存在某类事件使用fillna(0)再转整数读取 CSV 后列名乱码CSV 编码不是 UTF-8使用encodingutf-8-sig或尝试gbkpd.cut报等级错误minute 字段不是数值类型先用pd.to_numeric转换Plot 无法显示缺少 GUI 显示环境改用plt.savefig保存图片请求公开数据时 403网站反爬限制检查接口协议不要硬爬优先使用官方导出文件雷达图变形指标量纲不一致先做归一化或使用独立坐标轴7.2 事件数据质量问题最容易被忽略的是事件缺失。比如某场比赛补时阶段有进球事件时间写成了 453 或 905这种字符串如果不做解析pd.cut会直接报错。处理补时时间可以通过正则把补时分钟换算成小数import re def parse_minute(value): match re.match(r(\d)\(\d), str(value)) if match: return int(match.group(1)) int(match.group(2)) / 60 return int(value)当然也可以选择放弃补时事件只保留常规时间但这会在复盘中丢失重要进球时段所以建议保留。7.3 视角和坐标问题足球场坐标系在数据接口里非常不统一有的以进攻方向为基准有的以绝对位置为基准。石家庄功夫队主场作战时上半场从左向右攻或从右向左攻x 坐标的含义都不同。在做“左右路偏好”分析时需要先根据半场开场信息对坐标做镜像处理不能直接拿两队的 x/y 比较。这一点在真实项目中很容易被遗漏。8. 最佳实践与工程建议8.1 数据阶段的合规意识如果目标是分析真实中甲比赛建议从这几个方向考虑使用官方数据端口或授权数据平台。对二手爬取数据做来源登记。绝不使用账号窃取、付费接口绕过等非法方式。输出报告时注明数据来源和更新日期。尊重数据版权是体育数据分析项目能长期做下去的前提。8.2 用配置管理球队与场地信息不要把球队名、场地大小、字体路径硬编码在业务函数里。推荐建一个config.py# config.py TEAMS [石家庄功夫, 陕西联合] PITCH_SIZE {width: 68, length: 105} FONT_PATH data/fonts/SimHei.ttf OUTPUT_DIR data/output这样代码复用性会高很多以后分析别的场次不需要改主逻辑。8.3 时间与版本管理足球数据事件表是典型的时间序列数据。建议每条事件记录除了minute还要包含绝对时间戳event_time方便未来与视频时间轴对齐。events[event_time] pd.to_datetime(2025-07-01 19:35:00) pd.to_timedelta(events[minute], unitm)同时建议对分析脚本做 git 版本管理。因为事件字段口径一变你的统计脚本可能全部需要调整有版本记录才能追溯分析报告是哪一版数据代码生成的。8.4 日志与可观测性不要只 print 结果大型脚本请使用loggingimport logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logging.info(load events: %d rows, len(events))尤其在数据量达到数千条、并且要批量分析多轮比赛时清晰日志能帮你定位数据缺失阶段。8.5 将脚本沉淀为可复用工具包上面的代码按工程角度还可以模块化得更彻底比如loader模块负责不同格式的数据源解析。metrics模块包含射门、xG、传球成功率等各种指标算法。report模块负责生成 markdown、HTML 或 PDF 战报。以后要做“整个赛季34轮数据汇总”只需要增加一个轮次参数不用复制粘贴代码。9. 总结与下一步方向通过石家庄功夫队和陕西联合队这场中甲第18轮比赛的案例我们把足球数据复盘的完整链路走了一遍。从设计事件字段、加载 CSV到用 pandas 统计射门、犯规、黄牌等常规数据再到用雷达图对比两支球队的技术特点最后通过加权事件强度画出时间线并生成文本简报。即使你身边暂时没有结构化的比赛数据也能用模拟数据先跑通整个项目流程。如果继续深入下一步可以研究几个方向采集多轮比赛数据分析球队在主客场表现差异。增加球员级别的维度比如某位球员的传球成功率、被侵犯次数。学习真正的期望进球值 xG 模型通过大量历史射门坐标数据估算进球概率。把结果输出成 HTML 交互页甚至接入 echarts 做成动态可视化。使用定时任务在每轮比赛结束后自动生成数据战报。足球数据分析和普通业务数据分析没有本质区别关键都在于先理解业务事件再抽象数据结构最后用合适的技术手段展示结论。希望这篇教程能帮你打开“用代码看球”的新视角。如果你也在这场比赛的数据处理中遇到过什么有意思的问题欢迎收藏后慢慢折腾。

相关新闻

最新新闻

26年奇点智能大会分享智能体记忆管理:从短期上下文到长期知识库的架构演进与工程实践

26年奇点智能大会分享智能体记忆管理:从短期上下文到长期知识库的架构演进与工程实践

大会:2026 奇点智能技术大会 时间:2026 年 11 月 20-21 日 地点:中国北京万达文华酒店 参会报名:奇点智能研究院 一、为什么记忆是 Agent 的"阿喀琉斯之踵" 一个能对话的聊天机器人只需要记住当前对话的几轮内容。但一…

2026/9/3 14:50:41
26年奇点智能大会分享企业 AI 遗留系统集成策略:从数据孤岛到 AI 原生的渐进式改造路径

26年奇点智能大会分享企业 AI 遗留系统集成策略:从数据孤岛到 AI 原生的渐进式改造路径

大会:2026 奇点智能技术大会 时间:2026 年 11 月 20-21 日 地点:中国北京万达文华酒店 参会报名:奇点智能研究院 一、"最后一公里":比算法更难的是集成 企业 AI 落地的最大障碍,往往不是"模…

2026/9/3 14:50:41
华为昇腾950采购背后:大模型算力平台落地全流程拆解

华为昇腾950采购背后:大模型算力平台落地全流程拆解

从“范式智能拟出资超 10 亿元采购华为昇腾 950 芯片用于大模型落地”这条消息说起,多数人的第一反应是关注昇腾 950 的算力规格,另一部分人则在算一笔账:10 亿元到底能买到多大的算力盘子。但站在技术落地的角度看,真正值得拆解的…

2026/9/3 14:50:41
基于YOLO11与LUNA16构建智能肺结节检测系统:从数据处理到GUI部署全流程

基于YOLO11与LUNA16构建智能肺结节检测系统:从数据处理到GUI部署全流程

简介:本资源是一套面向医学影像AI初学者与课程设计者的肺结节检测实践系统,基于YOLOv11在LUNA16数据集上完成端到端开发,解决CT图像中肺结节自动定位与可视化诊断支持问题,适用于计算机辅助诊断大作业、深度学习课程设计及医疗AI入…

2026/9/3 14:50:41
基于MediaPipe与PyQt5的实时姿态动作识别系统开发实践

基于MediaPipe与PyQt5的实时姿态动作识别系统开发实践

简介:这是一套面向人工智能初学者与计算机视觉实践者的完整人体姿态与动作识别系统,基于Python开发并集成图形化操作界面,适用于健身动作评估、行为分析教学及人机交互原型开发等场景。资源包共45个文件,含18个核心Python源码&…

2026/9/3 14:50:41
Upscayl 免费 AI 图像放大完整指南:3 步把图片放大 4 倍

Upscayl 免费 AI 图像放大完整指南:3 步把图片放大 4 倍

Upscayl 免费 AI 图像放大完整指南:3 步把图片放大 4 倍 【免费下载链接】upscayl 🆙 Upscayl - #1 Free and Open Source AI Image Upscaler for Linux, MacOS and Windows. 项目地址: https://gitcode.com/GitHub_Trending/up/upscayl 想把老照…

2026/9/3 14:45:41