Power BI Desktop DAX перезапускает итоговый столбец

9

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

CALCULATE(
SUM(Leave[Daily Balance]),
FILTER(
   ALLEXCEPT(Leave, Leave[Employee Id]),
   Leave[Date] <= EARLIER(Leave[Date])
))

но мне нужно, чтобы промежуточный итог перезапускался с 1, если Type = Working И промежуточный итог Daily Balance меньше нуля, а тип предыдущей строки не равен Working. Ниже приведен скриншот из Excel. Обязательный столбец функции - это то, к чему мне нужно добраться.

введите описание изображения здесь

LynseyC
источник
1
В строке за 5 ноября, Лицо 1, предположим, что у наших тестовых данных был пустой тип. Будет ли «требуемая функция» возвращать 1 или 2 6 ноября?
Райан Б.
Он вернет 2 за 6 ноября. «Сброс» не произойдет, потому что 5 ноября будет 1 (не отрицательное число). Спасибо за ваш подробный пост. Я рассматриваю сегодня
LynseyC

Ответы:

1

Это не только промежуточный итог с условием, но и вложенный / кластерный, поскольку логика должна применяться на уровне идентификатора. Для больших таблиц M лучше, чем DAX, так как он не использует столько оперативной памяти. (Я писал об этом здесь: ссылка на блогпост

Следующая функция адаптирует эту логику к текущему случаю и должна применяться на уровне идентификатора: (Обязательные имена столбцов: «Тип», «Ежедневное пособие», «Корректировки»)

(MyTable as table) => let SelectJustWhatsNeeded = Table.SelectColumns(MyTable,{"Type", "Daily Allowance", "Adjustments"}), ReplaceNulls = Table.ReplaceValue(SelectJustWhatsNeeded,null,0,Replacer.ReplaceValue,{"Adjustments"}), #"Merged Columns" = Table.CombineColumns(ReplaceNulls,{"Daily Allowance", "Adjustments"}, List.Sum,"Amount"), TransformToList = List.Buffer(Table.ToRecords(#"Merged Columns")), ConditionalRunningTotal = List.Skip(List.Generate( () => [Type = TransformToList{0}[Type], Result = 0, Counter = 0], each [Counter] <= List.Count(TransformToList), each [ Result = if TransformToList{[Counter]}[Type] = "working" and [Result] < 0 and [Type] <> "working" then TransformToList{[Counter]}[Amount] else TransformToList{[Counter]}[Amount] + [Result] , Type = TransformToList{[Counter]}[Type], Counter = [Counter] + 1 ], each [Result] )), Custom1 = Table.FromColumns( Table.ToColumns(MyTable) & {ConditionalRunningTotal}, Table.ColumnNames(MyTable) & {"Result"} ) in Custom1

ImkeF
источник
Это решило проблему. Работает отлично и не замедлил отчет. Спасибо
LynseyC
5

обзор

Это сложная вещь, чтобы попросить PowerBI сделать, поэтому может быть трудно найти аккуратный подход.

Самая большая проблема заключается в том, что модель данных PowerBI не поддерживает концепцию промежуточного подсчета - по крайней мере, не так, как в Excel. В Excel столбец может ссылаться на значения, которые встречаются в «предыдущей строке» этого же столбца, а затем корректироваться с помощью некоторых «ежедневных изменений», перечисленных в другом столбце.

PowerBI может имитировать это только путем суммирования всех ежедневных изменений в некотором подмножестве строк. Мы берем значение даты в нашей текущей строке и создаем отфильтрованную таблицу, в которой все даты меньше даты текущей строки, а затем суммируем все ежедневные изменения из этого подмножества. Может показаться, что это небольшая разница, но она весьма значительна:

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

Мы должны отказаться от концепции «перезагрузки» и вместо этого представить себе создание столбца, который содержит значение «корректировки». Нашей корректировкой будет значение, которое может быть включено таким образом, чтобы при выполнении описанных условий сумма ежедневных остатков и корректировок составляла 1.

Если мы посмотрим на расчетный пробег, заданный OP, то увидим, что значение нашего промежуточного итога в «нерабочий» день непосредственно перед «рабочим» днем ​​дает нам ту необходимую сумму, которая, если ее изменить, суммируется до нуля и вызвать увеличение промежуточного итога в каждый следующий рабочий день на единицу. Это наше желаемое поведение (с одной проблемой, которая будет описана позже).

Результат

введите описание изображения здесь

Most Recent Date Prior to Work = 

CALCULATE(
Max(Leave[Date]),
FILTER(
   ALLEXCEPT(Leave, Leave[Id]),
   Leave[Date] = EARLIER(Leave[Date]) -1 && Leave[Type] <> "Working" && Earlier(Leave[Type]) = "Working"
))

Это помогает узнать разницу между контекстами строки и фильтра и тем, как EARLIER работает, чтобы следовать этим вычислениям. В этом сценарии вы можете думать о «РАННЕЕ» как о значении «эта ссылка указывает на значение в текущей строке», а в противном случае ссылка указывает на всю таблицу, возвращаемую «ALLEXCEPT (Leave, Leave [Id])». В этом Кстати, мы находим места, где текущая строка имеет тип «Рабочая», а строка предыдущего дня имеет какой-то другой тип.

Most Recent Date Prior to Work Complete = 

CALCULATE(
Max(Leave[Most Recent Date Prior to Work]),
FILTER(
   ALLEXCEPT(Leave, Leave[Id]),
   Leave[Date] <= EARLIER(Leave[Date])
))

Этот расчет имитирует операцию «заполнения». В нем говорится: «При просмотре всех строк, дата которых предшествует дате в ЭТОЙ строке, верните самое большое значение в« Самая последняя дата перед началом работы ».

Daily Balance Adjustment = 

CALCULATE(
SUM(Leave[Running Daily Balance]),
FILTER(
   ALLEXCEPT(Leave, Leave[Id]),
   Leave[Date] = EARLIER(Leave[Most Recent Date Prior to Work Complete])
))

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

Adjusted Daily Balance = Leave[Running Daily Balance] - Leave[Daily Balance Adjustment]

И, наконец, мы применяем корректировку к нашему промежуточному итогу для конечного результата.

Проблема

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

Так что это как далеко я могу принести вам. Надеюсь, поможет.

Райан Б.
источник
1
Что касается вашего последнего пункта, я считаю, что вы правы. DAX не может делать рекурсию.
Алексис Олсон
3

Надеюсь, в следующий раз вы вставите CSV-код или код, который генерирует образцы данных вместо изображения. :)

