本文目录导读:

识别球队的战术风格,本质上是把比赛中的行为数据映射到几个关键维度上,下面给出一套可直接落地的脚本思路,从数据采集到风格标签输出。
先定义战术风格的维度
不要一上来就分"传控/防反"这种大标签,先用连续变量描述:
| 维度 | 指标示例 | 含义 |
|---|---|---|
| 控球倾向 | possession%、传球数/90 | 是否主导球权 |
| 推进方式 | 长传占比、场均推进距离、中后场传球占比 | 地面渗透 vs 长传冲吊 |
| 进攻节奏 | 每次进攻用时、场均射门、PPDA | 快攻 vs 阵地 |
| 防守强度 | PPDA、抢断位置、犯规数 | 高位逼抢 vs 低位退防 |
| 空间利用 | 边路进攻占比、传中数、禁区内触球 | 边中结合 vs 中路 |
| 转换倾向 | 由守转攻平均时长、反击射门占比 | 反击型 vs 控制型 |
数据来源
# 常用数据源 # - StatsBomb Open Data (免费, JSON事件流) # - FBref (球队/球员赛季聚合数据, 可爬) # - Understat (xG相关, 部分联赛) # - football-data.org API (基础赛程比分) # - WhoScored / SofaScore (需注意ToS)
以 StatsBomb 事件数据为例:
from statsbombpy import sb matches = sb.matches(competition_id=11, season_id=90) # 西甲某赛季 events = sb.events(match_id=matches.iloc[0]['match_id'])
特征工程脚本骨架
import pandas as pd
import numpy as np
def extract_team_features(events: pd.DataFrame, team: str) -> dict:
ev = events[events['team'] == team].copy()
opp = events[events['team'] != team].copy()
passes = ev[ev['type'] == 'Pass']
total_passes = len(passes)
# 1. 控球率
possession = len(ev) / (len(ev) + len(opp))
# 2. 长传占比 (传球距离 > 32m)
if 'pass_length' in passes:
long_ball_ratio = (passes['pass_length'] > 32).mean()
else:
long_ball_ratio = np.nan
# 3. 进攻方向偏好 (边路 vs 中路)
if 'pass_end_x' in passes and 'pass_end_y' in passes:
# y 从 0-80, 中间 18-62 为中路
central = passes['pass_end_y'].between(18, 62).mean()
wide = 1 - central
else:
central, wide = np.nan, np.nan
# 4. 推进性传球占比 (向前 > 10m)
if 'pass_end_x' in passes and 'x' in passes:
progressive = ((passes['pass_end_x'] - passes['x']) > 10).mean()
# 5. PPDA (对手每次防守动作允许的传球数, 越低越高位逼抢)
opp_def_actions = len(opp[opp['type'].isin(
['Pressure','Interception','Tackle','Foul Committed'])])
ppda = total_passes / opp_def_actions if opp_def_actions else np.nan
# 6. 平均进攻发起位置
carries = ev[ev['type'] == 'Carry']
avg_x = ev['x'].mean() if 'x' in ev else np.nan
return {
'team': team,
'possession': possession,
'long_ball_ratio': long_ball_ratio,
'central_ratio': central,
'wide_ratio': wide,
'progressive_pass_ratio': progressive,
'ppda': ppda,
'avg_x': avg_x,
}
风格分类
方案 A:规则阈值(可解释性强)
def classify_style(f):
if f['possession'] > 0.58 and f['ppda'] < 10 and f['long_ball_ratio'] < 0.15:
return '控球压迫型' # 类似曼城/巴萨
if f['possession'] < 0.45 and f['ppda'] > 15 and f['progressive_pass_ratio'] > 0.25:
return '低位防反型' # 类似穆里尼奥球队
if f['long_ball_ratio'] > 0.25 and f['wide_ratio'] > 0.55:
return '长传边路型'
if f['ppda'] < 8 and f['possession'] < 0.5:
return '高压反击型' # 类似克洛普早期
return '均衡型'
方案 B:无监督聚类(更客观)
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
def cluster_styles(features_df: pd.DataFrame, k=5):
cols = ['possession','long_ball_ratio','central_ratio',
'progressive_pass_ratio','ppda','avg_x']
X = StandardScaler().fit_transform(features_df[cols].fillna(0))
km = KMeans(n_clusters=k, n_init=10, random_state=42)
features_df['cluster'] = km.fit_predict(X)
# 查看每个簇的均值, 人工命名
print(features_df.groupby('cluster')[cols].mean())
return features_df
聚类后每个簇的均值就是风格"指纹",人工对照命名即可。
方案 C:多维度雷达打分
输出 0–100 的风格分数,更适合作图:
def style_radar(f):
return {
'控球': min(100, f['possession'] * 150),
'高位逼抢': max(0, 100 - f['ppda'] * 5),
'直接性': min(100, f['long_ball_ratio'] * 300),
'边路倾向': min(100, f['wide_ratio'] * 150),
'推进性': min(100, f['progressive_pass_ratio'] * 300),
}
进阶:时序 + 对手校正
单一赛季均值会失真,建议:
- 滚动窗口:每 5 场滚动计算,看风格漂移
- 对手强度校正:用对手 PPDA / 控球率做回归,提取残差
- 情境分档:领先 / 落后 / 平局分别统计——很多球队落后时风格突变
- xG 链:把进球序列连成 possession chain,看每次进攻的推进路径
# 示例: 按比分状态分组
def split_by_scoreline(events):
# 需先从 shot/goal 事件推导比分时间线
...
工程化建议
- 事件数据用
polars或duckdb处理,比 pandas 快数倍 - 特征表落盘 Parquet,方便回测
- 加
matplotlib/mplsoccer画热图和雷达图,一眼看出风格 - 定期跑批:每周更新,监控风格变化(换帅、转会窗后常变)
一句话总结
采集事件 → 抽 6 个维度特征 → 标准化 → 规则或聚类打标签 → 结合情境和时序做校正 → 可视化输出。
核心三件套:possession、PPDA、long_ball_ratio,能把 80% 的战术风格区分开。
需要我针对某个具体数据源(如 StatsBomb / FBref)写一份完整可运行的脚本吗?