php项目如何评估教练换人得分能力?

wen PHP项目 3

PHP项目评估教练换人得分能力的技术方案

这是一个体育数据分析类的需求,下面给出一套完整的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
工程落地 预计算、索引、缓存、回测验证

核心思路:以"换人时间点 → 下次换人"为统计窗口,用时间线得分事件计算净胜分,再按维度加权得到教练的可解释评分。 权重和归一化边界是业务调参的核心抓手。

上一篇php项目认为青训球员上场影响几何?

下一篇当前分类已是最新一篇

抱歉,评论功能暂时关闭!

友情链接攻略窝 · 礼包窝 · 配置窝 · 换算窝 · 算法窝 · 查文窝 · 手册窝 · 问答窝 · 全文搜 · 手册库
相关站点礼包窝 · 查文窝