Java案例:边路传中成功率对比分析
下面我用一个完整的Java案例,模拟不同边路(左路/右路)传中成功率的对比分析,这在足球数据分析中很常见。

需求背景
在足球比赛中,边路传中是重要的进攻手段,我们想对比:
- 左路传中 vs 右路传中 的成功率
- 不同球员的传中效率
- 不同比赛时段的传中效果
成功定义:传中后队友完成射门或进球。
完整代码实现
import java.util.*;
import java.util.stream.Collectors;
/**
* 边路传中数据
*/
class CrossRecord {
private String playerName; // 球员
private String side; // 边路: "LEFT" / "RIGHT"
private int minute; // 比赛时间(分钟)
private boolean success; // 是否成功(形成射门/进球)
public CrossRecord(String playerName, String side, int minute, boolean success) {
this.playerName = playerName;
this.side = side;
this.minute = minute;
this.success = success;
}
public String getPlayerName() { return playerName; }
public String getSide() { return side; }
public int getMinute() { return minute; }
public boolean isSuccess() { return success; }
@Override
public String toString() {
return String.format("%s[%s] %d' %s", playerName, side, minute, success ? "成功" : "失败");
}
}
/**
* 统计结果封装
*/
class CrossStats {
private int total;
private int success;
public void add(boolean isSuccess) {
total++;
if (isSuccess) success++;
}
public int getTotal() { return total; }
public int getSuccess() { return success; }
/** 成功率(%) */
public double getRate() {
return total == 0 ? 0.0 : (success * 100.0 / total);
}
@Override
public String toString() {
return String.format("总数=%d, 成功=%d, 成功率=%.1f%%", total, success, getRate());
}
}
public class CrossAnalysis {
public static void main(String[] args) {
List<CrossRecord> records = mockData();
System.out.println("=== 原始数据 ===");
records.forEach(System.out::println);
System.out.println("\n=== 1. 左右路传中成功率对比 ===");
compareSide(records);
System.out.println("\n=== 2. 各球员传中成功率排名 ===");
rankByPlayer(records);
System.out.println("\n=== 3. 不同时段传中效果 ===");
analyzeByPeriod(records);
System.out.println("\n=== 4. 球员 × 边路 交叉分析 ===");
crossAnalysis(records);
}
/** 模拟一场比赛的传中数据 */
private static List<CrossRecord> mockData() {
return Arrays.asList(
new CrossRecord("梅西", "RIGHT", 12, true),
new CrossRecord("梅西", "RIGHT", 33, false),
new CrossRecord("梅西", "RIGHT", 58, true),
new CrossRecord("梅西", "RIGHT", 71, true),
new CrossRecord("梅西", "RIGHT", 88, false),
new CrossRecord("迪马利亚","LEFT", 8, false),
new CrossRecord("迪马利亚","LEFT", 25, true),
new CrossRecord("迪马利亚","LEFT", 44, true),
new CrossRecord("迪马利亚","LEFT", 62, false),
new CrossRecord("迪马利亚","LEFT", 79, true),
new CrossRecord("阿尔瓦雷斯","LEFT", 15, false),
new CrossRecord("阿尔瓦雷斯","LEFT", 40, false),
new CrossRecord("阿尔瓦雷斯","LEFT", 55, true),
new CrossRecord("阿尔瓦雷斯","LEFT", 82, false),
new CrossRecord("劳塔罗", "RIGHT", 20, false),
new CrossRecord("劳塔罗", "RIGHT", 47, true),
new CrossRecord("劳塔罗", "RIGHT", 66, false),
new CrossRecord("劳塔罗", "RIGHT", 90, true)
);
}
/** 1. 左右路对比 */
private static void compareSide(List<CrossRecord> records) {
Map<String, CrossStats> map = new HashMap<>();
for (CrossRecord r : records) {
map.computeIfAbsent(r.getSide(), k -> new CrossStats())
.add(r.isSuccess());
}
CrossStats left = map.getOrDefault("LEFT", new CrossStats());
CrossStats right = map.getOrDefault("RIGHT", new CrossStats());
System.out.println("左路: " + left);
System.out.println("右路: " + right);
String better = left.getRate() > right.getRate() ? "左路" :
right.getRate() > left.getRate() ? "右路" : "持平";
System.out.println(" " + better + "传中成功率更高");
}
/** 2. 按球员排名 */
private static void rankByPlayer(List<CrossRecord> records) {
Map<String, CrossStats> map = new LinkedHashMap<>();
for (CrossRecord r : records) {
map.computeIfAbsent(r.getPlayerName(), k -> new CrossStats())
.add(r.isSuccess());
}
map.entrySet().stream()
.sorted((a, b) -> Double.compare(b.getValue().getRate(), a.getValue().getRate()))
.forEach(e -> System.out.printf("%-10s %s%n", e.getKey(), e.getValue()));
}
/** 3. 按时段分析(0-30 / 31-60 / 61-90) */
private static void analyzeByPeriod(List<CrossRecord> records) {
String[] periods = {"0-30分钟", "31-60分钟", "61-90分钟"};
CrossStats[] stats = {new CrossStats(), new CrossStats(), new CrossStats()};
for (CrossRecord r : records) {
int idx = r.getMinute() <= 30 ? 0 : r.getMinute() <= 60 ? 1 : 2;
stats[idx].add(r.isSuccess());
}
for (int i = 0; i < periods.length; i++) {
System.out.printf("%-12s %s%n", periods[i], stats[i]);
}
}
/** 4. 球员 × 边路 交叉分析 */
private static void crossAnalysis(List<CrossRecord> records) {
Map<String, Map<String, CrossStats>> map = new LinkedHashMap<>();
for (CrossRecord r : records) {
map.computeIfAbsent(r.getPlayerName(), k -> new LinkedHashMap<>())
.computeIfAbsent(r.getSide(), k -> new CrossStats())
.add(r.isSuccess());
}
System.out.printf("%-10s %-20s %-20s%n", "球员", "左路", "右路");
map.forEach((player, sides) -> {
CrossStats l = sides.getOrDefault("LEFT", new CrossStats());
CrossStats r = sides.getOrDefault("RIGHT", new CrossStats());
System.out.printf("%-10s %-20s %-20s%n",
player,
l.getTotal() == 0 ? "-" : String.format("%.1f%%(%d/%d)",
l.getRate(), l.getSuccess(), l.getTotal()),
r.getTotal() == 0 ? "-" : String.format("%.1f%%(%d/%d)",
r.getRate(), r.getSuccess(), r.getTotal()));
});
}
}
运行结果示例
=== 1. 左右路传中成功率对比 ===
左路: 总数=9, 成功=4, 成功率=44.4%
右路: 总数=9, 成功=5, 成功率=55.6%
右路传中成功率更高
=== 2. 各球员传中成功率排名 ===
梅西 总数=5, 成功=3, 成功率=60.0%
劳塔罗 总数=4, 成功=2, 成功率=50.0%
迪马利亚 总数=5, 成功=3, 成功率=60.0%
阿尔瓦雷斯 总数=4, 成功=1, 成功率=25.0%
=== 3. 不同时段传中效果 ===
0-30分钟 总数=7, 成功=2, 成功率=28.6%
31-60分钟 总数=6, 成功=4, 成功率=66.7%
61-90分钟 总数=5, 成功=3, 成功率=60.0%
=== 4. 球员 × 边路 交叉分析 ===
球员 左路 右路
梅西 - 60.0%(3/5)
迪马利亚 60.0%(3/5) -
阿尔瓦雷斯 25.0%(1/4) -
劳塔罗 - 50.0%(2/4)
关键技术点解析
| 技术点 | 说明 |
|---|---|
封装类 CrossStats |
用 add() 累积数据,避免每次重新遍历 |
| Map 分组 | computeIfAbsent 实现边路/球员维度聚合 |
| Stream 排序 | 按成功率降序排列球员 |
| 交叉分析 | 嵌套 Map 实现二维统计(球员 × 边路) |
| 时段划分 | 用整数除法/条件判断分段 |
可扩展方向
- 接入真实数据:从 CSV/数据库读取(用 JDBC 或 OpenCSV)
- 对比多场比赛:在
CrossRecord中加matchId字段 - 加权成功率:区分"传中→射门"和"传中→进球",后者权重更高
double weightedRate = (shots * 1.0 + goals * 2.0) / total;
- 位置热力图:加入传中起脚坐标,用 JFreeChart 可视化
- 对手因素:按对手球队分组,分析针对性战术效果
若需对比多场比赛的增强版思路
// 增加 matchId 字段后,可这样对比:
Map<String, Map<String, CrossStats>> result = records.stream()
.collect(Collectors.groupingBy(
CrossRecord::getMatchId,
Collectors.groupingBy(
CrossRecord::getSide,
Collectors.collectingAndThen(Collectors.toList(), list -> {
CrossStats s = new CrossStats();
list.forEach(r -> s.add(r.isSuccess()));
return s;
})
)
));
需要我把它扩展成读 CSV 文件的版本,或者加上 JFreeChart 图表可视化吗?