Colonne de total d'exécution de redémarrage DAX Power BI Desktop

9

J'ai un tableau où chaque personne a un dossier pour chaque jour de l'année. J'ai utilisé cette fonction pour obtenir un total cumulé basé sur la colonne du solde quotidien

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

mais j'ai besoin du total cumulé pour redémarrer à partir de 1 si Type = Fonctionnement ET le total cumulé de Solde quotidien est inférieur à zéro ET le Type de la ligne précédente n'est pas égal à Fonctionnement. Ci-dessous, une capture d'écran d'Excel. La colonne de fonction requise est ce à quoi je dois accéder.

entrez la description de l'image ici

LynseyC
la source
1
Sur la ligne du 5 novembre, Personne 1, supposons que nos données de test aient un type vierge. La «fonction requise» retournerait-elle un 1 ou un 2 le 6 novembre?
Ryan B.3
Il retournerait un 2 pour le 6 novembre. La "réinitialisation" ne se produirait pas car le 5 novembre serait 1 (pas un nombre négatif). Merci pour votre message détaillé.
J'évalue

Réponses:

1

Il s'agit non seulement d'un total cumulé avec une condition, mais également d'une condition imbriquée / en cluster, car la logique doit être appliquée au niveau de l'ID. Pour les grandes tables, M est meilleur que DAX, car il n'utilise pas autant de RAM. (J'ai blogué à ce sujet ici: Lien vers Blogpost

La fonction suivante adapte cette logique au cas actuel et doit être appliquée au niveau de l'ID: (Les noms de colonnes requis sont: "Type", "Indemnité journalière", "Ajustements")

(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
la source
Cela a résolu le problème. Fonctionne parfaitement et n'a pas ralenti le rapport. Merci
LynseyC
5

Aperçu

Il est difficile de demander à PowerBI de le faire, donc une approche bien rangée peut être difficile à trouver.

Le plus gros problème est que le modèle de données de PowerBI ne prend pas en charge le concept d'un décompte en cours d'exécution - du moins pas comme nous le faisons dans Excel. Dans Excel, une colonne peut référencer des valeurs qui se produisent dans la `` ligne précédente '' de cette même colonne, puis être ajustée par certains `` changements quotidiens '' répertoriés dans une autre colonne.

PowerBI ne peut imiter cela qu'en additionnant toutes les modifications quotidiennes sur un sous-ensemble de lignes. Nous prenons la valeur de date dans notre ligne actuelle et créons un tableau filtré où toutes les dates sont inférieures à la date de cette ligne actuelle, puis résumons toutes les modifications quotidiennes de ce sous-ensemble. Cela peut sembler être une différence subtile, mais elle est assez significative:

Cela signifie qu'il n'y a aucun moyen de «remplacer» notre total cumulé. Le seul calcul en cours se produit sur la colonne contenant les modifications quotidiennes - la colonne contenant le «total cumulé» n'est qu'un résultat - elle n'est jamais utilisée dans le calcul d'une ligne ultérieure.

Nous devons abandonner le concept de «réinitialisation» et imaginer à la place de créer une colonne contenant une valeur «d'ajustement». Notre ajustement sera une valeur qui peut être incluse de sorte que lorsque les conditions décrites sont remplies, le total des soldes et ajustements quotidiens sera égal à 1.

Si nous regardons le calcul calculé donné par OP, nous voyons que la valeur de notre total cumulé un jour «non ouvrable» juste avant un jour «ouvrable» nous donne le montant nécessaire qui, s'il était inversé, serait égal à zéro et faire augmenter le total cumulé de chaque jour ouvrable suivant. C'est notre comportement souhaité (avec un problème qui sera décrit plus loin).

Résultat

entrez la description de l'image ici

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

Il permet de connaître la différence entre les contextes de ligne et de filtre et comment EARLIER fonctionne pour suivre ce calcul. Dans ce scénario, vous pouvez penser à «EARLIER» comme signifiant «cette référence pointe vers la valeur de la ligne actuelle» et sinon une référence pointe vers la table entière renvoyée par «ALLEXCEPT (Leave, Leave [Id])». façon, nous trouvons les endroits où la ligne actuelle a le type "Working" et la ligne de la veille a un autre type.

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

Ce calcul imite un type d'opération de «remplissage vers le bas». Il indique: «Lorsque vous examinez toutes les lignes dont la date est antérieure à la date sur CETTE ligne, renvoyez la plus grande valeur dans« Date la plus récente avant le travail ».

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

Maintenant que chaque ligne a un champ expliquant où aller pour trouver le solde quotidien à utiliser comme ajustement, nous pouvons simplement le rechercher dans le tableau.

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

Et enfin, nous appliquons l'ajustement à notre total cumulé pour le résultat final.

Le problème

Cette approche ne tient pas compte du fait que le décompte ne doit pas être réinitialisé à moins que le solde quotidien en cours soit inférieur à zéro. J'ai déjà eu tort, mais je dirais que cela ne peut pas être accompli uniquement dans DAX car cela crée une dépendance circulaire. Essentiellement, vous faites une exigence: utilisez la valeur agrégée pour déterminer ce qui doit être inclus dans l'agrégation.

Voilà pour autant que je puisse vous amener. J'espère que cela aide.

Ryan B.
la source
1
Concernant votre dernier point, je pense que vous avez raison. DAX ne peut pas faire de récursivité.
Alexis Olson
3

J'espère que la prochaine fois que vous collerez un csv ou un code qui génère des exemples de données au lieu d'une image. :)

