1、需求
有以下电商每个品类在每个月的销售额记录表,计算每个品类在最近3个月的移动平均销售额(若当前月前不足3个月,则按实际月份计算)。
例如:
- 家电2023-09月的移动平均 = (2023-07 + 2023-08 + 2023-09) / 3
- 家电2023-07月的移动平均 = (2023-06 + 2023-07) / 2(若无更早数据)
2、实现
2.1、测试数据准备
// 构造每月销售额记录
List<Sale> saleList = Arrays.asList(
new Sale("家电", "2023-06", new BigDecimal("100")),
new Sale("家电", "2023-07", new BigDecimal("110")),
new Sale("家电", "2023-08", new BigDecimal("120")),
new Sale("家电", "2023-09", new BigDecimal("130")),
new Sale("家电", "2023-10", new BigDecimal("140")),
new Sale("食品", "2023-02", new BigDecimal("150")),
new Sale("食品", "2023-03", new BigDecimal("160")),
new Sale("衣服", "2023-10", new BigDecimal("170")),
new Sale("衣服", "2023-11", new BigDecimal("180")),
new Sale("衣服", "2023-12", new BigDecimal("190"))
);
@Data
public class Sale {
// 品类
private String cateType;
// 年月
private String month;
// 销售额
private BigDecimal amount;
// 近3个月移动平均销售额
private BigDecimal avg3;
public Sale(String cateType, String month, BigDecimal amount) {
this.cateType = cateType;
this.month = month;
this.amount = amount;
}
}
2.2 stream流实现
思路
- 先按品类groupingBy分组,然后遍历处理每个组内的数据
- 组内数据先按月份升序排序,然后遍历每个月份数据,获取当前近3个月的销售记录,然后求销售额平均值
// 1. 按品类分组
Map<String, List<Sale>> salesByCategory = saleList.stream()
.collect(Collectors.groupingBy(Sale::getCateType));
// 2. 对每个品类的数据按月排序
salesByCategory.forEach((category, categorySales) -> {
// 按月份升序排序
List<Sale> sortedSales = categorySales.stream()
.sorted(Comparator.comparing(Sale::getMonth))
.collect(Collectors.toList());
// 3. 遍历每个月份,计算移动平均
IntStream.range(0, sortedSales.size()).forEach(i -> {
// 确定窗口范围:前2个月 + 当前月(共3个月)
int start = Math.max(0, i - 2);
List<Sale> window = sortedSales.subList(start, i + 1);
// 计算窗口内销售额总和
BigDecimal sum = window.stream()
.map(Sale::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 计算平均值(保留两位小数,四舍五入)
BigDecimal avg = sum.divide(
new BigDecimal(window.size()), 2, RoundingMode.HALF_UP
);
// 将结果设置到当前 Sale 对象的 avg3 字段
sortedSales.get(i).setAvg3(avg);
});
});
2.3 JDFrame实现
对于这种问题明显是一个滑动窗口问题,我们可以使用窗口函数去实现,刚好JDFrame提供了求平均值的窗口函数,接下来看看如何用代码实现。
saleList = SDFrame.read(saleList)
.window(Window.groupBy(Sale::getCateType).sortAsc(Sale::getMonth).roundBefore2CurrentRow(2))
.overAvgS(Sale::setAvg3,Sale::getAmount)
.toLists();
上述代码逻辑其实等价于以下窗口函数SQL
select avg(amount) over(partition by cateType order by month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
先调用window方法去分组开窗,然后使用overAvgS计算函数去计算滑动窗口内的平均销售额,并设置到avg3字段, 如此只需两个方法便实现了我们的需求。 更多高级用法见官方文档
3 最后
对比原生stream和JDFrame的使用大家有什么看法欢迎在评论区留下你的意见