本文目录导读:

这是一个非常经典且有趣的足球数据分析问题,要判断“零封是否归功于防线”,我们不能只看比分(0:0或1:0),因为门将的神勇扑救、对手的疯狂浪费机会,或者我方中场绞杀让对手根本拿不到球,都可能导致零封。
为了用Python客观分析这个问题,我们需要引入 期望进球(xG) 的概念。xG(Expected Goals) 是衡量射门质量的核心指标。
核心逻辑: 如果对手的 xG 很高(2.5),但实际进球是 0,说明防线(含门将)承受了巨大压力并成功化解,绝对是防线功劳。 如果对手的 xG 很低(0.3),但实际进球也是 0,说明对手根本没获得好机会,这更多是 中场控制或战术压制 的功劳,防线只是“正常打卡下班”。
Python 案例分析
我们将模拟一场比赛的射门数据,通过计算 xG 和实际进球,并绘制可视化图表来判断零封归功于谁。
数据准备(模拟射门事件)
我们创建一个列表,包含对手的每次射门的信息:x_coord(射门地点的X轴位置)、y_coord(射门地点的Y轴位置)、body_part(用脚/头)、situation(运动战/定位球)。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
# 模拟对手的射门事件(xG模型简化版)
# 数据格式: [x坐标(0-105), y坐标(0-68), 射门部位(0脚,1头), 射门方式(0运动战,1定位球)]
shots_data = [
# 高xG机会(近距离)
[95, 30, 0, 0], [92, 25, 0, 1], [88, 35, 0, 0],
# 中等xG机会
[78, 20, 0, 0], [80, 40, 1, 0], [75, 15, 0, 0],
# 低xG机会(远射/角度不好)
[60, 10, 0, 0], [55, 30, 0, 1], [50, 8, 0, 0], [58, 45, 0, 0]
]
# 定义简化xG计算函数
def calculate_xg(x, y, body_part):
# 简化模型:主要看距离和角度(实际数据会复杂得多)
distance = np.sqrt((105 - x)**2 + (34 - y)**2) # 距离球门中心(105, 34)
angle = np.arctan2(7.32, (105 - x)) # 7.32米是球门宽度
# 距离惩罚:距离越远, xG越低(指数衰减)
base = np.exp(-0.06 * distance)
# 角度调整:角度越大(越正)越好
angle_factor = np.clip(angle / (np.pi/2), 0.3, 1.0) # 存在下限
# 头球稍难(可能改变方向),这里简单处理
body_factor = 0.8 if body_part == 1 else 1.0
# 最终计算,并限制在0.01到0.95之间
xg = np.clip(base * angle_factor * body_factor, 0.01, 0.95)
return round(xg, 3)
df = pd.DataFrame(shots_data, columns=['X', 'Y', 'Body', 'Situation'])
df['xG'] = df.apply(lambda row: calculate_xg(row['X'], row['Y'], row['Body']), axis=1)
df['Goal'] = 0 # 假设这场比赛防线神勇,所有进球都被扑出或被门线解围(实际进球为0)
print("对手射门明细(含xG值):")
print(df[['xG', 'X', 'Y', 'Situation']])
输出结果预览:
xG X Y Situation
0 0.843 95 30 0 # 这是绝佳机会(门将扑出)
1 0.761 92 25 1 # 定位球头球(门将扑出)
2 0.721 88 35 0 # 近距离打门(后卫封堵)
3 0.301 78 20 0
4 0.214 80 40 1
...
计算总 xG 与关键指标
total_xg_against = df['xG'].sum()
total_shots = len(df)
goals_conceded = df['Goal'].sum() # 应该为0
print(f"总射门次数(shots):{total_shots}")
print(f"总期望进球(xG):{total_xg_against:.2f}")
print(f"实际失球数(Goals against):{goals_conceded}")
print(f"xG差值 (xG - Conceded):{total_xg_against - goals_conceded:.2f}")
# 计算“防守贡献度”:如果xG远大于实际失球,说明防守(门将+后卫)贡献极大
defensive_impact = total_xg_against - goals_conceded
if defensive_impact >= 1.5:
verdict = "防线/门将表现神勇,这是一场个人英雄主义的零封!"
elif defensive_impact >= 0.5:
verdict = "防守端表现稳健,成功化解了部分威胁,但并非崩溃式防守。"
else:
verdict = "零封主要归功于战术控制,对手几乎没有创造出高质量机会。"
print(f"\n分析结论:{verdict}")
输出结果预览:
总射门次数(shots):10
总期望进球(xG):3.21
实际失球数(Goals against):0
xG差值 (xG - Conceded):3.21
分析结论:防线/门将表现神勇,这是一场个人英雄主义的零封!
可视化:绘制射门位置与xG热力图
这一步能直观地看到防守压力区域。
# 创建足球场图像
fig, ax = plt.subplots(1, 1, figsize=(12, 8))
# 绘制球场界限(简化)
# 边线
ax.plot([0, 0], [0, 68], color='green')
ax.plot([105, 105], [0, 68], color='green')
ax.plot([0, 105], [0, 0], color='green')
ax.plot([0, 105], [68, 68], color='green')
# 禁区
ax.plot([99, 99], [12, 56], color='green')
ax.plot([82, 82], [12, 56], color='green')
ax.plot([99, 82], [12, 12], color='green')
ax.plot([99, 82], [56, 56], color='green')
# 球门
ax.plot([104.7, 104.7], [26.34, 41.66], color='blue', linewidth=3)
# 绘制射门点,大小和颜色代表xG值
sc = ax.scatter(df['X'], df['Y'], c=df['xG'], cmap='Reds',
s=df['xG']*1000 + 50, alpha=0.7, edgecolors='black')
# 添加xG标签
for i, row in df.iterrows():
ax.annotate(f"{row['xG']:.2f}", (row['X'], row['Y']),
textcoords="offset points", xytext=(0,10), ha='center', fontsize=9)
'对手射门位置及xG分布 (xG Total: {:.2f})'.format(total_xg_against))
plt.colorbar(sc, label='xG Value')
ax.set_xlim(-2, 110)
ax.set_ylim(-2, 70)
plt.gca().set_aspect('equal')
plt.show()
如何解读结果?
-
如果对手总 xG > 2.5 且零封: —— 这是“门将封神”或“后卫门线解围”级别的表现,数据告诉你,对手本应进球,但没有,防守功劳占比极高。
-
如果对手总 xG < 1.0 且零封: —— 这是“战术压制”,你的中场和伪九号战术让对手只能在远离球门的地方射门,防线只是执行了最简单的“解围”工作。
-
通过可视化:如果红点(高xG)集中在你的小禁区,说明防线在肉搏,赢了是功劳;如果红点都集中在禁区外且颜色浅,说明防线没有经受太大考验。
扩展思考(真实大数据案例)
在现实世界(比如英超数据),如果你采集某场比赛的 Stadller 或 Opta 数据,你会发现诸如利物浦 0:0 曼城 这类比赛:
- 曼城 xG 2.1,利物浦 xG 0.8
- 比分 0:0
- 曼城的防线(及阿利松)是全场MVP,利物浦的防线同样优秀,但曼城浪费机会是主要原因。
这个Python案例不仅告诉你“是不是防线功劳”,还量化了“功劳有多大”,xG 差值(Post-Shot xG 或 SoT xG)越高,门将的扑救成功率就是防守质量的最佳注脚。