看 JDFrame 如何简化计算近3个月的移动平均销售额

253 阅读3分钟

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的使用大家有什么看法欢迎在评论区留下你的意见