Java案例:统计界外球进攻威胁次数
下面是一个完整的Java案例,用于统计足球比赛中界外球进攻产生的威胁次数(如射门、角球、进球等)。

需求分析
界外球(Throw-in)进攻威胁通常指:
- 界外球掷出后 N 秒内(如15秒)形成的射门
- 界外球掷出后形成的角球、进球
- 界外球直接造成对方解围失误
这里我们模拟一段比赛事件流,识别"界外球 → 后续威胁事件"的关联。
核心代码
事件模型
import java.time.LocalTime;
import java.util.*;
// 事件类型
enum EventType {
THROW_IN, // 界外球
SHOT, // 射门
SHOT_ON_TARGET,// 射正
GOAL, // 进球
CORNER, // 角球
FOUL // 犯规
}
// 比赛事件
class MatchEvent {
private final int minute; // 比赛分钟
private final int second; // 秒
private final EventType type;
private final String team; // 进攻方
private final String detail;
public MatchEvent(int minute, int second, EventType type, String team, String detail) {
this.minute = minute;
this.second = second;
this.type = type;
this.team = team;
this.detail = detail;
}
public int getAbsoluteSeconds() { return minute * 60 + second; }
public EventType getType() { return type; }
public String getTeam() { return team; }
public String getDetail() { return detail; }
@Override
public String toString() {
return String.format("%02d:%02d %s [%s] %s",
minute, second, team, type, detail);
}
}
威胁统计器
public class ThrowInThreatAnalyzer {
/** 界外球后多长时间内的事件算作威胁(秒) */
private static final int THREAT_WINDOW_SECONDS = 15;
/** 视为威胁的事件类型 */
private static final Set<EventType> THREAT_TYPES = EnumSet.of(
EventType.SHOT,
EventType.SHOT_ON_TARGET,
EventType.GOAL,
EventType.CORNER
);
private final List<MatchEvent> events;
public ThrowInThreatAnalyzer(List<MatchEvent> events) {
this.events = new ArrayList<>(events);
// 按时间排序
this.events.sort(Comparator.comparingInt(MatchEvent::getAbsoluteSeconds));
}
/**
* 统计每个队伍的界外球进攻威胁次数
*/
public Map<String, Integer> countThreatsByTeam() {
Map<String, Integer> result = new HashMap<>();
for (int i = 0; i < events.size(); i++) {
MatchEvent throwIn = events.get(i);
if (throwIn.getType() != EventType.THROW_IN) continue;
String team = throwIn.getTeam();
int windowStart = throwIn.getAbsoluteSeconds();
int windowEnd = windowStart + THREAT_WINDOW_SECONDS;
// 向后查找威胁事件
for (int j = i + 1; j < events.size(); j++) {
MatchEvent next = events.get(j);
if (next.getAbsoluteSeconds() > windowEnd) break;
// 只统计进攻方自己造成的威胁
if (!next.getTeam().equals(team)) continue;
if (THREAT_TYPES.contains(next.getType())) {
result.merge(team, 1, Integer::sum);
break; // 一次界外球只算一次威胁
}
}
}
return result;
}
/**
* 详细输出每次威胁
*/
public void reportThreats() {
for (int i = 0; i < events.size(); i++) {
MatchEvent throwIn = events.get(i);
if (throwIn.getType() != EventType.THROW_IN) continue;
String team = throwIn.getTeam();
int windowEnd = throwIn.getAbsoluteSeconds() + THREAT_WINDOW_SECONDS;
for (int j = i + 1; j < events.size(); j++) {
MatchEvent next = events.get(j);
if (next.getAbsoluteSeconds() > windowEnd) break;
if (!next.getTeam().equals(team)) continue;
if (THREAT_TYPES.contains(next.getType())) {
System.out.printf("✅ %s 界外球(%02d:%02d) → 威胁 (%s %02d:%02d, %s)%n",
team, throwIn.getAbsoluteSeconds() / 60,
throwIn.getAbsoluteSeconds() % 60,
next.getType(), next.getAbsoluteSeconds() / 60,
next.getAbsoluteSeconds() % 60, next.getDetail());
break;
}
}
}
}
/**
* 算出威胁转化率 = 威胁次数 / 界外球总数
*/
public Map<String, Double> conversionRate() {
Map<String, Integer> threatCount = countThreatsByTeam();
Map<String, Integer> throwInCount = new HashMap<>();
for (MatchEvent e : events) {
if (e.getType() == EventType.THROW_IN) {
throwInCount.merge(e.getTeam(), 1, Integer::sum);
}
}
Map<String, Double> rate = new HashMap<>();
for (Map.Entry<String, Integer> entry : throwInCount.entrySet()) {
String team = entry.getKey();
int threats = threatCount.getOrDefault(team, 0);
rate.put(team, threats * 100.0 / entry.getValue());
}
return rate;
}
}
测试入口
public class Main {
public static void main(String[] args) {
List<MatchEvent> events = new ArrayList<>();
// 模拟一场比赛事件流
events.add(new MatchEvent(10, 15, EventType.THROW_IN, "中国队", "左侧边线"));
events.add(new MatchEvent(10, 22, EventType.SHOT, "中国队", "禁区内抽射"));
events.add(new MatchEvent(10, 30, EventType.CORNER, "中国队", "射门被挡出"));
events.add(new MatchEvent(20, 5, EventType.THROW_IN, "中国队", "右侧边线"));
events.add(new MatchEvent(20, 40, EventType.SHOT, "中国队", "远射")); // 超过15秒,不算
events.add(new MatchEvent(32, 10, EventType.THROW_IN, "日本队", "右侧边线"));
events.add(new MatchEvent(32, 18, EventType.GOAL, "日本队", "头球破门"));
events.add(new MatchEvent(45, 0, EventType.THROW_IN, "日本队", "左侧边线"));
events.add(new MatchEvent(45, 10, EventType.FOUL, "日本队", "对方犯规"));
// 事件流结束
events.add(new MatchEvent(60, 0, EventType.THROW_IN, "中国队", "左侧边线"));
events.add(new MatchEvent(60, 8, EventType.SHOT_ON_TARGET, "中国队", "射正被扑"));
ThrowInThreatAnalyzer analyzer = new ThrowInThreatAnalyzer(events);
System.out.println("====== 界外球进攻威胁明细 ======");
analyzer.reportThreats();
System.out.println("\n====== 各队威胁次数 ======");
analyzer.countThreatsByTeam().forEach((k, v) ->
System.out.println(k + ": " + v + " 次"));
System.out.println("\n====== 各队界外球威胁转化率 ======");
analyzer.conversionRate().forEach((k, v) ->
System.out.printf("%s: %.1f%%%n", k, v));
}
}
运行结果
====== 界外球进攻威胁明细 ======
✅ 中国队 界外球(10:15) → 威胁 (SHOT 10:22, 禁区内抽射)
✅ 日本队 界外球(32:10) → 威胁 (GOAL 32:18, 头球破门)
✅ 中国队 界外球(60:00) → 威胁 (SHOT_ON_TARGET 60:08, 射正被扑)
====== 各队威胁次数 ======
中国队: 2 次
日本队: 1 次
====== 各队界外球威胁转化率 ======
中国队: 66.7%
日本队: 50.0%
设计要点
| 要点 | 说明 |
|---|---|
| 时间窗口 | 用 THREAT_WINDOW_SECONDS 可配置(默认15秒),便于分析不同战术风格 |
| 进攻方判定 | 只统计"同队"后续事件,防止对方反击误判 |
| 一次界外球一次威胁 | 用 break 避免单次界外球被多次计数 |
| 可扩展 | 可加入 xG(预期进球)加权、区域过滤、对手强度等维度 |
| 事件模型解耦 | MatchEvent 与统计逻辑分离,方便接入真实数据源(如Opta、Wyscout) |
扩展建议
- xG加权威胁:把 SHOT 按射门位置的 xG 值加权,威胁次数变成"威胁值"。
- 时间窗口:可分别统计 5s / 10s / 15s 窗口内的威胁,做曲线对比。
- 按场区统计:前场界外球威胁概率通常远高于后场,可细分统计。
- 数据接入:将读取事件流的部分改成从 CSV / JSON / 数据库读取,直接套用于真实比赛数据。
如果你有具体的数据格式(CSV/JSON)或想统计特定赛制的界外球威胁,可以告诉我,我再给你改成对应版本。