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

先厘清概念:差异到底指什么
| 指标 | 含义 | 常见差异方向 |
|---|---|---|
| xG(Expected Goals) | 基于位置、角度、助攻类型、防守压力等,每次射门的平均进球概率 | 模型基准 |
| 实际进球 | 真实结果 | 进球 > xG = 超常发挥 |
| 射门质量(Shot Quality) | 单次射门的"应进球"程度,本质就是 xG 的分解 | 与 xG 同源 |
| xGOT(On-Target xG) | 射正后、考虑门将位置的期望进球 | 衡量射术/射正质量 |
| PSxG | Post-Shot xG,同 xGOT | 区分"机会质量"与"射门质量" |
关键区分:xG 差异 = 机会质量(xG)+ 射术(xGOT−xG)+ 运气(实际−xGOT)。 脚本的核心就是把这三层拆开。
差异的六大原因(分析维度)
- 样本量/随机波动(最重要,常被忽略)——小样本下进球与 xG 天然离散
- 射术差异:射正率、xGOT−xG、非优势脚/头球能力
- 机会选择:射门位置分布(禁区内/外、角度)
- 射门方式:一脚触球、盘带后、定位球、反击
- 对手与门将:门将扑救水平、防守压迫强度
- 数据/模型偏差: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")
如何解读输出(实操建议)
- 先看显著性检验:p>0.05 的"差异"≈随机噪声,别过度解读,足球单赛季 100 次射门的标准差约 ±10 球。
- 看 decompose 的 finishing 列:
finishing持续为正 → 真射术好(可复现)shot_quality_gain高 → 射正质量高(xGOT 高于 xG)
- 看分维度归因:差异集中在哪类射门?
- 禁区外远射进球多 → 可能是运气/远射能力
- 头球差异大 → 头球能力
- 高压迫下差异 → 抗压能力
- 做滚动窗口:把 100 次射门作滑动窗口,看 finishing 是否稳定——稳定的才是能力,波动的都是运气。
一句话结论模板
"该球员实际进球比 xG 高 X 球,其中约 A 球来自射正质量提升(xGOT−xG),B 球来自射术(实际−xGOT),剩余为随机波动(p=0.xx,不显著),差异主要集中于[禁区外远射/头球/反击]场景。"
需要我把脚本改成球队层面、接入 StatsBomb/FBref 数据,或加贝叶斯收缩估计(更适合小样本)吗?
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播