DAX Workout 015 - Calculated Counts by Time and Group

Hi Everyone -

This workout is focused on another common situation around creating counts of items during a given time span. However, in this case, the item being counted spans different time periods and thus not tied to a specific date.

In this example, you manage a gym that has three different membership levels:

– Basic
– Gold
– Platinum

The dataset provided shows the list of members, their membership level, and a corresponding time period for that membership. As you will see, people might have different levels of a membership if they choose to upgrade/change over time.

Your objective is to provide the number of members in a given membership for a particular time period.

Workout 015 Data.xlsx (11.4 KB)

Submission
Load the supplied data file, create your solution, and reply to this post. Upload a screenshot of your solution (or the PBI file) along with any DAX items. Please blur any solution or place it in a hidden section.

3 Likes

My solution feels very hacky but I guess it works. I don’t feel like I created all the necessary relationships, and it wouldn’t really allow for dynamic analysis.

My approach was to attempt to create a “subscriptions renewal” table that lists out each month for which a subscription continues to exist, then count the rows in that table to get the number of active subscriptions in each month.

Here’s how the DAX looks like:

Membership counts table = 
 VAR memberstable = VALUES('Members')
 VAR monthtable = ADDCOLUMNS(
                        VALUES(Dates[MonthInCalendar]), 
                        "dates", CALCULATE(MAXX(Dates, Dates[Date]))
                    )
 VAR crossed = CROSSJOIN(monthtable, memberstable)
 VAR activemembers = FILTER(crossed, 
                        AND([dates]>='Members'[Effective Date], [dates] <= 'Members'[End Date]))
 VAR summarytable = SUMMARIZE(activemembers, 
                        Dates[MonthInCalendar], [dates], 'Members'[ Gym Membership ], 
                        "Active Subscriptions", COUNTA('Members'[ Gym Membership ]))
RETURN
summarytable

I’ll likely revisit this later to clean it up, but I just thought I’d put in my submission anyway, lest I fail to submit at all. It was a great learning experience working out with DAX all Sunday

1 Like

@tweinzapfel ,

Thanks, fun problem.

As usual, data modeling FTW.

Nail your data model, and the DAX becomes much simpler.

Summary

I used a disconnected Dates table, but you could accomplish the same thing by making both relationships inactive. The key here is to have the time period selection not filter the main data table.

Total Active = 
VAR __vTable =
ADDCOLUMNS(
    Data,
    "@Active", [Active]
)

VAR __Result =
SUMX(
    __vTable,
    [@Active]
)
RETURN __Result
Get MonOffset = 
SELECTEDVALUE( 'Disconn Dates'[MonthOffset] )
Active = 
IF(
    AND(
        SELECTEDVALUE( Data[StartOffset]) <= [Get MonOffset],
        SELECTEDVALUE( Data[EndOffset]) >= [Get MonOffset]
    ),
    1, 
    0
)
  • Brian
1 Like

As I imagined. I already finished writing my DAX before realizing I skipped data modeling. And then going back to it at that point just broke my calculations. I should start again from scratch and see what I can create with a proper data model

@tweinzapfel,

My solution:

Data model:

Replicated visual:

These next two are extra and a start towards something potentially helpful for marketing and retention:

viz2

If you wanted to understand change in membership levels over time perhaps…like why your platinum membership starting shrinking in Sep 2022 or if reductions in basic memberships reflect members moving to higher levels (all but one case) versus leaving the club.

And you might want to examine movement within member, like
viz3
where you could work towards identifying certain groups of members, like members in basic or gold that haven’t moved to higher levels or members in platinum or gold that dropped to lower levels.

The way I did it, the key measures all depend on

Member Count
Member Count = 
VAR MinDate = MIN ( 'Date'[Date] )
VAR MaxDate = MAX ( 'Date'[Date] )
VAR Result =
    CALCULATE (
        COUNTROWS ( Membership )
        , Membership[Effective Date] <= MaxDate
        , Membership[End Date] > MinDate
        , REMOVEFILTERS ( 'Date' )
    )
RETURN
    Result

The two additional measures (for the month-over-month percentage change)

MOM%
PM Member Count = 
VAR __CurrentYearMonthNumber = SELECTEDVALUE ( 'Date'[Year Month Number] )
VAR __PreviousYearMonthNumber = __CurrentYearMonthNumber - 1
VAR __Result =
    CALCULATE (
        [Member Count]
        , REMOVEFILTERS ( 'Date' )
        , 'Date'[Year Month Number] = __PreviousYearMonthNumber
    )
RETURN
    __Result

MOM % Member Count = 
DIVIDE (
    [MOM Member Count]
    , [PM Member Count]
)

Great exercise!

3 Likes

@HufferD ,

Awesome post! Love the way you’ve expanded the analysis to look at changes over time.

This is what I’ve been doing too on the workouts - just using them to maximize my own learning by taking them in additional directions to encompass some new learnings.

Thanks for participating - keep up the great work!

  • Brian
1 Like

Thanks! These are great ways to spend restless nights and sleepless mornings. Because of the intensity of some of these workouts, I expect at some point the developers will tire of preparing them all. It’s a good distraction for now.

