[知识体系] 行上下文嵌套和EARLIER

  [复制链接]
查看109653 | 回复120 | 2021-2-21 19:33:38 | 显示全部楼层 |阅读模式
了解行上下文嵌套

同一张表有多层嵌套的行上下文似乎很少见,但实际上这种情况经常发生。让我们用一个例子来解释这个概念。假设你想针对每个产品计算价格高于它的其他产品的数量。本质上这将根据价格对产品进行排序。


为了解决这个问题,我们使用 FILTER 函数,FILTER 是一个迭代器,它迭代表的所有行,并返回一个新表,其中只包含满足第二参数的行。例如,如果要检索价格高于 100 美元的产品列表,可以使用:

  1. = FILTER ( Product, Product[UnitPrice] > 100 )
复制代码


细心的读者会注意到,FILTER 需要具备迭代功能,因为只有当产品表存在有效的行上下文时,才能计算表达式 Product[UnitPrice]>100。否则单价的有效值将是不确定的。FILTER 的确是一个迭代函数,它为第一个参数中的表的每一行创建行上下文,从而可以在第二参数中计算条件。


现在让我们回到原来的问题:创建一个计算列,对那些比目前产品价格更高的产品计数。如果将当前产品的价格命名为PriceOfCurrentProduct,就很容易理解下面的伪 DAX 公式将满足你的需求:

  1. Product[UnitPriceRank] =
  2. COUNTROWS (
  3.     FILTER (
  4.         Product,
  5.         Product[UnitPrice] > PriceOfCurrentProduct
  6.     )
  7. )
复制代码


FILTER 将筛选出那些比当前产品价格更高的产品,且 COUNTROWS 对那些由 FILTER 返回的表中的行的数目进行了统计。剩下唯一的问题是如何用有效的 DAX 语法来替换 PriceOfCurrentProduct,来表达当前产品价格(所谓「当前」,意思是计算列的当前行),这可能比你想象的要难。

EARLIER 出场

我们在产品表中定义这个新的计算列。因此,DAX 将在行上下文中对表达式求值。但是,表达式使用了 FILTER 函数在同一个表上创建了一个新的行上下文。实际上,在前一个表达式的第 5 行中使用的 Product[UnitPrice]是由 FILTER 迭代的产品表的当前行的单价,这是最内层的迭代。因此,这个新的行上下文隐藏了计算列引入的产品表的原始行上下文。你看到问题了吗?你希望访问单价的当前值,但不要使用最后引入的行上下文(FILTER 迭代的那个)。相反,你希望使用之前的行上下文,即计算列中的那个。


DAX 提供了一种使其成为可能的函数:EARLIEREARLIER 使用前一个行上下文而不是最后一个行上下文检索列的值。因此,你可以使用 EARLIER (Product[UnitPrice])来表示PriceOfCurrentProduct的值。

EARLIER 语法
  1. EARLIER ( <ColumnName>, [<Number>] )
复制代码


返回<ColumnName>列在外部,第<Number>层行上下文对应的值,其中<Number>是可选参数。


EARLIER 是 DAX 中最特立独行的函数。许多用户之所以对 EARLIER 感到害怕,是因为并未按照行上下文来思考,也没有考虑过行上下文可通过对同一表格创建多个迭代而实现嵌套这一事实。在现实中 EARLIER 是一个简单且有用的函数,且可变得熟能生巧。解决该问题的代码如下:

  1. Product[UnitPriceRank] =
  2. COUNTROWS (
  3.     FILTER (
  4.         Product,
  5.         Product[UnitPrice]
  6.             > EARLIER ( Product[UnitPrice] )
  7.     )
  8. ) + 1
复制代码


在下图中,你可以看到产品表中定义的计算列,它使用单价的降序排序。


7148211936391.png

UnitPriceRank 列是演示 EARLIER 如何在嵌套行上下文中导航的示例


因为单价相同的产品有十四种,所以排名都是 1;第十五种产品排名为 15,与其他产品价格相同。建议你仔细研究和理解这个小示例,因为这是一个非常好的测试,可以检查你使用和理解行上下文的能力、如何使用迭代器(在本例中为 FILTER)创建行上下文,以及如何通过 EARLIER 从外部访问自身的值。

EARLIER 第二参数

