Power BI Desktop DAX reiniciar ejecutando columna total

9

Tengo una mesa donde cada persona tiene un registro para cada día del año. Utilicé esta función para lograr un total acumulado basado en la columna de saldo diario

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

pero necesito el total acumulado para reiniciar desde 1 si Tipo = Funcionando Y el total acumulado de Saldo diario es menor que cero Y el Tipo de la fila anterior no es igual a Funcionando. A continuación se muestra una captura de pantalla de Excel. La columna de función requerida es a lo que necesito llegar.

ingrese la descripción de la imagen aquí

LynseyC
fuente
1
En la fila del 5 de noviembre, Persona 1, supongamos que nuestros datos de prueba tienen un tipo en blanco. ¿La 'función requerida' devolvería un 1 o un 2 el 6 de noviembre?
Ryan B.
Devolvería un 2 para el 6 de noviembre. El "reinicio" no ocurriría porque el 5 de noviembre sería 1 (no un número negativo). Gracias por tu publicación detallada. Estoy revisando hoy
LynseyC

Respuestas:

1

Este no es solo un total acumulado con una condición, sino también uno anidado / agrupado, ya que la lógica debe aplicarse en el nivel de ID. Para tablas grandes, M es mejor que DAX, ya que no usa tanta RAM. (He blogueado sobre eso aquí: Enlace a Blogpost

La siguiente función adapta esa lógica al caso actual y debe aplicarse en el nivel de ID: (Los nombres de columna obligatorios son: "Tipo", "Cantidad diaria", "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
fuente
Esto ha resuelto el problema. Funciona perfectamente y no ha ralentizado el informe. Gracias
LynseyC
5

Visión general

Es un desafío pedirle a PowerBI que haga, por lo que un enfoque ordenado puede ser difícil de encontrar.

El mayor problema es que el modelo de datos de PowerBI no admite el concepto de una cuenta corriente, al menos no de la forma en que lo hacemos en Excel. En Excel, una columna puede hacer referencia a valores que ocurren en la 'fila anterior' de esa misma columna y luego ser ajustados por algún 'cambio diario' que aparece en una columna diferente.

PowerBI solo puede imitar esto sumando todos los cambios diarios en algún subconjunto de filas. Tomamos el valor de la fecha en nuestra fila actual y creamos una tabla filtrada donde todas las fechas son menores que la fecha de esta fila actual, y luego resumimos todos los cambios diarios de ese subconjunto. Esto puede parecer una sutil diferencia, pero es bastante significativo:

Esto significa que no hay forma de 'anular' nuestro total acumulado. La única matemática que se está haciendo está sucediendo en la columna que contiene cambios diarios: la columna que contiene 'total acumulado' es solo un resultado, nunca se usa en el cálculo de una fila posterior.

Debemos abandonar el concepto de 'reinicio' y, en su lugar, imaginar hacer una columna que contenga un valor de 'ajuste'. Nuestro ajuste será un valor que se puede incluir para que cuando se cumplan las condiciones descritas, el total de los saldos diarios y los ajustes sumen 1.

Si observamos la ejecución calculada dada por OP, vemos que el valor de nuestro total acumulado en un día 'no laborable' justo antes de un día 'laborable' nos da esa cantidad necesaria que, si se invierte, sumaría cero y hacer que el total acumulado en cada día hábil siguiente aumente en uno. Este es nuestro comportamiento deseado (con un problema que se describirá más adelante).

Resultado

ingrese la descripción de la imagen aquí

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

Ayuda a conocer la diferencia entre los contextos de fila y filtro y cómo opera EARLIER para seguir este cálculo. En este escenario, puede pensar que "ANTERIOR" significa 'esta referencia apunta al valor en la fila actual "y, de lo contrario, una referencia apunta a toda la tabla devuelta por" ALLEXCEPT (Leave, Leave [Id]) ". De esta manera, encontramos los lugares donde la fila actual tiene el tipo "Working" y la fila del día anterior tiene otro 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 un tipo de operación de "relleno". Dice: "Al mirar todas las filas cuya fecha es anterior a la fecha en ESTA fila, devuelve el valor más grande en 'Fecha más reciente antes del trabajo".

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

Ahora que cada fila tiene un campo que explica a dónde ir para encontrar el saldo diario para usar como nuestro ajuste, podemos ir a buscarlo desde la tabla.

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

Y finalmente aplicamos el ajuste a nuestro total acumulado para el resultado final.

La cuestión

Este enfoque no aborda que el recuento no se debe restablecer a menos que el saldo diario en ejecución sea inferior a cero. Se me demostró que estaba equivocado antes, pero diría que esto no se puede lograr solo en DAX porque crea una dependencia circular. Esencialmente, usted hace un requisito: use el valor agregado para determinar qué debe incluirse en la agregación.

Así que hasta aquí puedo llevarte. Espero eso ayude.

Ryan B.
fuente
1
Con respecto a su último punto, creo que está en lo correcto. DAX no puede hacer recursividad.
Alexis Olson
3

Espero que la próxima vez pegue un csv o código que genere datos de muestra en lugar de una imagen. :)

Déjame sugerirte que hagas tus cálculos en PowerQuery. Traté de dividir el código por unos pocos pasos para mejorar la legibilidad. Esto puede parecer un poco más complejo, pero funciona bien. Simplemente péguelo en el editor avanzado y luego reemplace la fuente con sus datos fuente. ¡La mejor de las suertes!

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
fuente
No estoy seguro de que esto cubra todos los escenarios, pero parece ser el enfoque correcto.
Mike Honey
Solo puedo hacer que esto funcione si el primer tipo para cada persona es Trabajar. Además, como con los ejemplos de DAX, reinicia la numeración de un movimiento de trabajo cuando el total acumulado de la fila anterior es un número positivo. Supongo que mi imagen fue engañosa, ya que solo contenía este escenario. Debería haber incluido un momento en que el tipo cambió a trabajar, pero el total de la fila anterior fue positivo.
LynseyC
@LynseyC bueno, este código no es la solución perfecta y completa, por supuesto, sino más bien un ejemplo de los métodos que se pueden utilizar. Simplemente modifique si para su escenario.
Eugene
@LynseyC también, una de las ventajas de hacer estas matemáticas en PowerQuery en lugar de DAX es una manera fácil de mantener las columnas temporales fuera del modelo de datos.
Eugene
3

Creo que lo tengo!

Aquí está el resultado, basándose en la solución que publiqué anteriormente: (Los datos se han modificado para mostrar más comportamientos de "trabajo / no trabajo" y casos de uso)

RESULTADO

ingrese la descripción de la imagen aquí

DETALLES

(1) Suelte las columnas "Saldo diario ajustado ajustado" y "Ajuste de saldo diario". Obtendremos el mismo resultado con un paso menos en un momento.

(2) Cree la siguiente columna (RDB = "ejecución del saldo diario") ...

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

Después de haber creado la "Fecha más reciente antes de completar el trabajo", en realidad tenemos la pieza necesaria para hacer nuestro 'reinicio' que, según yo, era imposible antes. Al filtrar en este campo, tenemos la oportunidad de comenzar cada segmento en '1'

(3) Aún tenemos el mismo problema, no podemos ver el resultado en nuestra columna y usarlo para decidir qué hacer más adelante en esa misma columna. ¡Pero PODEMOS construir una nueva columna de ajuste que contendrá esa información! Y ya tenemos una referencia a 'Fecha más reciente antes del trabajo': ese es el último día del grupo anterior ... ¡la fila con la información que necesitamos!

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)

