Module 9 · Power BI DAX
Customer analytics patterns
Active and lapsed customers over a rolling window, Pareto concentration and ABC classes, and headcount on a date, built from patterns you can reuse on any data.
About 25 minutes
The problem
Kolanut's sales reps are judged on keeping customers ordering. The sales director wants a page that answers, for any month she picks:
- How many customers are active: they ordered in the last 30 days?
- How many have lapsed: they've ordered before, but not in the last 30 days?
- Which customers make up the 80% of revenue we can't afford to lose?
These are some of the most requested measures in any business with repeat customers (distributors, banks, telecoms, schools collecting fees), and they all follow a few patterns. Learn the patterns once and you can build them on any data.
The concept
Pattern 1: a rolling window from the selected date
"As of" the last date in the current filter, look back a fixed number of days:
DAX
Active Customers 30d =
VAR LastDay = MAX ( 'Date'[Date] )
RETURN
CALCULATE (
DISTINCTCOUNT ( orders[customer_id] ),
DATESINPERIOD ( 'Date'[Date], LastDay, -30, DAY )
)At June 2026, LastDay is 30 June and the window is 1 to 30 June.
Pattern 2: everything up to a date
"Ever ordered by the end of the period" needs every date up to LastDay. Remove the date filters explicitly, then add the condition:
DAX
Customers to Date =
VAR LastDay = MAX ( 'Date'[Date] )
RETURN
CALCULATE (
DISTINCTCOUNT ( orders[customer_id] ),
REMOVEFILTERS ( 'Date' ),
'Date'[Date] <= LastDay
)
Lapsed Customers 30d = [Customers to Date] - [Active Customers 30d]REMOVEFILTERS ( 'Date' ) matters: without it, a filter on Date[Year Month] from the visual would still be in place, and "to date" would mean "this month only".
The same pattern gives headcount on a date in HR data: employees hired on or before the date, who haven't left by it.
Pattern 3: cumulative share (Pareto)
Rank customers by revenue, then add up everyone whose revenue is at least the current customer's:
DAX
Cumulative Share =
VAR CurrentRevenue = [Revenue]
VAR AllCustomers = ALLSELECTED ( customers[customer_name] )
VAR RunningTotal =
SUMX ( FILTER ( AllCustomers, [Revenue] >= CurrentRevenue ), [Revenue] )
RETURN
DIVIDE ( RunningTotal, CALCULATE ( [Revenue], AllCustomers ) )
ABC Class =
SWITCH (
TRUE (),
ISBLANK ( [Revenue] ), BLANK (),
[Cumulative Share] <= 0.8, "A",
[Cumulative Share] <= 0.95, "B",
"C"
)Class A customers together make up the first 80% of revenue. They get the most attention from account managers.
Example
The sales director's page for June 2026, by channel:
| Channel | Customers to Date | Active 30d | Lapsed 30d |
|---|---|---|---|
| Kiosk | 39 | 24 | 15 |
| Supermarket | 30 | 21 | 9 |
| Wholesale | 21 | 21 | 0 |
| Total | 90 | 66 | 24 |
Every wholesaler ordered in June; 15 of 39 kiosks didn't. Before you send the kiosk list to the sales reps, check the window. Kiosks order small amounts, less often, and over the last three months (DATESINPERIOD with -3, MONTH), all 90 customers ordered. A 30-day window suits wholesalers; for kiosks, 60 days or three months may be the fairer test of "lapsed". The pattern is the same, so make the window a parameter and let the business choose.
Walkthrough
- Add
Active Customers 30d,Customers to DateandLapsed Customers 30d. Build the table from the example, with a slicer onDate[Year Month]set to 2026-06. - Remove
REMOVEFILTERS ( 'Date' )fromCustomers to Dateand watch "to date" collapse to June only. Put it back. - Change
-30to-60. Active customers at June 2026 rise to 84. - Add
Cumulative ShareandABC Classto a table ofcustomers[customer_name]and[Revenue], sorted by revenue, with no date filter. Find the row where the share first passes 80%.
Practice
Practice
At June 2026, how many Lapsed Customers 30d are there in the Kiosk channel?
Practice
Across all dates, how many customers are in class A: the customers who together make up the first 80% of revenue? Count the customer whose revenue takes the running share past 80%.
Task
6 minHR data: write Headcount for the employees table, giving the number of employees on the payroll on the last date of the current filter context (hired on or before it, and either no exit_date or an exit_date after it). Assume a date table that is not related to employees. Paste it here.
Your work is checked for
- Named Headcount
- Captures the last date in a variable
- Tests hire_date
- Handles a blank exit_date (ISBLANK) and an exit after the date
- Counts rows of a filtered employees table
More practice
Drill · optional
Using your Headcount measure, how many employees were on the payroll on 31 December 2024?
Drill · optional
At June 2026, how many customers are active in the last 60 days?
Check your understanding
Answer every question to check.