[知识体系] 自定义时间智能

  [复制链接]
查看112178 | 回复121 | 2021-2-21 18:54:21 | 显示全部楼层 |阅读模式
正如你到目前为止所看到的,标准的 DAX 时间智能函数只支持公历,当你想要按年、季度、月和日这些标准字段分析数据时,公历很实用。但是,你可能对日期区间的定义有不同的需求,比如不是按月对应,而是基于周来计算。无论出于什么原因,当你不能使用标准的时间智能函数时,你都需要重写时间智能函数的逻辑。


本文包含四部分:


  • 基于周的时间智能
  • 基于自定义日历表编写累计公式
  • 计算不连续周期
  • 区间自定义比较


当你无法使用标准时间智能函数时,要如何进行时间智能计算呢?,最常见的做法是将业务逻辑的一部分移动到日期表。标准 DAX 时间智能函数不会从日期表中读取除日期列以外的任何信息,这是 DAX 的设计初衷,因为这样语言的行为就不依赖于定义更多的元数据来识别用于确定日期的年份、季度和月份的列 (MDX 和多维分析服务就是这种情况),你可以在代码中做更多的假设,这有助于简化代码编写,以便处理与时间相关的自定义计算。

基于周的时间智能

DAX 不提供处理周的时间智能函数,原因是对周的定义和计算方式有许多不同的标准。周经常会跨越年、季度和月的界限。你必须自己编写代码来实现个性化的基于周的日历表的定义。例如,在 ISO 标准的周日期系统中,2011 年 1 月 1 日和 1 月 2 日所在的周是 2010 年的第 52 周,2011 年的第一周从 1 月 3 日开始。


即使有不同的标准,你也可以学习一种在大多数情况下都有效的通用方法,将日期表中的周分配到月/季度/年。后续更改规则只需要更改日期表的内容,而不需要修改度量值里的 DAX 代码。


例如,你可以使用以下计算列扩展日期表对周的划分:
  1. Date[Calendar Week Number] = WEEKNUM ( 'Date'[Date], 1 )
  2. Date[ISO Week Number] = WEEKNUM ( [Date], 21 )
  3. Date[ISO Year Number] =
  4. IF (
  5.     [ISO Week Number] < 5
  6.         && [Calendar Week Number] > 50,
  7.     [Calendar Year Number] + 1,
  8.     IF (
  9.         [ISO Week Number] > 50
  10.             && [Calendar Week Number] < 5,
  11.         [Calendar Year Number] - 1,
  12.         [Calendar Year Number]
  13.     )
  14. )
  15. Date[ISO Week] = "W" & [ISO Week Number] & "-" & [ISO Year Number]
  16. Date[ISO Week Sequential] = INT ( ( 'Date'[Date] - 2 ) / 7 )
  17. Date[ISO Year Day Number] =
  18. COUNTROWS (
  19.     FILTER (
  20.         Date,
  21.         AND (
  22.             'Date'[ISO Year Number] = EARLIER ( 'Date'[ISO Year Number] ),
  23.             'Date'[Date] <= EARLIER ( 'Date'[Date] )
  24.         )
  25.     )
  26. )
复制代码

你可以在图 7-44 中看到效果。ISO Week 列对用户是可见的,而 ISO Week Sequential Number 对客户端工具是不可见的,ISO 年的日期数是自 ISO 年开始以来的天数。这样的数字可以方便的用作不同时期的比较。


7086211936381.jpeg

图 7-44 计算列扩展后的日期表支持 ISO 标准周


你可以通过使用 ISO Year Number 列而不是从日期中提取年份来编写当前年份的累计至今度量值。该技巧和“IF 判断用于确保选择了单个 ISO Year 来执行 VALUES 函数,以避免报错。
  1. [ISO YTD Sales] :=
  2. IF (
  3.     HASONEVALUE ( 'Date'[ISO Year Number] ),
  4.     CALCULATE (
  5.         [Sales Amount],
  6.         FILTER (
  7.             ALL ( 'Date' ),
  8.             AND (
  9.                 'Date'[ISO Year Number] = VALUES ( 'Date'[ISO Year Number] ),
  10.                 'Date'[Date] <= MAX ( 'Date'[Date] )
  11.             )
  12.         )
  13.     )
  14. )
