Forum Discussion

mussaenda's avatar
mussaenda
Icon for Community Champion rankCommunity Champion
1 year ago
Solved

Inactive users (no data in the last 60 days)

Hello, 

 

I have a simple request received but I can't make it work. So I need to show daily who are the inactive users. Inactive users mean they don't have any activity in the last 60 days regardless of the category.

 

I have 3 tables: date table, activity_log, users

users

user

creation_date

aa 01/01/2024
bb 01/01/2024
cc 03/01/2025
dd 06/01/2025
ee 10/01/2025
ff 15/01/2025
gg 29/01/2025

 

activity_log

user activity_date category amount
aa 03/01/2024 open_trades 100
aa 15/01/2024 closed_trades 200
bb 12/04/2024 transaction 100
cc 07/07/2025 transaction 100
dd 14/05/2025 open_trades 100
dd 29/05/2025 closed_trades 50
dd 14/06/2025 open_trades 100
ee 20/06/2025 open_trades 50
ff 02/05/2025 transaction 100

 

 

and of course  the date table..

 

The chalIenge I am encountering is to show daily (via graph and table) who are those. I can't do the relationship between the users and the date.

 

The result I am achieving is something like this (according to the provided data)
Selected Filter: year: 2025, month: July
result:

daliy date (from date table) inactive users (from users)
 08/07/2025 4
 07/07/2025 4
 06/07/2025 5
 05/07/2025 5
 04/07/2025 5
 03/07/2025 5
 02/07/2025 5
 01/07/2025 4


4 = (gg, ff, bb, aa)

5 = (gg, ff, cc, bb, aa)

depends on the filter of year and month, inactive users will dynamically change according to the date that we have in the activity_log.

 

I know this is doable, I am lost how to calculate the inactive users.

  • Hi mussaenda 

     

    You should have a dedicated date table and have these relationships:

    activity_log[activity_date] → date[date] (Many-to-One)
    users[creation_date] → date[date] (Many-to-One)


    We don't need a direct relationship between users and date, we can use DAX to bridge them as follows:

     

     First, we need to create a measure for the last activity date for each user

    LastActivityDate = 
    CALCULATE(
        MAX(activity_log[activity_date]),
        FILTER(
            activity_log,
            activity_log[user] = users[user]
        )
    )

      

    Next, create a measure to calculate inactivity

     

    IsInactiveUser = 
    VAR CurrentDate = MAX('date'[date])
    VAR LastActivity = 
        CALCULATE(
            MAX(activity_log[activity_date]),
            FILTER(
                activity_log,
                activity_log[user] = users[user] &&
                activity_log[activity_date] <= CurrentDate
            )
        )
    RETURN
        IF(
            DATEDIFF(LastActivity, CurrentDate, DAY) > 60 || ISBLANK(LastActivity),
            1,
            0
        )

     

    Finally, count inactive users each day

     

    InactiveUsersCount = 
    CALCULATE(
        COUNTROWS(users),
        FILTER(
            users,
            [IsInactiveUser] = 1 &&
            users[creation_date] <= MAX('date'[date])
        )
    )

     

    To visualise the data, use a line chart or a table, add the date to the axis and add the measure 'inactiveusercount' to the value.

     

    I hope this helps, please give a thumbs up and mark as solved if it does, thanks!

  • Hi mussaenda 

    Create a measure that calculates the latest activity date per uyser which reverts back to the creation date if there's no activity log

    Latest Activity Date = 
    COALESCE (
        CALCULATE (
            LASTNONBLANK ( activity[activity_date], COUNTROWS ( activity ) ),
            ALLEXCEPT ( users,users[user ] )
        ),
        MIN ( users[creation_date] )
    )
    

    Create another measure that evaluates over the list of users and checks the  date difference betweeen their latest activity date aginst a specific date

    Inactive Users = 
    VAR _days = 60
    VAR _tbl =
        ADDCOLUMNS (
            SUMMARIZECOLUMNS (
                users[user ],
                "@latest activity date", [Latest Activity Date]
            ),
            "@days since last active", DATEDIFF ( [@latest activity date], MAX ( Dates[Date] ), DAY )
        )
    VAR _inactive =
        FILTER ( _tbl, [@days since last active] >= _days )
    RETURN
        COUNTROWS ( _inactive )
    

    However, I don't get how you came up with 3 for 1 July. aa, bb, ff, gg are past 60 days on this date

    Please see the attached pbix.

