本文来自伙伴从实际案例的问题。在 PowerBI 中,时间智能计算是一个老生常谈的问题,但在实际中可能会出现各种变种,这就要求我们灵活处理。
先来看问题是怎样的:
[图片上传失败...(image-6b879-1571722027209)]
同样是 MTD 的去年同期,会出现上下两种情况。请仔细观察后发现,我们需要的是上面这种。原因如下:
- 2019年(当前年)的未来月份还没到来,不应该进行计算对比。
- 2019年10月(当前月份)还没过完,应该按过完的日期区间同比去年同期而不是全年的整月。
MTD 的标准计算
这里给出标准计算:
-- 定义 KPI ,值得注意的是,这里考虑的单位。
KPI.Sales = [Core.Sales] / [Core.Unit]
-- 定义 KPI 的 MTD,如下:
KPI.Sales.MTD = CALCULATE( [KPI.Sales] , DATESMTD( Model_Calender[日期] ) )
使用嵌套定义 KPI 的方式是复用的标准用法。
MTD 的默认去年同期
继续基于已经定义好的 MTD 来计算去年同期,也很简单,如下:
-- 我们有一些对 DAX 编写的良好简称约定,例如:PY 指的是 Previous Year 即去年同期
KPI.Sales.MTD.PY =
CALCULATE( [KPI.Sales.MTD] , SAMEPERIODLASTYEAR( Model_Calender[日期] ) )
至此,我们就可以得到一个结果,如下:
可以看出,这种计算将得到开篇的错误形式,严格来说,从技术的逻辑来看是没有错误的,然而从业务的逻辑来看,它不符合常识。
正确计算 MTD 的去年同期
从业务的角度来计算 MTD 的去年同期,就应该满足开篇的条件:
- 2019年(当前年)的未来月份还没到来,不应该进行计算对比。
- 2019年10月(当前月份)还没过完,应该按过完的日期区间同比去年同期而不是全年的整月。
为此,我们定义如下的计算规则:
-- 当前数据的最后日期:
Date.LastDate.All = MAXX( ALL( Model_Order ) , [订单日期] )
-- 最后一个年月:
Date.LastYearMonth = YEAR( [Date.LastDate.All] ) * 100 + MONTH( [Date.LastDate.All] )
-- MTD 的去年同期:
V.MTD:Sales.MTD.PY = // 考虑当月不完全结束月,同比
VAR vCurrYearMonth = SELECTEDVALUE( Model_Calender[年月序号] )
VAR vLastYearMonth = [Date.LastYearMonth]
RETURN SWITCH( TRUE(),
vCurrYearMonth < vLastYearMonth , [KPI.Sales.MTD.PY] ,
vCurrYearMonth = vLastYearMonth ,
CALCULATE( [KPI.Sales.MTD.PY] ,
TREATAS( { [Date.LastDate.All] } , Model_Calender[日期] ) ) ,
BLANK()
)
从其中可以看出,在最后一个数据月之前,按照常规方式计算;在最后一个数据月按照最后一天来计算。因此,得到:
这样,就得到了正确的结果。
大部分高手做到这里就OK了,但此时其实隐藏了一个很细很细的 BUG,看你是否可以看出来?
修复细微的 BUG
怎么会有 BUG 呢?如果你仔细思考,并在大脑中一天天的过,你会发现,如果当月达到最后一天时,便成了满月,一旦变成满月就应该与去年的同期满月对比,这里就存在一个闰年2月的情况,例如:当日是 2017-02-28,应该同比 2016-02-29,因此应该特别处理。
修改此前的公式为:
Date.ClosedMonthDate =
VAR X = [Date.LastDate.All] -- 实际日期的最后一天
RETURN IF( X = EOMONTH( X , 0 ) , X , EOMONTH( X , -1 ) ) -- 如果本月已满,则返回本月月底日否则返回上月月底日
Date.IsClosedMonth = IF( MAX( Model_Calender[日期] ) <= [Date.ClosedMonthDate] , TRUE() , FALSE() )
V.MTD:Sales.MTD.PY.Fixed =
VAR vCurrYearMonth = SELECTEDVALUE( Model_Calender[年月序号] )
VAR vLastYearMonth = [Date.LastYearMonth]
RETURN SWITCH( TRUE(),
vCurrYearMonth < vLastYearMonth , [KPI.Sales.MTD.PY] ,
vCurrYearMonth = vLastYearMonth ,
IF( [Date.IsClosedMonth] , // 如果达到满月
[KPI.Sales.MTD.PY] , // 满月按正常计算
CALCULATE( [KPI.Sales.MTD.PY] , // 非满月按最后一天计算
TREATAS( { [Date.LastDate.All] } , Model_Calender[日期] ) )
) ,
BLANK()
)
这样就得到最准确的计算。
就在文章发布的时候又有了新的方法,如下:
V.MTD:Sales.MTD.PY.Fixed2 =
// 考虑当月不完全结束月,同比
// 如果当月正好过完最后一天,那么对当月而言,应该同比去年的整月,对于闰二月的情况,将相差1日,例如:当日是 2017-02-28,应该同比 2016-02-29,因此应该特别处理。
VAR vCurrentLastDate = MIN( Model_Calender[日期] )
VAR vLastDateInData = [Date.LastDate.All]
RETURN
IF ( vCurrentLastDate <= vLastDateInData ,
IF( [Date.IsClosedMonth] , [KPI.Sales.MTD.PY] , CALCULATE( [KPI.Sales.MTD.PY] , TREATAS( { vLastDateInData } , Model_Calender[日期] ) ) )
)
总结
MTD 并不是一个复杂的问题,默认计算也是正确的,但对于业务常识来看,可能存在一定差异,因此,我们就需要调整某些度量值的计算。一个通用的套路是:
- 先按常规的计算;
- 再按照业务实际调整;
- 最后复查边界条件。
本案例启发我们按层次定义度量值,可以在不同的场景进行复用,并且需要考虑边界条件以使得计算是更加准确的。示例文件已经共享至订阅会员星球,祝您玩得开心。