模拟绝对引用累计计数

题目:


对A列数据计算出现的次数,在工作表函数中可以使用绝对引用固定住A2实现,=COUNTIF(A$2:A2,A2)
在Power Query中实现类似的效果思路还是蛮多的,我们使用不同的方法并模拟了一万行的数据量来对比测试下效率。

最优解:

let
    源 = Excel.CurrentWorkbook(){[Name="表1"]}[Content],
    索引 = Table.AddIndexColumn(源, "索引", 1),
    分组 = Table.Group(索引, {"list"}, {"a", each Table.AddIndexColumn(_,"times",1)}),
    展开 = Table.ExpandTableColumn(分组, "a", {"索引", "times"}),
    排序 = Table.Sort(展开,{"索引", 0}),
    删除 = Table.RemoveColumns(排序,{"索引"})
in
    删除

核心步骤为分组,对分组后的表添加索引列再展开即可得到累计计数,但是会打乱原来的顺序,所以在分组前再添加一次索引,展开后按照索引排序还原顺序。
有人会纠结那个"a",那是分组后的新列名,因为分组是中间过渡所以列名无所谓,就随便起了个最短的列名。
该方法操作简单,基本上除了在分组内加索引需要嵌套一个函数外,其他步骤均可使用界面菜单完成。并且效率最高,实测1W行数据秒出结果。

其他解1:

let
    源 = Excel.CurrentWorkbook(){[Name="表1"]}[Content],
    索引 = Table.AddIndexColumn(源, "索引", 1),
    计数 = Table.AddColumn(索引, "times", each List.Count(List.PositionOf(List.FirstN(索引[list],[索引]),[list],2))),
    删除 = Table.RemoveColumns(计数,{"索引"})
in
    删除

核心函数为List.FirstN,原理和countif最接近,先添加索引,利用索引列作为List.FirstN的参数筛选出前N行来实现。
但实际会发现在1W行的数据量下效率挺低的,因为需要反复调用同一个列表,所以可以使用List.Buffer将需要反复调用的列表转为内存列表。

优化后的代码:

let
    源 = Excel.CurrentWorkbook(){[Name="表1"]}[Content],
    索引 = Table.AddIndexColumn(源, "索引", 1),
    转内存列表 = List.Buffer(索引[list]),
    计数 = Table.AddColumn(索引, "times", each List.Count(List.PositionOf(List.FirstN(转内存列表,[索引]),[list],2))),
    删除 = Table.RemoveColumns(计数,{"索引"})
in
    删除

优化前刷新需1分15秒,优化后刷新仅需2秒。

其他解2:

let
    源 = Excel.CurrentWorkbook(){[Name="表1"]}[Content],
    索引 = Table.AddIndexColumn(源, "索引", 1),
    计数 = Table.AddColumn(索引, "times", each Table.RowCount(Table.SelectRows(索引,(x)=>x[list]=[list] and x[索引]<=[索引]))),
    删除 = Table.RemoveColumns(计数,{"索引"})
in
    删除

这个方法用过无数遍了,效率不高但是很实用。先添加一列索引,然后对表筛选出[list]等于当前行[list]并且[索引]小于等于当前行[索引]的所有行,然后对筛选表计数。
同上一个方法一样,存在反复调用同一个表的情况,所以先将Table转为Records列表并使用List.Buffer转为内存列表。

优化后的代码:

let
    源 = Excel.CurrentWorkbook(){[Name="表1"]}[Content],
    索引 = Table.AddIndexColumn(源, "索引", 1),
    转为内存列表 = List.Buffer(Table.ToRecords(索引)),
    计数 = Table.AddColumn(索引, "times", each List.Count(List.Select(转为内存列表,(x)=>x[list]=[list] and x[索引]<=[索引]))),
    删除 = Table.RemoveColumns(计数,{"索引"})
in
    删除

优化前刷新大概需要三四分钟,优化后10秒。

总结:

同一个问题有不同的思路和方法,效率也千差万别。测试的1W行数据,最快的方法不到1秒,最慢的三四分钟,如果数据量再大效率也会相差更大。经常有人抱怨Power Query慢,但很有可能慢的不是Power Query而是方法。

