区间闭合方向不一致

如果匹配的对象不是离散数,区间的闭合方向不一致会使匹配稍微复杂一点,首先介绍一下演示用的参数表Para:

为了识别区间左右端的闭合性,需要两列布林型的字段:

很显然由于需要枚举的数值数量无限多,枚举法是不适合非离散数的匹配,同时由于区间的闭合方向不一致,以List.PositionOf()为主体的方案也无法直接套用到这个问题中。不过,筛选参数表的方案稍稍调整一下就可以使用了,演示代码如下:

let
    Buffering =
        List.Buffer(
            Table.ToRows(Para)
        ),
    Add_TB =
        Table.AddColumn(
            DB,
            "Des",
            (x)=>
                List.Select(
                    Buffering,
                    (y)=>
                            (
                                if
                                    y{2} = true
                                then
                                    y{0} <= x[Val]
                                else
                                    y{0} < x[Val]
                            )
                        and
                            (
                                if
                                    y{3} = true
                                then
                                    y{1} >= x[Val]
                                else
                                    y{1} > x[Val]
                            )
                ){0}{4},
            type text
        )
in
    Add_TB

以上代码的大意为:

(1)通过List.Buffer()把列表化的参数表缓冲至内存;

(2)通过if把左右闭合的四种情况考虑进来,让List.Select选出唯一匹配的区间,最后深化出区间的描述

使用这种方法处理百万级的数据需时9.3秒左右。动态的多重if方案经过调整也可以用来解决这个问题,不过有个问题不是那么容易察觉到: 除非显示地在公式中指定Precision.Decimal,否则Power Query的number数据默认精度为Double, 比如:1/3会被显示为0.33333333333333331,并且从0.33333333333333329到0.33333333333333334之间(包括两端)的数都被认为等于1/3。因此,需要确保参数表中的数值在转换为文字时需要保留足够的小数位,例如1/3应该被转换为"0.33333333333333331"而不是"0.33",演示代码如下:

let
    Source =
        Expression.Evaluate(
                "(x)=>"
            &
                Text.Combine(
                        Table.TransformRows(
                            Table.RemoveLastN(Para, 1),
                            (x)=>
                                Text.Format(
                                    "if x[Val] #{0} #{1} then #{2}",
                                    {
                                        if x[eIsClose] then "<=" else "<",
                                        x[End],
                                        Expression.Constant(x[Des])
                                    }
                                )
                        )
                    ﹠
                        {Expression.Constant(Table.Last(Para)[Des])},
                    " else "
                )
        )
in
    Source

F1的构造几乎与离散数的一样,仅仅多了一个判断用来选择"<="和"<"的步骤。得益于if的高效,相同条件下的运行时间约为6.7秒。既然来到AI的时代,不妨听听AI的思路: 以[0, 1/6]和(1/6, 1/3)这两个虚构区间为例,计算出1/6的下一个双精度浮点数x(只要不显式地指定Precision.Decimal,默认精度为Double),把区间修改为[0, x)和[x, 1/3)实现所有区间统一为左闭右开,但下一个浮点是很难计算的,AI构造的函数F如下:

(x as number) as number =>
    let
        abs = Number.Abs(x),
        rsl =
            if 
                abs < Min_Norm_EXP //Number.Power(2, -1022)
            then 
                Number.Epsilon
            else
                Number.Power(
                    2, 
                    Value.Subtract(
                        Number.RoundDown(
                            Number.Log(abs, 2),
                            0
                        ),
                        52
                    )
                )
    in
        rsl

然后需要更新参数表为Para_Adj:

let
    s_adj =
        Table.AddColumn(
            Para,
            "Start_Adj",
            each if
                [Start] = Number.NegativeInfinity
            then
                [Start]
            else if
                [sIsClose]
            then
                [Start]
            else
                [Start] + F([Start]),
            type number
        ),
    e_adj =
        Table.AddColumn(
            s_adj,
            "End_Adj",
            each if
                [End] = Number.PositiveInfinity
            then
                [End]
            else if
                [eIsClose]
            then
                [End] + F([End])
            else
                [End],
            type number
        ),
    rsl = Table.SelectColumns(e_adj, {"Start_Adj", "End_Adj", "Des"})
in
    rsl

这样所有区间都统一为左闭右开,筛选参数表法就可以简化为:

let
    Buffering =
        List.Buffer(
            Table.ToRows(Para_Adj)
        ),
    Add_TB =
        Table.AddColumn(
            DB,
            "Des",
            (x)=>
                List.Select(
                    Buffering,
                    (y)=> y{0}<= x[Val] and y{1} > x[Val]
                ){0}{2},
            type text
        )
in
    Add_TB

简化后运行时间稍微降低至8.7秒,由于区间闭合方向统一了,List.PositionOf()为主体的方案也能用了:

let
    S =
        List.Buffer(
            Para_Adj[Start_Adj]
        ),
    D =
        List.Buffer(
            Para_Adj[Des]
        ),
    Add_TB =
        Table.AddColumn(
            DB,
            "Des",
            (x)=>
                D{
                    List.PositionOf(
                        S,
                        x[Val],
                        Occurrence.Last,
                        (k, j)=> k <= j
                    )
                },
            type text
        )
in
    Add_TB

相同条件下以上代码的运行时间大约为8.3秒。由于统一闭合方向对于多重if的方案来说仅减少一个判断的步骤,可预见提升应该不显著。考虑性能和理解的难度,针对区间闭合不一致的问题,似乎应该首选多重if方案。

附件

发表回复

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