ПРОБЛЕМА
В PowerQuery мне нужно перейти из этой таблицы (вход)
к этой таблице (выход)
где каждый столбец YYYYMM (кроме первого, в данном случае 202401) преобразуется путем вычитания значения в столбце сразу слева.
Гарантировано, что в последовательности не будет пропущенных месяцев.
Количество столбцов месяцев при каждом обновлении неизвестно, поэтому мне нужно делать это динамически.
ПОКА
После исследования я нашел многообещающий подход в этом ТАК ответе.
Это работает:
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))}
)
)
)
Приведенный выше код не работает по двум причинам:
а. List.Transform не возвращает действительный TransformOperations, так как каждая ошибка преобразования строки выводится с
Expression.Error: Expected a TransformOperations value.
Details:
[List]
б. Он не будет обрабатывать январь, поскольку в предыдущем столбце будет другой год и месяц (например: когда мне нужно вычесть 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, "01")
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 работает очень быстро и решает мою проблему.
periods = List.Sort(List.Skip(Table.ColumnNames(input), 1), Order.Ascending)
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, например: N202401), но Power Query очень медленно пытается преобразовать столбцы на месте, и я не уверен, почему (хотелось бы понять хотя больше).
Итак, чтобы восстановить исходные имена столбцов, мне нужен дополнительный шаг:
clean = Table.RenameColumns(output, List.Transform(periods, (p) => {"N" & p, p}))
Этот шаг не требует каких-либо заметных накладных расходов, и я все равно получаю желаемый результат за несколько секунд, так что я определенно могу с этим смириться.
АЛЬТЕРНАТИВНОЕ, НО МЕДЛЕННОЕ РЕШЕНИЕ С Table.ReplaceValue
Поскольку изначально я хотел найти решение, не прибегая к новым столбцам, я использовал свой первый рабочий подход, упомянутый в вопросе.
Я придумал второе решение, в котором используются List.Accumulate и Table.ReplaceValue, но оно все равно очень медленное, я просто добавляю его для полноты картины.
periods = List.Sort(List.Skip(Table.ColumnNames(input), 1), Order.Ascending)
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. Ничего страшного, но все еще далеко от оптимального.
Также это нужно начинать с последнего месяца по направлению к первому (periods нужно сортировать с помощью Order.Descending), так как значения в столбцах теперь изменяются при дальнейшей замене значений в последующих столбцах.
Запрос с этим шагом занимает около 2 минут 45 секунд, что составляет около половины времени выполнения исходного рабочего метода, но все же примерно в 10 раз больше, чем мое предпочтительное решение.
ДРУГИЕ РЕШЕНИЯ
Хотя для меня этого решения достаточно, мне бы хотелось увидеть другие подходы от других пользователей PowerQuery.
Если вы предложите лучшее решение, не стесняйтесь опубликовать его, и я изменю проверенный ответ, если он будет быстрее и проще, чем мой.
Мне было бы интересно, если это будет быстрее. Другой подход заключался бы в том, чтобы
Shifted Value (чтобы не использовать столбец индекса).Данные
М-код
let
Source = Excel.CurrentWorkbook(){[Name = "Table8"]}[Content],
#"Unpivoted Other Columns" = Table.UnpivotOtherColumns(Source, {"ID"}, "Attribute", "Value"),
#"Grouped Rows" = Table.Group(#"Unpivoted Other Columns", {"ID"}, {
{"Subtract", (t)=>let
#"Add Shifted" =
Table.FromColumns(
Table.ToColumns(t) &
{{null} & List.RemoveLastN(t[Value])},
{"ID", "Attribute","Value","Shifted Value"}),
#"New Value" = Table.AddColumn(#"Add Shifted","New", each ([Value] - [Shifted Value])??[Value], type number),
#"Remove Columns" = Table.RemoveColumns(#"New Value",{"Value","Shifted Value"}),
#"Rename" = Table.RenameColumns(#"Remove Columns",{{"New","Value"}}),
#"Pivot" = Table.Pivot(#"Rename", #"Rename"[Attribute], "Attribute","Value")
in
#"Pivot"
}}),
#"Expanded Subtract" = Table.ExpandTableColumn(#"Grouped Rows", "Subtract", List.RemoveFirstN(Table.ColumnNames(Source),1)),
#"Type Data" = Table.TransformColumnTypes(#"Expanded Subtract",
List.Transform(List.RemoveFirstN(Table.ColumnNames(Source),1), each {_, Int64.Type}))
in
#"Type Data"
Результаты
Редактировать
Для техники, использующей функцию Table.ReplaceValue вместе с List.Accumulate, я предлагаю следующее:
let
Source = Excel.CurrentWorkbook(){[Name = "Table8"]}[Content],
#"Change Type" = Table.TransformColumnTypes(Source,
{{"ID", type text}} & List.Transform(List.RemoveFirstN(Table.ColumnNames(Source),1), each {_, Int64.Type})),
//Column pairs to process
cp= List.Reverse(List.RemoveLastN(
List.Zip({List.RemoveFirstN(Table.ColumnNames(Source),1), List.RemoveFirstN(Table.ColumnNames(Source),2)})
,1)),
#"Replace Values" = List.Accumulate(
cp,
#"Change Type",
(s,c)=> Table.ReplaceValue(
s,
each Record.Field(_,c{1}),
each Record.Field(_,c{0}),
(x,y,z) as number => y-z,
{c{1}}
)
)
in
#"Replace Values"
Я отметил это как принятый ответ, поскольку это не только быстрее, чем мое решение, адаптированное к моему конкретному набору данных (<10 с против ~ 15 с), но также удаляет шаг поворота (данные, которые я представил в вопросе, были развернуты ранее в запросе для отображения месяцев в столбцах), поэтому в моем случае лучше сместить значения. Спасибо за отзыв, приятно видеть другой подход.
Кстати, вы можете назначать примитивные типы данных в
Table.ReplaceValue, используя специальную функцию замены. например,Table.ReplaceValue(#"Change Type", each [202405], each [202404],(x,y,z) as number => y-z,{"202405"}назначит тип номера. К сожалению, вы не можете печатать не как примитивный тип.