EARLIER 接受第二参数,即要跳过的层数,这样你就可以跳过两层或多层行上下文。此外,还有一个名为 EARLIEST 的函数,它允许你直接访问表的最外层行上下文。老实说,EARLIEST 和 EARLIER 的第二个参数都不经常使用:虽然有两个嵌套的行上下文是常见的场景,但是有三个或更多的行上下文很少发生。


图解多层行上下文     图片:exceleratorbi.com.au

只有在同一种行上下文存在嵌套的时候才需要 EARLIER。如果 A,B,C,D 分别来自不同的表,你可以直接引用它们的列值,不需要使用 EARLIER

在结束这个示例之前,值得注意的是,如果你想将结果转换为一个更合理的排序(排名从 1 开始,之后每个名次加 1,即创建一个序列 1,2,3…),只要对价格计数而不是产品就可以了。这时,你可以借助 VALUES 函数:

  1. Product[UnitPriceRankDense] =
  2. COUNTROWS (
  3.     FILTER (
  4.         VALUES ( Product[UnitPrice] ),
  5.         Product[UnitPrice]
  6.             > EARLIER ( Product[UnitPrice] )
  7.     )
  8. ) + 1
复制代码


7148211936392.jpeg

UnitPriceRankDense 提供了更理想的排名,因为它计算的是价格,而不是产品

EARLIER 的使用建议

EARLIER 是一个作用比较抽象的函数,当你掌握了变量的用法之后,EARLIER 函数就可以被完全替换掉了。但是从理解多层行上下文的角度出发,我仍然建议你彻底地学习和理解 EARLIER,尤其是初学者。


定义变量(VAR)来代替 EARLIER 的好处是会使代码更易于阅读。例如,你可以使用以下表达式代替之前的计算列:

  1. Product[UnitPriceRankDense] =
  2. VAR CurrentPrice = Product[UnitPrice]
  3. RETURN
  4.     COUNTROWS (
  5.         FILTER (
  6.             VALUES ( Product[UnitPrice] ),
  7.             Product[UnitPrice] > CurrentPrice
  8.         )
  9.     ) + 1
复制代码


在这个示例中,通过定义变量,将当前单价存储在 CurrentPrice 中,并在稍后使用该变量来执行比较。为变量命名,可以使代码更易于阅读,而不必在每次阅读表达式时都通过遍历行上下文层级才能理解计值流。

EARLIER 只能用于计算列吗?



虽然我们通常都是在计算列中使用 EARLIER,但并不意味着 EARLIER 只能用于计算列,实际上只要存在多层行上下文都可以使用 EARLIER。只不过计算列因为自身提供行上下文,只需要再使用一个迭代函数即可实现两层行上下文,而度量值则需要嵌套两层迭代函数才能构建出 EARLIER 需要的环境,操作起来稍显繁琐,但是这种嵌套对于深入理解行上下文很有帮助,让我们通过下面这个案例介绍这两种用法:


7148211936393.png

案例原始数据


原始表包含 date、最大步骤 id 和用户 id 三列,最大步骤 id 代表完成的步骤数量,值越大说明该用户在当前日期完成的步骤越多,比如最大步骤 id=4 说明用户已经完成了步骤 1,2,3,4。


现在要求按天统计完成每个步骤的用户数,也就是只考虑来自 date 列和最大步骤 id 列的筛选器,

  • 对于计算列使用的公式,需要注意忽略来自用户 id 的筛选
  • 对于度量值,我们默认透视表已经提供了这两列作为外部筛选上下文,只需要在度量值中构建出双层上下文即可

  1. 通过人数 =
  2. CALCULATE (
  3.     COUNTA ( '表 1'[用户 id] ),
  4.     FILTER ( '表 1', [步骤 id] >= EARLIER ( [步骤 id] ) && '表 1'[date] = EARLIER ( [date] ) )
  5. )
复制代码
  1. 通过人数 _VAR =
  2. VAR x = '表 1'[最大步骤 id]
  3. VAR y = '表 1'[date]
  4. RETURN
  5.     CALCULATE (
  6.         COUNTA ( '表 1'[用户 id] ),
  7.         FILTER ( '表 1', [最大步骤 id] >= x && '表 1'[date] = y )
  8.     )
