Power BI Desktop DAX 重新启动运行总计列

9
我有一张表格,每个人在一年中的每一天都有一条记录。我使用这个函数来实现基于每日余额列的累计总和。
CALCULATE(
SUM(Leave[Daily Balance]),
FILTER(
   ALLEXCEPT(Leave, Leave[Employee Id]),
   Leave[Date] <= EARLIER(Leave[Date])
))

但是如果类型=工作且每日余额的累计总和小于零,并且前一行的类型不等于工作,则需要从1重新开始累计总和。以下是来自Excel的屏幕截图。所需的函数列是我需要达到的目标。

enter image description here


1
在11月5日的行中,假设我们的测试数据类型为空。在11月6日,“必需函数”会返回1还是2? - Ryan B.
它会返回11月6日的2。"重置"不会发生,因为11月5日将是1(而不是负数)。感谢您详细的帖子。我今天正在审核。 - LynseyC
6个回答

5

概述

要求PowerBI完成这项任务是一个具有挑战性的事情,因此可能很难找到一种整洁的方法。

最大的问题是PowerBI的数据模型不支持运行总数的概念——至少不是我们在Excel中所做的那样。在Excel中,一列可以引用出现在该列“前一行”的值,然后通过某个在不同列中列出的“每日变化”进行调整。

PowerBI只能通过对某些行的所有每日更改求和来模拟这一点。我们获取当前行中的日期值并创建一个过滤表,其中所有日期均小于此当前行的日期,然后从该子集中汇总所有每日更改。这似乎是一个微小的差异,但它是非常重要的:

这意味着没有办法“覆盖”我们的运行总数。正在执行的唯一计算是发生在包含每日更改的列上,包含“运行总数”的列仅是结果——它从不用于任何后续行的计算。

我们必须放弃“重置”的概念,而是想象制作一个包含“调整”值的列。我们的调整将是一个可以包含的值,以便在满足所描述的条件时,每日余额和调整的总和将总和为1。

如果我们查看OP给出的计算的累加值,我们会发现在“工作”日之前的“非工作”日上的运行总数的值给出了我们所需的金额,如果反转,将总和为零,并导致每个后续工作日的运行总数增加1。这是我们期望的行为(有一个稍后描述的问题)。

结果

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操作是如何进行计算的会有所帮助。在这种情况下,您可以将“EARLIER”理解为“此引用指向当前行中的值”,否则引用就指向由“ALLEXCEPT(Leave,Leave [Id])”返回的整个表。通过这种方式,我们找到了当前行类型为“Working”且前一天的行具有其他类型的地方。

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 无法进行递归。 - Alexis Olson

4

我想我有了!

以下是结果,基于我之前发布的解决方案构建:(为展示更多“工作/不工作”行为和用例,数据已进行修改)

结果

enter image description here

详细信息

(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调整”度量,以便在多个“工作/不工作”周期中传递大量休假累计。 - Ryan B.
2
嗨,感谢您的所有努力,非常感激。不幸的是,修改并没有解决问题。但是,如果我在筛选器中删除最后一个条件“请假[工作完成前最近日期] <>空()”,它就可以解决问题,但这样会再次破坏原始人员计算 :-( - LynseyC
拍住。嗯,我希望你能找到一些有效的东西。 - Ryan B.

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

我不确定这是否涵盖了所有情况,但似乎是正确的方法。 - Mike Honey
只有当每个人的第一个类型是“工作”时,我才能让它正常工作。与DAX示例一样,当前行的累计总数为正数时,它会重新开始对“工作”动作进行编号。我想我的图片有误导性,因为它只包含了这种情况。我应该包括一个时间,当类型变为“工作”,但前一行的总数是正数。 - LynseyC
@LynseyC 嗯,这段代码并不是完美和完整的解决方案,当然,它只是一些可能使用的方法的示例。根据你的情况进行修改即可。 - Eugene
@LynseyC 另外,使用 PowerQuery 进行数学计算的一个优点是可以轻松地将临时列保留在数据模型之外。相比之下,使用 DAX 则不太容易实现此功能。 - Eugene

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。当类型=工作时,它会重新启动运行总数,但当类型为空时,运行总数不起作用。对于11月7日,它减少到3,但是从11月8日至14日,它返回-2。您能否帮助修改代码以使运行总数在类型为空时正常工作?谢谢。 - LynseyC
嗨,Lynsey,我尝试了不同的计算方法。由于计算有点长,我已将其作为另一个答案添加。但希望新的计算方法有效。 - CR7SMS
@CR7SMS请避免在一个问题中添加多个答案。这会让其他可能搜索类似问题/解决方案的用户感到困惑,也不好。相反,您应该将您能想到的所有解决方案添加到一个答案中,并将每个不同的方面分成几个部分。 - Christos Lytras

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开始),那么我们就可以简单地计算出Prev_Blank2和当前记录日期之间具有“Working”或空白()天数的数量。使用这些天数,我们可以返回运行总数的最终值。
希望这个能解决问题 ;)

1
这不仅是带有条件的累计总数,还是一个嵌套/集群的累计总数,因为逻辑必须应用于ID级别。对于大型表格,M比DAX更擅长此项工作,因为它不使用太多RAM。(我在这里发表了博客文章:链接到博客文章)
以下函数将该逻辑适应于当前情况,并必须应用于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 = 如果 TransformToList{[Counter]}[Type] = "working" 且 [Result] < 0 且 [Type] <> "working" 则 TransformToList{[Counter]}[Amount] 否则 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


这解决了问题。工作得很完美而且也没有拖慢报表的速度。谢谢。 - LynseyC

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接