Python案例分析:守门员出击范围分析
问题定义
守门员出击范围指门将主动离开球门线、离开禁区甚至出击到禁区外拦截球的区域,分析这个指标可以评估门将的风格(保守型/激进型)、球队防守体系等。

数据来源
常用数据源:
- StatsBomb 开放数据(免费,含坐标)
- FBref / Understat
- Wyscout / Opta
- 懂球帝/创冰 等中文数据
核心字段:位置(x, y)、事件类型、时间、球员ID
完整案例:基于 StatsBomb 数据分析门将出击
环境准备
pip install statsbombpy mplsoccer pandas numpy matplotlib
加载数据
from statsbombpy import sb import pandas as pd import numpy as np import matplotlib.pyplot as plt from mplsoccer import Pitch # 以2022世界杯决赛为例 matches = sb.matches(competition_id=43, season_id=106) match_id = matches.iloc[0]['match_id'] events = sb.events(match_id=match_id) events['match_id'] = match_id
提取门将出击事件
StatsBomb 中门将相关事件类型:
Goalkeeper→Punch/Keeper Sweeper/Smother/Save/Collected- Keeper Sweeper(门将清道夫) 就是典型的"出击"
# 筛选门将事件
gk_events = events[
(events['type'] == 'Goal Keeper') &
(events['player'].notna())
].copy()
# 展开子类型
def parse_gk_type(row):
try:
return row['goalkeeper_type']['name']
except (TypeError, KeyError):
return None
gk_events['gk_type'] = gk_events.apply(parse_gk_type, axis=1)
# 只看"出击"类:Keeper Sweeper / Smother / Punch
sweeper_events = gk_events[gk_events['gk_type'].isin(
['Keeper Sweeper', 'Smother', 'Punch', 'Collected']
)]
# 获取坐标(StatsBomb 坐标:x: 0~120, y: 0~80,进攻方向固定)
sweeper_events = sweeper_events.assign(
x=sweeper_events['location'].apply(lambda loc: loc[0] if loc else np.nan),
y=sweeper_events['location'].apply(lambda loc: loc[1] if loc else np.nan)
)
print(sweeper_events[['player', 'gk_type', 'x', 'y', 'minute']].head())
计算关键指标
# 门将出击的关键指标
def analyze_gk_sweeper(df, gk_name):
gk_df = df[df['player'] == gk_name]
if len(gk_df) == 0:
return None
# 出击平均位置
avg_x = gk_df['x'].mean()
avg_y = gk_df['y'].mean()
# 最大出击距离(以球门线 x=0 为基准,注意 StatsBomb 门将在 x=0 一侧)
max_x = gk_df['x'].max()
# 出击范围面积(用位置的凸包或标准差近似)
x_range = gk_df['x'].max() - gk_df['x'].min()
y_range = gk_df['y'].max() - gk_df['y'].min()
# 出击次数
n_sweeper = len(gk_df)
# 禁区外出击占比(x < 18 为禁区内,18~40 为禁区外一定范围)
# 注:StatsBomb 场地长 120,禁区宽 18 码=18*(120/105)≈20.6
penalty_box_x = 18 # 简化
outside_box = (gk_df['x'] > 18 * (120/105)).sum()
return {
'player': gk_name,
'n_sweeper': n_sweeper,
'avg_x': round(avg_x, 1),
'avg_y': round(avg_y, 1),
'max_x': round(max_x, 1),
'x_range': round(x_range, 1),
'y_range': round(y_range, 1),
'outside_box_pct': round(outside_box / n_sweeper * 100, 1) if n_sweeper else 0
}
# 获取两个门将名字
gk_names = sweeper_events['player'].unique()
results = [analyze_gk_sweeper(sweeper_events, gk) for gk in gk_names]
results_df = pd.DataFrame(results).dropna()
print(results_df)
可视化:出击热力图
def plot_gk_sweeper_heatmap(df, gk_name, match_title=""):
gk_df = df[df['player'] == gk_name]
pitch = Pitch(pitch_type='statsbomb', pitch_color='#22312b',
line_color='white', half=True)
fig, ax = pitch.draw(figsize=(8, 6))
# 出拳/出击位置点
pitch.scatter(gk_df['x'], gk_df['y'], s=120, c='#ff4444',
edgecolors='white', ax=ax, zorder=3, label='出击点')
# 出击范围的凸包(虚拟"控制区")
if len(gk_df) >= 3:
from scipy.spatial import ConvexHull
points = gk_df[['x', 'y']].values
hull = ConvexHull(points)
hull_pts = points[np.append(hull.vertices, hull.vertices[0])]
ax.plot(hull_pts[:, 0], hull_pts[:, 1], 'b--', lw=1.5,
alpha=0.7, label='控制区凸包')
ax.set_title(f'{gk_name} 出击点分布\n{match_title}', color='white', fontsize=12)
ax.legend(loc='lower right')
plt.tight_layout()
plt.show()
for gk in gk_names:
plot_gk_sweeper_heatmap(sweeper_events, gk, "2022 WC Final")
多场次对比分析
# 分析某门将整个赛季的出击特征
def season_gk_analysis(competition_id, season_id, gk_name):
matches = sb.matches(competition_id=competition_id, season_id=season_id)
all_sweepers = []
for mid in matches['match_id']:
ev = sb.events(match_id=mid)
gk_ev = ev[(ev['type'] == 'Goal Keeper') & (ev['player'] == gk_name)].copy()
if gk_ev.empty:
continue
gk_ev['gk_type'] = gk_ev.apply(parse_gk_type, axis=1)
sweeper = gk_ev[gk_ev['gk_type'] == 'Keeper Sweeper'].copy()
if not sweeper.empty:
sweeper = sweeper.assign(
x=sweeper['location'].apply(lambda l: l[0] if l else np.nan),
y=sweeper['location'].apply(lambda l: l[1] if l else np.nan),
match_id=mid
)
all_sweepers.append(sweeper[['match_id', 'player', 'x', 'y', 'minute']])
return pd.concat(all_sweepers, ignore_index=True)
# 示例:某门将整个赛季
# gk_season = season_gk_analysis(43, 106, 'Emiliano Martínez')
进阶分析维度
| 指标 | 含义 | 计算方式 |
|---|---|---|
| 平均出击位置 x | 门将风格(越靠前越激进) | mean(x) |
| 出击半径 | 覆盖面 | max(distance to goal) |
| 出击频次 | 每90分钟 | n / 出场分钟 * 90 |
| 出禁区比例 | 活动范围 | 禁区外次数 / 总次数 |
| 出击成功率 | 效果 | 成功拦截 / 总出击 |
| 控制区面积 | 用Alpha Shape/凸包 | 凸包面积 |
与对手/球队风格联动
# 对阵高位逼抢球队时出击次数是否增加 # 加入对手信息(event中可通过则/球队字段) opponent = events[events['team'] != gk_team]['team'].iloc[0]
常见坑与建议
- 坐标系差异:StatsBomb 坐标固定 x∈[0,120];Opta 是百分比;FBref 是每半场归一化。转换前务必统一。
- 方向翻转:门将可能"守在 x=0 或 x=120"两端,需要按门将所在球门做镜像。
- 子类型命名不一致:"Keeper Sweeper" 是 StatsBomb 特有;Opta 中是
Goalkeeper下的Sweeper。 - 样本量:单场比赛通常只有 0~5 次出击,最好分析赛季或联赛。
- 战术背景:出击频率与球队防线高度强相关,最好与整队 x 坐标均值结合看。
可扩展项目
- 用 k-means 将联赛门将按出击风格聚类(保守 / 均衡 / 清道夫型)
- 用 DBSCAN 找出"高威胁出击"区域
- 构建 球场分区热图(按15分钟时段)
- 结合 xG 判断出击是否合理(阻止机会 vs 冒失出击)
如果你有具体数据集(StatsBomb JSON / Opta CSV)或想针对某场比赛/某位门将,我可以直接写对应的可运行代码。