Week-Based Time Intelligence in DAX

pivot-ytd

The DAX language provides several Time Intelligence functions that simplify writing calculations such as year-to-date (YTD), year-over-year (YOY) and so on. However, if you have special calendar structure such as 4-4-5 weeks’ calendar, you need to write your custom time intelligence calculation. In this article, you will learn how to write the required DAX formulas.

The Time Intelligence functions in DAX (such as TOTALYTD, SAMEPERIODLASTYEAR and many others) assume that every day in a month belongs to the same quarter regardless of the year. This assumption is not valid in a week-based calendar: each quarter and each year might contain days that are not “naturally” related. For example, January 1st and January 2nd of 2011 belongs to week 52 of year 2010, and the first week of 2011 starts on January 3rd. This approach is common in retail and manufacturing industry, where 4-4-5 calendar, 5-4-4 calendar and 4-5-4 calendar are used. By using 4-4-5 weeks in a quarter, you can easily compare uniform numbers between quarters, mainly because you have the same number of working days and weekends in each quarter. You can find further information about these calendars on Wikipedia (4-4-5 calendar and ISO week date pages).

The goal of this article is not to explain how to write such a calendar table. There are too many variations and custom rules for each business, but the DAX pattern to use is always the same and this is the topic discussed here. You can build your custom calendar table in Excel, PowerPivot or SSAS Tabular, and it works automatically for simple aggregation, but if you try to calculate YTD or YOY by using Time Intelligence DAX functions, you get wrong results, just because the assumption made by these functions is no longer valid. Colin Banfield wrote a useful Excel workbook to generate Calendar tables. Darren Gosbell has an interesting Power Query script to generate a Calendar table.

DAX is a powerful language built on a very small number of basic functions. All existing Time Intelligence functions can be rebuild in DAX by using mainly the CALCULATE, FILTER, VALUES functions. With this knowledge in mind, you can build a calculation working on any custom calendar table. In the examples you can download at the end of the article, you can find an ISO Calendar table used as an example. However, the DAX code can be easily adapted to any other custom calendar table.

Let’s start with an easy translation of Time Intelligence function in corresponding DAX code. You can calculate YTD by using the TOTALYTD function:

Cal YTD :=
TOTALYTD (
    SUM ( Sales[Sales Amount] ),
    Dates[Date]
)

In reality, under the cover this corresponds to the following CALCULATE statement:

Cal YTD:=
CALCULATE (
    SUM( Sales[Sales Amount] ),
    DATESYTD ( Dates[Date] )
)

The DATESYTD function returns a table containing a list of dates. If DAX would not have DATESYTD, you might obtain the same result by writing a FILTER that returns all the days of the year before:

Cal YTD :=
IF (
    HASONEVALUE ( Dates[Calendar Year] ),
    CALCULATE (
        SUM ( Sales[Sales Amount] ),
        FILTER (
            ALL ( Dates ),
            Dates[Calendar Year] = VALUES ( Dates[Calendar Year] )
                && Dates[Date] <= MAX ( Dates[Date] )
        )
    ),
    BLANK ()
)

By examining the function above, you can understand why the calculation does not work on a week-based calendar. The January 1st is included in a year that is different from its date, and the existing filter condition would fail this check. You cannot use the optional parameter of DATESYTD that specify the last day of a year to fix that, because each year has a different last day. In the following picture, you see that the result shown in Cal YTD column is wrong. The first week of ISO Year 2011 contains also the first two days of January 2011, which in reality belong to the ISO Year 2010. The column Iso YTD contains the correct result.

pivot-ytd

However, you can just replace the test over the year column in the previous formula in order to obtain the desired result. The following is the definition of the Iso YTD measure, which you have seen used in the pivot table.

Iso YTD :=
IF (
    HASONEVALUE ( Dates[ISO Year] ),
    CALCULATE (
        SUM ( Sales[Sales Amount] ),
        FILTER (
            ALL( Dates ),
            Dates[ISO Year] = VALUES ( Dates[ISO Year] )
                && Dates[Date] <= MAX ( Dates[Date] )
        )
    ),
    BLANK ()
)

You can use the same technique to write the quarter-to-date (QTD), month-to-date (MTD) and week-to-date (WTD) calculations.

Iso QTD :=
IF (
    HASONEVALUE ( Dates[ISO Year] )
        && HASONEVALUE (Dates[ISO Quarter] ),
    CALCULATE (
        SUM ( Sales[Sales Amount] ),
        FILTER (
            ALL ( Dates ),
            Dates[ISO Year] = VALUES ( Dates[ISO Year] )
                && Dates[ISO Quarter] = VALUES ( Dates[ISO Quarter] )
                && Dates[Date] <= MAX ( Dates[Date] )
        )
    ),
    BLANK ()
)