30 Replies to “模拟绝对引用累计计数”

  1. 谢谢老师的解释。学到很多。要想成为高手,就必须向高高手学习。
    “Surround yourself with people who hold you to a higher standard than you hold yourself”, this truly make sense...Thx!!

    1. 函数很简单,只有一个参数,就是table或list。
      在内存中缓存一个表,同时在计算期间使其与外部更改隔离,这是官方的函数说明。
      用法主要有两个:
      1、将不稳定的表或列表转为稳定状态,参考http://pqfans.com/1276.html
      2、本文中其他解1,为了得到list,首先用了Excel.CurrentWorkbook获取表,然后再对表深化。如果只进行一次那么耗时可以忽略不计,但是每行都进行了一次,有多少行就要进行多少次,累计耗时就有明显差别了。
      其次,普通表存在磁盘中,而内存表存在内存中,不涉及磁盘的IO操作。所以对于需要反复调用的表,先转为内存表会提升效率。
      这是我的理解,不一定准确。

      1. 感谢作者的回复。我的电脑配置一般,14年的机器,最近刚增加了一条内存形成双通道,希望提升效率。我的excel中会有大约50~60个查询,数据源涉及8~10个,有的数据源数据较多,但不超过5000行。当我引用查询构造新查询时遇到了刷新非常慢以致无法忍受,机器会隔一段时间才读取数据(查询少的时候没有问题),经判断是硬盘转速低,影响内存写入的效率。所以我看到你的文章中关于缓存数据的做法眼前一亮,网上查询发现构建索引以及缓存数据在数据库查询中是非常普遍的做法。我疑惑的地方一是缓存是否只在当前查询中有效,引用的查询是否也会有效?二是能否列举一些table.buffer和list.buffer的应用样例用来学习规范的查询做法。

        1. 最典型的应用就是本文的案例了,还有我的回复。
          buffer能提速,但也不是适用于任何场景,在本案例里提速明显,在其他情况下可能没有多大区别。
          8-10个数据源,有50-60个查询,这本身就是不合理的结构,通常查询的数量不会比数据源的数量还多。
          之所以刷新慢,是因为计算量大,而原因又有两个方面:
          1、思路方面。就像本文给了三种解,思路各不相同,至于哪一种效率更高需要自己不断地尝试优化。也许你几十个查询才能实现的功能,换个思路后仅仅几行代码就能实现。
          2、技巧方面。同一种解,方法和思路是相同的,但加没加buffer效率又相差很大。再比如本来一个函数能够实现的功能,你不知道这个函数,绕了一大圈用了很多个函数嵌套才实现,也会增加不必要的计算量,这可以通过学习M语言后优化代码解决。

          1. 我需要统计形成几十个表格,这些表格相互之间有关联也有区别,我也觉得有些不合理,还是感谢!

  2. 源 = Excel.CurrentWorkbook(){[Name="表1"]}[Content],
    索引 = Table.AddIndexColumn(源, "索引", 1),
    分组 = Table.Group(索引, {"list"}, {"a", each Table.AddIndexColumn(_,"times",1)}),
    展开 = Table.ExpandTableColumn(分组, "a", {"索引", "times"}),
    排序 = Table.Sort(展开,{"索引", 0}),
    删除 = Table.RemoveColumns(排序,{"索引"})

    有的地方,比如table.Group后面的第二个参数{"list"}外面的{}可加可不加
    还有排序 Table.Sort(展开,{"索引", 0}),0是默认升序可省略,1是降序,如果省略0,"索引"外面{}也可以省略
    不知道为什么,那些地方应该加,那些地方可以不加?

    1. 如果有多个分组条件,需要在list内部添加多个list,写作{{},{},{}}。
      当只有一个分组条件的时候,可以写作{{}},也可以把外层的大括号去掉写作{}。
      排序也是同理,因为{}和{{}}类型都是list,从语法角度来说都符合要求。
      当然这也不是绝对的,比如= #table({"a"},{{1}}),第二参数虽然只有一行,但是也不能去掉写作{1}。
      也没有为什么,就当作是经验记住就好了。

        1. 右侧搜索关键词"累计求和",很多篇里都有的。
          回复的这个Table.First,方法和累计计数差不多,只是在加完索引后多一步,取每一行的表中前N行后求和,N就是每一行的索引号。

          1. 老师,能给出全部代码吗?用关键词搜索“累计求和”,没找到类似的解法。
            感觉Table.FirstN应该在展开前用,那样才是同一分组的FirstN;可是不展开又没法取每行的数据。
            一旦展开FirstN又变成了全部数据的FirstN了。

          2. 累计求和

            let
                源 = Excel.CurrentWorkbook(){[Name="表1"]}[Content],
                索引 = Table.AddIndexColumn(源, "索引"),
                分组 = Table.Group(索引, {"产品"}, {"t", each [a=Table.AddIndexColumn(_,"index",1),b=Table.AddColumn(a,"累计求和",each List.Sum(Table.FirstN(a,[index])[金额]))][b]}),
                展开 = Table.Sort(Table.Combine(分组[t]),"索引"),
                结果 = Table.RemoveColumns(展开,{"索引", "index"})
            in
                结果
            
          3. 老师好,如果要把 2018年7月23日 上午2:41您发表的 累计求和的结果表反推源表要怎么办呢,数据量较大
            let
            源 = Excel.CurrentWorkbook(){[Name="表1"]}[Content],
            索引 = Table.AddIndexColumn(源, "索引"),
            分组 = Table.Group(索引, {"产品"}, {"t", each [a=Table.AddIndexColumn(_,"index",1),b=Table.AddColumn(a,"累计求和",each List.Sum(Table.FirstN(a,[index])[金额]))][b]}),
            展开 = Table.Sort(Table.Combine(分组[t]),"索引"),
            结果 = Table.RemoveColumns(展开,{"索引", "index"})
            in
            结果

  3. let
    select=(x as text ,y as number)=> Table.SelectRows(展开,each [对象]=x and [次数]<=y),
    源 = Table.TransformColumnTypes(
    #table({"对象","值"},
    {{"a",100},
    {"a",150},
    {"a",100},
    {"d",210},
    {"b",300},
    {"a",100},
    {"c",250},
    {"d",210},
    {"a",100},
    {"c",250},
    {"d",210},{"a",100}}),
    {"值",type number}),
    索引 = Table.AddIndexColumn(源,
    "序号",
    1,
    1),
    分组 = Table.Group( 索引,
    {"对象"},
    {{"计数",
    each Table.AddIndexColumn(_,"次数",1)}}),
    展开 = Table.ExpandTableColumn(
    分组,
    "计数",
    {"值",
    "序号",
    "次数"}),
    求和 = Table.AddColumn(展开,
    "求和",
    each List.Sum(
    select([对象],[次数])[值])),
    删除 = Table.SelectColumns(
    Table.Sort(
    求和,{"序号",0}),
    {"对象","求和"})
    in
    删除

  4. 您好。我需要操作上百万行的数据。我用您的方法做了一天的数据(大概10万行左右),excel原始文件大概300m左右。效率比较差。不知道还有什么更好的方法?请指教

  5. let
    源 = Excel.Workbook(File.Contents("D:\原始.xlsx"), null, true){[Item="表7",Kind="Table"]}[Data],
    索引 = Table.AddIndexColumn(源, "索引", 1),
    分组 = Table.Group(索引, {"list"}, {"a", each Table.AddIndexColumn(_,"times",1)})
    【报错Expression.Error: 找不到表的“list”列。
    详细信息: list】

  6. 噗哈哈哈哈 我知道我为啥没list了 无知的我笑出了声 不好意思 我太蠢了 list 指的是 我自己工作簿里头的列字段名称。。。
    开心~

  7. 如果是更复杂的多条件筛选后求每行的值(或者求最大、最小值),是不是只有其他解2的selectrows可以胜任
    就算加了buffer,数据一多还是有点慢
    不懂大神能不能提供个好的思路,非常感谢!!!

  8. 关于最优办法的去重问题,如下,
    如果我的原始数据里面,不止一列list,该如何处理呢?

    谢谢

    let
    源 = Excel.CurrentWorkbook(){[Name="表1"]}[Content],
    索引 = Table.AddIndexColumn(源, "索引", 1),
    分组 = Table.Group(索引, {"list"}, {"a", each Table.AddIndexColumn(_,"times",1)}),
    展开 = Table.ExpandTableColumn(分组, "a", {"索引", "times"}),
    排序 = Table.Sort(展开,{"索引", 0}),
    删除 = Table.RemoveColumns(排序,{"索引"})
    in
    删除

  9. let
    源 = Excel.CurrentWorkbook(){[Name="表1"]}[Content],
    已添加索引 = Table.AddIndexColumn(源, "索引", 1, 1),
    fx=(x,y)=>List.NonNullCount(Table.SelectRows(已添加索引, each ([list] = x) and [索引]<= y )[list]),
    aa=Table.AddColumn(已添加索引,"times",each fx([list],[索引])),
    删除的列 = Table.RemoveColumns(aa,{"索引"})
    in
    删除的列

  10. 请问分组那一个步骤是如何操作的?为什么我点击“分组依据”,做出来的结果不能展开?

  11. 底层原理分别是,检索整张表格;检索部份表格;索引+分组+索引。要我,顶天也只能想到第二种方法。施神神!

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注