复制代码

下图显示 2008 年第一周(包括 2007 年 12 月 31 日)ISO YTD Sales 指标的结果。


7086211936382.jpeg

图 7-45 在 2008 年的第一周,ISO YTD Sales 包括了 2007 年 12 月 31 日


公式将当前周数与前一年相同的周数进行比较。因为天数可能并不完全相同,因此使用日期表的其他列来比较更为简单。每年的周数分布很有规律,因为每周总是 7 天,而月份有不同的天数,不适用相同的假设。在基于周的日历中,可以通过在上一年份中查找与当前筛选上下文相同选择的对应日期来简化计算。


下面的 ISO PY Sales 度量值筛选前一年中相同的天数。这种方法也适用于外部上下文包含完整周的情况,因为所选天数使用的是 ISO Year Day Number 值,而不是日期值。
  1. [ISO PY Sales] :=
  2. IF (
  3.     HASONEVALUE ( 'Date'[ISO Year Number] ),
  4.     CALCULATE (
  5.         [Sales Amount],
  6.         FILTER (
  7.             ALL ( 'Date' ),
  8.             AND (
  9.                 'Date'[ISO Year Number]
  10.                     = VALUES ( 'Date'[ISO Year Number] ) - 1,
  11.                 CONTAINS (
  12.                     VALUES ( 'Date'[ISO Year Day Number] ),
  13.                     'Date'[ISO Year Day Number], 'Date'[ISO Year Day Number]
  14.                 )
  15.             )
  16.         )
  17.     )
  18. )
复制代码

7086211936383.jpeg

图 7-46 ISO PY Sales 显示了同一周在前一年的值


基于周的日历很容易管理,因为不同年份中周的天数始终是 7 天。而月份的天数可能会有变化,因此如果你想同时使用两个层级结构(月和周),你必须为每个层级结构创建不同的时间智能计算。

周累计(WTD)公式


将 ISO YTD Sales 稍作修改,可以很方便的编写出DATESYTD 一节中,你已经学会了如何用基础函数重写 DATESYTD 以及其他类似的时间智能函数。在本节中,我们将演示如何使用日期表的其他列替换从日期中提取的信息。


例如,观察下面这个 YTD 销售度量值:
  1. [YTD Sales] := CALCULATE ( [Sales Amount],DATESYTD ( 'Date'[Date] ))
复制代码

使用原生 DAX 函数重写后的语法:
  1. [YTD Sales] :=
  2. CALCULATE (
  3.     [Sales Amount],
  4.     FILTER (
  5.         ALL ( 'Date'[Date] ),
  6.         AND (
  7.             'Date'[Date] <= MAX ( 'Date'[Date] ),
  8.             YEAR ( 'Date'[Date] ) = YEAR ( MAX ( 'Date'[Date] ) )
  9.         )
  10.     )
  11. )
复制代码

如果使用自定义日历表,你需要将对 YEAR 函数的调用替换为对年份列的调用,例如下面的 YTD Sales Custom 度量值,FILTER 迭代整张日期表,以便行上下文可以访问所有列。如果外部上下文包含多个年份,则使用 MAX 获得最后一年。
  1. [YTD Sales Custom] :=
  2. CALCULATE (
  3.     [Sales Amount],
  4.     FILTER (
  5.         ALL ( 'Date' ),
  6.         AND (
  7.             'Date'[Date] <= MAX ( 'Date'[Date] ),
  8.             'Date'[Calendar Year Number] = MAX ( 'Date'[Calendar Year Number] )
  9.         )
  10.     )
  11. )
复制代码