11 Replies

  • Hi mussaenda 

     

    You should have a dedicated date table and have these relationships:

    activity_log[activity_date] → date[date] (Many-to-One)
    users[creation_date] → date[date] (Many-to-One)


    We don't need a direct relationship between users and date, we can use DAX to bridge them as follows:

     

     First, we need to create a measure for the last activity date for each user

    LastActivityDate = 
    CALCULATE(
        MAX(activity_log[activity_date]),
        FILTER(
            activity_log,
            activity_log[user] = users[user]
        )
    )

      

    Next, create a measure to calculate inactivity

     

    IsInactiveUser = 
    VAR CurrentDate = MAX('date'[date])
    VAR LastActivity = 
        CALCULATE(
            MAX(activity_log[activity_date]),
            FILTER(
                activity_log,
                activity_log[user] = users[user] &&
                activity_log[activity_date] <= CurrentDate
            )
        )
    RETURN
        IF(
            DATEDIFF(LastActivity, CurrentDate, DAY) > 60 || ISBLANK(LastActivity),
            1,
            0
        )

     

    Finally, count inactive users each day

     

    InactiveUsersCount = 
    CALCULATE(
        COUNTROWS(users),
        FILTER(
            users,
            [IsInactiveUser] = 1 &&
            users[creation_date] <= MAX('date'[date])
        )
    )

     

    To visualise the data, use a line chart or a table, add the date to the axis and add the measure 'inactiveusercount' to the value.

     

    I hope this helps, please give a thumbs up and mark as solved if it does, thanks!

    • mussaenda's avatar
      mussaenda
      Icon for Community Champion rankCommunity Champion

      Thanks wardy912 ,

      I wrapped the user field in the users table with VALUES to make it work in measures. 
      Will let you know of the results. 

      Thanks again, wardy912 

    • mussaenda's avatar
      mussaenda
      Icon for Community Champion rankCommunity Champion

      Hi wardy912 ,

       

      I tried the suggestion, it is working in the small dataset but when I applied it to my dataset (having around 90M rows), it cannot be handled. 

       

      • wardy912's avatar
        wardy912
        Icon for Super User rankSuper User

        Hi mussaenda 

         

         Unfortunately I can't replicate your issue as I don't have a dataset large enough, so I've asked Copilot to suggest a fix. This is AI Generated and unvalidated. I hope it helps but I can't guarantee it will.

        Copilot
        When working with a large dataset (like 90 million rows) in Power BI or DAX, performance issues often arise due to:

        🔍 What's Likely Going Wrong
        Row-by-row filtering:
        The use of FILTER(activity_log, activity_log[user] = users[user]) is row context dependent and not optimized for large datasets. This creates a row-by-row comparison, which is very slow at scale.

        Inefficient relationships:
        Even though you're using a date table, the indirect relationship between users and activity_log via DAX can be expensive.

        Repeated calculations:
        The LastActivityDate is recalculated inside IsInactiveUser, which is then recalculated for every row in InactiveUsersCount.

        ✅ Optimized Approach
        Here’s how you can optimize this logic:

        1. Precompute Last Activity Date per User
        Instead of calculating it dynamically, create a calculated column or a summary table:

         

        UserLastActivity = 
        SUMMARIZE(
            activity_log,
            activity_log[user],
            "LastActivityDate", MAX(activity_log[activity_date])
        )


        This creates a smaller table with one row per user and their last activity date.

        2. Join This Table to Users
        Create a relationship:

        users[user] → UserLastActivity[user] (One-to-One)
        Or use LOOKUPVALUE in a calculated column:

        users[LastActivityDate] = 
        LOOKUPVALUE(
            UserLastActivity[LastActivityDate],
            UserLastActivity[user], users[user]
        )


        3. Create a Calculated Column for Inactivity
        This avoids recalculating in measures:

        users[IsInactive] = 
        VAR LastActivity = users[LastActivityDate]
        VAR CurrentDate = TODAY()  -- or use MAX('date'[date]) in a measure
        RETURN
            IF(
                ISBLANK(LastActivity) || DATEDIFF(LastActivity, CurrentDate, DAY) > 60,
                1,
                0
            )


        4. Measure for Inactive Users Over Time
        Now this becomes much faster:

        InactiveUsersCount = 
        CALCULATE(
            COUNTROWS(users),
            FILTER(
                users,
                users[IsInactive] = 1 &&
                users[creation_date] <= MAX('date'[date])
            )
        )


        📈 Performance Tips
        Use summary tables instead of row-by-row filters.
        Avoid FILTER with table[column] = value inside measures for large datasets.
        Use calculated columns or Power Query to precompute values.
        Consider aggregating activity_log in Power Query before loading into the model.

         

  • Hi mussaenda 

    Create a measure that calculates the latest activity date per uyser which reverts back to the creation date if there's no activity log

    Latest Activity Date = 
    COALESCE (
        CALCULATE (
            LASTNONBLANK ( activity[activity_date], COUNTROWS ( activity ) ),
            ALLEXCEPT ( users,users[user ] )
        ),
        MIN ( users[creation_date] )
    )
    

    Create another measure that evaluates over the list of users and checks the  date difference betweeen their latest activity date aginst a specific date

    Inactive Users = 
    VAR _days = 60
    VAR _tbl =
        ADDCOLUMNS (
            SUMMARIZECOLUMNS (
                users[user ],
                "@latest activity date", [Latest Activity Date]
            ),
            "@days since last active", DATEDIFF ( [@latest activity date], MAX ( Dates[Date] ), DAY )
        )
    VAR _inactive =
        FILTER ( _tbl, [@days since last active] >= _days )
    RETURN
        COUNTROWS ( _inactive )
    

    However, I don't get how you came up with 3 for 1 July. aa, bb, ff, gg are past 60 days on this date

    Please see the attached pbix.

    • mussaenda's avatar
      mussaenda
      Icon for Community Champion rankCommunity Champion

      Hi danextian , 

       

      just one question, the -5 in days diff (from the screenshot) should also be flagged as inactive user, right? because it is not within the 60day period

      • v-lgarikapat's avatar
        v-lgarikapat
        Icon for Community Support rankCommunity Support

        Hi mussaenda ,

        Thanks for reaching out to the Microsoft fabric community forum.

         

        danextian ,

        wardy912 ,

        Thanks for your prompt response

        mussaenda Thanks for the  follow up question

         

        But actually, no, the Day Diff = -5 should not be flagged as an inactive user

        Based on logic

        Users are inactive if their latest activity date is more than 60 days before the selected date (in this case, July 1, 2025).

        Details:

        User cc has a Latest Activity Date of 2025-07-07.

        Your slicer-selected date is 2025-07-01.

        So the Day Diff is 2025-07-01 - 2025-07-07 = -6 ( report shows -5 might be adifferent calculation).

        This means cc was active after July 1 in other words, they are active, not inactive.

         

        If you are still encountering any challenges, we would be happy to assist you further.

        Best Regards,

        Lakshmi Narayana