Permettez-moi de vous suggérer de faire vos calculs dans PowerQuery à la place. J'ai essayé de diviser le code en quelques étapes pour améliorer la lisibilité. Cela peut sembler un peu plus complexe, mais fonctionne bien. Il vous suffit de le coller dans l'éditeur avancé, puis de remplacer la source par vos données source. Bonne chance!

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
Eugène
la source
Je ne suis pas certain que cela couvre tous les scénarios, mais cela semble être la bonne approche.
Mike Honey
Je ne peux le faire fonctionner que si le premier type pour chaque personne fonctionne. De même, comme avec les exemples DAX, il redémarre la numérotation pour un mouvement de travail lorsque le total cumulé pour la ligne précédente est un nombre positif. Je suppose que ma photo était trompeuse car elle ne contenait que ce scénario. J'aurais dû inclure un moment où le type a changé pour fonctionner, mais le total de la ligne précédente était positif.
LynseyC
@LynseyC bien, ce code n'est pas une solution parfaite et complète, bien sûr, mais plutôt un exemple de méthodes qui peuvent être utilisées. Modifiez simplement si pour votre scénario.
Eugene
@LynseyC également, l'un des avantages de ces calculs dans PowerQuery plutôt que dans DAX est un moyen facile de garder les colonnes temporaires hors du modèle de données.
Eugene
3

Je pense que je l'ai!

Voici le résultat, en s'appuyant sur la solution que j'ai publiée plus tôt: (Les données ont été modifiées pour montrer plus de comportements et de cas d'utilisation "travail / pas de travail")

RÉSULTAT

entrez la description de l'image ici

DÉTAILS

(1) Supprimez les colonnes "Solde quotidien ajusté" et "Ajustement du solde quotidien". Nous obtiendrons le même résultat avec un pas de moins en un instant.

(2) Créez la colonne suivante (RDB = "Running Daily Balance") ...

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

Après avoir créé la «date la plus récente avant la fin des travaux», nous avons en fait la pièce nécessaire pour effectuer notre «réinitialisation» qui, selon moi, était impossible auparavant. En filtrant sur ce champ, nous avons la possibilité de démarrer chaque tranche à '1'

(3) Nous avons toujours le même problème, nous ne pouvons pas regarder le résultat dans notre colonne et l'utiliser pour décider quoi faire plus tard dans cette même colonne. Mais nous POUVONS construire une nouvelle colonne d'ajustement qui contiendra cette information! Et nous avons déjà une référence à la «date la plus récente avant le travail» - c'est le dernier jour du groupe précédent ... la ligne avec les informations dont nous avons besoin!

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)