使用同样的方式我们可以编写出 QTDMTD 度量值,唯一的区别在于将 Calendar Year Number 列替换为所需的列。
  1. [QTD Sales Custom] :=
  2. CALCULATE (
  3.     [Sales Amount],
  4.     FILTER (
  5.         ALL ( 'Date' ),
  6.         AND (
  7.             'Date'[Date] <= MAX ( 'Date'[Date] ),
  8.             'Date'[Calendar Year Quarter Number]
  9.                 = MAX ( 'Date'[Calendar Year Quarter Number] )
  10.         )
  11.     )
  12. )

  13. [MTD Sales Custom] :=
  14. CALCULATE (
  15.     [Sales Amount],
  16.     FILTER (
  17.         ALL ( 'Date' ),
  18.         AND (
  19.             'Date'[Date] <= MAX ( 'Date'[Date] ),
  20.             'Date'[Calendar Year Month Number] = MAX ( 'Date'[Calendar Year Month Number] )
  21.         )
  22.     )
  23. )
复制代码

你可以使用这些公式实现基于标准日历表的计算(在必须使用 DirectQuery 的情况下)和自定义日历表的计算(在非标准时间区间的情况下)。

基于不连续周期的计算

标准的 DAX 时间智能函数不支持计算非连续时间段。正如你在 DATEADD 一文中看到的,DATEADD 函数仅适用于连续日期选择。但是借助自定义 DAX 公式,你可以在这个场景下计算。在基于周的时间智能一节中,你已经看到了一种使用 CONTAINS 的写法,它可以查找上一年中对应的相同日期。但是,如果你有一套基于月的标准日历,那情况将变得更加复杂。


为了便于讨论问题,我们先设定几个初始条件


  • 因为日历是基于月份的,所以我们先实现对单月的计算,然后对上下文中的每月重复相同的计算。
  • 如果选择一个月内的全部日期,平移后的月份也将包括全部日期(即使它们的值不同,例如,从 30 天移动到 31 天,反之亦然)。
  • 如果只选择一个月中的几天,平移后的月份只包含当月对应的相同天数,前提是它们必须存在于平移后的月份中。
对计算不连续周期而言,最重要的就是明确特定情形下使用的计算逻辑

为了简化 DAX 度量值中的计算,我们创建下面的计算列:
  1. Date[Month Sequential Number] = 'Date'[Calendar Year Number] * 12 + 'Date'[Month Number] - 1
  2. Date[Days in Month] =
  3. COUNTROWS (
  4.     FILTER (
  5.         ALL ( Date ),
  6.         'Date'[Month Sequential Number] = EARLIER ( 'Date'[Month Sequential Number] )
  7.     )
  8. )
  9. Date[Day of Month] = DAY ( 'Date'[Date] )
复制代码

Month Sequential Number 是每个月和年组合的唯一值,列值每月加 1。这也使得从 1 月移动到前一年的 12 月更容易,你只需在此列中查找 1 月对应的值再减去 1 即可。Days in Month 列包含该月的总天数(例如,1 月 31 日、2 月 28 日或 29 日,依此类推)。Day of Month 列是当前月中的日期序号。你可以在图 7-47 中看到这些列的内容示例。


7086211936384.jpeg

图 7-47 计算列扩展了日期表, 以支持在非连续区间上使用自定义时间智能计算


让我们看看如何逐步构建完整的公式,以在不使用 DATEADD 的情况下获取上个月的值,DATEADD 具有以下语法:
  1. DATEADD ( 'Date'[Date], -1, MONTH )
复制代码

正如我们在第一个假设中所说的,我们将迭代选定的月份,对筛选上下文中的每个月重复相同的计算:
  1. SUMX (
  2.     VALUES ( 'Date'[Month Sequential Number] ),
  3.     <calculation for the month>
  4. )
复制代码

每个月发生的计算是不同的,这取决于上下文中是否包括该月的所有日期。月份计算的第一部分执行此检查,将上下文中的天数与月份中的天数进行比较。请记住,此步骤是逐月执行的,这要归功于 SUMX 对 Month Sequential Number 列的迭代
  1. IF (
  2.     CALCULATE (
  3.         COUNTROWS (
  4.             VALUES ( 'Date'[Date] )
  5.         )
  6.     )
  7.         = CALCULATE (
  8.             VALUES ( 'Date'[Days in Month] )
  9.         ),
  10.     <月份全部选中时发生的计算>,
  11.     <月份部分选中时发生的计算>
  12. )
