Refinement of Semi-Additive Measures in DAX | by Salvatore Cagliari | Towards Data Science

In my Story, A hard lesson on Filter Context with Power BI and DAX, I explained some details about Semi-Additive Measures and possible side effects.
After discussing these solutions with some clients, we discovered some weird consequences of the base Measure. Now I would like to show you how to work around these consequences as well as give you inspiration for more elaborated solutions.
For this article, I used the Contoso dataset from Microsoft.
You can get the Power BI file from here: ContosoSalesForPowerBI
As the Power BI file lacks the Inventory table, I’ve got the original database from here: Microsoft Contoso BI Demo Dataset for Retail Industry
I loaded it into an Azure SQL database. I created Views on the database to create a pure Star-Schema, instead of the original Snowflake-Schema and imported only the needed data into a Power BI file.
I used a self-created calendar table instead of the original date table and used the Inventory table of the original dataset.
The base Measure has the following Definition:
Last Stock by Date Variant 1 =VAR LastDateWithInventory = LASTNONBLANK(‘Date’[Date],    SUM(‘Inventory’[OnHandQuantity]))VAR Result =    CALCULATE(        SUM(‘Inventory’[OnHandQuantity]),        LastDateWithInventory)RETURN    Result
This Measure gets the last Date with a Result of the SUM() expression. Then it returns the Results of the Expression for this date.
The problem is that when you’re working with a Date-Hierarchy, LASTNONBLANK() returns the last date of the Period with data.
Consequently, the Measure will return only a value when a value exists for this last date.
See the following picture for an example:
As we have data only for the last day of Oct 2009, we see a Sub-Total only for this Month. The Measure returns no Subtotal for the other months, and no total for 2009 as no data exists for 31. December 2009.
After some research in the book Analyzing Data with Power BI and Power Pivot for Excel (Business Skills)*, I found this variant:
Last Stock by Date Variant 2 =VAR LastDateWithInventory = LASTNONBLANK(‘Inventory’[DateKey],    NOT ( ISEMPTY(‘Inventory’) )    )VAR Result =    CALCULATE(        SUM(‘Inventory’[OnHandQuantity])        ,TREATAS(LastDateWithInventory                ,’Date’[Date])    )RETURN    Result
The difference is how the Measure evaluates the last date with data. LASTNONBLANK() gets the newest date from the Inventory table.
CALCULATE() uses the result of LASTNONBLANK() and maps the date from the Inventory table to the Date table with the help of TREATAS().
You can see the result in the following pictures.
In Figure 2, you can see that we get a result for each Month.
But, possibly this is not the correct result. As you can see in Figure 3, we get the value of the last day with data.
If we want to get the stock value of the last date in a month, but we don’t have data for the latest date of the month, then the result might be incorrect.
The following table shows the Stock for two products:
Look at February for the M2101 Blue Laptop.At the end of February, the Stock is 24.
But what’s for March?Is the Stock 19 or 0?
The same applies to the M1548 White Laptop.The Stock for February can be 24 or 0. Same for March. Is it 19 or 0?
It depends on the definition of the data. The data defines which of the two Measures is the correct one.
If a date without Stock-Amount means that the Stock is Empty, then 0 would be the correct result for March and both products. On the other hand, if we get a Stock-Amount only when something changes, the latest value (19) would be the correct result for both products and March.
The definition of the data to consider is one issue that you need to consider when working with Semi-Additive Measures. But there are more possible issues.
Sometimes I would like to create a report without setting a date Filter. In this case, I want to get the latest Stock-Amount from the Inventory table.
To solve such a request, I need to follow a two-step approach:
Step 1: Adding a Flag for the last Date with Inventory data.
I would add a calculated column to the Inventory table using context transition:
Is Last Snapshot =VAR LastSnapshot = CALCULATE(    MAXX(‘Inventory’    ,’Inventory’[DateKey])    ,ALL(‘Inventory’)    )VAR Flag = IF(‘Inventory’[DateKey] = LastSnapshot, TRUE(), FALSE())RETURN    Flag
Now I can create a measure, which checks if the Date is filtered. If not, the Measure will get only for the rows with Is Last Snapshot = TRUE().
The code for the needed Measure is the following:
Last Stock by Date Variant 3 =VAR LastDateWithData =    LASTNONBLANK(‘Date’[Date],        SUM(‘Inventory’[OnHandQuantity])    )VAR isTime =    ISFILTERED ( ‘Date’ )RETURN    IF (        isTime        ,CALCULATE(            SUM ( ‘Inventory’[OnHandQuantity] )            ,TREATAS( LastDateWithData            ,’Date’[Date]            )        )        ,CALCULATE (            SUM ( ‘Inventory’[OnHandQuantity] ),            FILTER ( ‘Inventory’                ,‘Inventory’[Is Last Snapshot] = TRUE ()                )            )        )
As you can see, this Measure is base on the first Variant of the base Measure.
The result is the following:
This Measure has two benefits:
You can define which Date you want to use if no Date is filtered.The performance is ~30% better as it is much easier to filter the Inventory based on a column with two distinct value than filter the data based on a related (date) table.
You can define which Date you want to use if no Date is filtered.
The performance is ~30% better as it is much easier to filter the Inventory based on a column with two distinct value than filter the data based on a related (date) table.
You will not need this solution if you use the second variant, as it will return the same result.
OK, now we have three working approaches for Semi-Additive Measures. But what happens when we have contradictory requirements?
One of my clients asked me to develop a measure to calculate the correct stock amount.
His ETL process to load Stock data are designed to load the current stock amount every day, even during the weekend.
The primary request was to show only the stock amount for the last day of each month.
When the user doesn’t select any date in the report, the measure has to return the last available stock amount.
The third Variant delivers exactly the needed result.
But what’s about the current month? There is no data available for the last day of the current month. This means that the measure would not return any value, which is wrong.
Now I needed the create a measure to change this behaviour.
The first step is to select the date with the latest dataset in the actual Filter context:
VAR LastDateWithData =LASTNONBLANK (        ‘Inventory’[DateKey]        ,SUM ( ‘Inventory’[OnHandQuantity] )        )
But, as I need the last possible date with data in the Inventory table as well, I need another Variable:
VAR LastDateWithData_FromData =CALCULATE (    LASTNONBLANK (        ‘Inventory’[DateKey]        ,NOT ISEMPTY ( ‘Inventory’ )        )    ,REMOVEFILTERS(‘Inventory’)    )
The use of REMOVEFILTERS(‘Inventory’) makes sure that I get the last possible date with data of the entire Inventory table.
You can use a more straightforward formula, in case that you have a calculated column that marks the latest stock values, like the formula shown above to create the [Is Last Snapshot] column,
VAR LastDateWithData_FromData =    CALCULATE(        MAX(‘Inventory’[DateKey])        ,’Inventory’[Is Last Snapshot]        )
Now I need to evaluate all days inside the month of the date stored in the Variable LastDateWithData_FromData:
VAR AllDatesWithLastData =    CALCULATETABLE(        SUMMARIZE(            ‘Date’            ,’Date’[Date]            )        ,FILTER(‘Date’        ,INT( ‘Date’[MonthKey] ) = ( YEAR(LastDateWithData_FromData) * 100 ) + MONTH(LastDateWithData_FromData)        )        ,ALL ( ‘Date’[Date] )        )
The last Variable is to check if a filter is present on the date table:
VAR isTime =    ISFILTERED ( ‘Date’ )
Now I can combine everything and return the correct value:
RETURN    IF (        AND ( isTime, NOT LastDateWithData IN AllDatesWithLastData )        ,CALCULATE(            SUM ( ‘Inventory’[OnHandQuantity] )            ,TREATAS( LastDateWithData                    ,’Date’[Date]                    )        )        ,CALCULATE (            SUM ( ‘Inventory’[OnHandQuantity] ),            FILTER ( ‘Inventory’                     ,‘Inventory’[Is Last Snapshot] = TRUE () )             )         )
The key part is the IF condition.
When a filter is present on the Date-Table ANDThe date in the actual Filter-Context is not part of the month of the latest dataset present in the Inventory table
When a filter is present on the Date-Table AND
The date in the actual Filter-Context is not part of the month of the latest dataset present in the Inventory table
2-a. Return the Stock Amount filtered by the Variable LastDateWithData. TREATAS() is needed to correct the lineage of the filter to the date table
3. If the IF condition is not true, then
3-a. We are either in the last month present in the dataOR
3-b. No filter is present on the date table
The result looks like the following screenshot:
The first table shows the result from the third Variant, and the second table shows the result from the combined measure.
In the following screenshot, you can compare the result of the third Variant of the measure vs the result of the combined Measure without any filter on the date table.
As you can see, the second table shows the stock amount of the last day available in the dataset: December 2009.
Here you can find the complete code of the Measure to combine all variants:
Last Stock by Date Variant 4 =VAR LastDateWithData =    LASTNONBLANK (        ‘Inventory’[DateKey]        ,SUM ( ‘Inventory’[OnHandQuantity] )        )VAR LastDateWithData_FromData =    CALCULATE(        MAX(‘Inventory’[DateKey])        ,’Inventory’[Is Last Snapshot]        )// CALCULATE (// LASTNONBLANK (// ‘Inventory’[DateKey]// ,NOT ISEMPTY ( ‘Inventory’ )// )// ,REMOVEFILTERS(‘Inventory’)// )VAR AllDatesWithLastData =    CALCULATETABLE(        SUMMARIZE(            ‘Date’            ,’Date’[Date]         )         ,FILTER(‘Date’         ,INT( ‘Date’[MonthKey] ) = ( YEAR(LastDateWithData_FromData) * 100 ) + MONTH(LastDateWithData_FromData)         )         ,ALL ( ‘Date’[Date] )         )VAR isTime =     ISFILTERED ( ‘Date’ )RETURN    IF (        AND ( isTime, NOT LastDateWithData IN AllDatesWithLastData )        ,CALCULATE(            SUM ( ‘Inventory’[OnHandQuantity] )            , TREATAS( LastDateWithData                       ,’Date’[Date]                       )        )        ,CALCULATE (            SUM ( ‘Inventory’[OnHandQuantity] ),            FILTER ( ‘Inventory’                   ,‘Inventory’[Is Last Snapshot] = TRUE () )                   ))
In this DAX-Measure, you can find both variants for the variable LastDateWithData_FromData.
Before you start copying the code shown in this article, you need to determine the desired result carefully.
Which Variant is the correct one for your needs?
The last, combined, Variant is the slowest one but is the most flexible. But, do you need such flexibility? If not, use the simpler variants.
The variants shown here works on Power BI and in SQL Server Analysis Services (SSAS).
But, when you work exclusively in Power BI, you may be happy with the use of
Last Stock by Date =    LASTNONBLANKVALUE('Date'[Date],        SUM('Inventory'[OnHandQuantity])    )
Again, you need to test the result with the different scenarios and decide if this Variant delivers the correct results in any case.
I always try to understand the requirements and show the different scenarios to my clients in order to be sure that we are talking about the same thing. It’s difficult to avoid thinking that “This is easy. I understood what’s my client is asking me”. But it’s paramount to go through at least two or three iterations before you can be sure that both sides have the same understanding of what to expect from the solution.
Last but not least, try to understand why a Measure returns a particular result.
It will make your life much easier when you understand how your Measures works and why the results are different, depending on the different scenarios.
As soon as you understand how your Measure works, you step closer to being a DAX master.
I hope you enjoyed the journey, and thank you for reading till the end.
*Book reference:Analyzing Data with Power BI and Power Pivot for Excel (Business Skills)Microsoft PressISBN-13: 978–1509302765ISBN-10: 150930276X