I see you’re in the DC area. Same here.

I expanded the start and end dates to a row for each day in that range before loading into the data model.

Key data transformation steps:
#"Added Custom" =
    Table.AddColumn(
        #"Changed Type",
        "Date", each
            List.Dates(
               [Effective Date],
               Duration.Days([End Date] - [Effective Date]) + 1,
               #duration(1,0,0,0)
            )
    ),
#"Expanded Date" = Table.ExpandListColumn(#"Added Custom", "Date"),
Date table definition

DAX calculated table

Dates = CALENDARAUTO()

Calculated column:

Month = EOMONTH ( Dates[Date], 0 )

Sorted by calculated column:

YYYYMM = YEAR ( Dates[Date] ) * 100 + MONTH ( Dates[Date] )

Data Model:

From here, the DAX measure is simple:

Count active members
Active Members = DISTINCTCOUNT ( Data[Member Name] )

DAX Workout 015 - Alexis Olson.pbix (40.6 KB)

1 Like

Here is my submission;

  1. I created two inactive relationships between the dates and the fact tables (by linking the effective and enddate columns in the fact table to the date column in the dates table.
  2. Did a countrow of the fact table over a time period removing any filter on the date table in the process.

Data Model

Summary

DAX Code

Summary

Replicated Report page

Summary

@AlexisOlson ,

Some very cool techniques here, but your analysis seems to be counting active days in each category, not members.

Nice workout,

pretty straightforward with a disconnected Date table :

VAR _actualStartDate = MIN ( 'Dates'[Date] )
VAR _actualEndDate = MAX ( 'Dates'[Date] )

VAR _memberCountInPeriod =
        CALCULATE(
            DISTINCTCOUNT( 'data'[ Member Name  ] ),
           'data'[ Effective Date ] <= _actualEndDate && 'data'[ End Date     ] >= _actualStartDate
        )

RETURN _memberCountInPeriod

Regards,

Thomas Ducroquetz

Ah, of course. It should be DISTINCTCOUNT ( Data[Member Name] ) instead.

Another good one - simple and straight forward.

dax_workout15

This is my first gif so I was just testing things out. Here’s my code.

Active Members = 

var minDate = min(dates[Date])
var maxDate = max(dates[Date])

var result = CALCULATE([number of members], ALL(dates), GymMemberships[ Effective Date ] <= maxDate,
                                                        GymMemberships[ End Date     ] >= minDate
)

return

result

I think I’m using the wrong site to create gifs :unamused:

I just came back to this as promised, but it seems I’ve lost the ability to edit the post.

Instead of creating a whole new table in the model, I’ve improved the code to simply perform the entire calculation in a single measure. The logic is still the same. Create a table that lists out the months where each member remained active, then count the rows in the table

Summary
Active members count = 

 VAR memberstable = VALUES('Members')
VAR monthtable = 
ADDCOLUMNS(
    VALUES(Dates[MonthInCalendar]), 
    "dates", CALCULATE(MAXX(Dates, Dates[Date]))
)
 VAR crossed = CROSSJOIN(monthtable, memberstable)
 VAR activemembers =
 FILTER(crossed, 
    AND([dates]>='Members'[Effective Date], 
    [dates] <= 'Members'[End Date])
)
VAR _result = COUNTROWS (active members)
RETURN
_result

I got to see the workout late, but I had to try it out.

Count Gym members over a period of 2 years =
var _MinDate = MIN ( ‘Date’[Date] )
Var _MaxDate = MAX ( ‘Date’[Date] )
Var _MemberDistribution =
CALCULATE( DISTINCTCOUNT( ‘Dax workout 15’[ Member Name ] ),
ALL( ‘Date’),
‘Dax workout 15’[ Effective Date ] <= _MaxDate ,
‘Dax workout 15’[ End Date ] >= _MinDate )
Return
_MemberDistribution

All - once again some great approaches to this. While it is certainly possible to use a Dates table relationship, as some have shown here, it’s also possible to do it without any relation to the Dates table.

My approach

Similar to others, a simple DAX measure can help with this. The key is getting the date logic to account for the various situations that can apply. I have added a visual as well to explain this. When are you trying to capture the total number of items that could occur during a date range, you have to account for the different situations. So using this example, if I want to count up the total number of memberships in a given year, some memberships were activated in a prior year, but will continue; some memberships might be created in that year - but also end. And some memberships will be created in the year but continue into a future year.

Workout 015 solution pic 2

Result

image

DAX Measures

This text will be hidden

Date Table =
ADDCOLUMNS (
    CALENDARAUTO (),
    "Year", YEAR ( [Date] ),
    "Month & Year",
        FORMAT ( [Date], "mmm" ) & " "
            & FORMAT ( [Date], "YY" ),
    "MonthnYear", FORMAT ( [Date], "YYYYMM" )
)
Active Members =
CALCULATE (
    COUNT ( data[ Member Name  ] ),
    data[ Effective Date ] <= MAX ( 'Date Table'[Date] )
        && data[ End Date     ] >= MIN ( 'Date Table'[Date] )
)