高球传中争顶成功率统计 - Java案例
业务背景
足球数据分析中,"高球传中争顶成功率"是评估球队/球员空中对抗能力的重要指标,本文用Java实现一个完整的统计模块。

核心定义:
- 传中事件:从边路/肋部起高球传入禁区
- 争顶事件:接球方与防守方球员同时起跳争抢落点
- 争顶成功:进攻方球员在争顶中赢下落点(形成射门/二次进攻)
领域模型设计
import java.time.LocalDateTime;
import java.util.*;
/** 球员 */
public class Player {
private final String id;
private final String name;
private final String teamId;
private final String position; // ST/Winger/CB...
public Player(String id, String name, String teamId, String position) {
this.id = id; this.name = name;
this.teamId = teamId; this.position = position;
}
public String getId() { return id; }
public String getName() { return name; }
public String getTeamId() { return teamId; }
public String getPosition() { return position; }
}
/** 事件类型 */
public enum EventType {
CROSS_HIGH, // 高球传中
AERIAL_DUEL, // 空中争顶
SHOT
}
/** 比赛事件 */
public class MatchEvent {
private final String id;
private final String matchId;
private final EventType type;
private final String playerId; // 事件主体
private final String opponentId; // 争顶对手(可空)
private final String teamId;
private final boolean success; // 是否成功
private final int minute;
private final LocalDateTime timestamp;
// 关联:传中事件 -> 后续争顶事件ID
private String crossRefId;
public MatchEvent(String id, String matchId, EventType type,
String playerId, String opponentId, String teamId,
boolean success, int minute) {
this.id = id; this.matchId = matchId; this.type = type;
this.playerId = playerId; this.opponentId = opponentId;
this.teamId = teamId; this.success = success;
this.minute = minute;
this.timestamp = LocalDateTime.now();
}
// getters / setters 省略(下同)
public String getId() { return id; }
public EventType getType() { return type; }
public String getPlayerId() { return playerId; }
public String getTeamId() { return teamId; }
public boolean isSuccess() { return success; }
public int getMinute() { return minute; }
public String getCrossRefId(){ return crossRefId; }
public void setCrossRefId(String s){ this.crossRefId = s; }
}
统计服务实现
/** 传中争顶统计结果 */
public class CrossAerialStat {
private final String playerId;
private final String playerName;
private int crossTotal; // 高球传中总数
private int crossSuccess; // 高球传中成功(找到己方)
private int duelTotal; // 争顶总数(该球员作为争顶方)
private int duelWon; // 争顶获胜
private int crossDuelTotal; // 由传中引发的争顶
private int crossDuelWon; // 由传中引发的争顶获胜
public CrossAerialStat(String playerId, String playerName) {
this.playerId = playerId; this.playerName = playerName;
}
public void addCross(boolean ok){ crossTotal++; if(ok) crossSuccess++; }
public void addDuel(boolean ok, boolean fromCross){
duelTotal++; if(ok) duelWon++;
if(fromCross){ crossDuelTotal++; if(ok) crossDuelWon++; }
}
/** 传中整体成功率 */
public double getCrossSuccessRate(){
return crossTotal == 0 ? 0.0 : round((double)crossSuccess / crossTotal);
}
/** 争顶胜率 */
public double getDuelWinRate(){
return duelTotal == 0 ? 0.0 : round((double)duelWon / duelTotal);
}
/** 传中→争顶转化后的胜率(核心指标) */
public double getCrossDuelWinRate(){
return crossDuelTotal == 0 ? 0.0
: round((double)crossDuelWon / crossDuelTotal);
}
private double round(double v){ return Math.round(v*1000)/1000.0; }
@Override public String toString(){
return String.format(
"%-8s 传中:%2d/%2d(%.1f%%) 争顶:%2d/%2d(%.1f%%) 传中争顶:%2d/%2d(%.1f%%)",
playerName, crossSuccess, crossTotal, getCrossSuccessRate()*100,
duelWon, duelTotal, getDuelWinRate()*100,
crossDuelWon, crossDuelTotal, getCrossDuelWinRate()*100);
}
// getters...
public int getCrossTotal() { return crossTotal; }
public int getCrossSuccess() { return crossSuccess; }
public int getDuelTotal() { return duelTotal; }
public int getDuelWon() { return duelWon; }
public int getCrossDuelTotal() { return crossDuelTotal; }
public int getCrossDuelWon() { return crossDuelWon; }
public String getPlayerId() { return playerId; }
}
核心算法
import java.util.*;
import java.util.stream.Collectors;
public class CrossAerialCalculator {
/**
* 统计每个球员的高球传中争顶数据
* 关键:需要先识别"传中引发的争顶"——通常表现为:
* 1) 有 crossRefId 直接指向该传中事件
* 2) 或时间窗口内(如传中后3秒内)且同一进攻半场发生的争顶
*/
public Map<String, CrossAerialStat> calculate(
List<MatchEvent> events,
Map<String, Player> playerMap,
int timeWindowSeconds) {
// 1) 分离事件
List<MatchEvent> crosses = events.stream()
.filter(e -> e.getType() == EventType.CROSS_HIGH)
.collect(Collectors.toList());
List<MatchEvent> duels = events.stream()
.filter(e -> e.getType() == EventType.AERIAL_DUEL)
.collect(Collectors.toList());
// 2) 建立"传中 -> 争顶"关联映射
Set<String> crossFromDuelIds = new HashSet<>();
Map<String, List<MatchEvent>> crossDuelMap = new HashMap<>();
for (MatchEvent cross : crosses) {
// 优先通过显式引用关联
List<MatchEvent> matched = duels.stream()
.filter(d -> cross.getId().equals(d.getCrossRefId()))
.collect(Collectors.toList());
// 隐式关联:时间窗口内,同一队伍源,或对手争顶
if (matched.isEmpty()) {
LocalDateTime start = cross.getTimestamp();
matched = duels.stream()
.filter(d -> {
long sec = java.time.Duration.between(
start, d.getTimestamp()).getSeconds();
return sec >= 0 && sec <= timeWindowSeconds
&& Math.abs(d.getMinute() - cross.getMinute()) <= 1
&& (d.getTeamId().equals(cross.getTeamId())
|| isOpponent(cross.getTeamId(), d.getTeamId()));
})
.limit(1) // 一次传中通常对应一次争顶
.collect(Collectors.toList());
}
crossDuelMap.put(cross.getId(), matched);
matched.forEach(d -> crossFromDuelIds.add(d.getId()));
}
// 3) 聚合统计
Map<String, CrossAerialStat> result = new LinkedHashMap<>();
// 3.1 传中方的传中数据
for (MatchEvent cross : crosses) {
result.computeIfAbsent(cross.getPlayerId(),
id -> new CrossAerialStat(id, playerMap.get(id).getName()))
.addCross(cross.isSuccess());
}
// 3.2 争顶数据(所有争顶)
for (MatchEvent duel : duels) {
boolean fromCross = crossFromDuelIds.contains(duel.getId());
result.computeIfAbsent(duel.getPlayerId(),
id -> new CrossAerialStat(id, playerMap.get(id).getName()))
.addDuel(duel.isSuccess(), fromCross);
}
return result;
}
private boolean isOpponent(String teamA, String teamB){
return !teamA.equals(teamB);
}
}
使用示例
public class Demo {
public static void main(String[] args) {
// 构造球员
Map<String, Player> players = new HashMap<>();
players.put("P1", new Player("P1","萨卡","ARS","RW"));
players.put("P2", new Player("P2","哈弗茨","ARS","ST"));
players.put("P3", new Player("P3","范戴克","LIV","CB"));
// 构造事件:P1传中 -> P2与P3争顶 -> P2获胜
MatchEvent cross = new MatchEvent("E1","M1",
EventType.CROSS_HIGH,"P1",null,"ARS",true,45);
MatchEvent duel = new MatchEvent("E2","M1",
EventType.AERIAL_DUEL,"P2","P3","ARS",true,45);
duel.setCrossRefId("E1");
MatchEvent cross2 = new MatchEvent("E3","M1",
EventType.CROSS_HIGH,"P1",null,"ARS",false,60);
MatchEvent duel2 = new MatchEvent("E4","M1",
EventType.AERIAL_DUEL,"P3","P2","LIV",true,60);
duel2.setCrossRefId("E3");
List<MatchEvent> events = List.of(cross, duel, cross2, duel2);
CrossAerialCalculator calc = new CrossAerialCalculator();
Map<String, CrossAerialStat> stats = calc.calculate(events, players, 3);
stats.values().stream()
.sorted(Comparator.comparingDouble(
CrossAerialStat::getCrossDuelWinRate).reversed())
.forEach(System.out::println);
}
}
输出:
萨卡 传中: 1/2(50.0%) 争顶: 0/0(0.0%) 传中争顶: 0/0(0.0%)
哈弗茨 传中: 0/0(0.0%) 争顶: 1/1(100.0%) 传中争顶: 1/1(100.0%)
范戴克 传中: 0/0(0.0%) 争顶: 1/1(100.0%) 传中争顶: 1/1(100.0%)
进阶扩展方向
| 方向 | 说明 |
|---|---|
| 时间窗口优化 | 用 Duration 精确匹配传中后 0~5 秒内的争顶,避免同分钟误配 |
| 区域过滤 | 结合坐标,仅统计禁区内传中(x > 83, -34 < y < 34) |
| 对手强度加权 | 对顶级中卫争顶成功给予更高权重 |
| 按比赛/赛季聚合 | 用 groupingBy(matchId) 分层统计,再做平均 |
| 数据持久化 | JPA/MyBatis 映射 match_event 表,加唯一索引避免重复刷统计 |
| 实时流处理 | 用 Flink/Kafka Stream 对事件流做窗口聚合(如 5 秒滑动窗口匹配传中→争顶) |
坑点提醒
- 一次传中可能引发多次争顶(禁区混战),应统计"首次落点争顶"或"最高优先级争顶"作为主指标;
- 争顶主体归属:要用进攻方视角判断
success,否则攻防双方胜率会都记成"赢"或"输"; - 数据缺失:无对应争顶的传中,
crossDuelTotal不增加,避免分母污染; - 传中≠争顶:直接找到己方球员头球形成射门的传中,应计为
crossSuccess但不计入"争顶"分母——业务、算法口径需与教练组对齐。
如需我继续给出 JPA实体 + Repository 层、Spring Boot 完整工程结构 或 Flink实时统计版,告诉我即可。
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播