本文目录导读:

核心思路
教练换人得分能力 = 换人后球队进攻/防守表现的变化,剔除比赛情境(比分、时间、对手强度)后的净效应。
关键要回答:
- 换人后进球/失球概率是否显著变化?
- 换人决策的时机是否合理?
- 换人选择(换谁上、换谁下)是否最优?
数据需求
| 数据类型 | 字段示例 |
|---|---|
| 比赛事件 | 进球、助攻、射门、传球、时间戳 |
| 换人记录 | 换人时间、下场球员、上场球员、当时比分 |
| 球员表现 | 上场前后xG/xA、跑动、对抗成功率 |
| 比赛情境 | 比分差、剩余时间、对手实力(Elo/排名) |
指标体系设计
换人后净得分效应(核心指标)
NetGoalEffect = Σ(换人后球队进球) - Σ(换人后球队失球)
按换人后在场时间归一化
更精细版本:用xG替代实际进球,减少样本噪声。
换人时机质量
TimingScore = 换人时球队xG差变化率 / 剩余时间
衡量是否在球队势头下滑时及时调整。
换人选择价值
SubValue = 上场球员下场后xG贡献 - 下场球员若留场的预期xG贡献
用球员个人xG/xA模型估算。
情境调整后的换人得分
用回归模型控制比分差、时间、对手强度:
ΔxG_per_min ~ β0 + β1·is_sub + β2·score_diff + β3·time + β4·opp_strength + ε
β1 即教练换人的净效应。
脚本实现(Python伪代码)
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
import statsmodels.api as sm
# ---------- 1. 数据加载 ----------
subs = pd.read_csv("substitutions.csv") # match_id, minute, player_off, player_on, score_diff
events = pd.read_csv("events.csv") # match_id, minute, type, team, xG
# ---------- 2. 构建换人前后窗口 ----------
WINDOW = 15 # 换人前后各15分钟
def window_xg(match_events, team, start, end):
mask = (match_events.team == team) & \
(match_events.minute >= start) & (match_events.minute < end)
return match_events.loc[mask, "xG"].sum()
records = []
for _, s in subs.iterrows():
ev = events[events.match_id == s.match_id]
pre = window_xg(ev, s.team, s.minute - WINDOW, s.minute)
post = window_xg(ev, s.team, s.minute, s.minute + WINDOW)
records.append({
"match_id": s.match_id,
"coach": s.coach,
"minute": s.minute,
"score_diff": s.score_diff,
"pre_xg": pre,
"post_xg": post,
"delta_xg": post - pre,
})
df = pd.DataFrame(records)
# ---------- 3. 情境调整回归 ----------
X = sm.add_constant(df[["score_diff", "minute"]])
y = df["delta_xg"]
model = sm.OLS(y, X).fit()
df["adjusted_delta"] = model.resid + model.params["const"] # 剔除情境后的净效应
# ---------- 4. 教练层面聚合 ----------
coach_score = df.groupby("coach").agg(
n_subs=("adjusted_delta", "count"),
mean_delta=("adjusted_delta", "mean"),
std_delta=("adjusted_delta", "std"),
).reset_index()
# 收缩估计(小样本教练向均值收缩)
global_mean = df["adjusted_delta"].mean()
k = 10 # 收缩强度
coach_score["shrunk_score"] = (
(coach_score.n_subs * coach_score.mean_delta + k * global_mean)
/ (coach_score.n_subs + k)
)
coach_score = coach_score.sort_values("shrunk_score", ascending=False)
print(coach_score.head(20))
进阶:换人决策的反事实评估
若要评估“这次换人是否比不换更好”,可用:
- 匹配法:找相似情境(比分、时间、对手)下未换人的比赛片段作为对照。
- 因果推断:用倾向得分匹配(PSM)或双重差分(DID)估计换人的因果效应。
- 球员级模型:用个人xG模型估算“若A留场 vs B上场”的期望xG差。
# 倾向得分匹配示例 from sklearn.linear_model import LogisticRegression from sklearn.neighbors import NearestNeighbors # 特征:比分差、时间、控球率、射门差 features = ["score_diff", "minute", "possession_diff", "shots_diff"] ps_model = LogisticRegression().fit(df[features], df["is_sub"]) df["ps"] = ps_model.predict_proba(df[features])[:, 1] # 对每个换人样本,在未换人样本中找最近邻 # 计算 ATT = E[Y|sub] - E[Y|no_sub, matched]
落地建议
| 步骤 | 工具 | 产出 |
|---|---|---|
| 数据管道 | Python + SQL | 换人事件表 + 事件流 |
| 指标计算 | pandas / statsmodels | 教练换人得分排名 |
| 可视化 | matplotlib / Tableau | 换人时机热图、净效应分布 |
| 验证 | 分赛季交叉验证 | 稳定性检验 |
注意事项:
- 样本量小:单教练单赛季换人次数有限,需多赛季聚合或贝叶斯收缩。
- 混杂因素:比分落后时换人本身更激进,必须做情境调整。
- xG优于实际进球:减少随机性,提高信度。
- 区分换人类型:战术换人 vs 伤病换人 vs 时间消耗换人,应分开评估。
如果需要,我可以进一步给出某个具体环节的完整代码(比如PSM匹配、贝叶斯层级模型、或可视化看板)。