Stream流进行统计分析

本文章参考黑马程序员的函数式编程,想要更详细的讲解,可以去看对应的视频

注意:这篇文章适合有stream基础的同学阅读,我后续会出一篇关于stream基础的文章。

这里准备了200多万行的数据,用户下单的信息,我们将使用stream流完成我们的需求。

每一列数据的含义分别是序号,下单时间,订单id、 商品id,分类id,分类编号 ,品牌  、价格、年龄、性别、地区,为避免代码中使用数字太抽象,我们定义一些常量来表示这些数字的含义。

/*
        数据格式
        0       1       2       3       4       5       6     7      8        9     10    11
        序号    下单时间  订单编号  商品编号 类别编号  类别码   品牌   价格   用户编号   年龄   性别   地区
     */
    static final int INDEX = 0;
    static final int TIME = 1;
    static final int ORDER_ID = 2;
    static final int PRODUCT_ID = 3;
    static final int CATEGORY_ID = 4;
    static final int CATEGORY_CODE = 5;
    static final int BRAND = 6;
    static final int PRICE = 7;
    static final int USER_ID = 8;
    static final int USER_AGE = 9;
    static final int USER_SEX = 10;
    static final int USER_REGION = 11;


 

Files.lines方法是jdk8就有了

Path.of是jdk11才有的,如果版本比较低可以使用Paths.get方法代替

 1、统计每月销售量

思路:先跳过(使用skip)第一行内容(不是数据)将流中的字符串进行分割,将流元素从大的字符串转化为字符串数据(使用map)

collect是进行元素收集的意思,下面的代码中使用groupingBy进行聚合,下游收集器是counting,流中的元素被收集为Map<YearMonth, Long>key部分是年月,value部分是销售量

static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");

    public static void main(String[] args) throws IOException {

        Stream<String> lines = Files.lines(Path.of("./data.txt"));

        Map<YearMonth, Long> collect = lines.skip(1)//跳过的第一个不是数据
         .map(array -> array.split(","))
           //提取出日期中的年月,并统计数量(一样的年月)
         .collect(groupingBy(array -> YearMonth.from(formatter.parse(array[1])), counting()));
         
        for (Map.Entry<YearMonth, Long> yearMonthLongEntry : collect.entrySet()) {
            System.out.println(yearMonthLongEntry);
        }
        lines.close();
    }

如果我们想实现按照年月排行,就不能使用无序的HashMap,而要用有序的TreeMap.在groupingBy的方法中传三个参数的方法可以执行收集容器。

public static void main(String[] args) throws IOException {

    Stream<String> lines = Files.lines(Path.of("./data.txt"));

    Map<YearMonth, Long> collect = lines.skip(1)
            .map(array -> array.split(","))
            //提取出日期中的年月
            .collect(groupingBy(array -> YearMonth.from(formatter.parse(array[1])), TreeMap::new, counting()));//指定TreeMap
    for (Map.Entry<YearMonth, Long> yearMonthLongEntry : collect.entrySet()) {
        System.out.println(yearMonthLongEntry);
    }
    lines.close();
}

2、销售最高 

①统计销售最高的月份

当我们听到“最高”的字眼,首先要想到max函数,max函数的参数是比较器,就是说以什么为标准来评判最高。我们在第一部分中统计出每月(key)的销售值(value),我们要以这里的value为评判的标准。可以使用Comparator.comparingLong(e -> e.getValue())
也可以使用Map.Entry.comparingByValue()

在刚刚的代码中加上
Map.Entry<YearMonth, Long> yearMonthLongEntry = collect.entrySet().stream().max(Comparator.comparingLong(e -> e.getValue())).get();
可以不用使用TreeMap

使用Entry.comparingByValue()也行
  Map.Entry<YearMonth, Long> yearMonthLongEntry
                = collect.entrySet().stream().max(Map.Entry.comparingByValue()).get();

所以完整的代码是

      Map.Entry<YearMonth, Long> yearMonthLongEntry = lines.skip(1)
                .map(array -> array.split(","))
                //提取出日期中的年月
                .collect(groupingBy(array -> YearMonth.from(formatter.parse(array[1])), counting()))
                .entrySet().stream().max(Map.Entry.comparingByValue()).get();

②统计销量最高的商品

这个需求就和上面的很类似了,甚至来说更加的简单,就是将元素收集为商品和销售值。

