PHP 项目结合 xGA 评估防守表现
先明确一下场景,因为 "xGA" 在不同领域含义不同,最常见的是足球数据里的 Expected Goals Against(预期失球),也可能是安全领域的某种评估,下面主要按足球/体育数据类 PHP 项目来讲,如果你指的是别的,可以补充说明。

理解 xGA 的本质
xGA = 对手所有射门的 xG 之和,衡量"球队让对手获得了多少高质量机会"。
- xGA 低 → 防守好(限制了对手机会质量)
- xGA 高 → 防守差(让对手频繁获得好机会)
- 对比实际失球 GA:xGA < GA 说明门将/运气差;xGA > GA 说明门将超神或对手射术差
核心指标组合: | 指标 | 含义 | |------|------| | xGA | 预期失球 | | GA | 实际失球 | | xGA/90 | 场均预期失球 | | PSxG | 射门后预期进球(考虑射门质量) | | xGA - GA | 防守超额表现 |
PHP 项目架构设计
数据层
-- 射门事件表(核心)
CREATE TABLE shots (
id BIGINT PRIMARY KEY,
match_id INT,
team_id INT, -- 射门方
opponent_id INT, -- 防守方
player_id INT,
minute INT,
x DECIMAL(5,2), -- 球场坐标
y DECIMAL(5,2),
body_part VARCHAR(20),
situation VARCHAR(30), -- open_play / set_piece / penalty
xg DECIMAL(5,4), -- 该次射门预期进球
is_goal TINYINT,
created_at TIMESTAMP
);
-- 预聚合表(性能关键)
CREATE TABLE team_defense_stats (
team_id INT,
season VARCHAR(10),
matches_played INT,
xga DECIMAL(8,2),
ga INT,
xga_per90 DECIMAL(5,3),
shots_faced INT,
updated_at TIMESTAMP
);
xG 模型(射门概率计算)
如果自己训练模型,可用逻辑回归,简化版:
<?php
class XGCalculator
{
// 简化示例,实际系数需用历史数据拟合
private array $weights = [
'distance' => -0.12,
'angle' => 0.35,
'is_header' => -0.40,
'is_penalty' => 2.10,
'is_counter' => 0.25,
'is_big_chance' => 0.90,
'intercept' => 0.85,
];
public function calculate(array $shot): float
{
$distance = $this->distance($shot['x'], $shot['y']);
$angle = $this->angle($shot['x'], $shot['y']);
$logit = $this->weights['intercept']
+ $this->weights['distance'] * $distance
+ $this->weights['angle'] * $angle
+ ($shot['body_part'] === 'head' ? $this->weights['is_header'] : 0)
+ ($shot['situation'] === 'penalty' ? $this->weights['is_penalty'] : 0)
+ ($shot['is_counter'] ?? 0 ? $this->weights['is_counter'] : 0)
+ ($shot['is_big_chance'] ?? 0 ? $this->weights['is_big_chance'] : 0);
return round(1 / (1 + exp(-$logit)), 4);
}
private function distance(float $x, float $y): float
{
// 以球门中心(50, 50)为原点,单位:米
return sqrt(pow($x - 50, 2) + pow($y - 50, 2)) * 1.05;
}
private function angle(float $x, float $y): float
{
// 射门可见球门的角度
$goalWidth = 7.32;
$px = $x - 50;
$py = $y - 50;
if (abs($px) < 0.01) return M_PI / 2;
return atan2($goalWidth * abs($px), $px * $px + $py * $py);
}
}
防守评估服务
<?php
class DefenseEvaluator
{
public function __construct(
private PDO $db,
private XGCalculator $xgCalc
) {}
/** 计算某队某赛季的 xGA */
public function calculateXGA(int $teamId, string $season): array
{
$sql = "SELECT s.xg, s.is_goal, s.situation
FROM shots s
JOIN matches m ON s.match_id = m.id
WHERE s.opponent_id = :team_id
AND m.season = :season";
$stmt = $this->db->prepare($sql);
$stmt->execute(['team_id' => $teamId, 'season' => $season]);
$shots = $stmt->fetchAll(PDO::FETCH_ASSOC);
$xga = 0.0;
$ga = 0;
$setPieceXga = 0.0;
foreach ($shots as $shot) {
$xga += (float)$shot['xg'];
$ga += (int)$shot['is_goal'];
if (in_array($shot['situation'], ['corner', 'free_kick', 'penalty'])) {
$setPieceXga += (float)$shot['xg'];
}
}
$matches = $this->getMatchCount($teamId, $season);
return [
'team_id' => $teamId,
'season' => $season,
'matches' => $matches,
'xga' => round($xga, 2),
'xga_per90' => $matches ? round($xga / $matches, 3) : 0,
'ga' => $ga,
'ga_per90' => $matches ? round($ga / $matches, 3) : 0,
'xga_over_ga' => round($xga - $ga, 2), // 防守超额表现
'set_piece_xga' => round($setPieceXga, 2),
'open_play_xga' => round($xga - $setPieceXga, 2),
];
}
/** 联赛排名与百分位 */
public function rankInLeague(string $season, string $league): array
{
$sql = "SELECT s.opponent_id AS team_id,
SUM(s.xg) AS xga, SUM(s.is_goal) AS ga
FROM shots s
JOIN matches m ON s.match_id = m.id
WHERE m.season = :season AND m.league = :league
GROUP BY s.opponent_id
ORDER BY xga ASC";
$stmt = $this->db->prepare($sql);
$stmt->execute(['season' => $season, 'league' => $league]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/** PSxG(排除门将扑救后的机会质量) */
public function calculatePSxG(int $teamId, string $season): float
{
// 需要射门后的追踪数据,这里示意接口
$stmt = $this->db->prepare(
"SELECT AVG(post_shot_xg) FROM shots
WHERE opponent_id = ? AND season = ?"
);
return (float)$stmt->execute([$teamId, $season]);
}
private function getMatchCount(int $teamId, string $season): int
{
$stmt = $this->db->prepare(
"SELECT COUNT(*) FROM matches
WHERE (home_id = ? OR away_id = ?) AND season = ?"
);
$stmt->execute([$teamId, $teamId, $season]);
return (int)$stmt->fetchColumn();
}
}
使用示例
$db = new PDO('mysql:host=localhost;dbname=football', 'user', 'pass');
$evaluator = new DefenseEvaluator($db, new XGCalculator());
$stats = $evaluator->calculateXGA(teamId: 12, season: '2024/25');
echo "xGA: {$stats['xga']} (场均 {$stats['xga_per90']})\n";
echo "实际失球: {$stats['ga']} (场均 {$stats['ga_per90']})\n";
echo "防守超额表现: {$stats['xga_over_ga']}\n";
评估维度建议
结合 xGA 做防守评估时,建议从这几个角度切:
- 整体防守:xGA/90 联赛排名
- 运动战 vs 定位球:分开算,定位球差往往是体系问题
- 机会质量分层:让对手打的 shots 里有多少是 big chance(>0.3 xG)
- 门将贡献:PSxG - GA,衡量门将超越预期
- 时间趋势:近 6 场 xGA 对比全季,判断状态走势
- 对手强度调整:面对强队 xGA 偏高是正常的,可做对手强度加权
可视化与 API 输出
// 输出给前端的 JSON
header('Content-Type: application/json');
echo json_encode([
'xga' => $stats['xga'],
'xga_per90' => $stats['xga_per90'],
'percentile' => $this->percentile($stats['xga'], $leagueData),
'breakdown' => [
'open_play' => $stats['open_play_xga'],
'set_piece' => $stats['set_piece_xga'],
],
'trend' => $this->last6MatchesTrend($teamId, $season),
], JSON_PRETTY_PRINT);
性能与工程建议
- 预计算:xGA 不要每次都从 shots 表现算,用定时任务(如 Laravel Scheduler)每天刷新
team_defense_stats - 缓存:联赛榜单用 Redis,TTL 1 小时
- 数据源:StatsBomb、Opta、Understat 都有 xG 数据,可直接采购而非自研模型
- 验证:自研 xG 模型要用 Brier Score 或 Log Loss 评估校准度
如果你的 "xGA" 不是足球领域(比如是某种安全评估/游戏评估/网络攻防指标),告诉我具体指代,我按那个方向重新给方案。