综合实用脚本
问题拆解
“中卫组合默契度”本质是两名中卫在协同防守中的配合效率,需从多维度量化:

| 维度 | 含义 | 可观测指标 |
|---|---|---|
| 空间互补 | 一人上抢时另一人是否补位 | 上抢-补位时间差、覆盖重叠率 |
| 距离协同 | 两人间距是否合理(过近漏边、过远留缝) | 平均间距、间距标准差、危险间距占比 |
| 移动同步 | 造越位/退防是否一致 | 防线同步率、越位成功/失败次数 |
| 传接默契 | 后场出球配合 | 互传成功率、受压下互传成功率 |
| 危机互补 | 一人失位时另一人补救 | 补位拦截次数、失位后30秒内被射门率 |
数据需求
追踪数据(每帧):两名中卫的 x/y 坐标、球坐标、事件标签(传球/抢断/拦截/越位线)。 事件数据:传球网络、防守动作、失球过程。
最低可行数据:每帧球员坐标 + 事件流(StatsBomb/Opta 风格),若无追踪数据,可用事件数据做降级版(仅传接+补位)。
量化脚本(Python)
import numpy as np
import pandas as pd
from itertools import combinations
# ============ 输入假设 ============
# frames: DataFrame, columns = [frame_id, player_id, x, y, team, is_ball]
# events: DataFrame, columns = [frame_id, event_type, player_id, success]
# 球场坐标归一化到 105x68(米),防守方向为 x 减小方向
PITCH_L, PITCH_W = 105, 68
def get_cb_pair(frames, cb_ids):
"""提取两名中卫每帧坐标"""
df = frames[frames.player_id.isin(cb_ids)].pivot_table(
index='frame_id', columns='player_id', values=['x','y'])
df.columns = [f'{c[0]}_{c[1]}' for c in df.columns]
return df.dropna()
def spatial_complementarity(pair_df, cb_ids):
"""空间互补:间距合理性 + 覆盖重叠"""
a, b = cb_ids
dx = pair_df[f'x_{a}'] - pair_df[f'x_{b}']
dy = pair_df[f'y_{a}'] - pair_df[f'y_{b}']
dist = np.sqrt(dx**2 + dy**2)
# 理想中卫间距 6~12 米(可调)
ideal = (dist >= 6) & (dist <= 12)
too_close = dist < 6 # 易被一打二
too_far = dist > 15 # 中间留缝
return {
'avg_dist': dist.mean(),
'dist_std': dist.std(), # 越小越稳定
'ideal_ratio': ideal.mean(), # 越高越好
'gap_risk_ratio': (too_close | too_far).mean(),
'y_overlap': 1 - abs(dy).mean() / PITCH_W # 纵向覆盖重叠
}
def movement_sync(pair_df, cb_ids, frames):
"""移动同步:两人速度向量夹角与防线同步率"""
a, b = cb_ids
va = pair_df[[f'x_{a}', f'y_{a}']].diff().dropna()
vb = pair_df[[f'x_{b}', f'y_{b}']].diff().dropna()
# 夹角余弦(同向为1)
cos = (va.values * vb.values).sum(1) / (
np.linalg.norm(va.values,axis=1)*np.linalg.norm(vb.values,axis=1)+1e-9)
# 同步率:夹角<30° 且都朝防守方向移动
sync = ((cos > 0.87) & (va.values[:,0] < 0)).mean()
return {'move_sync_rate': sync, 'avg_cos': np.nanmean(cos)}
def passing_chemistry(events, cb_ids):
"""后场互传默契"""
a, b = cb_ids
passes = events[(events.event_type=='pass') & (events.player_id.isin(cb_ids))]
# 假设有 pass_target 字段
mutual = passes[passes.get('pass_target').isin(cb_ids)]
return {
'mutual_pass_count': len(mutual),
'mutual_pass_success': mutual.success.mean() if len(mutual) else np.nan,
'under_pressure_success': mutual[mutual.get('under_pressure',False)].success.mean()
if len(mutual) else np.nan
}
def crisis_coverage(events, cb_ids, frames):
"""危机互补:补位拦截 + 失位后短时被射门率"""
a, b = cb_ids
# 补位拦截:A 上抢事件后 3 秒内 B 的拦截
cover = 0
for _, e in events[events.event_type=='tackle'].iterrows():
if e.player_id == a:
window = events[(events.frame_id > e.frame_id) &
(events.frame_id <= e.frame_id + 75) & # 3s@25fps
(events.player_id == b) &
(events.event_type.isin(['interception','clearance']))]
cover += len(window)
return {'cover_actions': cover}
def chemical_score(metrics):
"""综合默契度评分(0~100),权重可调"""
w = {'ideal_ratio':25,'dist_std':-15,'move_sync_rate':25,
'mutual_pass_success':20,'gap_risk_ratio':-15,'move_sync_rate2':0}
# 归一化示例
s = (metrics['ideal_ratio']*25
+ max(0,(1-metrics['dist_std']/5))*15
+ metrics['move_sync_rate']*25
+ (metrics.get('mutual_pass_success') or 0)*20
+ (1-metrics['gap_risk_ratio'])*15)
return round(min(max(s,0),100),1)
# ============ 主流程 ============
def evaluate_cb_pair(frames, events, cb_ids):
pair_df = get_cb_pair(frames, cb_ids)
m = {}
m.update(spatial_complementarity(pair_df, cb_ids))
m.update(movement_sync(pair_df, cb_ids, frames))
m.update(passing_chemistry(events, cb_ids))
m.update(crisis_coverage(events, cb_ids, frames))
m['chemical_score'] = chemical_score(m)
return m
# 示例:多组合对比
def rank_pairs(frames, events, cb_pool):
results = []
for pair in combinations(cb_pool, 2):
r = evaluate_cb_pair(frames, events, list(pair))
r['pair'] = pair
results.append(r)
return pd.DataFrame(results).sort_values('chemical_score', ascending=False)
使用与解读
- 准备数据:
frames需含每帧坐标(建议 25fps),events含传球/抢断/拦截/解围,带frame_id对齐。 - 调用:
table = rank_pairs(frames, events, cb_pool=[1,2,3,4]) print(table[['pair','chemical_score','ideal_ratio','move_sync_rate','gap_risk_ratio']])
- 解读要点:
ideal_ratio > 0.7且gap_risk_ratio < 0.15→ 间距默契好;move_sync_rate > 0.6→ 造越位/退防同步可靠;mutual_pass_success > 0.9→ 出球不慌;chemical_score横向比较不同搭档。
降级版(无追踪数据)
仅有事件数据时,用传球网络 + 补位事件窗口近似:
def events_only_score(events, a, b):
mutual = events[(events.event_type=='pass') &
(events.player_id==a) & (events.pass_target==b) |
(events.player_id==b) & (events.pass_target==a)]
cover = ... # 同上补位窗口逻辑
return mutual.success.mean()*60 + min(cover,10)*4 # 0~100
注意事项
- 坐标方向需统一(防守朝 x 减小),否则同步率符号错。
- 理想间距阈值(6~12m)需按球队战术调整:高压球队间距更小。
- 样本量:单场噪声大,建议至少 5~10 场滚动平均。
- 对手强度需加权,避免“虐菜默契”。
- 追踪数据缺失帧用插值,但插值比例 >20% 的组合应标记低置信度。
如需我针对你手上的具体数据字段(如 StatsBomb 的 3701 事件类型、或 Metrica 追踪格式)改写成可直接跑的版本,贴一下列名即可。