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

wen 实用脚本 1

射门质量与 xG 差异原因分析 —— 综合实用脚本

下面给你一套可直接落地的分析框架 + Python 脚本,用来回答"某球员/球队的射门质量与 xG 为什么有差异"。

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


先厘清概念:差异到底指什么

指标 含义 常见差异方向
xG(Expected Goals) 基于位置、角度、助攻类型、防守压力等,每次射门的平均进球概率 模型基准
实际进球 真实结果 进球 > xG = 超常发挥
射门质量(Shot Quality) 单次射门的"应进球"程度,本质就是 xG 的分解 与 xG 同源
xGOT(On-Target xG) 射正后、考虑门将位置的期望进球 衡量射术/射正质量
PSxG Post-Shot xG,同 xGOT 区分"机会质量"与"射门质量"

关键区分:xG 差异 = 机会质量(xG)+ 射术(xGOT−xG)+ 运气(实际−xGOT)。 脚本的核心就是把这三层拆开。


差异的六大原因(分析维度)

  1. 样本量/随机波动(最重要,常被忽略)——小样本下进球与 xG 天然离散
  2. 射术差异:射正率、xGOT−xG、非优势脚/头球能力
  3. 机会选择:射门位置分布(禁区内/外、角度)
  4. 射门方式:一脚触球、盘带后、定位球、反击
  5. 对手与门将:门将扑救水平、防守压迫强度
  6. 数据/模型偏差:xG 模型未捕捉的变量(如助攻质量、身体对抗)

综合实用脚本(Python)

"""
shot_xg_analysis.py
分析射门质量与实际进球差异的原因
依赖: pandas, numpy, scipy, matplotlib
输入: shots.csv  (每次射门一行)
必需列: player, team, xg, xgot(可选), goal(0/1), body_part,
        shot_type, distance, angle, pressure, assisted_by
"""
import pandas as pd
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
# ============ 1. 数据加载与基础校验 ============
def load_shots(path):
    df = pd.read_csv(path)
    need = {"player","xg","goal"}
    assert need.issubset(df.columns), f"缺列: {need - set(df.columns)}"
    df["goal"] = df["goal"].astype(int)
    if "xgot" not in df.columns:
        df["xgot"] = np.nan
    return df
# ============ 2. 核心分解: 实际 - xG = 射术 + 运气 ============
def decompose(df, by="player"):
    g = df.groupby(by).agg(
        shots=("goal","size"),
        goals=("goal","sum"),
        xg=("xg","sum"),
        xgot=("xgot","sum"),
    ).reset_index()
    g["diff"] = g["goals"] - g["xg"]                 # 总差异
    g["finishing"] = g["goals"] - g["xgot"]          # 射术(相对射正质量)
    g["shot_quality_gain"] = g["xgot"] - g["xg"]     # 射正带来的质量提升
    g["luck_or_goalkeeping"] = g["diff"] - g["finishing"] - g["shot_quality_gain"]
    # 标准化: 每90分钟或每100次射门
    g["diff_per100"] = g["diff"] / g["shots"] * 100
    return g.sort_values("diff", ascending=False)
# ============ 3. 统计显著性: 差异是否只是噪声 ============
def significance_test(df, by="player"):
    """
    对每个球员做二项检验: 实际进球是否显著偏离 xG 期望
    H0: goals ~ Binomial(shots, mean_xg)
    """
    rows = []
    for name, sub in df.groupby(by):
        n = len(sub)
        k = sub["goal"].sum()
        p = sub["xg"].mean()          # 平均单次进球概率
        if n < 10:                    # 小样本跳过
            continue
        # 双侧二项检验
        pval = stats.binomtest(int(k), n, p, alternative="two-sided").pvalue
        rows.append({by: name, "shots": n, "goals": int(k),
                     "expected": n*p, "p_value": pval,
                     "significant": pval < 0.05})
    return pd.DataFrame(rows).sort_values("p_value")
