累加和移动平均

最近从前一直使用的累加套路开始会给出超栈警告了,优化了一下后发现移动平均也可以用类似的套路调优而且运行时间还特别少,所以想和大家分享一下。以下为优化后的累加代码,DB为拥有唯一列(Val)的表格:

let
    Source = List.Buffer(DB[Val]),

    Cnt = List.Count(Source),

    cSum =
        List.Generate(
            ()=>[Pre_Val = Source{0}, n = 0],
            each [n] < Cnt,
            each
                [
                    Pre_Val = [Pre_Val] + Source{n},
                    n = [n] + 1
                ],
            each [Pre_Val]
        ),

    RSL =
        Table.FromColumns(
            {
                Source,
                cSum
            },
            type table [Val = number, Cum = number]
        )
in
    RSL

以上代码的大意为:

(1)把DB表格中的Val字段化为列表并通过List.Buffer()缓冲至内存中,结果存至Source中;

(2)通过List.Count()数Source的项目总数;

(3)通过List.Generate()完成累加的过程,第一次循环会把Source的第一个数作为结果,第二次循环会把上一次循环的结果加上Source的第二个数据作为结果,如此反复直到第二个参数不被满足(List.Generate的循环机制:第一个参数会跳过第三个参数的指示, 进行第二个参数的判断,如果不满足条件会跳出循环,否则完成第个四参数的指示并输出结果,但从第二次循环开始会完成第三个参数的指示后才把结果传递到第二参数进行判断,如果满足条件会完成第四参数的指示并输出结果,直至第二个参数不再满足前不断重复);

(4)通过Table.FromColumns()把Souce和cSum还原为表格

在百万级的数据中运行以上代码的时间大约为7.6秒(第八代i7和16GB内存)且不会超栈,关键在于不像List.Accumulate()一定要在累加的过程中把结果不断拼接至容器中。以下为移动平均的演示代码:

let
    Offset = 5,

    Vals = List.Buffer(DB[Val]),

    Cnt = List.Count(Vals),

   Avgs =
        List.Generate(
            ()=>
                [
                    Sum = null,
                    n = 0
                ],
            each [n] < Cnt, 
            each if
                [n] > Offset - 2
            then
                [
                    Sum = [Sum] - Vals{n - Offset} + Vals{n},
                    n = [n] + 1
                ]
            else if
                [n] <= Offset - 3
            then
                [
                    Sum = null,
                    n = [n] + 1
                ]
            else
                [
                    Sum = 
                        List.Sum(
                            List.FirstN(
                                Vals, 
                                n + 1
                            )
                        ),
                    n = [n] + 1
                ],
            each [Sum] / Offset
        ),

    Outcome =
        Table.FromColumns(
            {
                Vals,
                Avgs
            },
            type table [Val = number, mAvg = number]
        )
in
    Outcome

以上代码的大意为:

(1)选取步长Offset为5;

(2)把DB表格中的Val字段化为列表并通过List.Buffer()缓冲至内存中,结果存至Source中;

(3)使用List.Generate完成以下循环,考虑到if的惰性判断机制,n从5到1,000,002之间需要执行的指示放在if的顶部,n从0到3之间需要执行的指示放在if的中部,n等于4的情况放在最后;

(4)通过Table.FromColumns()把Souce和Avgs还原为表格

在百万级数据中运行这段移动平均的代码需时7.8秒左右,在设计代码时很容易联想到if在每次循环中最多要判断3次,那是否应该让n从4开始,然后在头部拼接4个null。实测还是用if比拼接的方案快,原因是(1)Power Query的if性能特别好和(2)很可能由于被拼接的对象Avgs太大导致耗时过多。考虑到逆向累加也有实际的应用场景,也趁此机会分享演示代码:

let
    Vals = List.Buffer(DB[Val]),

    Cnt = List.Count(Vals),

    Loop =
        List.Transform(
            {0..Cnt-1},
            each if
                _ > 0
            then
                Vals{_} - Vals{_-1}
            else
                Vals{_}
        ),

    Outcome =
        Table.FromColumns(
            {
                Vals,
                Loop
            },
            type table [Val = number, rsCum = number]
        )
in
    Outcome

逆向累加的逻辑十分简单,n等于0时为第一项,n不等于0时为下一项减去上一项,由于没有承上启下的需要以及循环的次数是可以预知的,因此选择List.Transform()就可以了(注:在Val头部拼接0的方案没有if的方案快)。

附件

发表回复

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