PHP项目统计“落叶球”射门成功率方案
“落叶球”(knuckleball / dipping shot)本质是一种射门轨迹特征,要统计它的成功率,核心难点不在 PHP,而在于如何判定一次射门是“落叶球”,下面给你一套可落地的方案。

先定义清楚“落叶球”和“成功率”
落叶球的判定标准(业务规则先行)
建议用可量化的射门事件数据来定义,
| 特征 | 判定条件示例 |
|---|---|
| 射门类型 | shot_type = 'knuckleball'(人工/模型标注) |
| 球速 | 射门初速 > 25 m/s |
| 旋转 | 球旋转速率 < 300 rpm(低旋转是关键) |
| 轨迹下坠 | 飞行末端垂直下落加速度明显(追踪数据) |
| 距离 | 射门距离 > 20m(远射才谈得上落叶) |
最简单可行的 MVP:依赖数据源里已有的 shot_type 或 technique,不自己算物理量。
成功率定义
成功率 = 落叶球射门进球数 / 落叶球射门总数 × 100%
可扩展:
- 射正率 = 射正次数 / 总数
- 预期进球对比 = 实际进球 / xG(衡量是否“超常发挥”)
- 分距离区间、分球员、分赛季统计
数据库表设计
-- 射门事件表
CREATE TABLE shots (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
match_id BIGINT NOT NULL,
player_id BIGINT NOT NULL,
team_id BIGINT NOT NULL,
shot_type VARCHAR(32) NOT NULL, -- 'knuckleball','curve','power'...
distance_m DECIMAL(5,1), -- 射门距离(米)
is_on_target TINYINT(1) DEFAULT 0, -- 是否射正
is_goal TINYINT(1) DEFAULT 0, -- 是否进球
xg DECIMAL(5,3), -- 预期进球
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_type (shot_type),
INDEX idx_player (player_id),
INDEX idx_match (match_id)
);
如果落叶球需要从原始追踪数据推断,再加一张 shot_tracking 表存速度、旋转、轨迹点,用离线任务打标后回写 shot_type。
PHP 统计代码(核心)
<?php
class KnuckleballStats
{
public function __construct(private PDO $pdo) {}
/**
* 统计落叶球射门成功率
* @param array $filter ['season'=>2024,'player_id'=>null,'min_distance'=>null]
*/
public function successRate(array $filter = []): array
{
$where = ["shot_type = :type"];
$params = [':type' => 'knuckleball'];
if (!empty($filter['season'])) {
$where[] = "YEAR(created_at) = :season";
$params[':season'] = $filter['season'];
}
if (!empty($filter['player_id'])) {
$where[] = "player_id = :player_id";
$params[':player_id'] = $filter['player_id'];
}
if (!empty($filter['min_distance'])) {
$where[] = "distance_m >= :min_d";
$params[':min_d'] = $filter['min_distance'];
}
$sql = "SELECT
COUNT(*) AS total,
SUM(is_goal) AS goals,
SUM(is_on_target) AS on_target,
AVG(xg) AS avg_xg,
SUM(xg) AS total_xg
FROM shots
WHERE " . implode(' AND ', $where);
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$total = (int)$row['total'];
$goals = (int)$row['goals'];
return [
'total_shots' => $total,
'goals' => $goals,
'success_rate' => $total > 0 ? round($goals / $total * 100, 2) : 0.0,
'on_target_rate'=> $total > 0 ? round((int)$row['on_target'] / $total * 100, 2) : 0.0,
'avg_xg' => $row['avg_xg'] !== null ? round((float)$row['avg_xg'], 3) : null,
'goals_vs_xg' => ($row['total_xg'] > 0)
? round($goals / (float)$row['total_xg'], 2)
: null,
];
}
/** 按球员分组排行(谁最擅长落叶球) */
public function rankByPlayer(int $season, int $minShots = 5): array
{
$sql = "SELECT p.id, p.name,
COUNT(*) AS shots,
SUM(s.is_goal) AS goals,
ROUND(SUM(s.is_goal)/COUNT(*)*100, 2) AS rate
FROM shots s
JOIN players p ON p.id = s.player_id
WHERE s.shot_type = 'knuckleball' AND YEAR(s.created_at) = :season
GROUP BY p.id, p.name
HAVING shots >= :min
ORDER BY rate DESC, goals DESC";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':season' => $season, ':min' => $minShots]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
调用:
$stats = new KnuckleballStats($pdo); $r = $stats->successRate(['season' => 2024, 'min_distance' => 20]); // ['total_shots'=>128, 'goals'=>11, 'success_rate'=>8.59, ...]
几个务实的注意点
- 判定口径统一比算法花哨更重要,落叶球常被误标,建议制定标注手册,多人标注取一致,或只统计明确标注的样本。
- 样本量问题,落叶球本身占比小,单赛季可能几十次,成功率波动极大,建议给出置信区间(如 Wilson 区间)或至少标注样本量,避免“1射1进=100%”。
- 用 xG 校准,单纯成功率受射门位置影响大,
goals / total_xg > 1才说明“落叶球有额外价值”。 - 性能,数据量大时给
(shot_type, created_at)建联合索引;统计结果可缓存到 Redis 或定时汇总表。 - 若要从原始数据推断落叶球:轨迹物理计算(速度、旋转、下落)更适合用 Python 离线跑批,PHP 只负责查询和展示,不要用 PHP 做大量数值计算。
需要的话,我可以再给你:Wilson 置信区间实现、按距离区间分层统计,或者从 tracking 数据判定落叶球的算法伪代码,你目前的数据里是否有 shot_type 标签,还是需要自己从轨迹判断?