也就是以商品id进行聚合,然后以他的数值作为value.转为set集合并使用max函数找出最大值。

  lines.skip(1)//跳过的第一个不是数据
                .map(array -> array.split(","))
                //这里第一个参数为key,第二个参数为value
                .collect(groupingBy(array->array[PRODUCT_ID],counting()))
             .entrySet().stream().max(Map.Entry.comparingByValue()).ifPresent(System.out::println);

3、排序实现

①下单最多的前10名用户

这个需求很简单,就是跟前面一样按照用户id进行聚合,然后下单数为value,调用sorted进行排序,参数是比较器

注意:默认是降序,要调用reversed反转,使用limit截取前几名

但是这种先排完序,再截取的方式对于大数据量来说不划算,内存占用太大。

  //这个集合是用户为key ,订单数为value
        Map<String, Long> collect = lines.skip(1).map(e -> e.split(","))
                .collect(groupingBy(array -> array[USER_ID], counting()));
        //对结果进行排序
        collect.entrySet().stream().sorted(Map.Entry.<String,Long>comparingByValue().reversed()).limit(10).forEach(System.out::println);

注意:Map.Entry.<String,Long>comparingByValue().reversed()  之所以要指定泛型是因为调用reversed方法之后他没办法推断出泛型是什么

那有没有更好的方法呢?

那就是使用堆排序 ,使用最小堆(根节点最小的完全二叉树)

思路:遍历流中的元素,将元素添加到最小堆中(默认大小为10),如果比根节点大就添加进来,如果比根节点小就舍弃。树会自己维护

注意:java提供了一个最小堆的类 PriorityQueue,但是这个类是无限大的。所以我们要自己去实现它的子类。满足我们的需求,重写offer方法。先添加进去,大于规定的大小之外在移除根节点,树内部会自动维护。

 public static void main(String[] args) throws IOException {

        Stream<String> lines = Files.lines(Path.of("./data.txt"));
        Map<String, Long> collect = lines.skip(1).map(array -> array.split(","))
                .collect(groupingBy(array -> array[USER_ID], counting()));

        MyPriorityQueue<Map.Entry<String, Long>> queue = collect.entrySet().stream().collect(
                () -> new MyPriorityQueue<>(Map.Entry.comparingByValue(), 10),
                MyPriorityQueue::offer,
                AbstractQueue::addAll
        );
        while (!queue.isEmpty()) {
            System.out.println(queue.poll());
        }
        lines.close();
    }
----------------------------------------------------------------------------------
class MyPriorityQueue<T> extends PriorityQueue<T>{

    private int max;//设置小顶堆的容量

    public MyPriorityQueue(Comparator<? super T> comparator, int max) {
        super(comparator);
        this.max = max;
    }

    //重写父类的添加方法

    @Override
    public boolean offer(T t) {

        boolean r = super.offer(t);//先添加进去 ,根节点是最小值
        if(this.size()>max){
            //如果节点数比容量大,删除根节点
            this.poll();
        }
        return r;

    }
}

②两次分组

打印出每一个地区的前五个用户(未排序)

地区需要聚合,每一个地区里面的用户id也需要聚合,value是下单次数

收集为Map<String,Map<String,Long>>

打印出每一个地区的前五个
public static void main(String[] args) throws IOException {

        Stream<String> lines = Files.lines(Path.of("./data.txt"));
        Map<String, Map<String, Long>> collect = lines.skip(1).map(array -> array.split(","))
                .collect(groupingBy(array -> array[USER_REGION], groupingBy(array -> array[USER_ID], counting())));//先按照地区分组,在按照用户分组


        for (Map.Entry<String, Map<String, Long>> stringMapEntry : collect.entrySet()) {
            System.out.println(stringMapEntry.getKey());
            System.out.println("---------------------------");
            int i=0;
            for (Map.Entry<String, Long> stringLongEntry : stringMapEntry.getValue().entrySet()) {
                if(i>=5){
                   
                    break;
                }
                i++;
                System.out.println(stringLongEntry);
            }

        }

    }
 每个地区下单最多用户

