赛事前瞻有深度
AI 分析更客观

7.4 构造示例①:期望进球 xG 的简化模型思路

构造示例
🕒 预计阅读:12 分钟 📅 更新时间:2026年09月14日 👁️ 1 次阅读 ✍️ 来源:球小策量化实验室
💡
提示:您当前为未登录状态,代码示例使用通用占位符展示。点击登录并免费领取 500 点体验金 后,所有代码将自动替换为您本人的专属 Key。

这一篇讲如何从零构造一个 xG 模型。为了让思路清晰,我们采用教学版简化模型。

一、xG 的本质:把射门转成概率

每一次射门,都可以看作一次「成功概率未知的伯努利试验」。xG 就是这次射门转化为进球的概率估计

所以问题变成:如何用已知信息估计这个概率?

二、三个可用信号

信号 直觉 可用性
射门距离 越近越容易进 需要坐标数据;若无,可用区域分档替代
射门方式 点球 > 单刀 > 头球 > 远射 通常可得
比赛情境 快攻机会质量高于阵地战 可从事件流推断

三、教学版:用「射门区域分档」构造 xG

如果没有精确坐标,可以把射门区域粗分为 5 档,用历史数据统计每一档的进球率。

第一步:定义区域与初始估计

区域 说明 经验进球率区间
点球点附近 小禁区及点球点周围 30% ~ 40%
禁区内(近) 大禁区内部靠近球门 15% ~ 25%
禁区内(远) 大禁区边缘 6% ~ 12%
禁区外近距 弧顶一带 3% ~ 6%
禁区外远距 30 米外 1% ~ 3%

第二步:用历史数据标定

import pandas as pd

# 假设 shots 是一份历史射门记录,含 region 与 is_goal 两列
# shots = pd.DataFrame({'region': [...], 'is_goal': [...]})

def calibrate(shots):
    """用历史样本标定每个区域的真实进球率"""
    result = shots.groupby('region')['is_goal'].agg(['sum', 'count'])
    result['rate'] = result['sum'] / result['count']
    # 样本量太小的区域需要收缩(见下方说明)
    return result

print(calibrate(shots))

第三步:处理小样本问题(关键技巧)

如果某个区域只有 12 次射门、2 个进球,直接算出的 16.7% 极不可靠。必须做「收缩估计(Shrinkage)」

def shrunk_rate(goals, shots, prior_rate, prior_weight=200):
    """收缩估计:样本少时向先验靠拢,样本多时靠近真实值

    goals / shots    : 该区域的实际进球数与射门数
    prior_rate       : 先验进球率(可用全局平均或经验值)
    prior_weight     : 先验的等效样本量,越大越保守
    """
    return (goals + prior_rate * prior_weight) / (shots + prior_weight)

# 示例:该区域 12 脚射门 2 个进球,先验 8%,先验权重 200
print(shrunk_rate(2, 12, 0.08))   # 输出约为 0.083,被拉向先验

为什么收缩估计是关键技术?
这是把「足球数据样本小」这个现实问题处理好的核心手法。
不做收缩,模型会对小样本区域的极端值过度反应;做了收缩,输出会稳定得多。
几乎所有成熟的体育数据模型都会用到类似的平滑技术。

第四步:合成单场 xG

def team_xg(shots, rate_table):
    """把一支球队的所有射门换算为期望进球并求和"""
    total = 0.0
    for region in shots:
        total += rate_table.get(region, 0.02)   # 未知区域用保守默认值
    return total

四、如何做得更接近专业水准

升级方向 做法 效果
增加特征维度 加入射门方式、是否快攻、防守压力 显著提升
改用逻辑回归 把特征映射为 0~1 的概率,而不是分档查表 显著提升
加入防守方信息 考虑防守球员位置与门将站位 中等提升
按联赛分别标定 不同联赛的防守强度不同 中等提升

逻辑回归的接口

from sklearn.linear_model import LogisticRegression

# X: 每次射门的特征矩阵(距离、角度、射门方式等)
# y: 是否进球(0/1)
model = LogisticRegression(max_iter=1000, C=1.0)
model.fit(X_train, y_train)

# 预测每脚射门的进球概率,即 xG
xg_values = model.predict_proba(X_test)[:, 1]

五、两个必须避免的错误

错误一:数据穿越(Look-ahead Bias)

如果你用「整赛季」的数据标定进球率,再回头计算「该赛季第 3 轮」的 xG,就发生了数据穿越——你用了未来信息。

正确做法:标定必须只用「当前时点之前」的数据,或者用「上赛季」的标定表。

错误二:不同口径的 xG 混用

不同模型的 xG 不可直接比较。你的模型算出的 1.8,和别人报告里的 1.8 含义不同。全流程必须用同一套。

六、教学示例与生产数据的差异说明

再次提醒(见 7.1 边界声明):
本教学示例的输出数值,与球小策接口返回的 xG 不会一致。
原因是特征集合、分箱方式、标定周期、收缩参数都有差异。这是有意设计,请勿做逐位比对。
你应当做的是:用你自己的数据,构造适合你自己用途的 xG。

下一步该看什么

常见问题

xG 的本质?把射门转成概率
每一次射门,都可以看作一次「成功概率未知的伯努利试验」。xG 就是这次射门转化为进球的概率估计。 所以问题变成:如何用已知信息估计这个概率?
关于「三个可用信号」,应该怎么理解?
信号 直觉 可用性 射门距离 越近越容易进 需要坐标数据;若无,可用区域分档替代 射门方式 点球 > 单刀 > 头球 > 远射 通常可得 比赛情境 快攻机会质量高于阵地战 可从事件流推断
教学版?用「射门区域分档」构造 xG
如果没有精确坐标,可以把射门区域粗分为 5 档,用历史数据统计每一档的进球率。
第一步?定义区域与初始估计
区域 说明 经验进球率区间 点球点附近 小禁区及点球点周围 30% ~ 40% 禁区内(近) 大禁区内部靠近球门 15% ~ 25% 禁区内(远) 大禁区边缘 6% ~ 12% 禁区外近距 弧顶一带 3% ~ 6% 禁区外远距 30 米外 1% ~ 3%
第二步?用历史数据标定
import pandas as pd # 假设 shots 是一份历史射门记录,含 region 与 is_goal 两列 # shots = pd.DataFrame({'region': [...], 'is_goal': [...]}) def calibrate(shots): """用历史样本标定每个区域的真实进球率""" result = shots.groupb…
第三步?处理小样本问题(关键技巧)
如果某个区域只有 12 次射门、2 个进球,直接算出的 16.7% 极不可靠。必须做「收缩估计(Shrinkage)」: def shrunk_rate(goals, shots, prior_rate, prior_weight=200): """收缩估计:样本少时向先验靠拢,样本多时靠近真实值 goals / shots : 该区域的实际进球数与射门数 prior_rate…
免责声明

本栏目内容为足球数据分析方法的教学与科普,全部指标、模型与推演过程仅用于体育数据科学研究与信息展示,不构成任何形式的预测承诺,亦不涉及任何交易行为。文中「市场参考值」「参考值」等表述,均指公开可得的市场公开信息,仅用于说明数据比对方法。请读者遵守所在地区法律法规,理性认识竞技体育的偶然性。

🏠
赛事
📊
数据
🧩
功能
👤
我的

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续提供更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫打赏

微信打赏二维码

登录

打开微信扫一扫,确认后自动完成登录