复制代码
对列的迭代保证了每次迭代在上下文转换后都只有一个当前行,这使得不必添加 HASONEVALUE 检测以保护对 VALUES 的调用。但是,我们仍然需要调用 CALCULATECALCULATETABLE 来执行上下文转换

如果所选天数等于该月的全部天数,我们需要创建一个筛选器参数,通过从 Month Sequential Number 列中减去一天来选择上个月的所有天数。请注意,下面摘录的代码包含在 SUMX 对日期表 Month Sequential Number 列的迭代中,该列提供了我们要引用的行上下文,使用的是 EARLIER 函数。
  1. CALCULATE (
  2.     [Sales Amount],
  3.     ALL ( 'Date' ),
  4.     FILTER (
  5.         ALL ( 'Date'[Month Sequential Number] ),
  6.         'Date'[Month Sequential Number]
  7.             = EARLIER ( 'Date'[Month Sequential Number] ) - 1
  8.     )
  9. )
复制代码

另一种情况,如果筛选器还包含了来自当前迭代月份中的日期筛选,公式会从 Day of Month 列获得日期筛选器,这段代码在下方加粗显示:
  1. CALCULATE (
  2.     [Sales Amount],
  3.     ALL ( 'Date' ),
  4.     CALCULATETABLE ( VALUES ( 'Date'[Day of Month] ) ),
  5.     FILTER (
  6.         ALL ( 'Date'[Month Sequential Number] ),
  7.         'Date'[Month Sequential Number]
  8.             = EARLIER ( 'Date'[Month Sequential Number] ) - 1
  9.     )
  10. )
复制代码

