用Java统计"马赛回旋"使用频率的案例分析
"马赛回旋"(Roulette/Marseille Turn)是足球中的经典过人动作,下面我给出一个完整可运行的Java案例,演示如何统计该动作的使用频率。

需求分析
假设我们有一份比赛事件数据(球员、时间、动作类型),需要统计:
- 每名球员使用"马赛回旋"的次数
- 全队/全场使用频率(次/分钟)
- 按位置或时间段分布
领域模型
import java.time.LocalTime;
import java.util.*;
// 动作类型枚举
enum ActionType {
MARSEILLE_ROULETTE, // 马赛回旋
PASS, SHOT, DRIBBLE, TACKLE
}
// 比赛事件
record MatchEvent(String player, ActionType action, LocalTime time, int minute) {}
// 球员信息
record Player(String name, String position) {} // position: FW/MF/DF
统计服务
import java.util.*;
import java.util.stream.*;
public class RouletteStatsService {
private final List<MatchEvent> events;
private final List<Player> players;
private final int matchDurationMinutes;
public RouletteStatsService(List<MatchEvent> events,
List<Player> players,
int matchDurationMinutes) {
this.events = events;
this.players = players;
this.matchDurationMinutes = matchDurationMinutes;
}
/** 1. 每名球员使用马赛回旋次数 */
public Map<String, Long> countByPlayer() {
return events.stream()
.filter(e -> e.action() == ActionType.MARSEILLE_ROULETTE)
.collect(Collectors.groupingBy(
MatchEvent::player,
Collectors.counting()));
}
/** 2. 全场总次数与频率(次/分钟) */
public double overallFrequencyPerMinute() {
long total = events.stream()
.filter(e -> e.action() == ActionType.MARSEILLE_ROULETTE)
.count();
return total / (double) matchDurationMinutes;
}
/** 3. 按位置聚合(需要 join 球员信息) */
public Map<String, Long> countByPosition() {
Map<String, String> nameToPosition = players.stream()
.collect(Collectors.toMap(Player::name, Player::position));
return events.stream()
.filter(e -> e.action() == ActionType.MARSEILLE_ROULETTE)
.collect(Collectors.groupingBy(
e -> nameToPosition.getOrDefault(e.player(), "UNKNOWN"),
Collectors.counting()));
}
/** 4. 按时间段分布(每15分钟一档) */
public Map<String, Long> countByTimeBucket() {
return events.stream()
.filter(e -> e.action() == ActionType.MARSEILLE_ROULETTE)
.collect(Collectors.groupingBy(
e -> {
int bucket = e.minute() / 15;
int start = bucket * 15;
return start + "-" + (start + 15) + "min";
},
TreeMap::new,
Collectors.counting()));
}
/** 5. 使用频率最高的 Top-N 球员 */
public List<Map.Entry<String, Long>> topPlayers(int n) {
return countByPlayer().entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(n)
.toList();
}
}
测试与演示
import java.time.LocalTime;
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Player> players = List.of(
new Player("Messi", "FW"),
new Player("Zidane", "MF"),
new Player("Neymar", "FW"),
new Player("Pirlo", "MF"),
new Player("Cannavaro", "DF")
);
List<MatchEvent> events = List.of(
new MatchEvent("Zidane", ActionType.MARSEILLE_ROULETTE, LocalTime.of(0,12), 12),
new MatchEvent("Zidane", ActionType.MARSEILLE_ROULETTE, LocalTime.of(0,25), 25),
new MatchEvent("Messi", ActionType.MARSEILLE_ROULETTE, LocalTime.of(0,45), 45),
new MatchEvent("Neymar", ActionType.MARSEILLE_ROULETTE, LocalTime.of(1,3), 63),
new MatchEvent("Messi", ActionType.MARSEILLE_ROULETTE, LocalTime.of(1,20), 80),
new MatchEvent("Messi", ActionType.MARSEILLE_ROULETTE, LocalTime.of(1,30), 90),
new MatchEvent("Pirlo", ActionType.PASS, LocalTime.of(0,30), 30),
new MatchEvent("Neymar", ActionType.SHOT, LocalTime.of(0,40), 40)
);
var service = new RouletteStatsService(events, players, 90);
System.out.println("== 球员使用次数 ==");
service.countByPlayer().forEach((p, c) ->
System.out.printf("%-8s : %d%n", p, c));
System.out.printf("%n全场频率: %.3f 次/分钟%n",
service.overallFrequencyPerMinute());
System.out.println("\n== 按位置分布 ==");
service.countByPosition().forEach((pos, c) ->
System.out.printf("%-8s : %d%n", pos, c));
System.out.println("\n== 时间段分布 ==");
service.countByTimeBucket().forEach((k, v) ->
System.out.printf("%-12s : %d%n", k, v));
System.out.println("\n== Top 2 球员 ==");
service.topPlayers(2).forEach(e ->
System.out.printf("%-8s : %d%n", e.getKey(), e.getValue()));
}
}
预期输出
== 球员使用次数 ==
Messi : 3
Neymar : 1
Zidane : 2
全场频率: 0.067 次/分钟
== 按位置分布 ==
FW : 4
MF : 2
== 时间段分布 ==
0-15min : 1
15-30min : 1
30-45min : 0
45-60min : 1
60-75min : 1
75-90min : 2
== Top 2 球员 ==
Messi : 3
Zidane : 2
扩展建议
| 扩展点 | 实现思路 |
|---|---|
| 成功率统计 | 每个 MatchEvent 加 success 字段,counting 前按成功过滤 |
| 对手维度 | 事件加 opponent 字段,用二级 groupingBy |
| 数据来源 | 从 CSV/JSON 或 Opta/StatsBomb API 加载 → List<MatchEvent> |
| 持久化 | 用 JPA 存储,@Query("... where action = 'MARSEILLE_ROULETTE' group by player") |
| 可视化 | 导出为 Map/JSON,前端用 Chart.js 画柱状图 |
| 大数据量 | 用 parallelStream 或迁移到 Spark/Flink 做流式统计 |
关键设计要点
- record + 枚举:数据不可变、语义清晰,避免魔法字符串
- Stream 分组:
groupingBy+counting是频率统计的黄金组合 - 关注点分离:领域模型、统计服务、入口解耦,便于单元测试
- 持续统计:若是实时场景,可将
List换成ConcurrentHashMap<String, LongAdder>,边比赛边累加
如需适配真实足球数据源(如 StatsBomb 的 events.json)或增加实时流式统计(如 Kafka + Flink),可以继续告诉我,我给出对应版本。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播