Power BI Desktop DAX перезапускает итоговый столбец - PullRequest
9 голосов
/ 29 января 2020

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

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

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

enter image description here

Ответы [ 6 ]

5 голосов
/ 03 февраля 2020

Обзор

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

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

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

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

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

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

Результат

enter image description here

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])
))

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

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

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

Проблема

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

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

3 голосов
/ 05 февраля 2020

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

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

РЕЗУЛЬТАТ

enter image description here

ДЕТАЛИ

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

(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]

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

3 голосов
/ 01 февраля 2020

Надеюсь, в следующий раз вы вставите 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
2 голосов
/ 01 февраля 2020

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

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 и датой текущей записи. Используя эти дни, мы можем вернуть окончательное значение для промежуточного итога.

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

2 голосов
/ 29 января 2020

Потребовалось время, но я смог найти обходной путь. Предполагая, что значение баланса для пробелов всегда равно -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

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

1 голос
/ 27 февраля 2020

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

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

(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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...