Python案例:统计射正数能说明进攻质量吗?
问题的本质
射正数(Shots on Target)≠ 进攻质量,它只是一个过程指标,需要结合其他数据才能推断进攻质量,下面用Python模拟/分析来演示这个结论。

为什么射正数不能单独说明进攻质量
| 问题 | 说明 |
|---|---|
| 射正 ≠ 进球 | 射正可能被门将扑出、被门线解围 |
| 射正 ≠ 威胁 | 30米外软绵绵的射门也算"射正" |
| 忽略射门位置 | 禁区内1次射正 > 禁区外5次射正 |
| 忽略xG(预期进球) | 现代足球的核心指标 |
| 忽略控球/推进 | 可能只是防守反击中的零星射门 |
更合理的进攻质量指标组合:
- xG(Expected Goals,预期进球)
- xT(Expected Threat)
- 射门位置分布(禁区内/外)
- 大机会(Big Chances)创造数
- 进攻三区传球成功率
Python 演示案例
案例1:用模拟数据说明"射正数会骗人"
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import pearsonr
np.random.seed(42)
# 模拟10支球队一个赛季的数据
n_teams = 10
teams = [f"Team_{i+1}" for i in range(n_teams)]
# 假设"真实进攻质量"是一个隐藏变量
true_quality = np.random.uniform(0.3, 1.0, n_teams)
# 射正数:受质量影响 + 噪声(有些队靠远射刷数据)
shots_on_target = (true_quality * 80 + np.random.normal(0, 25, n_teams)).clip(20, 150)
# 进球数:更接近真实质量(但仍受运气影响)
goals = (true_quality * 50 + np.random.normal(0, 8, n_teams)).clip(10, 90)
# xG:最接近真实质量的指标
xg = (true_quality * 55 + np.random.normal(0, 5, n_teams)).clip(10, 95)
df = pd.DataFrame({
"球队": teams,
"射正数": shots_on_target.round(0),
"进球": goals.round(0),
"xG": xg.round(1),
"真实质量": true_quality.round(2)
})
print(df.to_string(index=False))
# 相关性分析
r_sot_goals, _ = pearsonr(df["射正数"], df["进球"])
r_xg_goals, _ = pearsonr(df["xG"], df["进球"])
r_sot_quality, _ = pearsonr(df["射正数"], df["真实质量"])
r_xg_quality, _ = pearsonr(df["xG"], df["真实质量"])
print(f"\n射正数 vs 进球 相关系数: {r_sot_goals:.3f}")
print(f"xG vs 进球 相关系数: {r_xg_goals:.3f}")
print(f"射正数 vs 真实质量 相关系数: {r_sot_quality:.3f}")
print(f"xG vs 真实质量 相关系数: {r_xg_quality:.3f}")
典型输出(可能):
射正数 vs 进球 相关系数: 0.72
xG vs 进球 相关系数: 0.95
射正数 vs 真实质量 相关系数: 0.68
xG vs 真实质量 相关系数: 0.93
xG 与真实进攻质量的相关性远高于射正数。
案例2:可视化对比
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
axes[0].scatter(df["射正数"], df["进球"], color="steelblue", s=80)
axes[0].set_xlabel("射正数"); axes[0].set_ylabel("进球")
axes[0].set_title(f"射正 vs 进球 (r={r_sot_goals:.2f})")
axes[1].scatter(df["xG"], df["进球"], color="seagreen", s=80)
axes[1].set_xlabel("xG"); axes[1].set_ylabel("进球")
axes[1].set_title(f"xG vs 进球 (r={r_xg_goals:.2f})")
axes[2].scatter(df["射正数"], df["xG"], color="coral", s=80)
axes[2].set_xlabel("射正数"); axes[2].set_ylabel("xG")
axes[2].set_title("射正 vs xG(散乱=射正不代表质量)")
plt.tight_layout()
plt.show()
案例3:用真实数据(伪代码 / 说明思路)
真实场景可以用 statsbombpy(开源免费的世界杯数据):
# pip install statsbombpy
from statsbombpy import sb
# 取2022世界杯决赛的所有射门事件
events = sb.events(match_id=3869685) # 阿根廷 vs 法国
shots = events[events["type"] == "Shot"].copy()
# 关键字段
# shot_outcome: Goal / Saved / Off T / Blocked / Post / Wayward
# shot_statsbomb_xg: 预期进球
# location: 射门坐标
# 统计射正数
on_target = shots[shots["shot_outcome"].isin(["Goal", "Saved"])]
print(f"总射门: {len(shots)}")
print(f"射正数: {len(on_target)}")
print(f"xG总和: {shots['shot_statsbomb_xg'].sum():.2f}")
# 分组:射正 vs 非射正 的平均 xG
print("\n平均 xG 对比:")
print(shots.groupby(shots["shot_outcome"].isin(["Goal","Saved"]))["shot_statsbomb_xg"].mean())
更严谨的分析方法
多变量回归:预测"进球/胜率"时,射正数贡献多少?
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
X = df[["射正数", "xG"]].values
y = df["进球"].values
model = LinearRegression().fit(X, y)
print("系数(射正, xG):", model.coef_)
print("R²:", r2_score(y, model.predict(X)))
# 如果只放射正数
model_sot = LinearRegression().fit(df[["射正数"]], y)
print("只用射正数的 R²:", r2_score(y, model_sot.predict(df[["射正数"]])))
# 如果只放 xG
model_xg = LinearRegression().fit(df[["xG"]], y)
print("只用 xG 的 R²:", r2_score(y, model_xg.predict(df[["xG"]])))
预期结果:xG 单独解释的方差远大于 射正数。
控制射门位置后,射正数的边际贡献
# 特征工程:区分禁区内/外射正
df["禁区内射正"] = (df["射正数"] * np.random.uniform(0.4, 0.8, n_teams)).round()
df["禁区外射正"] = df["射正数"] - df["禁区内射正"]
X2 = df[["禁区内射正", "禁区外射正", "xG"]]
model2 = LinearRegression().fit(X2, y)
for name, coef in zip(X2.columns, model2.coef_):
print(f"{name}: {coef:.3f}")
通常发现:禁区内射正系数高、禁区外射正系数接近0,说明位置比"是否射正"更重要。
- 射正数与进球有正相关,但相关性低于 xG。
- 射正数是"过程指标",不能直接等同于进攻质量:它忽略了射门位置、机会质量、对手强度。
- 正确的做法:用 xG / xT / Big Chances 等能反映"机会质量"的指标,并将射正数作为辅助参考。
- 足球分析黄金原则:
"射正多不代表进攻强,可能只是浪射多;射正少不代表进攻弱,可能是效率高。"
用一句话总结:射正数只能说明"打到了门框范围内",不能说明"这次进攻是好机会"。