问题
在 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
的列表,所以我尝试动态生成转换列表,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))}
)
)
)
我没有使用这个答案来解决我自己的问题的原因是,这在我提供的测试表上工作得很快,但在我的有数百行甚至可能数千行的主表上速度非常慢。
目前不确定我是否应该尝试这样做,但我很确定这可以通过合理的性能来完成。如果我发现任何更引人注目的内容,我会更新/回答问题。
快速解决方案
我将其标记为已回答,因为在
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 用户的不同方法。
如果您提出更好的解决方案,请随时发布,如果它比我的更快、更简单,我将更改经过验证的答案。