Динамическое преобразование значений столбца путем вычитания предыдущего столбца

ПРОБЛЕМА

В 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))}
        )
    )
)  

Причина, по которой я не использую этот ответ для своего собственного вопроса, заключается в том, что он работает быстро в предоставленной мной тестовой таблице, но невероятно медленно в моей основной таблице с сотнями, а возможно, и тысячами строк.

На данный момент я не уверен, стоит ли мне вообще пытаться это сделать, но я почти уверен, что это можно сделать с разумной производительностью. Если я найду что-нибудь более убедительное, я обновлю/отвечу на вопрос.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
139
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

БЫСТРОЕ РЕШЕНИЕ
Я публикую это как ответ, поскольку использование 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.

Если вы предложите лучшее решение, не стесняйтесь опубликовать его, и я изменю проверенный ответ, если он будет быстрее и проще, чем мой.

Кстати, вы можете назначать примитивные типы данных в Table.ReplaceValue, используя специальную функцию замены. например, Table.ReplaceValue(#"Change Type", each [202405], each [202404],(x,y,z) as number => y-z,{"202405"} назначит тип номера. К сожалению, вы не можете печатать не как примитивный тип.

Ron Rosenfeld 06.04.2024 14:06
Ответ принят как подходящий

Мне было бы интересно, если это будет быстрее. Другой подход заключался бы в том, чтобы

  • Отменить поворот всего, кроме столбца идентификатора
  • Группировать по идентификатору
  • Внутри каждой группы
    • Добавьте столбец 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 с), но также удаляет шаг поворота (данные, которые я представил в вопросе, были развернуты ранее в запросе для отображения месяцев в столбцах), поэтому в моем случае лучше сместить значения. Спасибо за отзыв, приятно видеть другой подход.

andreucci 06.04.2024 21:46

Другие вопросы по теме

Похожие вопросы

Можно ли использовать сценарии R в качестве источника данных для PowerBI при публикации в службе PowerBI?
Прогнозирование множественных тенденций линейной регрессии при визуализации данных одного и того же временного ряда
Визуализация динамической матрицы с входными данными «строки», поступающими в результате выбора слайсера (power bi)
Динамический заголовок Power BI — как вернуть «Все», если выбраны все параметры среза
DAX SUMMARIZECOLUMNS, почему функция FORMAT вызывает отображение пустых строк из таблицы дат
Как заполнить меру в Power BI
PowerQuery – заменить значения списка объединением и объединить без расширения
Рассчитать среднюю продолжительность по тексту в степени bi
Power-BI/Dax: подсчет электронных писем несколько раз с помощью функции на основе одного и того же измерения (#Order)
Сумма с начала года по месяцам, используя только последнее значение для каждого месяца