计算表、计算字段和度量
计算表
返回值是一个二维表,比如下面返回一个只有一个时间列的表。时间是连续的,结束于6月。会扫描模型里的最大时间和最小时间,然后涵盖掉。
Due Date = CALENDARAUTO(6)
计算列
单行内计算,非聚合。和我们的非聚合型计算字段类似。
Due Fiscal Year =
"FY"
& YEAR('Due Date'[Due Date])
+ IF(
MONTH('Due Date'[Due Date]) > 6,
1
)
Due Month =
FORMAT('Due Date'[Due Date], "yyyy mmm")
注意,单行内的计算,是不能跨表的,这点和我们是一样的。但是也允许通过RELATED 、 RELATEDTABLE和LOOKUPVALUE函数引用其他关联表的字段,比如下面这样,跨sales表和product两张表的字段:
Discount Amount =
(
Sales[Order Quantity]
* RELATED('Product'[List Price])
) - Sales[Sales Amount]
度量
聚合后的字段称为度量。度量分为隐式度量和显式度量,用DAX创建的叫显示度量,在报表里展示的数值,叫隐式度量。显式度量是聚合的数值,类似于我们的聚合型计算字段、高级计算。尤其要提到的是,dax中,所有的高级计算,都是一个单独的度量,在建模时就创建好的。
在不麻烦的情况下,PowerBI推荐建模者在建模的时候就创建好显式度量,并且把隐式度量隐藏掉,防止做报表的人做出错误的聚合度量。
Revenue = SUM(Sales[Sales Amount])
Context
Power BI最最最重要的概念,可以说,Power BI的一切计算,都是围绕Context进行的。
记住,在DAX中,是没有group by的概念的。pbi在计算一个单元格或者表达时的值时,总是根据这个单元格或表达时所处的上下文来执行计算的。
Row Context
行上下文的意思就是当前行,一般在非聚合型计算字段上使用这个概念,只在行内计算。或者在迭代函数上使用这个概念,代表内外循环的两个“当前行”。
需要注意的是,行上下文默认是不跨关联表的。比如你有Product 和Sales两张表通过order_id关联在一起,你在Product表上新建一个totalCost=sum(Sales[shipping_cost])计算字段,那么所有行上的totalCost都是同一个值:shipping_cost总计值。它不会根据Product表上的维度做分组聚合。如果需要的话,必须通过RELATED 之类的函数引用关联表字段,或者使用CALCULATE函数,让Row Context转为Filter Context(下面会讲)。
Query Context
查询上下文就是在报表查询时确定一个单元格的上下文,它由单元格上的行头列头、报表上的各类过滤器、切片器共同决定,然后单元格上的表达式(可能是一个最简单的聚合)就是在这些上下文中执行的。
Filter Context
这是所有context中最灵活最强大的。可以先把它看成是对数据的过滤条件,比如制作报表的时候,filter context就是添加的各种过滤器比如切片器、查询控件,更为强大的是,他可以在创建度量的时候,直接写到表达式中,影响表达式计算的范围。更重要的是,你可以通过各种函数去改变动态修改filter context。由于filter context的优先级比其他context都要高,所以它可以改变聚合、计算的结果。
context转移
指的是将Row Context 和Query Context转为Filter Context,可以直白的理解为图表上的单元格所在行的维度值(row context)变成了(转移成)该单元格上表达式执行时的过滤条件(filter context)。发生了转移之后,就能够通过修改函数去修改表达式的context了,这也是dax能够实现各种高级计算的秘诀。
写好dax的关键是掌握context
写好dax,尤其是复杂的dax,关键是在理解各种dax概念(context)的基础上,用好各种聚合函数和filter context修改函数。
1.理解filter context 的概念,以及它的作用机制
2.掌握何时以及用什么方法去改变filter context以获得正确的表达式结果
3.将各种表达式组合成更复杂的dax语句。
迭代函数
所有的聚合函数基本上都有一个迭代函数版本,函数名是聚合函名加上X后缀,比如 SUMX, COUNTX, MINX, MAXX 等等迭代函数是迭代一个表的所有行,他的声明如下:
SUMX( table , expression)
第一个入参是一个表或者能够返回一个表的表达式,第二个入参是一个表达式。迭代函数的做法是迭代table的每一行,并且在每一行的row context下执行expression,然后对每一行结果做入参执行聚合计算,得到一个出参。如果第一个入参是返回表的表达式,这个表达式是在当前filter context中执行的。
实际上,普通的聚合函数其实是迭代聚合的语法糖,在pbi中,简单聚合都会变成迭代聚合函数,比如sum聚合
Revenue = SUM(Sales[Sales Amount])
其实是下面这个SUMX迭代函数的语法糖,它迭代了Sales表。
Revenue =
SUMX(
Sales,
Sales[Sales Amount]
)
迭代函数的作用
创建复杂聚合的计算字段
Discount =
SUMX(
Sales,
Sales[Order Quantity]
* (
RELATED('Product'[List Price]) - Sales[Unit Price]
)
)
这个表达式迭代了Sales表,并且以Sales表每一行的row context去计算单行的折扣量,最后汇总成总的折扣。需要注意的是,这里用了一个RELATED函数,这是因为pbi的row context默认是不能跨关联表的,如果不加这个函数,Product'[List Price]的值是不会受到Sales表当前行的维值约束的。
实现高阶聚合
比如想实现每类商品的平均利润,我们可能这样写
Revenue Avg =
AVERAGEX(
Sales,
Sales[Order Quantity] * Sales[Unit Price] * (1 - Sales[Unit Price Discount Pct])
)
但这其实不对,他计算的是每一行的的平均利润,但是同一类商品可能会有很多很多行。我们真正想实现的,其实是先计算每一类商品的利润,然后相加起来,再除以商品种类。
Revenue Avg Order =
AVERAGEX(
VALUES('Product'[Product Type]),
[Revenue]
)
这里的VALUES('Product'[Product Type])返回了所有的产品类型作为一个表,这个表只有一列,那就是产品类型,然后AVERAGEX函数迭代了这个表,在每一行(每种产品类型)以改行的row context去计算Revenue,最后算平均值。这里我们引用了Revenue度量,如果我们不是用引用,直接写成下面这样:
Revenue Avg Order =
AVERAGEX(
VALUES('Product'[Product Type]),
SUM(Sales[Sales Amount])
)
结果也是不一样的,因为row context是不会跨关联表的,所以产品类型不会影响SUM(Sales[Sales Amount]),那么每一行的SUM(Sales[Sales Amount])结果都是一样的。如果要让产品类型作用与SUM(Sales[Sales Amount]),则需要让row context转移成SUM(Sales[Sales Amount])表达式的filter context
Revenue Avg Order =
AVERAGEX(
VALUES('Product'[Product Type]),
CALCULATE(SUM(Sales[Sales Amount]))
)
calculate函数
calculate函数的用于修改filter context,实现强大的dax计算,他的基本格式如下
CALCULATE(<expression>, [[<filter1>], <filter2>]…)
表达式部分必须返回一个具体的值(数值、文本、时间等)。
筛选器部分,每个筛选器必须返回bool值或者一个数据表,各个filter之间是and关系。
bool筛选器
bool筛选器比较简单,但是它有以下限制
- 每个filter只能引用一个列
- 不能引用度量
- 不能用聚合函数
举几个引用bool筛选器的例子:
Revenue Red = CALCULATE([Revenue], 'Product'[Color] = "Red")
Revenue Red or Blue = CALCULATE([Revenue], 'Product'[Color] IN {"Red", "Blue"})
Revenue Expensive Products = CALCULATE([Revenue], 'Product'[List Price] > 1000)
表筛选器
表筛选器是一个表,可以直接用一个数据表充当表筛选器,但更常见的是用一个函数去创建表筛选器,最常用的函数就是FILTER函数。这是一个迭代器函数,有两个入参:表和过滤条件,申明如下
FILTER(<table>,<filter>)
FILTER函数会返回一个表,结构和传入的表一模一样,这个返回表的数据是经过filter表达式运算后为true的那些行。
比如下面这样,FILTER函数的返回结果就是Product表中所有满足售价大余成本2倍的行。
Revenue High Margin Products =
CALCULATE(
[Revenue],
FILTER(
'Product',
'Product'[List Price] > 'Product'[Standard Cost] * 2
)
)
把这个表筛选器用于CALCULATE函数,Revenue的计算就是在筛选出来的Product行数据组成的Filter Context下计算的。
其实所有的过滤器最终都会化成表过滤器,即使是bool过滤器,在pbi的内部,也会转为表过滤器,bool过滤器可以看做是一种语法糖,只是为了便于使用。比如上面的bool过滤器例子,其实会转为一下表过滤器:
Revenue Red =
CALCULATE(
[Revenue],
FILTER(
'Product',
'Product'[Color] = "Red"
)
)
筛选器表达式与Filter Context的关系
一共有两种情况:
- 如果Filter Context已经有了某一列(或者某个表)的约束条件,那么筛选器会直接覆盖掉这个列(或者表)的条件。
- 如果Filter Context没有了某一列(或者某个表)的约束条件,那么筛选器会把这列(或者表)的筛选条件加上。
比如上面写的Revenue Red度量,只计算红色的收入,那如果拖入颜色和Revenue Red,可以看到所有行的Revenue Red都是一样的,这是在Filter Context中因为发生了Color列上的条件覆盖,筛选器覆盖了行维度。
筛选器修改函数
除了写死筛选器求覆盖Filter Context中的条件,还可以用修改函数去修改Filter Context。
REMOVEFILTERS 可以删除Filter Context中的过滤条件,它可以从一个或多个列或从单个表的所有列中删除筛选器。在算总小计的时候,这个修改器函数很有用
KEEPFILTERS 可以在保留已有筛选条件的基础上增加新的筛选条件,比如把它用在Revenue Red中
Revenue Red =
CALCULATE(
[Revenue],
KEEPFILTERS('Product'[Color] = "Red")
)
那么其他颜色行的数据就会为空,只有Red行有数据
原因是行维度的值作为Filter Context的条件和表达式中的条件是and关系,所以只有Color='Red' and Color='Red'为true,其他的行都是false。
USERELATIONSHIP是个非常有特色的函数,因为它体现了一个Power BI数据建模中的一个隐藏限制:表之间的关联关系只能是单个字段的关联,但是允许有多组关联关系(都是单字段关联),但只有其中一组为活跃关联,其他为非活跃关联。 如果要引用非活跃关联关系,那么就必须用USERELATIONSHIP函数指定一个非活跃关联。
Revenue Shipped =
CALCULATE (
[Revenue],
USERELATIONSHIP('Date'[DateKey], Sales[ShipDateKey])
)
CROSSFILTER 能够影响关联关系的影响方向(维表到事实表,事实表到维表),甚至删除关联关系,更强大。
快捷计算
为了减轻写dax的难度,pbi提供了快捷方式创建度量,分成以下几类
分类聚合
分类聚合其实就是聚合的聚合,有下面这些
过滤
时间智能
前提:
必须要有时间表,时间表是一个连续的时间字段,且数据跨度满1年。
限制:
不支持自定义财年,否则需要自己写复杂的过滤函数。
同环比计算
Revenue YoY % =
VAR RevenuePriorYear = CALCULATE([Revenue], SAMEPERIODLASTYEAR('Date'[Date]))
RETURN
DIVIDE(
[Revenue] - RevenuePriorYear,
RevenuePriorYear
)
xtd计算
Revenue YTD =
TOTALYTD([Revenue], 'Date'[Date], "6-30")
计算拉新数
New Customers =
VAR CustomersLTD =
CALCULATE(
DISTINCTCOUNT(Sales[CustomerKey]),
DATESBETWEEN(
'Date'[Date],
BLANK(),
MAX('Date'[Date])
),
'Sales Order'[Channel] = "Internet"
)
VAR CustomersPrior =
CALCULATE(
DISTINCTCOUNT(Sales[CustomerKey]),
DATESBETWEEN(
'Date'[Date],
BLANK(),
MIN('Date'[Date]) - 1
),
'Sales Order'[Channel] = "Internet"
)
RETURN
CustomersLTD - CustomersPrior
快照计算(库存等等,其实就是最后一天聚合)
Stock on Hand =
CALCULATE(
SUM(Inventory[UnitsBalance]),
LASTDATE('Date'[Date])
)
度量嵌套
在定义一个度量的DAX中,可以引用其他已经存在的度量,还能创建变量,这就使得DAX非常复杂了,像一门编程语言。
和LOD区别
DAX是基于context的,LOD是基于聚合的。举个例子,计算每个商品类型的平均售价。
DAX
在DAX中,可以直接创建 price average per product_type这样一个度量。
price average per product_type =
AVERAGEX(
KEEPFILTERS(VALUES('quickbi_test company_sales_record'[product_type])),
CALCULATE(SUM('quickbi_test company_sales_record'[price]))
)
如果不想编辑,还可以通过快捷按钮方式直接创建常见的度量。
将度量拖到交叉表中,这时候PowerBI会根据row context,在表格上的每行都做一次品类平均值的计算。比如我们把area维度和度量price average per product_type拖入交叉表中,这时候row context是区域的值,比如第一行,就是在area=东北这个条件下,计算商品类型的售价均值。注意,度量的聚合方式是不可以更改的。
LOD
再看在LOD中,我们没有办法直接创建一个 price average per product_type度量,但是我们可以创建按照product_type聚合的计算字段,然后在使用该计算字段时,选择average聚合。
首先,创建 扩展 product_type聚合的计算字段sum price per product_type:
{INCLUDE [product_type]:SUM([price])}
然后,把这个计算字段拖到交叉表中,并且选择 average聚合:
这里的计算逻辑就是:
首先,根据area和product_type两个维度做sum聚合。
然后,因为聚合的维度比图表上展示的多(也就是LOD聚合粒度比图表聚合粒度更细),这样每行下面会有多个聚合值,所以需要进行二次聚合,这时候根据所选的聚合方式average再聚合一次。
最终结果就是在每个区域下计算商品类型的售价均值,和DAX异曲同工。
可以看到,结果和在PowerBI中用dax创建的price average per product_type是一样的。