DAX:本月一列中的值是否与上月相同?

DAX: Is the value in one column the same this month as it was last?

我需要创建一个计算列。我有一个带有序列号的项目列表,这些项目每个月都会分配给某人。我需要知道 (0/1) 本月该物品的所有者是否与上个月该物品的所有者相同。 (所以我可以创建一个衡量指标来平均每个月有多少人在更换所有者。)

基本上,我正在努力实现最后一列:

Month       ItemID  Owner   Same Owner as Prev Mth
2015/01/31  A1      Al  
2015/01/31  A2      Bob 
2015/01/31  A3      Carl    
2015/02/28  A1      Al      1
2015/02/28  A2      Carl    0
2015/02/28  A3      Carl    1
2015/03/31  A1      Bob     0
2015/03/31  A2      Bob     0
2015/03/31  A3      Bob     0
2015/04/30  A1      Bob     1
2015/04/30  A2      Bob     1
2015/04/30  A3      Al      0

我尝试了 CALCULATE(Max([Owner]), FILTER(tbl, DATEADD([Month],-1,MONTH)=EARLIER([Month]), FILTER(tbl, [ItemID] = EARLIER( [物品 ID]))

但 Max 不适用于文本字段。所以我有点难过。我知道这不应该那么难...

日期逻辑几乎总是建模问题,而不是巧妙的函数问题。

您将需要一个日期 table,其中包含几个月的单调递增整数 ID。我通常将其称为 MonthSequential 或 MonthIndex,具体取决于模型的目标受众。该字段在日期 table 中每个月简单地增加 1,而不在年份边界换行。因此,如果模型中的第一个月是 2014 年 1 月,则该月的 MonthSequential=1。 2014 年 2 月的 MonthSequential=2,依此类推到 2014 年 12 月的 MonthSequential=12。 2015 年 1 月的 MonthSequential=13。

这允许非常简单的算法来识别从当前月份开始的任意时间段内的任何月份或月份范围。一旦你在你的日期维度中有了这个字段(以及你的 Items[Month] 字段与你的 DimDate[Date] 字段相关),生活就会变得非常简单:

SameOwnerPreviousMonth=
IF(
    CALCULATE(
        VALUES(Items[Owner])
        ,FILTER(
            ALLEXCEPT(Items, Items[ItemID])
            ,RELATED(DimDate[MonthSequential]) =
                EARLIER(RELATED(DimDate[MonthSequential])) - 1
        )
    ) = Items[Owner]
    ,1
    ,0
)

这里有一些关于行上下文的有趣之处,我将对此进行解释。

任何计算列都是由一些公式定义的。该公式在 table 的行上下文中计算。发生的是通过 table 的逐行迭代。您提供的公式每行计算一次,并为该计算列创建值。

也就是说,DAX 背后的存储引擎和公式引擎没有行排序的概念。这意味着如果我们需要这样做,我们为计算列定义的任何公式都必须提供自己的排序或对另一行的引用。

那么,我们如何才能找到上个月的失主呢?好吧,我们需要查看整个 Items table 并找到具有相同 [ItemId] 且落在当前行的月份之前的月份的行。我们的 [MonthSequential] 使得查找上个月的日期变得微不足道,DAX 提供了许多上下文操作函数来保留或消除上下文。

注意:我将按位置引用函数参数,函数的第一个参数由 (1) 表示。

让我们逐步了解解决方案。我们将忽略 IF() 因为那是微不足道的。公式的核心在于 CALCULATE(),它标识上个月的 [Owner]:

CALCULATE(
    VALUES(Items[Owner])
    ,FILTER(
        ALLEXCEPT(Items, Items[ItemID])
        ,RELATED(DimDate[MonthSequential]) =
            EARLIER(RELATED(DimDate[MonthSequential])) - 1
    )
)

CALCULATE() 首先计算参数 (2)-(n),以创建一个新的过滤器上下文。然后使用该过滤器上下文计算 (1) 中的表达式。

FILTER() 通过 (1) 中提供的 table 逐行迭代,并为 (1) 中的每一行计算 (2) 中的布尔表达式。它 returns table 由 (1) 的行的子集组成,其中 (2) 的计算结果为真。由于我们在评估计算列时已经遍历了整个 Items table,因此我们最终得到两组行上下文。外行上下文是对整个 table 的迭代。内部行上下文是通过我们过滤器的 (1) 的迭代。外部行上下文影响内部,我们必须根据需要 modify/remove select 部分外部上下文。

我们迭代的table是ALLEXCEPT(Items, Items[ItemId])。 ALLEXCEPT() 去除所有上下文,除了指定的字段。在我们外部上下文中的任何给定行上,我们保留 Items[ItemId] 的值并去除所有其他上下文([Month] 和 [Owner],以及您未在示例数据中命名的任何其他字段)。这为我们的 FILTER() 提供了一个 table,由 Items 中的每一行组成,这些行在外部过滤器上下文中共享当前行的 [ItemId]。这个子集 table 成为我们内部行上下文的生成器。

现在我们正在迭代 FILTER() 的 (1),如上所述。 RELATED() 允许我们调用以从与当前值相关的另一个 table 获取值。我们在内部行上下文中获取绑定到当前行的 [MonthSequential] 值。我们想要在外行上下文中找到紧接当前月份之前的月份。要引用外部行上下文中的值,我们需要转义内部。

EARLIER() 允许我们转义当前(内部)行上下文并引用最后一个有效(外部)行上下文。这可以通过任意级别的上下文嵌套来实现。幸运的是,我们只有两个。 EARLIER(RELATED(DimDate[MonthSequential])) 在外部上下文中查找当前行的 [MonthSequential] 值。我们只需从中减去 1 即可得到前一个月(并且由于我们使用的是 [MonthSequential],因此我们无需实施任何逻辑来处理环绕年份障碍)。

因此,我们评估 VALUES(Items[Owner]) 的上下文是我们项目 table 的子集,其中 [ItemId] 等于我们外部行上下文中的当前行,并且值[MonthSequential] 比外部行上下文中的当前行少一。 VALUES() returns 组成列引用的值列表。在这种情况下,由于每个 [ItemId] 在任何给定月份仅与一个 [Owner] 相关联,因此该列表只是一个可以隐式转换为标量值并在我们的计算列中表示的值。

我们的 IF() 只是根据外部行上下文中当前行的值测试此 [Owner] 值,并根据需要 returns 1 或 0。

如果您有一个 [ItemId] 在给定月份有多个不同的 [Owner],这将会中断。

模型图: