PHP项目:统计界外球进攻威胁次数
这是一个足球数据分析的需求,下面我给你一个完整的实现方案。

需求拆解
"界外球进攻威胁"需要定义清楚:
- 界外球:throw-in 事件
- 进攻威胁:界外球后 N 秒内(或 N 次传球内)发生的威胁事件,如射门、进球、进入禁区、关键传球等
数据库设计
-- 比赛事件表
CREATE TABLE match_events (
id INT PRIMARY KEY AUTO_INCREMENT,
match_id INT NOT NULL,
team_id INT NOT NULL,
player_id INT,
event_type VARCHAR(32) NOT NULL, -- throw_in, pass, shot, goal, cross, corner...
event_time INT NOT NULL, -- 秒
x_coord DECIMAL(5,2), -- 场地坐标 0-100
y_coord DECIMAL(5,2),
end_x DECIMAL(5,2),
end_y DECIMAL(5,2),
outcome VARCHAR(16), -- success / fail
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_match_time (match_id, event_time),
INDEX idx_team_type (match_id, team_id, event_type)
);
-- 威胁事件定义表(可配置)
CREATE TABLE threat_rules (
id INT PRIMARY KEY AUTO_INCREMENT,
event_type VARCHAR(32) NOT NULL,
min_x DECIMAL(5,2), -- 事件终点 x 坐标阈值
weight DECIMAL(3,2) DEFAULT 1.0, -- 威胁权重
description VARCHAR(128)
);
INSERT INTO threat_rules (event_type, min_x, weight, description) VALUES
('shot', 70, 1.0, '射门'),
('goal', 70, 1.5, '进球'),
('cross', 80, 0.6, '传中'),
('through_ball', 60, 0.8, '直塞');
核心统计逻辑
思路:遍历每支球队的界外球事件,检查其后一个时间窗口内(如 15 秒)是否发生威胁事件。
<?php
class ThrowInThreatAnalyzer
{
private PDO $pdo;
private int $windowSeconds;
private array $threatRules;
public function __construct(PDO $pdo, int $windowSeconds = 15)
{
$this->pdo = $pdo;
$this->windowSeconds = $windowSeconds;
$this->threatRules = $this->loadThreatRules();
}
private function loadThreatRules(): array
{
$stmt = $this->pdo->query("SELECT * FROM threat_rules");
$rules = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
$rules[$r['event_type']] = $r;
}
return $rules;
}
/**
* 统计一场比赛某队的界外球威胁次数
*/
public function countForMatch(int $matchId, int $teamId): array
{
// 1. 取出该队所有界外球
$stmt = $this->pdo->prepare(
"SELECT id, event_time FROM match_events
WHERE match_id = ? AND team_id = ? AND event_type = 'throw_in'
ORDER BY event_time"
);
$stmt->execute([$matchId, $teamId]);
$throwIns = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($throwIns)) {
return ['total_throw_ins' => 0, 'threats' => 0, 'threat_throw_ins' => []];
}
$threatEvents = $this->fetchThreatEvents($matchId, $teamId);
$threatThrowIns = [];
$threatScore = 0.0;
foreach ($throwIns as $ti) {
$start = (int)$ti['event_time'];
$end = $start + $this->windowSeconds;
$hits = array_filter($threatEvents, function ($e) use ($start, $end) {
$t = (int)$e['event_time'];
return $t > $start && $t <= $end;
});
if (!empty($hits)) {
$weighted = 0.0;
foreach ($hits as $h) {
$rule = $this->threatRules[$h['event_type']] ?? null;
if ($rule) $weighted += (float)$rule['weight'];
}
$threatThrowIns[] = [
'throw_in_id' => (int)$ti['id'],
'time' => $start,
'threat_count' => count($hits),
'weighted_score'=> round($weighted, 2),
'events' => array_values($hits),
];
$threatScore += $weighted;
}
}
return [
'match_id' => $matchId,
'team_id' => $teamId,
'total_throw_ins' => count($throwIns),
'threats' => count($threatThrowIns),
'threat_rate' => round(count($threatThrowIns) / count($throwIns), 3),
'threat_score' => round($threatScore, 2),
'threat_throw_ins'=> $threatThrowIns,
];
}
private function fetchThreatEvents(int $matchId, int $teamId): array
{
$types = array_keys($this->threatRules);
if (empty($types)) return [];
$in = implode(',', array_fill(0, count($types), '?'));
$sql = "SELECT id, event_type, event_time, end_x, end_y
FROM match_events
WHERE match_id = ? AND team_id = ?
AND event_type IN ($in)
AND (end_x IS NULL OR end_x >= ?)
ORDER BY event_time";
$stmt = $this->pdo->prepare($sql);
// 只取规则中最低的 x 阈值做粗过滤
$minX = min(array_column($this->threatRules, 'min_x'));
$stmt->execute(array_merge([$matchId, $teamId], $types, [$minX]));
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* 批量统计整个赛季某队
*/
public function countForTeamSeason(int $teamId, int $seasonId): array
{
$stmt = $this->pdo->prepare(
"SELECT id FROM matches WHERE season_id = ? AND (home_team_id = ? OR away_team_id = ?)"
);
$stmt->execute([$seasonId, $teamId, $teamId]);
$matchIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
$result = ['total_throw_ins'=>0, 'threats'=>0, 'threat_score'=>0.0, 'matches'=>[]];
foreach ($matchIds as $mid) {
$r = $this->countForMatch((int)$mid, $teamId);
$result['total_throw_ins'] += $r['total_throw_ins'];
$result['threats'] += $r['threats'];
$result['threat_score'] += $r['threat_score'];
$result['matches'][] = $r;
}
$result['threat_score'] = round($result['threat_score'], 2);
$result['threat_rate'] = $result['total_throw_ins']
? round($result['threats'] / $result['total_throw_ins'], 3) : 0;
return $result;
}
}
调用示例
$pdo = new PDO('mysql:host=127.0.0.1;dbname=football;charset=utf8mb4', 'user', 'pass');
$analyzer = new ThrowInThreatAnalyzer($pdo, windowSeconds: 15);
// 单场
$r = $analyzer->countForMatch(matchId: 1001, teamId: 5);
print_r($r);
// 赛季累计
$season = $analyzer->countForTeamSeason(teamId: 5, seasonId: 2024);
echo "界外球总数: {$season['total_throw_ins']}\n";
echo "威胁界外球: {$season['threats']}\n";
echo "威胁率: {$season['threat_rate']}\n";
echo "加权威胁分: {$season['threat_score']}\n";
性能优化建议
| 场景 | 优化方案 |
|---|---|
| 大量比赛批量统计 | 用一条 JOIN 查询一次性拉出所有事件的"下一次威胁事件",避免 N+1 |
| 高频查询 | 把结果预计算写入 throw_in_threat_stats 汇总表,赛后可异步计算 |
| 事件时间跨半场 | 注意半场间存在 15 分钟间隔,需按 half 字段或 period 分段,避免跨半场误判 |
| 事件粒度 | 若数据源提供 xG(预期进球),直接用 SUM(xG) 替代权重更准确 |
SQL 版一次性方案(推荐大数据量)
SELECT
t.id AS throw_in_id,
t.event_time,
COUNT(e.id) AS threat_count
FROM match_events t
LEFT JOIN match_events e
ON e.match_id = t.match_id
AND e.team_id = t.team_id
AND e.event_type IN ('shot','goal','cross','through_ball')
AND e.event_time > t.event_time
AND e.event_time <= t.event_time + 15
WHERE t.match_id = :mid
AND t.team_id = :tid
AND t.event_type = 'throw_in'
GROUP BY t.id, t.event_time
HAVING threat_count > 0;
需要你确认的关键点
- 威胁定义:是"射门/进球"算威胁,还是也要包含"进入前场 30 米区域"?
- 时间窗口:15 秒是否合适?(一般 10–20 秒,界外球直接掷入禁区可单独处理)
- 半场边界处理:是否需要防止跨半场 / 跨加时?
- 数据来源:自有采集、第三方 API(StatsBomb / Opta / 雷速)?字段名需对齐。
- "进攻威胁"是否要求同一控球阶段:更严谨的做法是追踪 possession 链,而非固定时间窗口。
告诉我你的实际字段结构和威胁定义,我可以把上面的代码改成直接可用的版本。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播