本文目录导读:

这是一个非常具体的足球数据分析(或体育分析)问题,在Python中复盘“防守失误是否导致丢球”,通常需要结合事件数据(Event Data)或轨迹数据(Tracking Data)。
以下是一个完整的案例分析流程,帮助你用Python判断丢球是否由防守失误造成。
核心思路
- 数据准备:获取事件数据(传球、抢断、射门、失误等)或球员位置数据。
- 定义“防守失误”:常见的防守失误包括:
- 被突破(Dribbled Past):防守人1v1被过。
- 传球失误/解围失误:在后场传给对手或解围踢呲。
- 位置错误(Positional Error):失位、漏人(需要轨迹数据或人工标注)。
- 拦截失败(Tackle Failed):上抢被晃过。
- 判断逻辑:如果在进球前的倒数第二个或第三个事件中,发生了上述防守失误事件,且该事件直接导致对手获得射门机会或形成单刀,则归类为“防守失误导致丢球”。
- 可视化复盘:用传球网络图、热力图或动画来直观展示失误过程。
案例:使用Python分析一次“被突破”导致的丢球
假设我们有一场比赛的JSON格式事件数据(模拟数据)。
模拟数据
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import Circle, FancyBboxPatch
import numpy as np
# 模拟一次进攻到进球的事件序列(单位:米,球场105x68)
events = [
{'time': 0.0, 'team': 'A', 'player': 'GK', 'event': 'pass', 'x': 5, 'y': 30, 'to_x': 20, 'to_y': 35},
{'time': 2.0, 'team': 'A', 'player': 'CB', 'event': 'pass', 'x': 20, 'y': 35, 'to_x': 40, 'to_y': 50},
# 防守失误点:后卫被突破
{'time': 4.0, 'team': 'B', 'player': 'Forward', 'event': 'dribble_past', 'x': 50, 'y': 30, 'to_x': 60, 'to_y': 35, 'defender': 'CB2'},
{'time': 5.0, 'team': 'B', 'player': 'Forward', 'event': 'shot', 'x': 60, 'y': 35, 'to_x': 90, 'to_y': 30, 'result': 'goal'},
]
# 转换为DataFrame
df_events = pd.DataFrame(events)
print(df_events)
输出:
| time | team | player | event | x | y | to_x | to_y | result | defender |
|---|---|---|---|---|---|---|---|---|---|
| 0 | A | GK | pass | 5 | 30 | 20 | 35 | NaN | NaN |
| 0 | A | CB | pass | 20 | 35 | 40 | 50 | NaN | NaN |
| 0 | B | Forward | dribble_past | 50 | 30 | 60 | 35 | NaN | CB2 |
| 0 | B | Forward | shot | 60 | 35 | 90 | 30 | goal | NaN |
逻辑判断:是否因防守失误导致丢球
def is_goal_from_defensive_error(df_events):
"""
判断丢球是否由防守失误导致
逻辑:找到goal事件,向前追溯最近的dribble_past或tackle_fail事件
"""
# 找到进球事件
goal_event = df_events[df_events['result'] == 'goal']
if goal_event.empty:
return False, "没有进球"
goal_index = goal_event.index[0]
# 在当前进球前,向前搜索(最多看5个事件)
start = max(0, goal_index - 5)
preceding_events = df_events.iloc[start:goal_index]
# 检查是否存在防守失误事件(dribble_past, tackle_fail, pass_error)
error_types = ['dribble_past', 'tackle_fail', 'pass_error']
error_events = preceding_events[preceding_events['event'].isin(error_types)]
if not error_events.empty:
# 检查失误是否发生在危险区域(本方半场或大禁区前沿)
last_error = error_events.iloc[-1]
# 示例:如果失误发生在本方半场(x < 52.5)或大禁区附近
if last_error['x'] < 52.5 and last_error['x'] > 30: # 中后场区域
return True, f"防守失误导致丢球!球员 {last_error['player']} 在第{last_error['time']}秒被突破"
else:
return False, "有防守失误,但不在直接危险区域"
else:
return False, "未发现直接防守失误"
# 执行判断
is_error, reason = is_goal_from_defensive_error(df_events)
print(f"判断结果: {reason}")
# 输出: 判断结果: 防守失误导致丢球!球员 Forward 在第4.0秒被突破
可视化复盘:绘制防守失误瞬间
使用 matplotlib 绘制球场,展示失误发生的空间位置。
def draw_pitch(ax):
"""绘制标准足球场"""
# 球场尺寸105x68
pitch = Rectangle((0, 0), 105, 68, linewidth=2, edgecolor='black', facecolor='green', alpha=0.1)
ax.add_patch(pitch)
# 中线
ax.plot([52.5, 52.5], [0, 68], 'k-', linewidth=2)
# 中圈
circle = Circle((52.5, 34), 9.15, fill=False, edgecolor='black', linewidth=2)
ax.add_patch(circle)
# 禁区
ax.plot([0, 0], [13.84, 54.16], 'k-', linewidth=2) # 左侧
ax.plot([105, 105], [13.84, 54.16], 'k-', linewidth=2) # 右侧
ax.set_xlim(-5, 110)
ax.set_ylim(-5, 73)
ax.set_aspect('equal')
ax.axis('off')
def plot_defensive_error(df_events):
"""可视化防守失误导致丢球的过程"""
fig, ax = plt.subplots(1, 1, figsize=(12, 8))
draw_pitch(ax)
# 绘制事件点
for idx, event in df_events.iterrows():
if event['event'] == 'pass':
ax.arrow(event['x'], event['y'],
event['to_x'] - event['x'], event['to_y'] - event['y'],
head_width=2, head_length=2, fc='blue', ec='blue', alpha=0.7)
ax.scatter(event['x'], event['y'], color='blue', s=50, zorder=5)
elif event['event'] == 'dribble_past':
# 防守失误:红色大叉
ax.scatter(event['x'], event['y'], color='red', s=200, marker='x', linewidth=4, zorder=10, label='防守失误(被突破)')
ax.text(event['x']+1, event['y']+1, f"失误: {event['player']}", fontsize=9, color='red')
elif event['event'] == 'shot' and event['result'] == 'goal':
# 进球:金色圆圈
ax.scatter(event['x'], event['y'], color='gold', s=300, edgecolors='black', linewidth=3, zorder=10, label='进球')
# 画一条射门线
ax.plot([event['x'], 105], [event['y'], event['to_y']], 'g--', linewidth=2, label='射门路径')
# 添加图例
handles = [plt.Line2D([0],[0], marker='o', color='w', markerfacecolor='blue', markersize=10, label='传球'),
plt.Line2D([0],[0], marker='x', color='red', markersize=10, linewidth=0, label='防守失误'),
plt.Line2D([0],[0], marker='o', color='w', markerfacecolor='gold', markersize=10, label='进球')]
ax.legend(handles=handles, loc='upper right')
plt.title("防守失误导致丢球复盘", fontsize=15)
plt.show()
# 调用可视化
plot_defensive_error(df_events)
可视化效果:
- 蓝色箭头:A队的传球路线。
- 红色大X:B队前锋在
(50,30)位置突破了A队后卫(CB2)。 - 金色圆圈:随后在
(60,35)完成进球。
进阶:更严谨的判断标准(使用实际比赛数据)
如果你有真实的比赛中所有球员的位置数据(Tracking Data),可以计算更客观的指标:
局势压力指数 (Pressure / PPDA)
- 逻辑:如果丢球前,防守方球员离持球人的距离很远(例如超过5米),且没有阻拦传球线路,说明整体防守阵型松散,这属于系统性防守失误。
def calculate_pressure_at_moment(df_tracking, event_time, ball_carrier_team):
"""
计算某一时刻,防守方对持球人的平均距离(压迫强度)
"""
# 筛选当前时间点的数据
moment = df_tracking[df_tracking['time'] == event_time]
# 获取持球人位置
ball = moment[moment['role'] == 'ball']
# 获取防守方球员位置
defenders = moment[(moment['team'] != ball_carrier_team) & (moment['role'] == 'player')]
# 计算距离
distances = np.sqrt((defenders['x'] - ball['x'])**2 + (defenders['y'] - ball['y'])**2)
avg_distance = distances.mean()
return avg_distance
# 假设 average distance > 8 meters 表示压迫不足,属于防守失误
预期防守失误 (xDE - Expected Defensive Error)
- 使用机器学习模型,根据球员的速度、加速度、与球门的夹角,预测该防守动作失败的概率,如果概率 > 70%,则认定为失误。
空间控制覆盖率
- 丢球前,防守球员是否覆盖了对手的传球线路?可以用Voronoi图(泰森多边形)计算,如果进攻球员处于防守方Voronoi图控制的盲区(即离最近防守人距离很远),则说明防守站位出现巨大失误。
判断逻辑流程图
[ 发现进球事件 ]
|
v
[ 向前追溯3-5个事件 ]
|
v
[ 是否存在以下事件? ]
- 被突破 (dribble_past)
- 抢断失败 (tackle_fail)
- 传球失误导致球权转换 (pass_error)
|
↓ 是
[ 该事件是否发生在危险区域? ]
(本方半场/禁区前沿/边路内切区域)
|
↓ 是
[ 防守失误导致丢球 ]
|
↓ 否
[ 属于优秀的战术执行/运气球/无法防守的情况 ]
使用Python进行复盘的最终优势
- 客观性:避免人为偏见,用数据说话。
- 可量化:评估失误的严重程度(失误带来的xG提升)。
- 可重复:对成千上万个进球进行相同的自动化分析。
代码运行结果:如果你的 df_events 中包含 dribble_past 并且位置在危险区域,Python会准确判断出 “防守失误导致丢球”。