复制代码
  1. 通过人数 _EARLIER:=
  2. AVERAGEX (
  3.     ADDCOLUMNS (
  4.         '表 1',
  5.         "COUNT", CALCULATE (
  6.             COUNTROWS ( '表 1' ),
  7.             FILTER (
  8.                 ALL ( '表 1' ),
  9.                 '表 1'[最大步骤 id] >= EARLIER ( '表 1'[最大步骤 id] )
  10.                     && '表 1'[date] = EARLIER ( '表 1'[date] )
  11.             )
  12.         )
  13.     ),
  14.     [COUNT]
  15. )
复制代码
  1. 通过人数 _VAR :=
  2. AVERAGEX (
  3.     ADDCOLUMNS (
  4.         '表 1',
  5.         "COUNT",
  6.         VAR x = '表 1'[date]
  7.         VAR y = '表 1'[最大步骤 id]
  8.         RETURN
  9.             CALCULATE (
  10.                 COUNTROWS ( '表 1' ),
  11.                 FILTER ( ALL ( '表 1' ), '表 1'[最大步骤 id] >= y && '表 1'[date] = x )
  12.             )
  13.     ),
  14.     [COUNT]
  15. )
复制代码
  1. 通过人数 _ 推荐写法 :=
  2. CALCULATE (
  3.     COUNT ( [用户 id] ),
  4.     FILTER (
  5.         ALL ( '表 1'[最大步骤 id] ), '表 1'[最大步骤 id] >= MAX ( '表 1'[最大步骤 id] )
  6.     )
  7. )
复制代码


7148211936394.png

两种写法结果对比


度量值写法通过两个高亮的迭代函数 ADDCOLUMNS 和 FILTER 构建了两层行上下文,使得 EARLIER 可以正常计值,内层度量值[AVERAGEX 取平均以确保获得准确结果。


度量值的前两种写法是出于演示 EARLIER 的目的,故意将公式复杂化,实际上如果只是解决问题本身,你完全可以用更简单的写法,参考最后一个度量值。需要指出的是,两者在明细行结果相同,总计行稍有不同:前两种写法在总计行计算的是整体的平均值,而最后一个写法只考虑最大步骤 id 一个筛选条件,这通常是没有意义的。


注:你可以在文章末尾下载到这个案例的源文件

小测试

7148211936395.png

测试表两个计算列的结果

  1. 计算列 1 = COUNTROWS(FILTER('测试表',EARLIER('测试表'[月份])='测试表'[月份]))

  2. 计算列 2 = COUNTROWS(FILTER('测试表',EARLIER('测试表'[月份])="1 月"))
复制代码


基于上面的公式,你认为这两列的结果是什么,原因是?


答案


计算列 1:筛选条件是 FILTER 函数的月份值等于当前计算列创建的行上下文的月份值,所以每行返回 1


计算列 2:此时 FILTER 函数的筛选条件是与一个固定的值「一月」做判断,这是一个静态布尔表达式,不会随着外部迭代行的变化而变化,只在计算列的第一行返回 True,其他行均为 False。进一步看,在计算列的第一行,公式内的 FILTER 迭代的表每行都返回 True,所以 COUNTORWS 计算了整张表。这里的关键是理解计算列 2 中 FILTER 的筛选条件其实是个静态的布尔表达式。

案例文件下载
游客,如果您要查看本帖隐藏内容请回复

回复

使用道具 举报

GlitterMai | 2021-4-24 19:02:03 来自手机 | 显示全部楼层
支持,楼下的跟上哈~
回复

使用道具 举报

超越 | 2021-5-21 23:48:40 | 显示全部楼层
路过的帮顶
回复

使用道具 举报

Cici | 2021-7-24 21:13:22 来自手机 | 显示全部楼层
站位支持
回复

使用道具 举报

峰磊 | 2021-7-31 18:06:36 | 显示全部楼层
发发呆,回回帖,工作结束~
回复

使用道具 举报

技安 | 2021-9-19 18:47:27 | 显示全部楼层
楼猪V5啊
回复

使用道具 举报

风影 | 2021-10-15 18:16:25 | 显示全部楼层
楼猪V5啊
回复

使用道具 举报

wugang8023 | 2021-11-20 17:19:08 | 显示全部楼层
加油站加油
回复

使用道具 举报

sdifernya | 2022-1-18 08:55:03 来自手机 | 显示全部楼层
路过 帮顶 嘿嘿
回复

使用道具 举报

8891819 | 2022-1-30 08:04:24 来自手机 | 显示全部楼层
沙发~支持云发教育
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则