Iso MTD :=
IF (
    HASONEVALUE ( Dates[ISO Year] )
        && HASONEVALUE (Dates[ISO Month Number] ),
    CALCULATE (
        SUM ( Sales[Sales Amount] ),
        FILTER (
            ALL ( Dates ),
            Dates[ISO Year] = VALUES ( Dates[ISO Year] )
                && Dates[ISO Month Number] = VALUES ( Dates[ISO Month Number] )
                && Dates[Date] <= MAX ( Dates[Date] )
        )
    ),
    BLANK ()
)

Iso WTD :=
IF (
    HASONEVALUE ( Dates[ISO Year] )
        && HASONEVALUE (Dates[ISO Week Number] ),
    CALCULATE (
        SUM ( Sales[Sales Amount] ),
        FILTER (
            ALL ( Dates ),
            Dates[ISO Year] = VALUES ( Dates[ISO Year] )
                && Dates[ISO Week Number] = VALUES ( Dates[ISO Week Number] )
                && Dates[Date] <= MAX ( Dates[Date] )
        )
    ),
    BLANK ()
)

Another common calculation is the year-over-year (YOY). In order to do that, you need to calculate the same period last year before. This is the Time Intelligence calculation available in DAX for regular calendars, based on SAMEPERIODLASTYEAR function:

Cal PY :=
CALCULATE (
    SUM ( Sales[Sales Amount] ),
    SAMEPERIODLASTYEAR ( Dates[Date] )
)

Under the cover, SAMEPERIODLASTYEAR iterates all the dates that have the same day and month but have one year less. This assumption is not valid in a week-based calendar, because the last day of each year (and of each period) can be different between different years. In the following picture, you see that Cal YOY measure returns a wrong value for ISO Week 09 in 2012, whereas the Iso YOY displays the correct value.

pivot-yoy

In order to write a simple DAX calculation for the Iso YOY measure, you need to create a column in the calendar table that simplifies the required DAX code. This column contains the number of days elapsed in the current year for each date. Something like that:

isocalendar

By using such a column, writing the previous year calculation is simple. You just have to check that the ISO Year Day Number is the same between different years. Please note that the CONTAINS function has the same expression in two arguments: the second argument defines the column that is checked for each row of the table passed as the first argument, whereas the third argument is an expression that is resolved in a scalar value before the CONTAINS function is called. Thus, the measure executes such expression in the row context defined by the outer FILTER function. You can easily confused by that syntax!

Iso PY :=
IF (
    HASONEVALUE ( Dates[ISO Year] ),
    CALCULATE (
        SUM ( Sales[Sales Amount] ),
        FILTER (
            ALL ( Dates ),
            Dates[ISO Year Number] = VALUES ( Dates[ISO Year Number] ) - 1
                && CONTAINS(
                    VALUES ( Dates[ISO Year Day Number] ),
                    Dates[ISO Year Day Number],
                    Dates[ISO Year Day Number] )
        )
    ),
    BLANK ()
)

At this point, calculating year-over-year (YOY) calculation is simple:

Iso YOY := [Sales] - [Iso PY]

Iso YOY % := IF ( [Iso PY] <> 0, [Iso YOY] / [Iso PY], BLANK () )

Finally, if you want to calculate the previous-year-to-date value, you just have to merge the two techniques presented in this article, using the ISO Year Day Number column instead of the simple date in order to get the corresponding day in the previous year as the last day of the days to consider in the calculation.

Iso PYTD :=
IF (
    HASONEVALUE ( Dates[ISO Year] ),
    CALCULATE (
        SUM ( Sales[Sales Amount] ),
        FILTER (
            ALL ( Dates ),
            Dates[ISO Year Number] = VALUES ( Dates[ISO Year Number] ) - 1
                && Dates[ISO Year Day Number] <= MAX ( Dates[ISO Year Day Number] )
        )
    ),
    BLANK ()
)

There are many other calculations that you can build on a Calendar. In this article, you have seen the more important techniques that you need to write time intelligence calculations over week-based calendars. You can also use the same techniques on any custom calendar, creating the columns (such as ISO Year Day Number) that can help you in writing a simple DAX formula.

The ZIP demo file you can download below includes two examples, one for Excel 2010 and the other for Excel 2013.

Download


Download Demo (ZIP)