# ============ 4. 分维度归因: 差异来自哪里 ============
def attribute(df, by="player"):
    """按射门方式/身体部位/距离拆解 xG 与进球分布"""
    out = {}
    for dim in ["body_part","shot_type","distance_bin","pressure"]:
        if dim not in df.columns:
            continue
        if dim == "distance_bin":
            df["distance_bin"] = pd.cut(df["distance"], [0,6,12,18,30,100],
                                        labels=["0-6","6-12","12-18","18-30","30+"])
        t = df.groupby([by, dim]).agg(
            shots=("goal","size"), goals=("goal","sum"), xg=("xg","sum")
        ).reset_index()
        t["diff"] = t["goals"] - t["xg"]
        out[dim] = t
    return out
# ============ 5. 可视化: xG vs 实际进球散点 ============
def plot_xg_vs_goals(g, title="xG vs Goals"):
    plt.figure(figsize=(7,7))
    plt.scatter(g["xg"], g["goals"], alpha=0.6)
    lim = max(g["xg"].max(), g["goals"].max()) * 1.1
    plt.plot([0,lim],[0,lim], "r--", label="y = x (无差异)")
    for _, r in g.iterrows():
        if abs(r["diff"]) > g["diff"].std():   # 标注离群
            plt.annotate(r.iloc[0], (r["xg"], r["goals"]), fontsize=8)
    plt.xlabel("xG"); plt.ylabel("实际进球")
    plt.title(title); plt.legend(); plt.tight_layout()
    plt.savefig("xg_vs_goals.png", dpi=120)
    print("已保存 xg_vs_goals.png")
# ============ 6. 一键运行 ============
def run(path, by="player"):
    df = load_shots(path)
    print("="*50, "\n【1】差异分解 (实际 - xG = 射术 + 运气)")
    g = decompose(df, by); print(g.head(15).to_string(index=False))
    print("\n【2】统计显著性 (差异是否只是噪声)")
    print(significance_test(df, by).head(15).to_string(index=False))
    print("\n【3】分维度归因")
    attr = attribute(df, by)
    for k,v in attr.items():
        print(f"\n-- 按 {k} --")
        print(v.sort_values("diff", key=abs, ascending=False).head(8).to_string(index=False))
    plot_xg_vs_goals(g)
    return g, attr
if __name__ == "__main__":
    run("shots.csv")

如何解读输出(实操建议)

  1. 先看显著性检验:p>0.05 的"差异"≈随机噪声,别过度解读,足球单赛季 100 次射门的标准差约 ±10 球。
  2. 看 decompose 的 finishing 列:
    • finishing 持续为正 → 真射术好(可复现)
    • shot_quality_gain 高 → 射正质量高(xGOT 高于 xG)
  3. 看分维度归因:差异集中在哪类射门?
    • 禁区外远射进球多 → 可能是运气/远射能力
    • 头球差异大 → 头球能力
    • 高压迫下差异 → 抗压能力
  4. 做滚动窗口:把 100 次射门作滑动窗口,看 finishing 是否稳定——稳定的才是能力,波动的都是运气。

一句话结论模板

"该球员实际进球比 xG 高 X 球,其中约 A 球来自射正质量提升(xGOT−xG),B 球来自射术(实际−xGOT),剩余为随机波动(p=0.xx,不显著),差异主要集中于[禁区外远射/头球/反击]场景。"

需要我把脚本改成球队层面、接入 StatsBomb/FBref 数据,或加贝叶斯收缩估计(更适合小样本)吗?

上一篇实用脚本对这次撞墙配合是否赞赏?

下一篇当前分类已是最新一篇

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

友情链接攻略窝 · 礼包窝 · 配置窝 · 换算窝 · 算法窝 · 查文窝 · 手册窝 · 问答窝 · 全文搜 · 手册库
相关站点礼包窝 · 查文窝