Coluna total de reinicialização do Power BI Desktop DAX em execução

9

Eu tenho uma mesa onde cada pessoa tem um recorde para todos os dias do ano. Eu usei essa função para obter um total corrente com base na coluna de saldo diário

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

mas preciso que o total em execução seja reiniciado a partir de 1 se Tipo = Trabalhando E o total em execução do Saldo Diário for menor que zero E o Tipo da linha anterior não for igual a Trabalhar. Abaixo está uma captura de tela do Excel. A coluna de função necessária é o que eu preciso acessar.

insira a descrição da imagem aqui

LynseyC
fonte
11
Na linha de 5 de novembro, Pessoa 1, suponha que nossos dados de teste tenham um tipo em branco. A 'função necessária' retornaria 1 ou 2 em 6 de novembro?
Ryan B.
Ele retornaria 2 para 6 de novembro. A "redefinição" não aconteceria porque 5 de novembro seria 1 (não um número negativo). Obrigado pelo seu post detalhado. Estou revendo hoje
LynseyC

Respostas:

1

Este não é apenas um total em execução com uma condição, mas também um agrupado / aninhado, pois a lógica deve ser aplicada no nível do ID. Para tabelas grandes, o M é melhor do que o DAX, pois não usa tanta RAM. (Eu escrevi sobre isso aqui: Link para o Blogpost

A função a seguir adapta essa lógica ao caso atual e deve ser aplicada no nível do ID: (os nomes das colunas necessárias são: "Tipo", "Dose diária", "Ajustes")

(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
fonte
Isso resolveu o problema. Funciona perfeitamente e não diminuiu a velocidade do relatório. Obrigado
LynseyC
5

Visão geral

É algo difícil de pedir ao PowerBI, portanto, pode ser difícil encontrar uma abordagem organizada.

O maior problema é que o modelo de dados do PowerBI não suporta o conceito de contagem em execução - pelo menos não da maneira que fazemos no Excel. No Excel, uma coluna pode fazer referência a valores que ocorrem na 'linha anterior' da mesma coluna e, em seguida, ser ajustada por algumas 'alterações diárias' listadas em uma coluna diferente.

O PowerBI só pode imitar isso adicionando todas as alterações diárias em algum subconjunto de linhas. Pegamos o valor da data em nossa linha atual e criamos uma tabela filtrada em que todas as datas são inferiores à data dessa linha atual e, em seguida, resumimos todas as alterações diárias desse subconjunto. Isso pode parecer uma diferença sutil, mas é bastante significativa:

Isso significa que não há como "substituir" nosso total de rodadas. A única matemática que está sendo feita está acontecendo na coluna que contém alterações diárias - a coluna que contém 'total em execução' é apenas um resultado - nunca é usada no cálculo de nenhuma linha subsequente.

Devemos abandonar o conceito de 'reset' e, em vez disso, imaginar fazer uma coluna que contenha um valor de 'ajuste'. Nosso ajuste será um valor que pode ser incluído para que, quando as condições descritas forem atendidas, o total de saldos e ajustes diários totalize 1.

Se observarmos a execução calculada fornecida pelo OP, veremos que o valor de nossa execução total em um dia 'não útil', imediatamente antes de um dia 'útil', nos fornece a quantidade necessária que, se revertida, somará zero e aumentar o número de rodadas em cada dia útil seguinte em um. Este é o nosso comportamento desejado (com um problema a ser descrito posteriormente).

Resultado

insira a descrição da imagem aqui

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

Ajuda a saber a diferença entre os contextos de linha e filtro e como o EARLIER opera para seguir esse cálculo. Nesse cenário, você pode pensar em "EARLIER" como significando 'esta referência aponta para o valor na linha atual "e, caso contrário, uma referência aponta para toda a tabela retornada por" ALLEXCEPT (Deixar, Deixar [Id]). " Dessa forma, encontramos os locais em que a linha atual tem o tipo "Working" e a linha do dia anterior tem outro tipo.

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

Este cálculo imita um tipo de operação 'preenchimento'. Ele diz: "Ao examinar todas as linhas cuja data é anterior à data nesta linha, retorne o maior valor em 'Data mais recente antes do trabalho".

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

Agora que todas as linhas têm um campo explicando para onde encontrar o saldo diário a ser usado como ajuste, podemos apenas procurar na tabela.

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

E, finalmente, aplicamos o ajuste ao nosso total corrente para o resultado final.

O problema

Essa abordagem falha ao abordar que a contagem não deve ser redefinida, a menos que o saldo diário em execução esteja abaixo de zero. Eu já me provei errado antes, mas diria que isso não pode ser realizado apenas no DAX, porque cria uma dependência circular. Essencialmente, você faz um requisito: use o valor agregado para determinar o que deve ser incluído na agregação.

Então é isso que eu posso te trazer. Espero que ajude.

Ryan B.
fonte
11
Em relação ao seu último ponto, acredito que você está correto. O DAX não pode fazer recursão.
Alexis Olson
3

Espero que da próxima vez que você cole um csv ou código que gere dados de amostra em vez de imagem. :)

Deixe-me sugerir que você faça seus cálculos no PowerQuery. Tentei dividir o código em algumas etapas para melhorar a legibilidade. Isso pode parecer um pouco mais complexo, mas funciona bem. Basta colá-lo no editor avançado e substituir a fonte pelos dados de origem. Boa sorte!

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
Eugene
fonte
Não tenho certeza se isso cobre todos os cenários, mas parece ser a abordagem correta.
Mike Honey
Só consigo fazer isso funcionar se o primeiro tipo de cada pessoa estiver trabalhando. Além disso, como nos exemplos do DAX, ele reinicia a numeração de um movimento de trabalho quando o total acumulado da linha anterior é um número positivo. Acho que minha imagem era enganosa, pois continha apenas esse cenário. Eu deveria ter incluído um momento em que o tipo mudou para funcionando, mas o total da linha anterior foi positivo.
LynseyC
@ LynseyC bem, esse código não é uma solução perfeita e completa, é claro, mas sim um exemplo de métodos que podem ser usados. Apenas modifique se for para o seu cenário.
Eugene
@LynseyC também, uma das vantagens de fazer essa matemática no PowerQuery, em vez do DAX, é uma maneira fácil de manter as colunas temporárias fora do modelo de dados.
Eugene
3

Eu acho que tenho!

Aqui está o resultado, baseado na solução que eu postei anteriormente: (Os dados foram modificados para mostrar mais comportamentos e casos de uso "sem trabalho"

RESULTADO

insira a descrição da imagem aqui

DETALHES

(1) Solte as colunas "Saldo diário em execução ajustado" e "Ajuste do saldo diário". Obteremos o mesmo resultado com apenas um passo em apenas um momento.

(2) Crie a seguinte coluna (RDB = "executando o saldo diário") ...

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

Depois de criar a "Data mais recente antes do término do trabalho", na verdade temos a peça necessária para fazer nossa 'redefinição' que eu alegava ser impossível antes. Ao filtrar esse campo, temos a oportunidade de iniciar cada fatia no '1'

(3) Ainda temos o mesmo problema, não podemos olhar para o resultado em nossa coluna e usá-lo para decidir o que fazer posteriormente nessa mesma coluna. Mas PODEMOS construir uma nova coluna de ajuste que conterá essa informação! E já temos uma referência a 'Data mais recente antes do trabalho' - esse é o último dia do grupo anterior ... a linha com as informações de que precisamos!

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)

Então, olhamos para o último dia em Each grupo anterior e, se a soma total desses ajustes tiver um valor positivo, aplicamos e, se for negativo, deixamos em paz. Além disso, se os primeiros dias de nossa pessoa são dias não úteis, não queremos esse bit negativo inicial em nosso ajuste, para que ele também seja filtrado.

(4) Este último passo trará o ajuste para o resultado final. Resuma as duas novas colunas e finalmente teremos nosso saldo diário de execução ajustado. Voila!

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

Construímos muitas colunas extras ao longo do caminho para esse resultado, o que geralmente não é a minha coisa favorita a fazer. Mas, isso foi complicado.

Ryan B.
fonte
Oi @ Ryan B. Isso funciona perfeitamente para mais de 200 pessoas na minha organização, mas uma não está funcionando. Eu tentei alterar o código pessoalmente, mas não consigo nada para resolver o problema. Eu acho que é porque eles trabalharam muito tempo e depois trabalharam apenas um dia antes de terem mais tempo de folga. Eu vinculei a uma imagem para mostrar o problema. Obrigado Image
LynseyC
Modifiquei a medida "Ajuste agrupado de RDB" para que ela passe grandes acréscimos de férias em vários ciclos "sem trabalho / sem trabalho".
Ryan B.
2
Olá, obrigado por todo o esforço, muito apreciado. Infelizmente a modificação não resolveu o problema. No entanto, se eu removi a última condição no filtro "Deixar [Data mais recente antes do término do trabalho] <> Em branco ()", ele resolveu o problema, mas quebrou os cálculos das pessoas originais novamente :-(
LynseyC
Tiro. Bem, espero que você encontre algo que funcione.
Ryan B.
2

Demorou um pouco, mas consegui encontrar uma solução alternativa. Supondo que o valor do saldo para espaços em branco seja sempre -1 e o valor seja 1 para "Trabalhando" e que os dados estejam disponíveis para todas as datas sem intervalo, algo como o cálculo abaixo poderia funcionar:

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

Lembre-se de que esse pode não ser um produto final, pois trabalhei com uma pequena amostra, mas isso deve ajudá-lo a começar. Espero que isto ajude.

CR7SMS
fonte
Obrigado @ CR7SMS. Reinicia o total em execução quando o tipo = Trabalhando, mas o total em execução quando o tipo está em branco não está funcionando. Em 7 de novembro, ele reduz para 3, mas, de 8 a 14 de novembro, ele retorna -2. Você pode ajudar a alterar o código para que o total em execução funcione quando o tipo está em branco? Obrigado
LynseyC 30/01
Olá Lynsey, tentei um cálculo diferente. Eu o adicionei como outra resposta, já que o cálculo foi um pouco longo. Mas espero que o novo cálculo funcione.
CR7SMS 31/01
@ CR7SMS, evite adicionar mais de uma resposta a uma única pergunta. Confunde outros usuários que podem procurar um problema / solução semelhante e não é bom. Em vez disso, você deve adicionar o que quer que seja a solução para uma resposta e dividir cada aspecto diferente em seções.
Christos Lytras
2

O cálculo é um pouco demorado, mas parece estar funcionando nos dados de amostra que estou usando. Faça uma tentativa:

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

Eu usei um monte de variáveis ​​aqui. Você talvez consiga criar uma versão mais curta. Basicamente, a idéia é encontrar a primeira ocorrência anterior de "Trabalhando" para descobrir de onde iniciar o cálculo. Isso é calculado na variável "Prev_Blank2". Depois de conhecermos o ponto de partida (começa com 1 aqui), podemos simplesmente contar o número de dias com "Working" ou em branco () entre Prev_Blank2 e a data do registro atual. Usando esses dias, podemos retornar o valor final para a execução total.

Espero que isso faça o truque;)

CR7SMS
fonte