对上一步收集的每个地区的每一个用户的下单数的集合,进行处理。处理的关键点是用户的下单数(收集为Map<String Entry<String,Long>

 public static void main(String[] args) throws IOException {

        Stream<String> lines = Files.lines(Path.of("./data.txt"));
        Map<String, Map<String, Long>> collect = lines.skip(1).map(array -> array.split(","))
                .collect(groupingBy(array -> array[USER_REGION], groupingBy(array -> array[USER_ID], counting())));


        Stream<Map.Entry<String, Optional<Map.Entry<String, Long>>>> entryStream = collect.entrySet().stream().map(
                //转化为新的map集合
                e -> Map.entry(
                        e.getKey(),//原先的key就是地区
                        //原先的value是每个地区的键值对(键是用户名,值是下单次数)
                        //value转化为  原先键值对中的值最大的那个键值对
                        e.getValue().entrySet().stream().max(Map.Entry.comparingByValue())
                )
        );
        entryStream.forEach(System.out::println);


    }

组内前三(每个省份的前三名)

跟第二步差不多,只不过这个是统计前三名的,也是先排序再截取,可以使用堆排序,但这里就不演示了

public static void main(String[] args) throws IOException {

        Stream<String> lines = Files.lines(Path.of("./data.txt"));
        Map<String, Map<String, Long>> collect = lines.skip(1).map(array -> array.split(","))
                .collect(groupingBy(array -> array[USER_REGION], groupingBy(array -> array[USER_ID], counting())));


        List<Map.Entry<String, Stream<Map.Entry<String, Long>>>> list = collect.entrySet().stream().map(e -> Map.entry(
                e.getKey(), e.getValue().entrySet().stream().sorted(Map.Entry.<String, Long>comparingByValue().reversed()).limit(3)
        )).toList();
        for (Map.Entry<String, Stream<Map.Entry<String, Long>>> stringStreamEntry : list) {
            System.out.println(stringStreamEntry.getKey());
            stringStreamEntry.getValue().forEach(System.out::println);
        }


    }

 

4、按类别统计

Map<String, Long> collect = lines.skip(1).map(array -> array.split(","))
 .filter(array->!array[CATEGORY_CODE].isEmpty())
                .collect(groupingBy(array -> array[CATEGORY_CODE], counting()));
        for (Map.Entry<String, Long> stringLongEntry : collect.entrySet()) {
            System.out.println(stringLongEntry);
        }

但是这里的类别有多重,我们只想要第一种类别进行统计,比如安卓手机、苹果手机都是手机类别。 

 Map<String, Long> collect = lines.skip(1).map(array -> array.split(","))
                .filter(array->!array[CATEGORY_CODE].isEmpty())
                .collect(groupingBy(array -> {
                    String c = array[CATEGORY_CODE];
                    String result = c.split("\\.")[0];
                    return result;
                  /*  int index = c.indexOf(".");
                    String result = c.substring(0, index);
                    return result;*/
                }, counting()));

5、按区间统计

 public static void main(String[] args) throws IOException {



        // 7) 按价格区间统计销量
       
            try (Stream<String> lines = Files.lines(Path.of("./data.txt"))) {
                Map<String, Long> collect = lines.skip(1)
                        .map(line -> line.split(","))
                        .map(array -> Double.valueOf(array[PRICE]))
                        .collect(groupingBy(mytest::priceRange, counting()));

                for (Map.Entry<String, Long> e : collect.entrySet()) {
                    System.out.println(e);
                }

            } catch (IOException e) {
                throw new RuntimeException(e);
            }


    }
    static String priceRange(Double price) {
        if (price < 100) {
            return "[0,100)";
        } else if (price >= 100 && price < 500) {
            return "[100,500)";
        } else if (price >= 500 && price < 1000) {
            return "[500,1000)";
        } else {
            return "[1000,∞)";
        }
    }

private static void case8() {
    try (Stream<String> lines = Files.lines(Path.of("./data.txt"))) {
        Map<String, Map<String, Long>> map = lines.skip(1)
                .map(line -> line.split(","))
                .filter(array -> array[USER_SEX].equals("女"))
                .filter(array -> !array[CATEGORY_CODE].isEmpty())
                .collect(groupingBy(AnalysisTest::ageRange,
                        groupingBy(AnalysisTest::firstCategory, TreeMap::new, counting())));

        for (Map.Entry<String, Map<String, Long>> e1 : map.entrySet()) {
            for (Map.Entry<String, Long> e2 : e1.getValue().entrySet()) {
                System.out.printf("%-12s%-15s%d%n", e1.getKey(), e2.getKey(), e2.getValue());
            }
        }

    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

static String ageRange(String[] array) {
    int age = Double.valueOf(array[USER_AGE]).intValue();
    if (age < 18) {
        return "[0,18)";
    } else if (age < 30) {
        return "[18,30)";
    } else if (age < 50) {
        return "[30,50)";
    } else {
        return "[50,∞)";
    }
}

 static String firstCategory(String[] array) {
        String c = array[CATEGORY_CODE];
        int idx = c.indexOf(".");
        return c.substring(0, idx);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值