Así que miramos el último día en Cada grupo anterior y si la suma total de esos ajustes tiene un valor positivo, lo aplicamos y, si es negativo, lo dejamos en su lugar. Además, si los primeros días de nuestra persona son días no laborables, no queremos ese bit negativo inicial en nuestro ajuste para que también se filtre.

(4) Este último paso traerá el ajuste al resultado final. Resuma las dos nuevas columnas y finalmente deberíamos tener nuestro Saldo diario ajustado ajustado. Voila!

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

Creamos muchas columnas adicionales en el camino hacia este resultado, que generalmente no es lo que más me gusta hacer. Pero, esto fue complicado.

Ryan B.
fuente
Hola, @Ryan B. Esto funciona perfectamente para más de 200 personas en mi organización, pero una no funciona. Intenté cambiar el código yo mismo, pero no consigo nada para resolver el problema. Creo que es porque han trabajado mucho tiempo y luego trabajaron solo un día antes de tener más tiempo libre. He vinculado a una imagen para mostrar el problema. Gracias Imagen
LynseyC
He modificado la medida de "Ajuste de RDB agrupado" para que pase grandes acumulaciones de licencia en múltiples ciclos de "trabajo / sin trabajo".
Ryan B.
2
Hola, gracias por todo el esfuerzo, muy apreciado. Lamentablemente, la modificación no resolvió el problema. Sin embargo, si eliminé la última condición en el filtro "Dejar [Fecha más reciente antes de completar el trabajo] <> En blanco ()", resolvió el problema, pero luego rompió los cálculos de la gente original nuevamente :-(
LynseyC
Disparar. Bueno, espero que puedas encontrar algo que funcione.
Ryan B.
2

Tomó un tiempo, pero pude encontrar una solución alternativa. Suponiendo que el valor del saldo para espacios en blanco es siempre -1 y el valor es 1 para "Trabajo" y que los datos están disponibles para todas las fechas sin espacio, algo como el siguiente cálculo podría 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

Tenga en cuenta que esto podría no ser un producto terminado ya que trabajé con una pequeña muestra, pero esto debería ayudarlo a comenzar. Espero que esto ayude.

CR7SMS
fuente
Gracias @ CR7SMS. Reinicia el total acumulado cuando type = Working pero el total acumulado cuando el tipo está en blanco no funciona. Para el 7 de noviembre se reduce a 3 pero luego del 8 al 14 de noviembre devuelve -2. ¿Puedes ayudar a enmendar el código para que el total acumulado funcione cuando el tipo está en blanco? Gracias
LynseyC
Hola Lynsey, probé un cálculo diferente. Lo he agregado como otra respuesta ya que el cálculo fue un poco largo. Pero con suerte el nuevo cálculo funciona.
CR7SMS
@ CR7SMS, evite agregar más de una respuesta a una sola pregunta. Confunde a otros usuarios que pueden buscar un problema / solución similar y no es agradable. En su lugar, debe agregar lo que pueda encontrar como solución a una respuesta y dividir cada aspecto diferente en secciones.
Christos Lytras
2

El cálculo es un poco largo, pero parece estar funcionando en los datos de muestra que estoy usando. Prueba esto:

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

He usado un montón de variables aquí. Tal vez puedas llegar a una versión más corta. Básicamente, la idea es encontrar la primera aparición anterior de "Trabajo" para encontrar desde dónde comenzar el cálculo. Esto se calcula en la variable "Prev_Blank2". Una vez que conocemos el punto de partida (comienza con 1 aquí), entonces podemos simplemente contar la cantidad de días con "Working" o blank () entre Prev_Blank2 y la fecha del registro actual. Con estos días, podemos devolver el valor final para el total acumulado.

Esperemos que esto haga el truco;)

CR7SMS
fuente