Позвольте мне просто предложить вам сделать расчеты в PowerQuery. Я попытался разделить код на несколько шагов, чтобы улучшить читаемость. Это может выглядеть немного сложнее, но работает хорошо. Просто вставьте его в расширенный редактор и замените источник исходными данными. Удачи!

let
    Source = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("i45WMjDUMzDSMzIwtFTSUQpILSrOz1MwBDLL84uyM/PSlWJ1gGqMsKuBSBrjkzQhwnRTItSYEaHGHJ9DLPBJWhI23dAAjwGGOAIRIokj9OCmxwIA", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type text) meta [Serialized.Text = true]) in type table [date = _t, name = _t, #"type" = _t]),
    SetTypes = Table.TransformColumnTypes(Source,{{"date", type date}, {"name", type text}, {"type", type text}}),
    TempColumn1 = Table.AddColumn(SetTypes, "LastOtherType", (row)=>List.Max(Table.SelectRows(SetTypes, each ([name] = row[name] and [type] <> row[type] and [date] <= row[date]))[date], row[date]), type date) //Here for each row we select all rows of other type with earlier date, and take max that date. Thus we know when was previous change from one type to another
 //Here for each row we select all rows of other type with earlier date, and take max that date. Thus we know when was previous change from one type to another
,
    TempColumn2 = Table.AddColumn(TempColumn1, "Count", (row)=>
(if row[type]="working" then 1 else -1) * 
Table.RowCount(
Table.SelectRows(SetTypes, each ([name] = row[name] and [type] = row[type] and [date] <= row[date] and [date] > row[LastOtherType])) /* select all rows between type change (see prev step) and current row */
), /*and count them*/
Int64.Type) // finally multiply -1 if they are not working type
,
    FinalColumn = Table.AddColumn(TempColumn2, "FinalFormula", (row)=> 
(if row[type] = "working" then row[Count] else /* for working days use Count, for others take prev max Count and add current Count, which is negative for non-working*/
Table.LastN(Table.SelectRows(TempColumn2, each [name] = row[name] and [type] = "working" and [LastOtherType] <= row[LastOtherType]),1)[Count]{0}
+ row[Count])
, Int64.Type),
    RemovedTempColumns = Table.RemoveColumns(FinalColumn,{"LastOtherType", "Count"})
in
    RemovedTempColumns
Евгений
источник
Я не уверен, что это охватывает каждый сценарий, но кажется, что это правильный подход.
Майк Хани
Я могу заставить это работать, только если первый тип для каждого человека Работает. Также, как и в примерах DAX, он перезапускает нумерацию для рабочего движения, когда кумулятивная сумма для предыдущего ряда является положительным числом. Я предполагаю, что моя картина вводила в заблуждение, поскольку она содержала только этот сценарий. Я должен был включить время, когда тип изменился на рабочий, но итог предыдущего ряда был положительным.
LynseyC
@LynseyC ну, этот код, конечно, не идеальное и законченное решение, а скорее пример методов, которые можно использовать. Просто измените, если для вашего сценария.
Евгений
@LynseyC также, одним из преимуществ выполнения этой математики в PowerQuery, а не в DAX, является простой способ сохранить временные столбцы вне модели данных.
Евгений
3

Я думаю, что у меня есть это!

Вот результат, основанный на решении, которое я опубликовал ранее: (Данные были изменены, чтобы показать больше поведения "работа / нет работы" и варианты использования)

РЕЗУЛЬТАТ

введите описание изображения здесь

ПОДРОБНОСТИ

(1) Отбросьте столбцы «Скорректированный текущий дневной баланс» и «Регулировка ежедневного баланса». Мы получим тот же результат с одним меньшим шагом за мгновение.

(2) Создайте следующий столбец (RDB = "текущий дневной баланс") ...

Grouped RDB = 

CALCULATE(
SUM(Leave[Daily Balance]),
FILTER(
   ALLEXCEPT(Leave, Leave[Id], Leave[Most Recent Date Prior to Work Complete]),
   Leave[Date] <= EARLIER(Leave[Date]) 
))

Создав «Самую последнюю дату перед завершением работы», у нас фактически есть кусок, необходимый для нашей «перезагрузки», которую, как я утверждал, раньше было невозможно. Фильтруя это поле, мы получаем возможность начинать каждый срез с «1».

(3) У нас все еще есть та же проблема, но мы не можем посмотреть на результат в нашем столбце и использовать его, чтобы решить, что делать позже в этом же столбце. Но мы МОЖЕМ построить новый столбец настроек, который будет содержать эту информацию! И у нас уже есть ссылка на «Самая последняя дата перед началом работы» - это последний день в предыдущей группе ... строка с информацией, которая нам нужна!

Grouped RDB Adjustment = 

VAR CalculatedAdjustment =
CALCULATE(
SUM(Leave[Grouped RDB]),
FILTER(
   ALLEXCEPT(Leave, Leave[Id]),
   Leave[Date] IN SELECTCOLUMNS(
        FILTER(
            Leave,
            Leave[Most Recent Date Prior to Work] <> BLANK() &&
            Leave[id] = EARLIER(Leave[Id])), "MRDPtW", Leave[Most Recent Date Prior to Work]) &&
   Leave[Most Recent Date Prior to Work Complete] < EARLIER(Leave[Most Recent Date Prior to Work Complete]) &&
   Leave[Most Recent Date Prior to Work Complete] <> Blank()
))

RETURN if (CalculatedAdjustment > 0, CalculatedAdjustment, 0)

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

(4) Этот последний шаг приведет корректировку к окончательному результату. Суммируйте два новых столбца, и мы наконец-то получим наш скорректированный ежедневный баланс. Вуаля!

Adjusted Running Daily Balance = Leave[Grouped RDB] + Leave[Grouped RDB Adjustment]

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

Райан Б.
источник
Привет @Ryan B. Это отлично работает для более чем 200 человек в моей организации, но один не работает. Я пытался изменить код самостоятельно, но я не могу получить ничего, чтобы решить проблему. Я думаю, это потому, что они работали долгое время, а затем работали всего один день, прежде чем у них было больше свободного времени. Я связался с изображением, чтобы показать проблему. Спасибо Изображение
LynseyC
Я изменил меру «Сгруппированная корректировка RDB», чтобы она проходила большие начисления отпуска за несколько циклов «работа / без работы».
Райан Б.
2
Привет, спасибо за все усилия, высоко ценится. К сожалению, модификация не решила проблему. Однако, если я удалил последнее условие в фильтре «Оставить [Самая последняя дата до завершения работы] <> Blank ()», это решило проблему, но затем снова сломало вызовы исходных людей :-(
LynseyC
Shoot. Ну, я надеюсь, что вы можете найти что-то, что работает.
Райан Б.
2

Прошло некоторое время, но я смог найти обходной путь. Предполагая, что значение баланса для пробелов всегда равно -1, а значение «Работает» равно 1, и эти данные доступны для всех дат без пропусков, может сработать что-то вроде приведенного ниже расчета:

Running Total = 
    VAR Employee = Leave[Employee ID]
    VAR Date1 = Leave[Date]
    VAR Prev_Blank = CALCULATE(MAX(Leave[Date]),
                        FILTER(Leave,Leave[Date] < Date1),
                        FILTER(Leave,Leave[Employee ID]=Employee),
                        FILTER(Leave,Leave[Type]=BLANK()))  
    VAR Day_count_Working = CALCULATE(COUNT(Leave[Date]),
                        FILTER(Leave,Leave[Date] > Prev_Blank),
                        FILTER(Leave,Leave[Date] <= Date1),
                        FILTER(Leave,Leave[Employee ID]=Employee),
                        FILTER(Leave,Leave[Type]="Working")) 
    VAR Day_count = CALCULATE(COUNT(Leave[Date]),
                        FILTER(Leave,Leave[Date] >= Prev_Blank),
                        FILTER(Leave,Leave[Date] <= Date1),
                        FILTER(Leave,Leave[Employee ID]=Employee)) 
RETURN (IF(Day_count_Working=BLANK(),Day_count,Day_count-1)-Day_count_Working)*-1 + Day_count_Working

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

CR7SMS
источник
Спасибо @ CR7SMS. Он перезапускает промежуточную сумму, когда тип = Рабочая, но промежуточная сумма, когда тип пуст, не работает. 7 ноября оно уменьшается до 3, а с 8-14 ноября возвращается -2. Можете ли вы помочь с внесением поправок в код, чтобы заставить работать промежуточный итог, когда тип пуст? Спасибо
LynseyC
Привет, Линси, я попробовал другой расчет. Я добавил это как другой ответ, так как расчет был немного длинным. Но, надеюсь, новый расчет работает.
CR7SMS
@ CR7SMS, пожалуйста, избегайте добавления более одного ответа на один вопрос. Это сбивает с толку других пользователей, которые могут искать похожую проблему / решение, и это нехорошо. Вместо этого вы должны добавить все, что вы можете придумать в качестве решения, к одному ответу и разбить каждый аспект на разделы.
Христос Литрас
2

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

Running Total = 
    VAR Employee = Leave[Employee ID]
    VAR Date1 = Leave[Date]
    VAR Prev_Blank = CALCULATE(MAX(Leave[Date]),
                        FILTER(Leave,Leave[Date] < Date1),
                        FILTER(Leave,Leave[Employee ID]=Employee),
                        FILTER(Leave,Leave[Type]=BLANK()))  
    VAR Prev_Working = CALCULATE(MAX(Leave[Date]),
                        FILTER(Leave,Leave[Date] < Date1),
                        FILTER(Leave,Leave[Employee ID]=Employee),
                        FILTER(Leave,Leave[Type]="Working"))    
    VAR Prev_Blank1 = CALCULATE(MAX(Leave[Date]),
                        FILTER(Leave,Leave[Date] < Prev_Working),
                        FILTER(Leave,Leave[Employee ID]=Employee),
                        FILTER(Leave,Leave[Type]=BLANK()))  
    VAR Prev_type = CALCULATE(MAX(Leave[Type]),
                        FILTER(Leave,Leave[Date] = Date1-1),
                        FILTER(Leave,Leave[Employee ID]=Employee))
    VAR Prev_Blank2 = IF(Leave[Type]="Working" && (Prev_Blank1=BLANK() || Prev_type=BLANK()),Date1-1,Prev_Blank1)    
    VAR Day_count_Working = CALCULATE(COUNT(Leave[Date]),
                        FILTER(Leave,Leave[Date] > Prev_Blank2),
                        FILTER(Leave,Leave[Date] <= Date1),
                        FILTER(Leave,Leave[Employee ID]=Employee),
                        FILTER(Leave,Leave[Type]="Working")) 
    VAR Day_count = CALCULATE(COUNT(Leave[Date]),
                        FILTER(Leave,Leave[Date] >= Prev_Blank2),
                        FILTER(Leave,Leave[Date] <= Date1),
                        FILTER(Leave,Leave[Employee ID]=Employee)) 
RETURN (IF(Day_count_Working=BLANK(),Day_count,Day_count-1)-Day_count_Working)*-1 + Day_count_Working

Я использовал кучу переменных здесь. Возможно, вы сможете придумать более короткую версию. По сути, идея состоит в том, чтобы найти предыдущее первое вхождение «Working», чтобы найти, с чего начать расчет. Это рассчитывается в переменной «Prev_Blank2». Как только мы узнаем начальную точку (здесь она начинается с 1), мы можем просто посчитать количество дней с «Working» или blank () между Prev_Blank2 и датой текущей записи. Используя эти дни, мы можем вернуть окончательное значение для промежуточного итога.

Надеюсь, это поможет;)

CR7SMS
источник