从数据到信息
从信息到洞察

行上下文嵌套和EARLIER

了解行上下文嵌套

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

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

= FILTER ( Product, Product[UnitPrice] > 100 )

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

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

Product[UnitPriceRank] =
COUNTROWS (
    FILTER (
        Product,
        Product[UnitPrice] > PriceOfCurrentProduct
    )
)

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

EARLIER 出场

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

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

EARLIER 语法

EARLIER ( <ColumnName>, [<Number>] )

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

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

Product[UnitPriceRank] =
COUNTROWS (
    FILTER (
        Product,
        Product[UnitPrice]
            > EARLIER ( Product[UnitPrice] )
    )
) + 1

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

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 函数:

Product[UnitPriceRankDense] =
COUNTROWS (
    FILTER (
        VALUES ( Product[UnitPrice] ),
        Product[UnitPrice]
            > EARLIER ( Product[UnitPrice] )
    )
) + 1

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

EARLIER 的使用建议

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

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

Product[UnitPriceRankDense] =
VAR CurrentPrice = Product[UnitPrice]
RETURN
    COUNTROWS (
        FILTER (
            VALUES ( Product[UnitPrice] ),
            Product[UnitPrice] > CurrentPrice
        )
    ) + 1

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

EARLIER 只能用于计算列吗?

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

案例原始数据

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

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

  1. 对于计算列使用的公式,需要注意忽略来自用户 id 的筛选
  2. 对于度量值,我们默认透视表已经提供了这两列作为外部筛选上下文,只需要在度量值中构建出双层上下文即可
通过人数 =
CALCULATE (
    COUNTA ( '表 1'[用户 id] ),
    FILTER ( '表 1', [步骤 id] >= EARLIER ( [步骤 id] ) && '表 1'[date] = EARLIER ( [date] ) )
)
通过人数 _VAR = 
VAR x = '表 1'[最大步骤 id]
VAR y = '表 1'[date]
RETURN
    CALCULATE (
        COUNTA ( '表 1'[用户 id] ),
        FILTER ( '表 1', [最大步骤 id] >= x && '表 1'[date] = y )
    )
通过人数 _EARLIER:=
AVERAGEX (
    ADDCOLUMNS (
        '表 1',
        "COUNT", CALCULATE (
            COUNTROWS ( '表 1' ),
            FILTER (
                ALL ( '表 1' ),
                '表 1'[最大步骤 id] >= EARLIER ( '表 1'[最大步骤 id] )
                    && '表 1'[date] = EARLIER ( '表 1'[date] )
            )
        )
    ),
    [COUNT]
)
通过人数 _VAR :=
AVERAGEX (
    ADDCOLUMNS (
        '表 1',
        "COUNT",
        VAR x = '表 1'[date]
        VAR y = '表 1'[最大步骤 id]
        RETURN
            CALCULATE (
                COUNTROWS ( '表 1' ),
                FILTER ( ALL ( '表 1' ), '表 1'[最大步骤 id] >= y && '表 1'[date] = x )
            )
    ),
    [COUNT]
)
通过人数 _ 推荐写法 :=
CALCULATE (
    COUNT ( [用户 id] ),
    FILTER (
        ALL ( '表 1'[最大步骤 id] ), '表 1'[最大步骤 id] >= MAX ( '表 1'[最大步骤 id] )
    )
)

两种写法结果对比

度量值写法通过两个高亮的迭代函数 ADDCOLUMNS 和 FILTER 构建了两层行上下文,使得 EARLIER 可以正常计值,内层度量值[COUNT]为表 1 的每行计算通过人数,由于这里的表 1 已经被透视表的 date 列和最大步骤 id 列筛选,如果筛选后的表存在多行,[COUNT]将得到相同的结果,所以外层使用 AVERAGEX 取平均以确保获得准确结果。

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

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

小测试

测试表两个计算列的结果

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

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

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

本文隐藏内容查看价格为2G币,请先
单独购买的内容长期有效,不受时间限制(购买前先刷新当前页面)。加入VIP会员可享受全站权益,性价比更高。

