综合实用脚本,射门质量与xG差异原因?

wen 实用脚本 4

射门质量与xG差异原因分析

下面给你一套可直接落地的分析框架+Python脚本,用于诊断"某球员/球队实际进球 vs xG 差异"到底来自哪里。

综合实用脚本,射门质量与xG差异原因?


先理清:xG差异的归因框架

实际进球(Goals) − 预期进球(xG) = G−xG 差值,原因可拆为四层:

层级 含义 关键指标
① 射门数量 创造机会多少 射门数/90、射门占比
② 射门位置质量 射门在哪 平均xG/射门、禁区内射门占比、平均距离、角度
③ 终结能力 同样机会把握 G−xG、射正率、转化率、post-shot xG
④ 样本与噪声 是否真实能力 样本量、稳定性、回归检验

核心结论先行:G−xG 的差异大部分是噪声(尤其小样本),真实"终结能力"差异在统计上很弱且难稳定,脚本要做的就是把"数量/位置/终结/噪声"分开量化。


综合实用脚本(Python)

"""
shot_xg_diagnosis.py
诊断射门质量与xG差异来源。
输入: 射门级数据 shots.csv
必填列: player, team, xg, goal, distance, angle(可选), body_part(可选), match_id
"""
import pandas as pd
import numpy as np
from scipy import stats
# ---------- 1. 读取 ----------
df = pd.read_csv("shots.csv")
df["goal"] = df["goal"].astype(int)
df["xg"]   = df["xg"].astype(float)
# ---------- 2. 单实体汇总 ----------
def summarize(d, entity_col="player"):
    g = d.groupby(entity_col).agg(
        shots      = ("xg", "size"),
        goals      = ("goal", "sum"),
        xg         = ("xg", "sum"),
        xg_per_shot= ("xg", "mean"),
        avg_dist   = ("distance", "mean"),
    ).reset_index()
    g["g_minus_xg"]   = g["goals"] - g["xg"]
    g["conversion"]   = g["goals"] / g["shots"]
    g["shots_per_90"] = np.nan  # 若有分钟数据再填
    return g.sort_values("g_minus_xg", ascending=False)
player_sum = summarize(df, "player")
# ---------- 3. 位置质量分层: 禁区内/外 ----------
df["in_box"] = df["distance"] <= 16.5  # 近似禁区半径(米), 或直接用坐标列
box = df.groupby("player").apply(lambda x: pd.Series({
    "box_shot_share": x["in_box"].mean(),
    "box_xg_per_shot": x.loc[x.in_box, "xg"].mean() if x.in_box.any() else np.nan,
    "out_xg_per_shot": x.loc[~x.in_box, "xg"].mean() if (~x.in_box).any() else np.nan,
}), include_groups=False).reset_index()
player_sum = player_sum.merge(box, on="player", how="left")
# ---------- 4. 分解 G-xG:数量 vs 位置 vs 终结 ----------
# 用"同位置基准转化率"估算位置应得,剩余归为终结
base_conv_by_zone = df.groupby("in_box")["goal"].mean()  # 每区域平均转化
df["expected_by_zone"] = df["in_box"].map(base_conv_by_zone)
decomp = df.groupby("player").apply(lambda x: pd.Series({
    "goals": x["goal"].sum(),
    "xg": x["xg"].sum(),
    "zone_expected": x["expected_by_zone"].sum(),  # 按区域平均转化应得
}), include_groups=False).reset_index()
decomp["finishing_effect"] = decomp["goals"] - decomp["zone_expected"]   # 终结超出
decomp["position_effect"]  = decomp["zone_expected"] - decomp["xg"]       # 位置质量差
decomp["g_minus_xg"]       = decomp["goals"] - decomp["xg"]
# ---------- 5. 显著性检验:G-xG 是否显著偏离0 ----------
def binom_test_player(x):
    n, k = len(x), x["goal"].sum()
    p = x["xg"].mean()  # 以平均xG作基准概率(近似)
    if n < 10: return np.nan
    return stats.binomtest(k, n, p).pvalue
pvals = df.groupby("player").apply(
    lambda x: pd.Series({"p_value": binom_test_player(x)}), include_groups=False
).reset_index()
player_sum = player_sum.merge(decomp[["player","finishing_effect","position_effect"]], on="player", how="left")
player_sum = player_sum.merge(pvals, on="player", how="left")
# ---------- 6. 稳定性检验(分半信度): 前半赛季 vs 后半赛季 G-xG 相关 ----------
if "match_id" in df:
    df["half"] = df["match_id"] % 2
    halves = df.groupby(["player","half"]).apply(
        lambda x: x["goal"].sum() - x["xg"].sum(), include_groups=False
    ).unstack()
    if {0,1}.issubset(halves.columns):
        r, p = stats.pearsonr(halves[0].fillna(0), halves[1].fillna(0))
        print(f"[稳定性] 分半 G-xG 相关系数 r={r:.3f}, p={p:.3f}  (接近0=多为噪声)")
# ---------- 7. 输出 ----------
cols = ["player","shots","goals","xg","g_minus_xg","xg_per_shot",
        "box_shot_share","position_effect","finishing_effect","p_value"]
print(player_sum[cols].head(20).to_string(index=False))
player_sum.to_csv("xg_diagnosis_output.csv", index=False)

如何读结果(归因判断表)

现象 判断 建议
shots少但 xg_per_shot 高 机会少而精 提升创造量
shots多但 xg_per_shot 低 浪射/远射多 优化射门选择
box_shot_share 低 位置质量差 打进禁区
finishing_effect 大且 p<0.05 可能有真实终结力 需更大样本确认
finishing_effect 大但 p>0.05 大概率噪声 别过度解读
分半相关 r≈0 G−xG 不稳定 视为随机波动

实用建议(避免踩坑)

  1. 样本量门槛:<50 射门的 G−xG 基本不可信;单赛季球员波动极大。
  2. 优先看 xG/射门 和 位置分布,而不是只看 G−xG。
  3. 用 post-shot xG(射门后门将位置)可分离"射门质量"与"门将扑救",比单纯 G−xG 更细。
  4. 回归到均值:对 G−xG 极端值做收缩(如贝叶斯/经验贝叶斯),预测下赛季更准。
  5. 区分点球:点球 xG≈0.76 固定,会污染终结力评估,建议单独剔除。

需要我把脚本改成球队维度、接入 Understat/FBref 抓取,或加上经验贝叶斯收缩预测版本吗?告诉我你的数据源格式即可。

抱歉,评论功能暂时关闭!