PHP项目评估教练换人得分能力的技术方案
这是一个体育数据分析类的需求,下面给出一套完整的PHP实现思路,涵盖指标定义、数据模型、评分算法、代码实现。

明确"换人得分能力"的业务定义
教练换人得分能力,本质是衡量替补球员上场后对球队得分的正向贡献,核心指标建议包括:
| 指标 | 说明 | 计算方式 |
|---|---|---|
| 换人净胜分 (Net Rating) | 换人后球队得分差 | 换人后得分 - 换人后失分 |
| 替补得分贡献 | 替补上场期间得分 | 统计换人后至下次换人/结束的得分 |
| 换人效率值 | 单位时间得分贡献 | 净胜分 / 上场分钟 |
| 关键换人成功率 | 落后/胶着时换人效果 | 换人后分差缩小/反超次数占比 |
| 换人正负值 (Plus/Minus) | 替补上场期间球队净分 | 期间球队得分 - 对手得分 |
数据模型设计(MySQL)
-- 比赛表
CREATE TABLE matches (
id INT PRIMARY KEY,
home_team_id INT,
away_team_id INT,
match_time DATETIME
);
-- 换人记录表(核心)
CREATE TABLE substitutions (
id INT PRIMARY KEY,
match_id INT,
team_id INT,
coach_id INT,
player_out_id INT, -- 下场球员
player_in_id INT, -- 上场球员
minute INT, -- 换人发生的比赛分钟
score_diff_at_sub INT, -- 换人时球队分差
period TINYINT -- 节次
);
-- 得分事件表(时间线)
CREATE TABLE score_events (
id INT PRIMARY KEY,
match_id INT,
team_id INT,
player_id INT,
minute INT,
points INT, -- 得分数(2/3/1/0)
is_sub_player TINYINT -- 是否为替补得分
);
-- 教练表
CREATE TABLE coaches (
id INT PRIMARY KEY,
name VARCHAR(50),
team_id INT
);
核心评分算法
计算单次换人的净胜分
class SubstitutionEvaluator
{
private PDO $db;
public function __construct(PDO $db)
{
$this->db = $db;
}
/**
* 计算某次换人后的净胜分
* 区间:本次换人 -> 下一次换人(或比赛结束)
*/
public function evaluateSubstitution(int $subId): array
{
// 1. 获取本次换人信息
$sub = $this->getSubstitution($subId);
if (!$sub) {
throw new RuntimeException("换人记录不存在: {$subId}");
}
// 2. 确定统计区间结束点
$nextSubMinute = $this->getNextSubMinute(
$sub['match_id'],
$sub['team_id'],
$sub['minute']
);
$endMinute = $nextSubMinute ?? 48; // 篮球按48分钟,可按项目调整
// 3. 统计区间内双方得分
$ownPoints = $this->getPoints($sub['match_id'], $sub['team_id'], $sub['minute'], $endMinute);
$oppPoints = $this->getPoints($sub['match_id'], $this->getOpponentId($sub), $sub['minute'], $endMinute);
$net = $ownPoints - $oppPoints;
$minutes = max($endMinute - $sub['minute'], 1);
return [
'sub_id' => $subId,
'player_in' => $sub['player_in_id'],
'minute' => $sub['minute'],
'own_points' => $ownPoints,
'opp_points' => $oppPoints,
'net_rating' => $net,
'efficiency' => round($net / $minutes, 3), // 每分钟净胜分
'duration' => $minutes,
];
}
private function getSubstitution(int $subId): ?array
{
$stmt = $this->db->prepare("SELECT * FROM substitutions WHERE id = ?");
$stmt->execute([$subId]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
private function getNextSubMinute(int $matchId, int $teamId, int $afterMinute): ?int
{
$stmt = $this->db->prepare(
"SELECT MIN(minute) FROM substitutions
WHERE match_id = ? AND team_id = ? AND minute > ?"
);
$stmt->execute([$matchId, $teamId, $afterMinute]);
$val = $stmt->fetchColumn();
return $val !== false && $val !== null ? (int)$val : null;
}
private function getPoints(int $matchId, int $teamId, int $from, int $to): int
{
$stmt = $this->db->prepare(
"SELECT COALESCE(SUM(points), 0) FROM score_events
WHERE match_id = ? AND team_id = ? AND minute >= ? AND minute < ?"
);
$stmt->execute([$matchId, $teamId, $from, $to]);
return (int)$stmt->fetchColumn();
}
private function getOpponentId(array $sub): int
{
$stmt = $this->db->prepare(
"SELECT home_team_id, away_team_id FROM matches WHERE id = ?"
);
$stmt->execute([$sub['match_id']]);
$m = $stmt->fetch(PDO::FETCH_ASSOC);
return $m['home_team_id'] == $sub['team_id']
? (int)$m['away_team_id']
: (int)$m['home_team_id'];
}
}
教练综合评分(加权模型)
class CoachSubstitutionScorer
{
private SubstitutionEvaluator $evaluator;
private PDO $db;
// 权重配置(可按项目特点调整)
private array $weights = [
'avg_net_rating' => 0.40, // 平均净胜分
'efficiency' => 0.30, // 每分钟效率
'clutch_success' => 0.20, // 关键换人成功率
'positive_rate' => 0.10, // 正贡献换人占比
];
public function __construct(PDO $db, SubstitutionEvaluator $evaluator)
{
$this->db = $db;
$this->evaluator = $evaluator;
}
public function scoreCoach(int $coachId, ?int $seasonId = null): array
{
$subIds = $this->getCoachSubstitutions($coachId, $seasonId);
if (empty($subIds)) {
return ['coach_id' => $coachId, 'score' => 0, 'detail' => []];
}
$results = [];
foreach ($subIds as $id) {
$results[] = $this->evaluator->evaluateSubstitution($id);
}
$count = count($results);
$avgNet = array_sum(array_column($results, 'net_rating')) / $count;
$avgEff = array_sum(array_column($results, 'efficiency')) / $count;
$posCount = count(array_filter($results, fn($r) => $r['net_rating'] > 0));
$posRate = $posCount / $count;
$clutch = $this->clutchSuccessRate($coachId, $seasonId);
// 归一化处理(示例:以 ±10 分净胜为满分边界)
$normNet = $this->normalize($avgNet, -10, 10);
$normEff = $this->normalize($avgEff, -1, 1);
$finalScore =
$this->weights['avg_net_rating'] * $normNet * 100 +
$this->weights['efficiency'] * $normEff * 100 +
$this->weights['clutch_success'] * $clutch * 100 +
$this->weights['positive_rate'] * $posRate * 100;
return [
'coach_id' => $coachId,
'final_score' => round($finalScore, 2),
'avg_net' => round($avgNet, 2),
'avg_efficiency'=> round($avgEff, 3),
'positive_rate' => round($posRate, 3),
'clutch_rate' => round($clutch, 3),
'sample_size' => $count,
];
}
/**
* 关键换人成功率:仅统计分差 ≤ 5 分时的换人,且换人后缩小/反超分差
*/
private function clutchSuccessRate(int $coachId, ?int $seasonId): float
{
$sql = "SELECT s.id, s.score_diff_at_sub, s.match_id, s.team_id, s.minute
FROM substitutions s
WHERE s.coach_id = ? AND ABS(s.score_diff_at_sub) <= 5";
// (seasonId 过滤略)
$stmt = $this->db->prepare($sql);
$stmt->execute([$coachId]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) return 0.5; // 无样本给中性值
$success = 0;
foreach ($rows as $row) {
$eval = $this->evaluator->evaluateSubstitution($row['id']);
if ($eval['net_rating'] > 0) $success++;
}
return $success / count($rows);
}
private function getCoachSubstitutions(int $coachId, ?int $seasonId): array
{
$stmt = $this->db->prepare("SELECT id FROM substitutions WHERE coach_id = ?");
$stmt->execute([$coachId]);
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
private function normalize(float $v, float $min, float $max): float
{
// 映射到 [0,1]
return max(0, min(1, ($v - $min) / ($max - $min)));
}
}
调用示例
$db = new PDO('mysql:host=localhost;dbname=sports', 'user', 'pass');
$evaluator = new SubstitutionEvaluator($db);
$scorer = new CoachSubstitutionScorer($db, $evaluator);
$result = $scorer->scoreCoach(101);
print_r($result);
/*
Array (
[coach_id] => 101
[final_score] => 72.35
[avg_net] => 3.20
[avg_efficiency] => 0.185
[positive_rate] => 0.62
[clutch_rate] => 0.70
[sample_size] => 45
)
*/
工程化建议
性能优化
- 预计算 + 缓存:换人评分随比赛结束即固定,可写入
coach_scores汇总表,避免每次请求全量重算。 - 定时任务:用 cron 每日批量刷新教练评分、生成排行榜。
- 索引:对
substitutions(coach_id, match_id, minute)和score_events(match_id, team_id, minute)建复合索引。
数据质量
- 回合制项目(足球/篮球)需处理补时,建议用
minute存小数(如 45.5)或额外存second。 - 区分垃圾时间(分差 > 15 或最后 2 分钟且分差大),可降权或剔除,避免刷分。
- 样本量下限:换人次数 < 10 的教练应加置信度衰减(如贝叶斯平滑到联盟均值)。
可扩展性
- 权重
$weights外置到配置文件或数据库,便于按联赛/项目调参。 - 支持多运动:抽象
SportRule接口处理节次、时长、得分规则差异。 - 暴露 REST API:
GET /api/coaches/{id}/substitution-score GET /api/coaches/ranking?season=2024
验证与可视化
- 用历史赛果做回测:教练评分与球队赛季胜率的相关性验证模型有效性。
- 前端用 ECharts 展示教练得分雷达图(净胜分/效率/关键/正贡献率四个维度)。
| 步骤 | 关键产出 |
|---|---|
| 定义指标 | 净胜分、效率、关键换人率、正贡献率 |
| 数据建模 | substitutions + score_events 时间线关联 |
| 单次换人评估 | SubstitutionEvaluator |
| 教练综合评分 | 加权归一化模型 CoachSubstitutionScorer |
| 工程落地 | 预计算、索引、缓存、回测验证 |
核心思路:以"换人时间点 → 下次换人"为统计窗口,用时间线得分事件计算净胜分,再按维度加权得到教练的可解释评分。 权重和归一化边界是业务调参的核心抓手。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播