很多小伙伴问罗叔,虽然在网上找了大量教程,但到底如何理解 RANKX 还是没有找到好的方法,这篇文章就彻底把 RANKX 给大家讲清楚。很多高手也会认为自己理解了 RANKX,但遇到复杂问题了还是会掉链子,让我们结合一些问题来看一看。
RANKX 的表面含义
很明显,RANK 表示排序;而一个 X 表示这和 SUMX,MAXX,MINX 是一族,是一种扩展版本,这种扩展版本,都有一种统一的语义:对第一个返回表的表达式,迭代每行并计算。
刚刚学习 RANKX 一定会写出:
正确公式:
代码语言:javascript复制KPI.Rank.Product.SubCategory =
RANKX( ALLSELECTED( Model_Product[产品子类别] ) , [KPI] )
或者
KPI.Rank.Product.SubCategory =
RANKX( ALL( Model_Product[产品子类别] ) , [KPI] )
这两者都可以,对 ALLSELECTED 和 ALL 的区别暂时不纠结。大部分伙伴的直观理解就形成了:
RANKX 是对 ALL( Model_Product[产品子类别] ) 按 [KPI] 进行排序。
这的确是最自然的理解,但在很简单的情况就会恰好正确,除此以外,都会出现与理解不一致的情况。
上图就是最简单的情况。但注意最后一行:
总计行的排名一样会进行计算,但却显示1。
这是为什么呢?
自己实现 RANKX
这里分享一个小经验,如果要理解一个函数,有一种方法,就是自己动手去实现一遍,意思是,如果 PowerBI 没有内置的 RANKX 函数的话,是否还可以完成排名计算呢?
我们自己来试试看。
首先,对于要排名的一个元素,如果你仔细考量,它其实并不在表里,它在报表的图表的行布局中,也就是身处筛选上下文中。
所以,排序二字,的准确讲法应该是:对身处筛选环境中的某个元素在一个表中排序。例如:
这里需要:
- 一个排序的参考表,表中有元素和参考值
- 在筛选环境中的待排序元素 Y 通过某个计算得到一个值
- 将该值在排序参考表中比对,找到卡位
- 这个卡位就是排序索引号
另外需要注意,对于重复元素可能存在紧排序或松排序;以及从大到小或从小到大。这些排序设置先不考虑,因为并非主体部分。
为了实现,可以写出这样的 DAX 表达式:
代码语言:javascript复制KPI.Rank.DIY =
VAR Items = ALL( Model_Product[产品子类别] ) // 参考元素
VAR ItemsWithValue = ADDCOLUMNS( Items , "Value" , [KPI] ) // 参考元素的计算值
VAR Y = [KPI] // 筛选环境的计算值
VAR K = COUNTROWS( FILTER( ItemsWithValue , [Value] >= Y ) ) // 在参考元素表中找 Y 的位置
RETURN IF( K = 0 , 1 , K ) // 如果没有任何元素值大于 Y,Y 就是第一,反之,Y 的排名为 K
效果如下:
从而,就实现了和系统的 RANKX 类似的效果,我们来看下这里实际所需要的参数:
KPI.Rank.DIY( 排序参考元素列表 , 排序参考元素计算表达式 , 筛选环境计算表达式 )
小结一下:
KPI.Rank.DIY 作为排序计算公式,它只能计算出一个值,而不是一个表;这一个值,其实是当前环境下数据模型被计算出的值,如:Y 的值是180。要返回一个序号,就需要一个列表来“卡位”。为了通用性,给出一个参考元素列表X{x1,..,xn},并对其迭代,迭代中对每个元素 xi 使用同一个计算表达式求值来计算该 xi 的参考值。最后,看 Y 的值可以卡在哪个位置。
这也就清楚地解释了在总计行,其排名是 1 ,是因为对于总计行的筛选环境的值是 2067,大于所有参考值,所以排名是 1。
理解 RANKX
自己写出 RANKX 的效果后,就可以感悟到:即使 DAX 系统并没有 RANKX 这个函数,也不实质上影响计算。由于这是一个通用的需要,在 DAX 中内置一个 RANKX 就更加方便了。再来看 RANKX 的含义就更加清楚了。
代码语言:javascript复制RANKX(<table>, <expression>[, <value>[, <order>[, <ties>]]])
对于 RANKX,是计算当前上下文的一个排序值。一个常见的误解是:
从直观上容易产生一个误解是 RANKX 将一个未排序的表进行了排序,这是错误的。经过上述分析,RANKX 是在给定的一个表中计算一个卡位索引号。
RANKX 前两个参数是必须的,再进行迭代,对于每一个行中计算,计算后的表记作排序参考表;再用对排序参考表的每行进行比对以确定的卡位,便是结果。参数指定是从大到小或从小到大;参数指定如果之前出现并列排序值,按收紧方式还是放松方式计算当前值。如:对 15 在 10,20,20,40 中的紧排序就是 3,而松排序就是 4。
符合自然理解的 RANKX
了解了这些以后,再来看:
代码语言:javascript复制RANKX( ALLSELECTED( Model_Product[产品子类别] ), [KPI] )
其本质就是:
代码语言:javascript复制RANKX( ALLSELECTED( Model_Product[产品子类别] ), [KPI], [KPI], DESC, SKIP )
这让人很自然地理解:
而其背后的准确表述实际是:
对于在图表中的某行 X ;先计算 ALLSELECTED( Model_Product[产品子类别] ) 每行的 [KPI],这个计算应考虑 ALLSELECTED( Model_Product[产品子类别] ) 外部的上下文以及 ALLSELECTED( Model_Product[产品子类别] ) 迭代时所产生的行上下文,计算完毕得到排序参考表;对 X 在其所处的上下文中计算 [KPI];此后,再到排序参考表进行比对,得到卡位值,就是最终的 RANKX 返回结果。
可见,符合自然的直观理解其实是按照严格约束计算的一种理想情况以及一种希望的巧合。
这种巧合就在于:排序参考表的元素和外部环境完全相同。因此,对于度量值 [KPI],在行上下文中的计算结果和在外部上下文中的计算结果是一样的。
不符合自然理解的 RANKX
只需要对图表结构稍作变化,就可以得到一个不那么自然的结果,例如:
对于此前完全相同的排序计算公式,在左右两边是不同效果。对于右边的图表,排名只是在某个类别下进行,而非全部元素的排名了。当然,这可能是需要的一种显示效果。另一种效果就是希望和左边的图表是一致的。
先来分析下,右边图表为何会在某类别分区下显示排序,而非总体排序。不难发现:产品类别作为上下文,影响着:计算 ALLSELECTED( Model_Product[产品子类别] ) 每行的 [KPI]。会是这样的:
假设,现在正欲计算位于家具下的椅子的 Rank 值,其过程如上所示,家具会作为统一的上下文,并由 ALLSELECTED( Model_Product[产品子类别] ) 迭代,在迭代中计算每个元素的值,只不过从复印机开始,它不属于家具,而家具又是当前的筛选环境,导致计算的参考结果为0。而 RANKX 的第三参数在不指定的情况下,默认又是第二参数,但其计算环境是家具,椅子,所以这个结果永远大于非家具类别的其他元素的参考结果。这样一来,排序就仅仅在当前类别下了。
再回味一遍,就会发现这句话的重要性:计算 ALLSELECTED( Model_Product[产品子类别] ) 每行的 [KPI],这个计算应考虑 ALLSELECTED( Model_Product[产品子类别] ) 外部的上下文以及 ALLSELECTED( Model_Product[产品子类别] ) 迭代时所产生的行上下文,计算完毕得到排序参考表。
在上例中,外部环境家具一直制约着计算,导致非家具范围的元素在计算[KPI]时都是空。
这就给我们了一个重要启发,如果要让排序公式更加灵活,或者说更加灵活地驾驭排序公式,应该考虑四点:
- 第一,注意外部上下文的影响。 如本例的家具,可能会被情不自禁地忽略而误判。
- 第二,构造更加合理的参考表元素结构。 这个结构应该考虑到外部上下文的影响。
- 第三,关于 RANKX 第二参数,由于迭代参考表时会产生行上下文,在行上下文中的计算第二参数仍然受到外部上下文的影响,应该考虑利用之或规避之。
- 第四,关于 RANKX 第三参数,并非都与第二参数相同,且要考虑外部上下文的影响而构造合理的计算。
把握了上述四点,就可以彻底驾驭 RANKX 函数。而忽略其中某点都可能是造成计算与预期不符的错误来源。
下面用这个思路来解决本例如果想继续在全局排序,则需要修改计算公式。
原有计算公式是:
代码语言:javascript复制RANKX( ALLSELECTED( Model_Product[产品子类别] ), [KPI] )
其等价版本是:
代码语言:javascript复制RANKX( ALLSELECTED( Model_Product[产品子类别] ), [KPI], [KPI], DESC, SKIP )
现在需要把它修改以便上述右图可以在全局排序。
如果考虑刚刚的四点,就可以发现第四条在这里是不需要改变的,就是计算[KPI]。而通过第二条或第三条都可以修改出一个新的符合预期的公式。
先来看通过利用第二条:构造更加合理的参考表元素结构。
可以修改公式为:
代码语言:javascript复制RANKX( ALLSELECTED( Model_Product[产品类别], Model_Product[产品子类别] ) , [KPI] )
如下:
这让右图的排序和左图相同,原因是充分考虑了外部筛选环境,构造了更加合理的参考表元素结构。
现在不使用第二条启发,而利用第三条启发:由于迭代参考表时会产生行上下文,在行上下文中的计算第二参数仍然受到外部上下文的影响,应该考虑利用之或规避之。
可以修改公式为:
代码语言:javascript复制RANKX( ALLSELECTED( Model_Product[产品子类别] ), CALCULATE( [KPI] , REMOVEFILTERS( Model_Product[产品类别] ) ), [KPI] )
这让右图的排序和左图相同,原因是 RANKX 的第二参数充分考虑了在行上下文计算时利用了上下文转换并清除了外部已经存在的产品类别上的筛选。
在计算列中计算排序
计算列的复杂性在于:
- 它天然存在一个行上下文
- 只要写度量值就会上下文转换
- 上下文转换会携带所有的列
排序公式很容易在计算列中出错,例如:
其原因很简单,以 RANKX 第二参数身份计算 [KPI] 时,由于上下文转换,导致所计算的 [KPI] 总是当前产品ID位置的 [KPI];而以 RANKX 第三参数身份计算 [KPI] 时,外部上下文正是行上下文,导致所计算的 [KPI] 总是当前产品ID位置的 [KPI],因此返回结果是 1。
在洞悉了 RANKX 的四大注意事项后,可以将公式修改为:
代码语言:javascript复制RANKX( ALL( Model_Product[产品子类别] ) , CALCULATE( [KPI] , ALLEXCEPT( Model_Product , Model_Product[产品子类别] ) ) )
对于修改后的计算结构,以 RANKX 第二参数或第三参数身份计算时总是针对 Model_Product[产品子类别],所以可以正确得到结果。
RANKX 的特殊用法:计算价格带
在理解了 RANKX 以后,不难使用很多精炼的计算技巧,例如,对产品价格计算所属价格带,在产品表创建计算列如下:
代码语言:javascript复制RankPrice =
VAR X = RANKX( { 1,10,100,1000,10000 } , [Value] , [产品单价] )
RETURN POWER( 10 , 5 - X )
该计算相当巧妙。效果如下:
RANKX 的特殊用法:计算年龄区间
在理解了 RANKX 以后,不难使用很多精炼的计算技巧,例如,对客户计算所属年龄带,在客户表创建计算列如下:
代码语言:javascript复制AgeGroup =
VAR X1 = 0
VAR X2 = 60
VAR Step = 10
RETURN
( X2/Step 1 - RANKX( GENERATESERIES( X1 , X2 , Step ) , [Value] , [客户年龄] ) ) * Step
该计算相当凶残。当然,如果这么复杂的公式记不住,可以借助价格带的方式,写个简单的,如下:
代码语言:javascript复制AgeGroupFast =
VAR X = RANKX( { 0, 10, 20, 30, 40, 50, 60, 70, 80 } , [Value] , [客户年龄] )
RETURN 10 * ( 9 - X )
对于传统的静态年龄分组计算,在这个技巧面前,简直是弱了。效果如下:
如果觉得这个不够美观,可以调整下公式:
代码语言:javascript复制AgeGroupFast.Cool =
VAR X = RANKX( { 30, 40, 50 } , [Value] , [客户年龄] )
RETURN IF( X = 1 , "50以上" , IF( X = 4 , "30以下" , ( 50 - 10 * X 10 ) & "到" & ( 50 - 10 * X 20 ) ) )
效果如下:
这个效果就非常美妙了。
总结
本文前所未有地详解了 RANKX 的一切。首先,给出表面含义;然后,自行实现;接着,深入解读;再有,自然理解;再跟,反常理解;并给出了驾驭 RANKX 的四大启发点,并按照这种启发点给出了如何从有问题的结果到需要的结果。当然,还给出了在计算列中的特殊注意事项。最后,本文给出了价格带和年龄分组的快速实现。
而掌握这些后,您可以试试用非中间表的方式来计算这个问题:
一个简单现实案例挑战 PowerBI 水平测