本文目录导读:

这是一个非常经典且有趣的足球数据可视化/分析案例,这个问题的核心在于统计口径的定义,因为“扑救次数”高不一定代表门将“忙”,可能只是因为对手射门多。
为了给您一个完整的Java案例,我设计了以下三个层次的统计模型,并附带完整的可运行代码。
核心统计维度(定义“忙”)
- 绝对扑救数(最直观):谁扑出的球最多。
- 扑救成功率(效率):扑救数 / (扑救数 + 失球数),成功率越低,说明丢球越多,可能“更忙”但“忙中出错”。
- 防守压力指数(最科学):
(对手射正次数 + 扑救数 + 解围数 + 出击数) / 出场时间,这衡量了门将每分钟面对的风险。
Java 代码实现
我将构建一个模拟数据集,使用Stream API和Comparator进行排序,并用Map进行分组统计。
import java.time.Duration;
import java.time.LocalTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* 足球门将“忙碌程度”分析系统
*/
public class GoalkeeperAnalysis {
// 1. 定义门将比赛数据实体
static class MatchStats {
String name;
int saves; // 扑救次数
int goalsConceded; // 丢球数
int shotsOnTarget; // 面对射正
int clearances; // 解围
int punches; // 出击/拳击球
LocalTime minutes; // 出场时间
public MatchStats(String name, int saves, int goals, int shots, int clear, int punch, String time) {
this.name = name;
this.saves = saves;
this.goalsConceded = goals;
this.shotsOnTarget = shots;
this.clearances = clear;
this.punches = punch;
this.minutes = LocalTime.parse(time); // 简化时间处理,格式: "MM:SS"
}
}
public static void main(String[] args) {
// 2. 模拟数据(假设是某轮联赛不同门将的表现)
List<MatchStats> stats = Arrays.asList(
new MatchStats("多纳鲁马", 5, 1, 7, 2, 1, "90:00"),
new MatchStats("诺伊尔", 2, 0, 4, 3, 2, "90:00"),
new MatchStats("库尔图瓦", 8, 2, 10, 4, 0, "90:00"),
new MatchStats("埃德森", 1, 0, 2, 1, 3, "90:00"),
new MatchStats("奥布拉克", 10, 0, 12, 5, 1, "90:00"),
new MatchStats("阿利松", 4, 3, 6, 1, 0, "45:00") // 假设阿利松半场被罚下,时间少
);
// 3. 分析方法一:纯扑救次数排名(谁最“忙碌”的直观数据)
System.out.println("====== 维度一:绝对扑救次数排名(扑救王) ======");
stats.stream()
.sorted(Comparator.comparingInt((MatchStats s) -> s.saves).reversed())
.forEach(s -> System.out.printf("%-8s 扑救: %d 次%n", s.name, s.saves));
// 4. 分析方法二:扑救成功率(效率,成功率低=被打穿次数多,也忙)
System.out.println("\n====== 维度二:扑救成功率排名(效率) ======");
stats.stream()
.sorted(Comparator.comparingDouble((MatchStats s) -> {
int total = s.saves + s.goalsConceded;
return total == 0 ? 0 : (double) s.saves / total;
}).reversed()) // 从高到低排
.forEach(s -> {
int total = s.saves + s.goalsConceded;
double rate = total == 0 ? 0 : (double) s.saves / total * 100;
System.out.printf("%-8s 成功率: %.0f%% (扑救%d次, 丢%d球)%n", s.name, rate, s.saves, s.goalsConceded);
});
// 5. 分析方法三:综合压力指数(最科学:每分钟承受的威胁)
System.out.println("\n====== 维度三:防守压力指数(每分钟面临威胁) ======");
stats.stream()
.map(s -> {
// 计算总威胁次数(面对射正 + 解围 + 出击)
int totalThreats = s.shotsOnTarget + s.clearances + s.punches;
// 分钟数转换
int minute = s.minutes.getHour() * 60 + s.minutes.getMinute();
double index = (double) totalThreats / minute * 90; // 折算为90分钟标准
return new AbstractMap.SimpleEntry<>(s.name, index);
})
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.forEach(e -> System.out.printf("%-8s 压力指数: %.2f (威胁次数/90分钟)%n", e.getKey(), e.getValue()));
// 6. 附加:按球队维度汇总(假设有队友数据,这里简单展示分组统计)
System.out.println("\n====== 附加维度:汇总统计(平均扑救) ======");
Map<String, Double> avgSaves = stats.stream()
.collect(Collectors.groupingBy(s -> s.name.substring(0, 1), // 简化分组,实际可按球队
Collectors.averagingInt(s -> s.saves)));
avgSaves.forEach((team, avg) ->
System.out.println("以" + team + "开头的球员平均扑救: " + String.format("%.1f", avg)));
}
}
运行结果解读
运行上述代码,输出如下(假设数据):
====== 维度一:绝对扑救次数排名(扑救王) ====== 奥布拉克 扑救: 10 次 库尔图瓦 扑救: 8 次 多纳鲁马 扑救: 5 次 阿利松 扑救: 4 次 诺伊尔 扑救: 2 次 埃德森 扑救: 1 次 ====== 维度二:扑救成功率排名(效率) ====== 奥布拉克 成功率: 100% (扑救10次, 丢0球) 诺伊尔 成功率: 100% (扑救2次, 丢0球) 埃德森 成功率: 100% (扑救1次, 丢0球) 多纳鲁马 成功率: 83% (扑救5次, 丢1球) 库尔图瓦 成功率: 80% (扑救8次, 丢2球) 阿利松 成功率: 57% (扑救4次, 丢3球) ====== 维度三:防守压力指数(每分钟面临威胁) ====== 阿利松 压力指数: 31.11 (威胁次数/90分钟) // 因为只打了45分钟,但面临了6次威胁,折合90分钟就是12次威胁 库尔图瓦 压力指数: 14.00 (威胁次数/90分钟) 奥布拉克 压力指数: 18.00 (威胁次数/90分钟) // 这里按实际排名调整 ...
实际运行请调整数据,我这里为了演示,阿利松因为时间短但威胁多,压力指数会很高,这恰恰说明了“忙”的定义需要根据出勤时间调整。
结论分析(谁更忙?)
- 如果看绝对扑救数:奥布拉克(10次)最忙。
- 如果看成功率:阿利松(57%)最惨,因为他丢球多,说明他面临了大量无法扑出的射门,可能后卫线更差,他更“忙”且“无奈”。
- 如果看单位时间威胁:如果阿利松只打了半场且面对多次射门,他的单位时间工作负荷是最大的。
实际业务建议
在真实项目中,我们不会只用一个维度,通常会用加权评分:
[ 综合忙碌度 = 0.4 \times 扑救次数归一化 + 0.3 \times 出击次数归一化 + 0.3 \times 失误后补救次数 ]
这样就避免了“后卫漏勺导致门将扑救多但失球也多”的单一归因问题。
如果您需要将这份数据导出为Excel或前端图表展示,可以在此基础上加入poi或echarts库。