本文目录导读:

在PHP项目中结合xGA(预期进球助攻,Expected Goals Assisted)来评估防守表现,需要理解一个核心逻辑:xGA本质上是衡量球队防守端让对手获得了多少质量的射门机会,下面我从数据接入、指标计算、防守评估三个层面来说明。
xGA与防守的关系
| 指标 | 含义 | 防守视角 |
|---|---|---|
| xGA | 对手每次射门的预期进球值之和 | 值越低,防守越好 |
| xGA/90 | 每90分钟让对手获得的xG | 衡量防守稳定性 |
| xGA per shot | 每次被射门的平均xG | 衡量防守质量(是否逼迫对手远射) |
| PSxG-xGA | 实际丢球 - xGA | 衡量门将表现 |
PHP项目中的技术架构
数据源接入
// 示例:使用 Understat / FBref / Opta 数据
class XGADataProvider
{
private $apiKey;
private $baseUrl = 'https://api.football-data.org/v4/';
public function getMatchShots(int $matchId): array
{
// 获取该场比赛所有射门事件(含坐标、部位、助攻类型)
$response = $this->httpGet("/matches/{$matchId}/shots");
return json_decode($response, true)['shots'] ?? [];
}
private function httpGet(string $path): string
{
$ch = curl_init($this->baseUrl . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["X-Auth-Token: {$this->apiKey}"],
]);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
}
xGA计算模型(简化版)
如果没有现成的xG模型,需要自己训练或用公开模型,PHP适合做聚合计算,不适合训练模型。
class XGACalculator
{
/**
* 单次射门的xG值(简化版,实际应使用逻辑回归/GBDT模型输出)
* 特征:距离、角度、射门部位、助攻类型、防守压力
*/
public function calculateShotXG(array $shot): float
{
$distance = $this->calcDistance($shot['x'], $shot['y']);
$angle = $this->calcAngle($shot['x'], $shot['y']);
// 简化公式:距离越近、角度越大,xG越高
$xg = 1 / (1 + exp(-(0.1 * (30 - $distance) + 0.05 * $angle)));
// 部位修正
$xg *= match($shot['body_part']) {
'head' => 0.85,
'foot' => 1.0,
default => 0.9,
};
return round(min($xg, 0.99), 4);
}
/**
* 计算整场比赛某队的xGA
*/
public function calculateMatchXGA(array $opponentShots): float
{
return array_sum(array_map(
fn($s) => $this->calculateShotXG($s),
array_filter($opponentShots, fn($s) => !$s['is_own_goal'])
));
}
private function calcDistance(float $x, float $y): float
{
// 假设球门中心在 (100, 50)
return sqrt(pow(100 - $x, 2) + pow(50 - $y, 2));
}
private function calcAngle(float $x, float $y): float
{
// 球门两柱之间的张角
$goalTop = atan2(66 - $y, 100 - $x);
$goalBottom = atan2(34 - $y, 100 - $x);
return abs(rad2deg($goalTop - $goalBottom));
}
}
数据库设计
CREATE TABLE matches (
id INT PRIMARY KEY,
home_team_id INT,
away_team_id INT,
match_date DATE
);
CREATE TABLE shots (
id INT PRIMARY KEY,
match_id INT,
team_id INT,
player_id INT,
x DECIMAL(5,2),
y DECIMAL(5,2),
body_part VARCHAR(20),
situation VARCHAR(30), -- open_play / set_piece / counter
xg DECIMAL(5,4),
is_goal TINYINT(1)
);
CREATE TABLE team_defensive_stats (
id INT PRIMARY KEY,
team_id INT,
match_id INT,
xga DECIMAL(5,2),
shots_faced INT,
xga_per_shot DECIMAL(5,4),
xga_per_90 DECIMAL(5,2),
goals_conceded INT,
psxg_minus_xga DECIMAL(5,2) -- 门将扑救表现
);
防守表现评估服务
class DefenseEvaluator
{
public function __construct(
private PDO $db,
private XGACalculator $calc
) {}
/**
* 计算某队某场比赛的防守指标
*/
public function evaluateMatch(int $teamId, int $matchId): array
{
$stmt = $this->db->prepare(
"SELECT * FROM shots WHERE match_id = ? AND team_id != ?"
);
$stmt->execute([$matchId, $teamId]);
$opponentShots = $stmt->fetchAll(PDO::FETCH_ASSOC);
$xga = $this->calc->calculateMatchXGA($opponentShots);
$shotCount = count($opponentShots);
$goalsConceded = array_sum(array_column($opponentShots, 'is_goal'));
return [
'xga' => $xga,
'shots_faced' => $shotCount,
'xga_per_shot' => $shotCount ? $xga / $shotCount : 0,
'goals_conceded'=> $goalsConceded,
'psxg_minus_xga'=> $goalsConceded - $xga, // 负值=门将超常发挥
];
}
/**
* 赛季防守评分(综合多个维度)
*/
public function seasonDefenseScore(int $teamId, int $seasonId): float
{
$stats = $this->db->query(
"SELECT AVG(xga) as avg_xga, AVG(xga_per_shot) as avg_xgaps
FROM team_defensive_stats
WHERE team_id = {$teamId}"
)->fetch();
// 归一化到 0-100 分
// 假设联赛平均 xga=1.4, 标准差=0.5
$xgaScore = max(0, min(100, 100 - (($stats['avg_xga'] - 1.4) / 0.5) * 20));
$qualityScore = max(0, min(100, 100 - ($stats['avg_xgaps'] - 0.10) * 500));
return round(0.7 * $xgaScore + 0.3 * $qualityScore, 2);
}
}
防守表现的深度评估维度
单纯看xGA绝对值不够,应结合以下维度:
分场景拆分
$situations = ['open_play', 'set_piece', 'counter', 'penalty'];
foreach ($situations as $sit) {
$xgaBySit[$sit] = $this->calc->calculateMatchXGA(
array_filter($opponentShots, fn($s) => $s['situation'] === $sit)
);
}
// 定位球xGA高 → 防空/盯人问题
// 反击xGA高 → 攻防转换防守差
防守区域热图
统计对手射门分布(禁区中路 vs 边路 vs 远射),判断是防线被打穿还是只能远射。
时间维度
按15分钟窗口聚合xGA,识别比赛末段崩盘或开局慢热。
对比基准
// 与联赛均值、对手均值对比 $xgaPercentile = $this->getLeaguePercentile($teamId, 'xga'); // 90分位 = 防守最差10%,10分位 = 防守最好10%
实战注意事项
-
xG模型的选择至关重要:不同数据商的xG模型差异可达±0.3/场,务必统一数据源,推荐使用 Understat、FBref(StatsBomb)、Opta 的公开数据。
-
PHP不适合做ML训练:建议用 Python 训练xG模型(LightGBM/XGBoost),导出为 ONNX 或系数表,PHP只做推理和聚合。
-
样本量问题:单场xGA波动大,至少需要10场以上才能稳定评估防守水平,可以引入贝叶斯平滑。
-
区分“防守好”与“门将好”:
goals_conceded - xGA反映门将表现,防守体系的评价应只看xGA本身。 -
缓存与性能:xGA计算涉及大量射门事件,建议将赛季聚合结果物化到
team_defensive_stats表,实时查询直接读表。
推荐技术栈组合
数据采集 → Python (requests + pandas)
xG模型 → Python (LightGBM) → 导出 JSON/ONNX
存储 → MySQL/PostgreSQL
聚合计算 → PHP (Laravel/Symfony + 队列)
API输出 → PHP + Redis缓存
可视化 → 前端 (ECharts/Chart.js) 消费PHP API
这样PHP项目可以专注于业务逻辑、API服务、报表生成,把重计算交给Python,两者通过数据库或消息队列解耦。