Nous examinons donc le dernier jour de chaque groupe précédent et si la somme totale de ces ajustements a une valeur positive, nous l'appliquons et si elle est négative, nous la laissons à la place. De plus, si les premiers jours de notre personne sont des jours chômés, nous ne voulons pas du tout ce bit négatif initial dans notre ajustement afin qu'il soit également filtré.

(4) Cette dernière étape amènera l'ajustement dans le résultat final. Résumez les deux nouvelles colonnes et nous devrions enfin avoir notre solde journalier ajusté. Voila!

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

Nous avons construit beaucoup de colonnes supplémentaires en cours de route vers ce résultat, ce qui n'est généralement pas mon truc préféré. Mais c'était difficile.

Ryan B.
la source
Salut @Ryan B. Cela fonctionne parfaitement pour plus de 200 personnes dans mon organisation mais une ne fonctionne pas. J'ai essayé de changer le code moi-même mais je ne trouve rien pour résoudre le problème. Je pense que c'est parce qu'ils ont travaillé longtemps et n'ont travaillé qu'un jour avant de prendre plus de temps libre. J'ai lié à une image pour montrer le problème. Merci Image
LynseyC
J'ai modifié la mesure «Ajustement groupé de la RDB» de façon à ce qu'elle fasse passer d'importantes accumulations de congés sur plusieurs cycles «travail / pas de travail».
Ryan B.
2
Salut, merci pour tout l'effort, très apprécié. Malheureusement, la modification n'a pas résolu le problème. Cependant, si j'ai supprimé la dernière condition dans le filtre "Laisser [la date la plus récente avant la fin des travaux] <> vide ()", cela a résolu le problème, mais il a de nouveau cassé les calculs des personnes d'origine :-(
LynseyC
Tirer. Eh bien, j'espère que vous pourrez trouver quelque chose qui fonctionne.
Ryan B.
2

Cela a pris un certain temps, mais j'ai pu trouver une solution de contournement. En supposant que la valeur du solde pour les blancs est toujours -1 et que la valeur est 1 pour "Travail" et que les données sont disponibles pour toutes les dates sans écart, quelque chose comme le calcul ci-dessous pourrait fonctionner:

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

Gardez à l'esprit que ce n'est peut-être pas un produit fini car j'ai travaillé avec un petit échantillon, mais cela devrait vous aider à démarrer. J'espère que cela t'aides.

CR7SMS
la source
Merci @ CR7SMS. Il redémarre le total cumulé lorsque le type = Working mais le total cumulé lorsque le type est vide ne fonctionne pas. Pour le 7 novembre, il est réduit à 3, mais du 8 au 14 novembre, il renvoie -2. Pouvez-vous aider à modifier le code pour que le total cumulé fonctionne lorsque le type est vide? Merci
LynseyC
Salut Lynsey, j'ai essayé un calcul différent. Je l'ai ajouté comme autre réponse car le calcul était un peu long. Mais j'espère que le nouveau calcul fonctionne.
CR7SMS
@ CR7SMS, veuillez éviter d'ajouter plusieurs réponses à une seule question. Cela confond les autres utilisateurs qui peuvent rechercher un problème / solution similaire et ce n'est pas agréable. Au lieu de cela, vous devez ajouter tout ce que vous pourriez trouver comme solution à une réponse et diviser chaque aspect différent en sections.
Christos Lytras
2

Le calcul est un peu long, mais il semble fonctionner dans les exemples de données que j'utilise. Essayez ceci:

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

J'ai utilisé un tas de variables ici. Vous pourrez peut-être trouver une version plus courte. Fondamentalement, l'idée est de trouver la première occurrence précédente de "Working" pour trouver d'où commencer le calcul. Ceci est calculé dans la variable "Prev_Blank2". Une fois que nous connaissons le point de départ (il commence par 1 ici), nous pouvons simplement compter le nombre de jours avec "Working" ou blank () entre Prev_Blank2 et la date de l'enregistrement en cours. En utilisant ces jours, nous pouvons retourner la valeur finale pour le total cumulé.

Espérons que cela fera l'affaire;)

CR7SMS
la source