Articles   \  

Semi-Additive Measures in DAX



Values such as inventory and balance account, usually calculated from a snapshot table, require the use of semi-additive measures. In Multidimensional you have specific aggregation types, like LastChild and LastNonEmpty. In PowerPivot and Tabular you use DAX, which is flexible enough to implement any calculation, as described in this article.

UPDATE 2014-02-03: fixed errors in ClosingNonBlank functions thanks to Franj Tonsen’s comment.

A semi-additive measure does not aggregate data over all attributes like a regular additive measure. For example, measures like balance account and product inventory units can be aggregated over any attribute but time. Instead of considering the period selected (one year, one month …) you consider only a particular moment in time related to the period selected. It could be the first day, the last day, the last day that had transactions, and so on.

This condition is typical for tables containing snapshots over time, such as products inventory or accounts balance. In the following table, you can see an excerpt of a Product Inventory table. The same product has a Units Balance value for each date and you cannot sum such a column for two different dates (you might want to calculate the average over different dates). If you want to calculate the value of Units Balance for July 2001, you need to filter the rows for the last day such a month, ignoring rows for all the other days. However, you have to use regular aggregation for other measures such as Units In and Units Out, which are regular additive measures.

Product Name Date Key Units In Units Out Units Balance
Road-650 Red, 44 20050630 0 0 170
Road-650 Red, 44 20050701 0 103 67
Road-650 Red, 44 20050702 102 0 169
Road-650 Red, 62 20050630 0 0 185
Road-650 Red, 62 20050701 0 135 50
Road-650 Red, 62 20050702 129 0 179

In order to implement a semi-additive measure in DAX, you use a technique that is similar to the one used to compute aggregations and comparisons over time. You change the filter over the date in a CALCULATE statement, but in this case you limit the range of dates selected instead of extending it (like year-to-date) or moving it (like prior year).

First and Last Date

You can use LASTDATE to get the last day active in the current filter context for a particular date column passed as an argument. The result is a table of one column and one value that you can use as a filter argument in a CALCULATE statement, as in the following definition:

Units LastDate := CALCULATE ( SUM ( Inventory[UnitsBalance] ), LASTDATE ( 'Date'[Date] ) )

The result shows that the total for each quarter corresponds to the value of the last month in the quarter (for example, Q1 value is the same as March). Each month value corresponds to the value of the last day in that month (not represented here).

FIG_02_08

First and Last Non Blank

The Units LastDate calculation assumes that there are data for every year in every month. If the inventory is daily, this is not an issue, but it could become a problem in case the inventory is written only for working days: if a month would be on a Saturday, you would see the entire month. The problem is evident for future dates. In the following picture you see what happens using Units LastDate with an Inventory table that has rows until December 15, 2006: you do not see the total for year 2006, for Q4 and for December!

FIG_02_09

The reason is that the LASTDATE formula operates on dates available in the filter context and the Date table contains all the days for year 2007 (which is a best practice, otherwise other Time Intelligence functions would not work correctly). You can use another DAX function, LASTNONBLANK, which returns the last date that satisfy a non-blank condition for an expression passed as second argument.

Units LastNonBlank :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ), 
        LASTNONBLANK ( 
            'Date'[Date],
            CALCULATE ( SUM ( Inventory[UnitsBalance] ) )
        )
    )

It is important that the second argument of the LASTNONBLANK applies the context transition using an implicit or explicit CALCULATE, otherwise you would apply the expression without filtering by each date in the period and the result would be identical to LASTDATE. You can see the result in the following picture, where December, Q4 and the total for 2007 are all displayed.

FIG_02_10

If you need the first date of a period instead of the last one, you can use FIRSTDATE. You also have FIRSTNONBLANK to get the first date having some data, similar to what you do with LASTNONBLANK for the last date having some data. All these functions returns a table of one column and one row: for this reason, you can use them in a filter argument of a CALCULATE call. A common mistake is assuming that LASTDATE and MAX would produce the same result. While this is true from a logical point of view, there is an important syntactic difference. You cannot write the following expression:

Units MaxDate :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ),
        MAX ( 'Date'[Date] )
    )

The MAX function returns a scalar value and the filter argument of a CALCULATE function requires a table expression or a logical condition referencing only one column. Thus, you use MAX instead of LASTDATE by using the following definition:

Units MaxDate :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ),
        FILTER ( 
            ALL( 'Date'[Date] ), 
            'Date'[Date] = MAX ( 'Date'[Date] ) 
        ) 
    )

The best practice is using LASTDATE when you write a filter expression, whereas MAX is better when you are writing a logical expression in a row context, because LASTDATE implies a context transition that hides the external filter context.

Opening and Closing Periods

You have other Time Intelligence functions useful in semi-additive measure for getting the first and last date of a period (year, quarter, or month), which are useful whenever you need to get that value of a selection that is smaller than the entire period considered. For example, looking at the month level (which may be displayed in rows), you might want to display also the value of the end of the quarter and the end of the year in the same row, as you can see in the following picture.

FIG_02_11

The definition of ClosingMonth, ClosingQuarter, ClosingYear, OpeningMonth, OpeningQuarter and OpeningYear measures used in the previous pivot table is the following:

ClosingMonth := CLOSINGBALANCEMONTH ( SUM ( Inventory[UnitsBalance] ), 'Date'[Date]  )
ClosingQuarter := CLOSINGBALANCEQUARTER ( SUM ( Inventory[UnitsBalance] ), 'Date'[Date]  )
ClosingYear := CLOSINGBALANCEYEAR ( SUM ( Inventory[UnitsBalance] ), 'Date'[Date]  )
OpeningMonth := OPENINGBALANCEMONTH ( SUM ( Inventory[UnitsBalance] ), 'Date'[Date]  )
OpeningQuarter := OPENINGBALANCEQUARTER ( SUM ( Inventory[UnitsBalance] ), 'Date'[Date]  )
OpeningYear := OPENINGBALANCEYEAR ( SUM ( Inventory[UnitsBalance] ), 'Date'[Date]  )

The previous measures corresponds to the following ones defined using CALCULATE and the filter provided by ENDOFMONTH, ENDOFQUARTER, ENDOFYEAR, STARTOFMONTH, STARTOFQUARTER and STARTOFYEAR functions, respectively:

ClosingEOM := CALCULATE ( SUM ( Inventory[UnitsBalance] ), ENDOFMONTH ( 'Date'[Date] ) )
ClosingEOQ := CALCULATE ( SUM ( Inventory[UnitsBalance] ), ENDOFQUARTER ( 'Date'[Date] ) )
ClosingEOY := CALCULATE ( SUM ( Inventory[UnitsBalance] ), ENDOFYEAR ( 'Date'[Date] ) )

StartingSOM :=
CALCULATE ( 
    SUM ( Inventory[UnitsBalance] ), 
    DATEADD ( STARTOFMONTH ( 'Date'[Date] ), -1, DAY )
)

StartingSOQ := 
CALCULATE ( 
    SUM ( Inventory[UnitsBalance] ), 
    DATEADD ( STARTOFQUARTER ( 'Date'[Date] ), -1, MONTH )
)

StartingSOY := 
CALCULATE ( 
    SUM ( Inventory[UnitsBalance] ), 
    DATEADD ( STARTOFYEAR ( 'Date'[Date] ), -1, YEAR )
)

No functions for opening and closing period consider the non-blank condition. You can see in the following picture the behavior of the previous closing measures for year 2007, where data is available only until December 15.

FIG_02_12

Instead of OPENING/CLOSING functions, you should use the LASTNONBLANK function as filter in a CALCULATE statement, applying an extension of the considered period using the PARALLELPERIOD function. Here are the corresponding definitions:

OpeningMonthNonBlank :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ), 
        CALCULATETABLE ( 
            LASTNONBLANK ( 'Date'[Date], CALCULATE ( SUM ( Inventory[UnitsBalance] ) ) ),
            PARALLELPERIOD ( 'Date'[Date], -1, MONTH ) 
        ) 
    )

OpeningQuarterNonBlank :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ), 
        CALCULATETABLE ( 
            LASTNONBLANK ( 'Date'[Date], CALCULATE ( SUM ( Inventory[UnitsBalance] ) ) ),
            PARALLELPERIOD ( 'Date'[Date], -1, QUARTER ) 
        ) 
    )

OpeningYearNonBlank :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ), 
        CALCULATETABLE ( 
            LASTNONBLANK ( 'Date'[Date], CALCULATE ( SUM ( Inventory[UnitsBalance] ) ) ),
            PARALLELPERIOD ( 'Date'[Date], -1, YEAR ) 
        ) 
    )

ClosingMonthNonBlank :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ), 
        CALCULATETABLE ( 
            LASTNONBLANK ( 'Date'[Date], CALCULATE ( SUM ( Inventory[UnitsBalance] ) ) ),
            PARALLELPERIOD ( 'Date'[Date], 0, MONTH ) 
        ) 
    )

ClosingQuarterNonBlank :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ), 
        CALCULATETABLE ( 
            LASTNONBLANK ( 'Date'[Date], CALCULATE ( SUM ( Inventory[UnitsBalance] ) ) ),
            PARALLELPERIOD ( 'Date'[Date], 0, QUARTER ) 
        ) 
    )

ClosingYearNonBlank :=
    CALCULATE ( 
        SUM ( Inventory[UnitsBalance] ), 
        CALCULATETABLE ( 
            LASTNONBLANK ( 'Date'[Date], CALCULATE ( SUM ( Inventory[UnitsBalance] ) ) ),
            PARALLELPERIOD ( 'Date'[Date], 0, YEAR ) 
        ) 
    )

The following is the final result using these measures for year 2007.

FIG_02_13

The filter calculation might be different according to the logic you want to implement, but the pattern for a semi-additive measure is to filter a single date based on the initial selection of dates in the filter context. Such a logic is usually in a filter argument of a CALCULATE function call, unless a special time intelligence function is used, hiding the internal calculation that is always applied on a CALCULATE statement.

You can hear me explain some aspects of semi-additive measures in the DAX Time Intelligence video.

Download Demo (ZIP)











 
Want to read more?