通过减去前一列来动态转换列值

问题描述 投票:0回答:1

问题

在 PowerQuery 中我需要从这个表开始(input

到此表(输出

其中每个

YYYYMM
列(第一列除外,在本例中为
202401
)通过减去紧邻左侧的列中的值进行转换。
保证序列中不会跳过月份。
每次刷新时月份列的数量都是未知的,所以我需要动态地执行此操作。


到目前为止
经过研究,我在这个SO答案中发现了一种有前途的方法。

这有效:

output = Table.TransformRows(
    input,
    (r) =>  Record.TransformFields(
        r,
        {{"202405", each _ - r[202404]},
        {"202404", each _ - r[202403]},
        {"202403", each _ - r[202402]},
        {"202402", each _ - r[202401]},
        {"202401", each _ - r[202312]}}
    )
)

但正如您所看到的,这种方法需要硬编码值,因此需要进行泛化。

Record.TransformFields
接受
TransformOperations
的列表,所以我尝试动态生成转换列表,但最终失败了最后成功了(参见下面的UPDATE)。

periods = List.Sort(List.Skip(Table.ColumnNames(input), 1), Order.Ascending)

output = Table.TransformRows(
    input,
    (r) =>  Record.TransformFields(
        r,
        List.Transform(
            List.Skip(periods, 1),
            each (p) => {p, each (e) => e - Record.Field(r, Text.From(Number.From(p) - 1))}
        )
    )
)

上面的代码不起作用有两个原因:

a.

List.Transform
未返回有效
TransformOperations
,因为每行转换都会出错

Expression.Error: Expected a TransformOperations value.
Details:
    [List]

b。它不会处理一月,因为上一列将具有不同的年份和月份换行(例如:当我需要从

202312
中减去
202401
时)。我想这可以通过放置在
if
转换中的
List.Transform
语句来处理(如果最后一位数字是
89
,则减去
1
)。

我还研究了

Table.TransformColumns
,但我相信
transformOperations
无法访问正在转换的列之外的值。

我什至不确定这是正确的方法,而且我找不到其他任何东西,所以我将不胜感激。


更新 - 有效但速度极慢的解决方案
我设法使上述方法发挥作用。 我错误地将

each
与显式函数声明
(a) => something(a)
一起使用。我还插入了这一年结束时的逻辑。

periods = List.Sort(List.Skip(Table.ColumnNames(input), 1), Order.Ascending)

output = Table.TransformRows(
    input,
    (r) =>  Record.TransformFields(
        r,
        List.Transform(
            List.Skip(periods, 1),
            (p) => if Text.EndsWith(p, "1")
            then {p, (e) => e - Record.Field(r, Text.From(Number.From(p) - 89))}
            else {p, (e) => e - Record.Field(r, Text.From(Number.From(p) - 1))}
        )
    )
)  

我没有使用这个答案来解决我自己的问题的原因是,这在我提供的测试表上工作得很快,但在我的有数百行甚至可能数千行的主表上速度非常慢。

目前不确定我是否应该尝试这样做,但我很确定这可以通过合理的性能来完成。如果我发现任何更引人注目的内容,我会更新/回答问题。

powerbi powerquery powerbi-desktop
1个回答
0
投票

快速解决方案
我将其标记为已回答,因为在

Table.AddColumn
 内使用 
List.Accumulate
速度非常快,它解决了我的问题。

output = Table.RemoveColumns(
    List.Accumulate(
        periods,
        input,
        (tbl, item) => Table.AddColumn(
            tbl,
            "N" & item,
            (e) => if Text.EndsWith(item, "1")
            then try Record.Field(e, item) - Record.Field(e, Text.From(Number.From(item) - 89))
                otherwise Record.Field(e, item)
            else try Record.Field(e, item) - Record.Field(e, Text.From(Number.From(item) - 1))
                otherwise Record.Field(e, item), 
            type number
        )
    ),
    periods
)

整个查询包括此步骤在我的真实数据集上运行大约 15 秒。 另一种方法在同一数据集上需要 5 分钟及以上。

使用

try ... otherwise ...
的另一个好处是不需要跳过
periods
列表中的第一个元素,并避免在缺少列时出现任何查询失败。

这并不完全是转换列,因为我正在使用临时名称创建新列(

"N" & period
),但是当尝试就地转换任何列时,Power Query 非常慢,我不确定为什么(很想理解)但更多)。

因此,为了恢复原始列名称,我需要一个额外的步骤:

clean = Table.RenameColumns(output, List.Transform(periods, (p) => {"N" & p, p}))

此步骤不会带来任何明显的开销,并且我在几秒钟内就获得了所需的结果,所以我绝对可以忍受这一点。


替代解决方案

Table.ReplaceValue

由于我最初想在不求助于新列的情况下实现解决方案,因此我探索了问题中提到的第一种工作方法。
我想出了第二个使用
List.Accumulate
Table.ReplaceValue
的解决方案,但它仍然非常慢,我只是为了完整性而添加它。

output = List.Accumulate(
    periods,
    input,
    (tbl, item) => Table.ReplaceValue(
        tbl,
        (src) => Record.Field(src, item),
        (dest) => if Text.EndsWith(item, "1")
            then Record.Field(dest, item) - Record.Field(dest, Text.From(Number.From(item) - 89))
            else Record.Field(dest, item) - Record.Field(dest, Text.From(Number.From(item) - 1)), 
        Replacer.ReplaceValue,
        periods
    )
)

这种方法除了速度慢之外,还具有将列类型更改回

Any
的缺点。没什么大不了的,但还远未达到最佳状态。
此外,这需要从上个月开始到第一个月,因为当进一步替换连续列中的值时,列中的值会发生更改。

此步骤的查询大约需要 2 分 45 秒,大约是原始工作方法运行时间的一半,但仍然比我首选的解决方案多 ~x10。


其他解决方案
尽管这对我来说已经足够了,但我希望看到其他 PowerQuery 用户的不同方法。

如果您提出更好的解决方案,请随时发布,如果它比我的更快、更简单,我将更改经过验证的答案。

© www.soinside.com 2019 - 2024. All rights reserved.