你可能会惊讶于筛选器参数的语法可以使用下面这种形式:
  1. CALCULATETABLE ( VALUES ( 'Date'[Day of Month] )
复制代码

原因是 CALCULATETABLE 执行上下文转换,转换了 SUMX筛选上下文中迭代的 Month Sequential Number 的当前值。这样,只有与迭代月份中的活动日期相对应的 Day of Month 值才会包含在新的筛选上下文中,计值销售金额。


将所有步骤组合起来,你就得到了计算前一个月销售(PM sales Custom)的完整公式:
  1. [PM Sales Custom] :=
  2. SUMX (
  3.     VALUES ( 'Date'[Month Sequential Number] ),
  4.     IF (
  5.         CALCULATE (
  6.             COUNTROWS (
  7.                 VALUES ( 'Date'[Date] )
  8.             )
  9.         )
  10.             = CALCULATE (
  11.                 VALUES ( 'Date'[Days in Month] )
  12.             ),
  13.         CALCULATE (
  14.             [Sales],
  15.             ALL ( 'Date' ),
  16.             FILTER (
  17.                 ALL ( 'Date'[Month Sequential Number] ),
  18.                 'Date'[Month Sequential Number]
  19.                     = EARLIER ( 'Date'[Month Sequential Number] ) - 1
  20.             )
  21.         ),
  22.         CALCULATE (
  23.             [Sales],
  24.             ALL ( 'Date' ),
  25.             CALCULATETABLE (
  26.                 VALUES ( 'Date'[Day of Month] )
  27.             ),
  28.             FILTER (
  29.                 ALL ( 'Date'[Month Sequential Number] ),
  30.                 'Date'[Month Sequential Number]
  31.                     = EARLIER ( 'Date'[Month Sequential Number] ) - 1
  32.             )
  33.         )
  34.     )
  35. )
复制代码

你可以在图 7-48 中看到,该度量值现在也适用于不连续的选择,因为在 2008 年 3 月到 2008 年 8 月之间的月份没有在行上显示。但是,PM Sales Custom 列在 2008 年 1 月返回对应的 2007 年 12 月的值,在 2008 年 9 月返对应的 2008 年 8 月的值


7086211936385.png

图 7-48 PM Sales Custom 显示了前一个月的销售额


使用相同的公式,你可以通过简单地从 Month Sequential Number 中减去 3 或 12 而不是 1 来实现与前一个季度或前一年的比较。

自定义区间比较

在上一节中,你已经了解了如何在含月份的日期表中实现对前一个时间段的计算。你可以把同样的方法应用到一年中的任何时间段。但是,自定义日历可能需要应用特殊的规则才能进行比较,将一年中的特定时段(例如劳动节、端午节、复活节、新年以及任何其他可能的假日)排除在外。在本节中,你将学习一种在数据模型中处理这些规则的方法,而不必在 DAX 表达式中加入判断逻辑。


最初的假设是,应该可以为每一天定义前一年对应的日期。例如,我们假设默认情况下,它们都是同一工作日,用当前的天数减去 364 天(正好是 52 周)。不过,春节等例外情况可能会修改结果(例如,2019 年的 2 月 5 日和 2020 年的 1 月 25 日)。在时间智能函数内部处理这种情况会增加计算复杂性,并消耗更长的执行时间。因此,我们考虑在日期表中定义一个列,用前一年对应的日期填充它,如图 7-49 所示。


7086211936386.jpeg

图 7-49 列 Date Previous Year 定义了在与前一年对比时使用的日期


使用自定义公式计算上年度销售额的写法如下:
  1. [PY Sales Custom] :=
  2. CALCULATE (
  3.     [Sales Amount],
  4.     FILTER (
  5.         ALL ( 'Date' ),
  6.         CONTAINS (
  7.             VALUES ( 'Date'[Date Previous Year] ),
  8.             'Date'[Date Previous Year], 'Date'[Date]
  9.         )
  10.     )
  11. )
复制代码

FILTER 函数返回与前一年日期列中的活动日期对应的日期。例如,用户选择 2005 年 1 月 8 日;Date Previous Year 列的当前筛选器是 2004 年 1 月 10 日。FILTER 迭代所有日期并返回日期列等于 2004 年 1 月 10 日的行。使用 CONTAINS 可以处理所选的任意数量的日期。


FILTER 返回的日期必须存在于日期表的日期列中。此外,结果将永远不包括重复的日期。如果在 Date Previous Year 列中具有重复值,则重复值将仅由筛选器返回一次。由于以上原因,FILTER 返回的行数可能低于筛选上下文中最初选择的日期数。


在图 7-50 中可以看到 PY Sales Custom 的结果。为了显示计算去年所考虑的日期范围,还增加了下列度量值:
  1. [MIN Date PY] := MIN ( 'Date'[Date Previous Year] )
  2. [MAX Date PY] := MAX ( 'Date'[Date Previous Year] )
复制代码

7086211936387.png

图 7-50 PY Sales Custom 返回在 MIN Date PY 和 MAX Date PY 之间计算的日期销售额.


使用此方法计算上一年的值,你不需要对 DAX 度量值做任何修改,只能通过修改日期表 Date Previous Year 列的数据来处理例外情况。

7086211936388.rar (7.72 MB, 下载次数: 0)
回复

使用道具 举报

hansonlc | 2021-4-24 14:04:47 | 显示全部楼层
看起来好像不错的样子
回复

使用道具 举报

容总 | 2021-4-26 09:28:33 | 显示全部楼层
努力~~各位。。。
回复

使用道具 举报

肥宝 | 2021-6-29 12:57:44 | 显示全部楼层
我也来支持一下,这里的老师和其他机构不一样,其他就是让付费。
回复

使用道具 举报

2110495 | 2021-7-16 14:12:42 | 显示全部楼层
LZ是天才,坚定完毕
回复

使用道具 举报

大个仔 | 2021-8-30 14:01:56 | 显示全部楼层
小白一个 顶一下
回复

使用道具 举报

水泡鱼 | 2021-10-16 06:18:08 来自手机 | 显示全部楼层
我也来支持一下,很喜欢这里老师
回复

使用道具 举报

Ougeng | 2021-10-24 09:07:13 | 显示全部楼层
众里寻他千百度,蓦然回首在这里!
回复

使用道具 举报

600016 | 2021-10-26 13:42:16 | 显示全部楼层
LZ敢整点更有创意的不?兄弟们等着围观捏~
回复

使用道具 举报

枫树 | 2021-11-2 18:49:36 来自手机 | 显示全部楼层
边撸边过
回复

使用道具 举报

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

本版积分规则