219
说点什么

1000
 
鼓掌微笑开心憧憬爱你色并不觉得吃瓜doge二哈喵喵思考笑哭捂脸悲伤大哭抓狂汗偷笑打脸捂眼黑线问号晕拜拜闭嘴衰咒骂ok作揖
68 评论数
171 被回复的评论
67 订阅评论的人数
 
查看最近回复
查看最热评论
  订阅本文评论  
最新 最旧 得票最多
提醒
成员
selenemoon

note: 计算列 2,filter函数会迭代12次,filter的行上下文(行变量)没有拿来进行处理。取而代之的是一个静态值“一月”。那么在这12次的循环/迭代中,“一月”被拿来和外层行上下文(行变量)比较了12次,每一次的值都为真。所以结果为12。(感激老师! 爱你

成员
txwdhw

老师,我在计算列中添加如下DAX表达式求累计值
”’累计求和 = SUMX(FILTER(‘累计求和’,’累计求和'[总分] <= EARLIER('累计求和'[总分])),'累计求和'[总分])
我理解这里应该有3层行上下文嵌套才对呢?
最外层:计算列提供的行上下文
中间层:SUMX迭代产生的行上下文
最里层:FILTER迭代产生的行上下文
为啥我把EARLIER的第二参数改成2,会报错呢,告诉我没有这么多层级。所以想请教老师问题出在哪里?

测试图.png
成员
txwdhw

分析完筛选器参数后,我们再来看一下calculate第一参数计值所面临的上下文到底是啥。第一步,找到初始的筛选上下文,即矩阵第一行的也就是(date=2019,最大步数id=1),第二步,calculate将行上下文转换成筛选上下文,并与初始筛选上下文交互(交互的意思是,筛选器来自同一列则覆盖,来自不同列产生笛卡尔积),此时的行上下文就是filter产生的,转换为筛选上下文就是最大步数id in {1,3,4,5,6},所以由行上下文转换的筛选上下文与初始筛选上下文的交互的结果就是(date,最大步数id)in {(2019),(1,3,4,5,6)}。第三步,筛选调节器ALL,应用该筛选调节器后,外部的筛选环境只有date=2019,第四步,最先计算的calculate筛选器参数(filter返回的单列表)与第三步外部筛选器交互,又变成了(date,最大步数id)in {(2019),(1,3,4,5,6)},由这个筛选器去基础表中筛选数据,能筛出来6条,这6条数据就是calculate第一参数最终的计值上下文,count返回6,矩阵的其余行都按照这个分析思路进行。

成员
txwdhw

对于推荐写法的分析,这里涉及到calculate的一些行为,希望老师能点评一下是否准确,太长了,剩下的在评论区:
第一步,计算calculate筛选器参数在外部初始环境中的值。此处,calculate的筛选器参数是filter,针对filter迭代函数,其第一参数在外部筛选上下文环境中计值,当前外部只有矩阵的2个行标签是筛选器,一个是date,一个是最大id步数,而filter第一参数使用了ALL函数清除掉了最大步数id上的筛选器,所以返回只有2019/11/1的6条记录,又因为去重只剩下{1,3,4,5,6}5条记录。然后来看filter的,第二参数,第二参数的计值环境既要考虑外部上下文,也要考虑filter迭代产生的行上下文。我们现在以矩阵表的第1行来例分析(date=2019/11/1,最大步数id=1),此时第二参数的外部筛选上下文就是矩阵的第一行2个标签,这2个标签从基础表中筛选出2条记录(最大步数id等于1的2条),此时max(’测试表’最大步数id)=1,又因为filter函数对第一参数进行迭代,取出的每一个值与max比较,都能通过判断(迭代取出的值分别是1,3,4,5,6),所以此时的filter最终返回的就是一个单列表{1,3,4,5,6},假设计算的是矩阵的第二行,931filter最终返回的是{3,4,5,6}。

成员
似水无痕

高老师,图片红框中的公式,CALCULATE的筛选器参数可以使用EARLIER,应该是FILTER的语法糖形式
FILTER(
ALL( ‘客户表'[国家] ),
‘客户表'[国家] = EARLIER( ‘客户表'[国家] )
)

01.png
成员
188****1692

老师,您好!度量值_推荐写法有些疑问,不知道怎么理解,您能帮我看下图片的3个问题吗

111.png
游客
li

老师,我在表中创建一个计算列,公式如下:计算列 = SUMX(ALL(‘表1′[颜色]),’表1′[数量])。
从计算列的结果来看 ‘表1′[数量] 使用的是计算列产生的行上下文。
是不是可以这么理解,在计算列中使用迭代函数,当第二参数使用的是不存在于第一参数中的列时,自动使用外层的行上下文。

游客
water

filter的第一参数决定了第二参数的迭代次数

成员
132****5925

我:创建列时,有第一层行上下文;filter查询时,又创建filter的行上下文,即第二层行上下文。两层,行上下文都要一行一行的从上到下迭代,但迭代的次序是这样的:
先说1. (filter第二参数,是动态变量)
首先,第一层行上下文,即列的行上下文,先迭代至其第1行,然后第二层行上下文,即filter从头至尾,将filter自身的行上下文迭代遍历一遍,与其进行比对(并不是,你第一层只迭代至1行,我第二层也只迭代1行)。此处,创建列的行上下文,只迭代到第1行,所以目前earliaer(’测试表'[月份])这里,一直都只是被赋值为创建列行上下文第1行的数值,即1月;而‘测试表'[月份]这个动态变量,会随着filter自身从头至尾的遍历,分别被赋值为相应行的具体数值,如filter遍历第1行时,被赋值为1月,第2行时又被重新赋值为2月。直至filter遍历一整遍,并分别与创建列的第1行这个值 1月,进行比对。
其次,接着说下,返回值和新的上下文:如果比对得上,filter第二参数那里,返回值为布尔值true,比对不上,则返回值为false,返回值为true时 这些filter的行上下文,会被filter保留下来,形成新的 filter的行上下文(即经过了filter条件筛选的,产生的新的filter的行上下文。至此,这个filter的新的经过筛选过的上下文,顾名思义,也可以取名叫做 筛选上下文),返回值为false的那些filter的行上下文,会被filter遗弃,不会被包含在,filter的新的经过筛选过的新行上下文,之中。
这里有些拗口,filter在查询时,创建自身上下文,叫旧的filter行上下文,经过filter第二参数筛选之后,filter又形成自身新的行上下文,叫做新的filter行上下文。两者行数不同,旧的行数多,新的行数少,因为不符合条件筛选的,被filter遗弃掉。
然后,第一层行上下文,即列的行上下文,再迭代至其第2行,filter再次从头至尾,将filter自身的行上下文迭代遍历一遍,与其(此时,测试表'[月份]这个动态变量,被赋值为2月)分别进行比对。
3.静态变量 filter从头至尾迭代遍历自己时,本来’测试表'[月份]这个值,会动态被赋值为1月,2月。。,这里全部静止为“1 月”,不再改变

游客
jeremy

先看下一章 理解上下文转换, 这一篇就好理解多了。 建议调整下顺序

游客
jeremy

请求可否添加, 度量值情形下的双层嵌套构成的进一步分步骤详释, Averagex 和 Filter 两个迭代函数如何共同作用。 感谢

成员
RecordLiu

有评论说可以从软件开发的角度来理解行上下文的嵌套,打开了理解思路。

确实,从编程的思路来理解行上下文的嵌套,本质上是形成了不同的作用域,很像一些编程语言中的闭包概念。

比如在 PHP 语言中,闭包内的作用域不能直接访问闭包外的变量值,需要定义参数,来进行传递访问。

放在 DAX 语言中,内层的行上下文就像形成了一个闭包作用域,不能直接访问外层行上下文的值,需要定义变量来访问或者使用 EARLIER 来实现访问。

作用域可以理解成访问权限,内层行上下文不具备直接访问外层行上下文的值。

嵌套.png
成员
Alfred

案例无法下载,没有权限,能否把表的内容放到正文里面看下

成员
学徒

案例文件无法下载,没有权限

成员
153****1882

高老师您好,原文中计算列的变量写法中用了calculate,那是不是要发生行上下文转换成筛选上下文?比如迭代第一行的时候就有了筛选条件date=2019/11/1,最大步骤id=1,用户id=a,那么calculate里面的表1是不是要先受筛选?然而好像并没有,这是为什么?

“通过人数 _VAR
VAR x = ‘表 1′[最大步骤 id]
VAR y = ‘表 1′[date]
RETURN
CALCULATE (
COUNTA ( ‘表 1′[用户 id] ),
FILTER ( ‘表 1’, [最大步骤 id] >= x && ‘表 1′[date] = y )
)”

成员
Clarlechen

通过人数 _EARLIER:=
AVERAGEX (
ADDCOLUMNS (
‘表 1’,
“COUNT”, CALCULATE (
COUNTROWS ( ‘表 1’ ),
FILTER (
ALL ( ‘表 1’ ),
‘表 1′[最大步骤 id] >= EARLIER ( ‘表 1′[最大步骤 id] )
&& ‘表 1′[date] = EARLIER ( ‘表 1′[date] )
)
)
),
[COUNT]
)
试验过用sumx,maxx等迭代函数也能算出一样结果,
粗略的理解是 利用迭代函数迭代addcolums形成的被filter筛选过的表,取出“count”列的值
请问这样的理解是否正确?

通过人数 _ 推荐写法 =
CALCULATE (
COUNT ( [用户id] ),
FILTER (
ALL ( ‘表1′[最大步骤id] ), ‘表1′[最大步骤id] >= MAX ( ‘表1′[最大步骤id] )
)
)
这个推荐写法是怎么取出原始的行上下文的???
请老师解答~

成员
evajiaiesec@gmail.com

第一个例子就没太看懂,计算列是计算每行产品的单价,filter里面也是每行迭代,这两个行上下文应该是一样的吧?为什么只要计算列的上下文而不要filter里的行上下文呢

成员
windpursuer

最后的推荐方案实在是太经典太有艺术性了,能理解但是让我独立设计还真困难。感觉想写好DAX需要同时关注数据结构、外部呈现环境,在此基础上去设计算法。再次感慨下,公式写的漂亮啊漂亮。

成员
cat

老师,您好:
为什么 度量值1 对应红色标记的单元格会显示数值的?
但是数据源并没有 data=2019-11-1 且 最大步骤id=2 的数据

成员
neetsonic

不是很理解为什么推荐写法里面用的聚合函数是max?是要计算每天完成了这个步骤的不是应该用>=min吗?而且最奇怪的是我换成min后居然结果也是一模一样的,有点蒙了,看看是哪里理解错了。
我的理解:因为用了ALL,所以FILTER先从表1全表里面逐行取最大步骤id来与【max(最大步骤id)】比较,而这个max受到日期列筛选器的影响,所以对于这一行的比较,max(最大步骤id)是2019/11/1里面最大的也就是6,然后这一行就不能入选,这样最后应该只有一行入选,是怎么得出正确结果的?
______________
通过人数 _ 推荐写法 :=
CALCULATE (
COUNT ( [用户 id] ),
FILTER (
ALL ( ‘表 1′[最大步骤 id] ), ‘表 1′[最大步骤 id] >= MAX ( ‘表 1′[最大步骤 id] )
)
)

DAX 圣经

导读

初识 DAX

DAX 基础知识

DAX 原理

DAX 高级原理

基础函数类型

迭代函数

CALCULATE 函数

CALCULATE 调节器

基础表函数

条件判断函数

查找匹配函数

时间智能函数

统计类函数

投影函数

分组/连接函数

集合函数

其他函数