Wicked Smart Data
LearnInsightsAboutContact
Sign InLet's Build
LearnInsightsAboutContact
Sign InLet's Build
Wicked Smart Data

Intelligence, automation, and expert execution — plus an elite library of free knowledge. We turn complexity into competitive advantage.

Start a conversation

Platform

  • Learning Paths
  • Insights
  • RSS Feed

Company

  • About
  • Contact
  • Work With Us

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Wicked Smart Data. All rights reserved.

Intelligence · Automation · Advantage

All Insights
Power BI

DAX Customer Lifetime Value Patterns: Acquisition Cohort Revenue, Repeat Purchase Rate, and Churn Probability

Learn to build a production-quality Customer Lifetime Value dashboard in Power BI using DAX iterator and window functions. This lesson covers acquisition cohort revenue matrices, repeat purchase rate calculation, churn probability scoring, and how to combine them into an actionable CLV composite score.

⚡ Practitioner18 min readNov 6, 2026Updated Nov 6, 2026
DAX Customer Lifetime Value Patterns: Acquisition Cohort Revenue, Repeat Purchase Rate, and Churn Probability
On this page
  • Introduction
  • Prerequisites
  • Setting Up Your Data Model
  • The Core Tables
  • The CohortMonth Calculated Column
  • Part 1: Acquisition Cohort Revenue
  • Building the Cohort Revenue Measure
  • Cumulative Cohort Revenue
  • Revenue Per Customer in Cohort
  • Part 2: Repeat Purchase Rate
  • Defining "Repeat" Correctly
  • The Repeat Purchase Rate Measure
  • A More Performant Approach Using Pre-Aggregation
  • Part 3: Churn Probability
  • Days Since Last Purchase
  • Churn Risk Tier — Calculated Column
  • Churn Probability Score Using WINDOW
  • Part 4: Combining the Metrics into a CLV Score
  • Part 5: Dynamic Segmentation Using CLV Score
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Mistake 1: Cohort Revenue That Doesn't Add Up
  • Mistake 2: Repeat Rate = 100% for Recent Cohorts
  • Mistake 3: Churn Score Is the Same for All Customers
  • Mistake 4: Performance Collapse on Large Datasets
  • Mistake 5: Acquisition Date vs First Order Date Mismatch
  • Summary & Next Steps
  • DAX Customer Lifetime Value Patterns: Calculating Acquisition Cohort Revenue, Repeat Purchase Rate, and Churn Probability Using Window and Iterator Functions

    Introduction

    Here's a scenario that plays out in boardrooms everywhere: the marketing team is celebrating record-breaking customer acquisition numbers, while the finance team is quietly worried about margin. The disconnect? Nobody has a reliable model of what each customer is actually worth over time. You can pour budget into acquisition all day long, but if 60% of new customers never make a second purchase, you're running a leaky bucket.

    Customer Lifetime Value (CLV) analysis is how you plug that bucket. And while dedicated ML platforms can build sophisticated CLV models, Power BI with well-crafted DAX measures can give your business analysts and decision-makers the interactive, slice-and-dice CLV intelligence they need without waiting on a data science team. By the end of this lesson, you'll be able to build a production-quality CLV dashboard that segments customers by acquisition cohort, tracks how their revenue evolves month over month, identifies customers at churn risk, and calculates repeat purchase rates — all dynamically responding to slicers and filters.

    What you'll learn:

    • How to identify acquisition cohorts and calculate cumulative cohort revenue using iterator functions and virtual tables
    • How to calculate repeat purchase rate as a true measure of customer engagement, not just order count
    • How to build a churn probability score using recency-based logic and DAX window functions
    • How to structure your data model to support all three CLV metrics without duplicating logic
    • How to avoid the most common performance and correctness traps in CLV DAX patterns

    Prerequisites

    You should be comfortable with:

    • CALCULATE, filter context, and row context — if you need a refresher, see Master DAX CALCULATE and Filter Context in Power BI
    • Iterator functions like SUMX, AVERAGEX, and COUNTX — the article on DAX Iterators Explained: How SUMX, AVERAGEX, and MAXX Evaluate Row by Row to Solve Problems SUM Cannot is a solid foundation
    • Window functions (RANK, ROWNUMBER, OFFSET) — covered in Mastering DAX Window Functions: RANK, ROWNUMBER, and OFFSET for Advanced Comparative Analysis
    • Basic time intelligence patterns

    You do not need a machine learning background. Everything here is deterministic DAX.


    Setting Up Your Data Model

    Before writing a single measure, you need a data model that can support CLV analysis without fighting against you. CLV calculations involve multiple "time axes" simultaneously: the calendar date of a transaction, and the customer's acquisition month relative to that transaction. Getting this wrong in your model means your measures will be slow, incorrect, or both.

    The Core Tables

    For this lesson, we'll work with a realistic e-commerce data model:

    FactOrders — one row per order

    • OrderID, CustomerID, OrderDate, Revenue, ProductCategory

    DimCustomer — one row per customer

    • CustomerID, CustomerName, AcquisitionDate, AcquisitionChannel, Region

    DimDate — standard date table

    • Date, Year, Month, MonthYear, MonthOffset (months since an arbitrary epoch)

    DimCohort — a helper table we'll build

    • CohortMonth, CohortLabel

    The relationship structure is: FactOrders[OrderDate] → DimDate[Date] (active), and DimCustomer[AcquisitionDate] → DimDate[Date] (inactive — we'll handle this with USERELATIONSHIP when needed).

    The CohortMonth Calculated Column

    Add this calculated column to DimCustomer:

    -- In DimCustomer table
    CohortMonth = 
        DATE(YEAR(DimCustomer[AcquisitionDate]), MONTH(DimCustomer[AcquisitionDate]), 1)
    

    This normalizes every customer's acquisition date to the first of their acquisition month. All customers acquired in March 2023 get 2023-03-01. This is the foundation of cohort analysis.

    Then add MonthsSinceAcquisition to FactOrders as a calculated column:

    -- In FactOrders table
    MonthsSinceAcquisition = 
        VAR AcqDate = RELATED(DimCustomer[CohortMonth])
        VAR OrderMonth = DATE(YEAR(FactOrders[OrderDate]), MONTH(FactOrders[OrderDate]), 1)
        RETURN
            DATEDIFF(AcqDate, OrderMonth, MONTH)
    

    Note

    We're using calculated columns here for MonthsSinceAcquisition because this is a fixed property of each row — the relationship between a specific order and its customer's acquisition month never changes. This is exactly the right use case for a column versus a measure. For a deeper look at this tradeoff, see DAX Fundamentals: When to Use Calculated Columns vs Measures in Power BI.


    Part 1: Acquisition Cohort Revenue

    The classic cohort revenue chart shows a matrix: acquisition month on rows, months since acquisition (0, 1, 2, 3...) on columns, and total revenue in each cell. Month 0 is what customers spent in their acquisition month. Month 1 is what that same cohort spent in the following month. And so on.

    This sounds straightforward but has a subtle trap: the column context in a matrix visual doesn't know about "months since acquisition" unless you explicitly build that logic.

    Building the Cohort Revenue Measure

    Cohort Revenue = 
        VAR CurrentCohortMonth = MAX(DimCustomer[CohortMonth])
        VAR CurrentMonthOffset = MAX(FactOrders[MonthsSinceAcquisition])
        RETURN
            CALCULATE(
                SUM(FactOrders[Revenue]),
                FILTER(
                    ALL(FactOrders),
                    FactOrders[MonthsSinceAcquisition] = CurrentMonthOffset
                ),
                FILTER(
                    ALL(DimCustomer),
                    DimCustomer[CohortMonth] = CurrentCohortMonth
                )
            )
    

    Place DimCustomer[CohortMonth] on the row axis and FactOrders[MonthsSinceAcquisition] on the column axis. This measure will now populate each cell with the revenue generated by that cohort at that maturity stage.

    Warning

    Notice the ALL(FactOrders) and ALL(DimCustomer) inside the FILTER calls. Without these, the existing filter context from the visual would restrict your FILTER to a subset of data, causing cells to either show wrong values or go blank. You're explicitly overriding the filter context on those fields while letting other slicers (like Region or Channel) continue to filter naturally.

    Cumulative Cohort Revenue

    Raw revenue by period is useful, but cumulative revenue tells the real story of customer value building over time:

    Cumulative Cohort Revenue = 
        VAR CurrentCohortMonth = MAX(DimCustomer[CohortMonth])
        VAR CurrentMonthOffset = MAX(FactOrders[MonthsSinceAcquisition])
        RETURN
            CALCULATE(
                SUM(FactOrders[Revenue]),
                FILTER(
                    ALL(FactOrders),
                    FactOrders[MonthsSinceAcquisition] <= CurrentMonthOffset
                ),
                FILTER(
                    ALL(DimCustomer),
                    DimCustomer[CohortMonth] = CurrentCohortMonth
                )
            )
    

    The only change is <= instead of =. Now each cell shows the total revenue that cohort has generated from acquisition up to and including that month. This is what you'd divide by cohort size to get cumulative revenue per customer — a true CLV progress metric.

    Revenue Per Customer in Cohort

    Revenue Per Cohort Customer = 
        VAR CurrentCohortMonth = MAX(DimCustomer[CohortMonth])
        VAR CurrentMonthOffset = MAX(FactOrders[MonthsSinceAcquisition])
        VAR CohortSize = 
            CALCULATE(
                DISTINCTCOUNT(DimCustomer[CustomerID]),
                FILTER(ALL(DimCustomer), DimCustomer[CohortMonth] = CurrentCohortMonth)
            )
        VAR CohortRevenue = 
            CALCULATE(
                SUM(FactOrders[Revenue]),
                FILTER(
                    ALL(FactOrders),
                    FactOrders[MonthsSinceAcquisition] <= CurrentMonthOffset
                ),
                FILTER(
                    ALL(DimCustomer),
                    DimCustomer[CohortMonth] = CurrentCohortMonth
                )
            )
        RETURN
            DIVIDE(CohortRevenue, CohortSize)
    

    Key insight

    Cohort size is calculated once using ALL(DimCustomer) to get the total number of customers in that acquisition cohort regardless of whether they've made purchases in later months. If you filter by FactOrders, churned customers disappear from your denominator, inflating your per-customer revenue. That's survivorship bias, and it will make your CLV look better than reality.


    Part 2: Repeat Purchase Rate

    Repeat purchase rate answers the question: "Of all the customers who made their first purchase in a given period, what percentage came back and bought again?" This is one of the most important early signals of product-market fit and customer satisfaction.

    Defining "Repeat" Correctly

    A customer is a repeater if they have at least two distinct orders. This seems obvious, but you need to define the time window carefully. Are we measuring repeat within 30 days? Within 90 days? Ever? For this lesson, we'll build a measure that works with a configurable window, using a What-If parameter.

    First, create a What-If parameter in Power BI: go to Modeling → New Parameter, name it "Repeat Window Days", minimum 30, maximum 365, increment 30, default 90. This generates a table called Repeat Window Days and a measure Repeat Window Days Value.

    The Repeat Purchase Rate Measure

    Repeat Purchase Rate = 
        VAR WindowDays = [Repeat Window Days Value]
        VAR CustomersInContext = 
            CALCULATETABLE(
                DISTINCT(DimCustomer[CustomerID]),
                ALLEXCEPT(DimCustomer, DimCustomer[CohortMonth], DimCustomer[AcquisitionChannel], DimCustomer[Region])
            )
        VAR RepeatCustomers = 
            COUNTROWS(
                FILTER(
                    CustomersInContext,
                    VAR ThisCustomer = [CustomerID]
                    VAR FirstOrderDate = 
                        CALCULATE(
                            MIN(FactOrders[OrderDate]),
                            FILTER(ALL(FactOrders), FactOrders[CustomerID] = ThisCustomer)
                        )
                    VAR SecondOrder = 
                        CALCULATE(
                            COUNTROWS(FactOrders),
                            FILTER(
                                ALL(FactOrders),
                                FactOrders[CustomerID] = ThisCustomer
                                && FactOrders[OrderDate] > FirstOrderDate
                                && FactOrders[OrderDate] <= FirstOrderDate + WindowDays
                            )
                        )
                    RETURN SecondOrder > 0
                )
            )
        VAR TotalCustomers = COUNTROWS(CustomersInContext)
        RETURN
            DIVIDE(RepeatCustomers, TotalCustomers)
    

    This measure iterates over every customer in the current filter context (respecting cohort, channel, and region slicers), finds their first order date, then checks whether they placed any additional order within the window.

    Warning

    This measure uses nested FILTER and CALCULATE inside a row iteration — it's doing real work per customer. On datasets with hundreds of thousands of customers, this will be slow. We'll address performance optimization in the troubleshooting section. For now, understand the logic before optimizing it.

    A More Performant Approach Using Pre-Aggregation

    For large datasets, pre-calculate a IsRepeatCustomer flag as a calculated column in DimCustomer:

    -- Calculated column in DimCustomer
    IsRepeatCustomer_90d = 
        VAR ThisCustomer = DimCustomer[CustomerID]
        VAR FirstOrder = 
            CALCULATE(
                MIN(FactOrders[OrderDate]),
                FILTER(ALL(FactOrders), FactOrders[CustomerID] = ThisCustomer)
            )
        VAR HasRepeat = 
            CALCULATE(
                COUNTROWS(FactOrders),
                FILTER(
                    ALL(FactOrders),
                    FactOrders[CustomerID] = ThisCustomer
                    && FactOrders[OrderDate] > FirstOrder
                    && FactOrders[OrderDate] <= FirstOrder + 90
                )
            ) > 0
        RETURN IF(HasRepeat, 1, 0)
    

    Then your measure becomes:

    Repeat Purchase Rate (Fast) = 
        DIVIDE(
            SUM(DimCustomer[IsRepeatCustomer_90d]),
            COUNTROWS(DimCustomer)
        )
    

    This pushes the row-by-row work to data refresh time (a one-time cost) rather than query time (paid on every visual render). The tradeoff is losing the dynamic window — you'd need separate columns for different windows (30d, 60d, 90d). Whether that's acceptable depends on your refresh cadence and user needs. For most production dashboards, hardcoded windows are fine. See the performance discussion in Performance Tuning DAX: Optimize Slow Measures with DAX Studio for guidance on measuring which approach actually matters for your dataset size.


    Part 3: Churn Probability

    Churn probability is trickier than cohort revenue or repeat rate because "churn" in a non-subscription business isn't a discrete event — customers don't send you a cancellation email. They just... stop buying. The standard approach is recency-based: a customer who hasn't purchased in a long time is increasingly likely to have churned.

    We'll build a tiered churn risk score based on recency segments, using window functions to rank customers within their cohort.

    Days Since Last Purchase

    First, a measure for context:

    Days Since Last Purchase = 
        VAR LastPurchase = 
            CALCULATE(
                MAX(FactOrders[OrderDate]),
                ALL(DimDate)
            )
        VAR Today = TODAY()
        RETURN DATEDIFF(LastPurchase, Today, DAY)
    

    Note

    We use ALL(DimDate) to clear any date filter that might be active on the visual — we want the actual most recent purchase regardless of what date range is selected on the report page.

    Churn Risk Tier — Calculated Column

    Add this to DimCustomer:

    -- Calculated column in DimCustomer
    ChurnRiskTier = 
        VAR ThisCustomer = DimCustomer[CustomerID]
        VAR LastPurchaseDate = 
            CALCULATE(
                MAX(FactOrders[OrderDate]),
                FILTER(ALL(FactOrders), FactOrders[CustomerID] = ThisCustomer)
            )
        VAR DaysSinceLastPurchase = 
            IF(
                ISBLANK(LastPurchaseDate),
                999,  -- Never purchased — treat as highest risk
                DATEDIFF(LastPurchaseDate, TODAY(), DAY)
            )
        RETURN
            SWITCH(
                TRUE(),
                DaysSinceLastPurchase <= 30, "Active",
                DaysSinceLastPurchase <= 90, "At Risk",
                DaysSinceLastPurchase <= 180, "High Risk",
                DaysSinceLastPurchase <= 365, "Very High Risk",
                "Likely Churned"
            )
    

    This gives you a simple categorical risk tier. But for more analytical use, you want a numeric score.

    Churn Probability Score Using WINDOW

    Here's where window functions earn their keep. We'll use WINDOW to calculate a percentile-based churn score — customers with the longest recency get a score near 1.0 (almost certainly churned), while recent customers score near 0.

    Churn Probability Score = 
        VAR AllCustomers = 
            ADDCOLUMNS(
                SUMMARIZE(
                    ALL(DimCustomer),
                    DimCustomer[CustomerID],
                    DimCustomer[CohortMonth]
                ),
                "LastPurchase",
                CALCULATE(
                    MAX(FactOrders[OrderDate]),
                    FILTER(ALL(FactOrders), FactOrders[CustomerID] = EARLIER(DimCustomer[CustomerID]))
                ),
                "DaysSincePurchase",
                IF(
                    ISBLANK(CALCULATE(MAX(FactOrders[OrderDate]), FILTER(ALL(FactOrders), FactOrders[CustomerID] = EARLIER(DimCustomer[CustomerID])))),
                    999,
                    DATEDIFF(
                        CALCULATE(MAX(FactOrders[OrderDate]), FILTER(ALL(FactOrders), FactOrders[CustomerID] = EARLIER(DimCustomer[CustomerID]))),
                        TODAY(),
                        DAY
                    )
                )
            )
        VAR CurrentCustomer = MAX(DimCustomer[CustomerID])
        VAR CurrentDays = 
            MAXX(
                FILTER(AllCustomers, [CustomerID] = CurrentCustomer),
                [DaysSincePurchase]
            )
        VAR MaxDays = MAXX(AllCustomers, [DaysSincePurchase])
        VAR MinDays = MINX(AllCustomers, [DaysSincePurchase])
        RETURN
            DIVIDE(CurrentDays - MinDays, MaxDays - MinDays)
    

    Key insight

    This is a min-max normalization of recency into a 0–1 churn probability score. It's not a machine learning model, but it's directionally correct and immediately actionable. The customer with the highest recency (most days since last purchase) scores 1.0. The most recent customer scores 0.0. This is a measure designed for use in a table or scatter chart visual where CustomerID is in the row context.

    For a cleaner implementation using native window functions (available in Power BI as of 2023), here's a leaner approach using WINDOW directly in a measure context:

    Customer Recency Rank = 
        VAR CurrentCustomer = SELECTEDVALUE(DimCustomer[CustomerID])
        VAR CustomerDaysSince = 
            CALCULATE(
                DATEDIFF(MAX(FactOrders[OrderDate]), TODAY(), DAY),
                FILTER(ALL(FactOrders), FactOrders[CustomerID] = CurrentCustomer)
            )
        VAR TotalCustomers = 
            CALCULATE(
                DISTINCTCOUNT(DimCustomer[CustomerID]),
                ALL(DimCustomer)
            )
        VAR CustomersWithMoreRecency = 
            CALCULATE(
                DISTINCTCOUNT(DimCustomer[CustomerID]),
                FILTER(
                    ALL(DimCustomer),
                    VAR OtherCustomer = DimCustomer[CustomerID]
                    VAR OtherDays = CALCULATE(
                        DATEDIFF(MAX(FactOrders[OrderDate]), TODAY(), DAY),
                        FILTER(ALL(FactOrders), FactOrders[CustomerID] = OtherCustomer)
                    )
                    RETURN OtherDays > CustomerDaysSince
                )
            )
        RETURN
            DIVIDE(CustomersWithMoreRecency, TotalCustomers)
    

    This ranks each customer by how many other customers have been inactive longer. A score of 0.95 means 95% of customers are less active — this person is in the top 5% churn risk.


    Part 4: Combining the Metrics into a CLV Score

    Individual metrics are useful, but executives want a single number. We can build a composite CLV score that weighs cohort position, revenue contribution, repeat behavior, and churn risk together.

    CLV Score = 
        VAR TotalRevenue = 
            CALCULATE(
                SUM(FactOrders[Revenue]),
                ALL(DimDate)
            )
        VAR MaxCustomerRevenue = 
            CALCULATE(
                MAXX(
                    VALUES(FactOrders[CustomerID]),
                    CALCULATE(SUM(FactOrders[Revenue]))
                ),
                ALL(DimCustomer)
            )
        VAR RevenueScore = DIVIDE(TotalRevenue, MaxCustomerRevenue)
        VAR RepeatFlag = MAX(DimCustomer[IsRepeatCustomer_90d])  -- 1 or 0
        VAR ChurnRisk = [Customer Recency Rank]  -- 0 to 1, higher = more churn risk
        -- Weighted composite: 50% revenue, 30% repeat behavior, 20% churn (inverted)
        RETURN
            ROUND(
                (RevenueScore * 0.5) + (RepeatFlag * 0.3) + ((1 - ChurnRisk) * 0.2),
                3
            )
    

    Tip

    The weights (50/30/20) are business decisions, not mathematical truths. Consider exposing them as What-If parameters so business stakeholders can explore different weighting strategies interactively. This also makes the model self-documenting — when someone asks "why does this customer score higher?" you can point to the sliders. The Writeback Patterns in DAX article covers this pattern in depth.


    Part 5: Dynamic Segmentation Using CLV Score

    Once you have a CLV score, you want to segment customers into actionable tiers — Champions, Loyal, At Risk, and Dormant — and make those segments filterable.

    CLV Segment = 
        VAR Score = [CLV Score]
        RETURN
            SWITCH(
                TRUE(),
                Score >= 0.75, "Champion",
                Score >= 0.50, "Loyal",
                Score >= 0.25, "At Risk",
                "Dormant"
            )
    

    For use as a slicer, this needs to be a calculated column rather than a measure. Create it in DimCustomer by substituting [CLV Score] with the full inline formula (or reference the calculated column intermediate steps). The pattern of building dynamic segmentation logic using SWITCH and score thresholds applies broadly.


    Hands-On Exercise

    Build a complete CLV mini-dashboard using the patterns in this lesson. Here's your brief:

    Dataset: Use a dataset with at least 500 customers, 2 years of order history, and an identifiable acquisition date per customer. The AdventureWorks sample data works perfectly.

    Task 1 — Cohort Grid: Create a matrix visual with DimCustomer[CohortMonth] on rows and FactOrders[MonthsSinceAcquisition] (0 through 11) on columns. Apply the Revenue Per Cohort Customer measure. Apply conditional formatting (background color scale) to immediately visualize where cohorts are strong or weak.

    Task 2 — Repeat Rate Trend: Create a line chart with DimCustomer[CohortMonth] on the X-axis and Repeat Purchase Rate (Fast) on Y-axis. Add a slicer for AcquisitionChannel. Do certain channels produce measurably better repeat rates? How does that change your interpretation of acquisition cost?

    Task 3 — Churn Risk Table: Build a table visual with DimCustomer[CustomerName], DimCustomer[ChurnRiskTier], Days Since Last Purchase, Total Revenue (all time), and Customer Recency Rank. Sort by Customer Recency Rank descending. These are your customers most likely to churn. Add a filter to show only "High Risk" and "Very High Risk" tiers.

    Task 4 — CLV Scatter: Create a scatter chart: X-axis = Revenue Per Cohort Customer, Y-axis = Repeat Purchase Rate (Fast), legend = CLV Segment. Which quadrant do you want most customers in? Which quadrant is your business actually in?

    Reflection question: If your most recent cohort shows lower Month 0 revenue but higher repeat rate than older cohorts, what does that signal about your acquisition strategy or product changes?


    Common Mistakes & Troubleshooting

    Mistake 1: Cohort Revenue That Doesn't Add Up

    Symptom: The cohort matrix totals don't match simple SUM(Revenue) measures elsewhere on the page.

    Cause: You're stripping filter context with ALL() inside the FILTER calls, which also removes any active page-level slicers you actually want to respect.

    Fix: Be surgical with your ALL usage. Use ALLEXCEPT to strip only the fields you're explicitly re-applying, leaving other slicers intact:

    -- Instead of ALL(DimCustomer), use:
    FILTER(
        ALLEXCEPT(DimCustomer, DimCustomer[Region], DimCustomer[AcquisitionChannel]),
        DimCustomer[CohortMonth] = CurrentCohortMonth
    )
    

    Mistake 2: Repeat Rate = 100% for Recent Cohorts

    Symptom: Your newest cohorts show 100% repeat rate while older cohorts show 20–30%.

    Cause: You're calculating repeat within a window that hasn't expired yet for recent cohorts. A customer acquired last week can't have failed to repeat within 90 days — there hasn't been 90 days yet.

    Fix: Add a maturity filter. Only include cohorts where TODAY() - CohortMonth > WindowDays:

    Repeat Purchase Rate (Mature Cohorts) = 
        VAR WindowDays = [Repeat Window Days Value]
        VAR CohortDate = MAX(DimCustomer[CohortMonth])
        VAR CohortIsMature = DATEDIFF(CohortDate, TODAY(), DAY) > WindowDays
        RETURN
            IF(CohortIsMature, [Repeat Purchase Rate (Fast)], BLANK())
    

    Tip

    Returning BLANK() instead of 0 for immature cohorts means Power BI won't plot those points on trend lines or include them in averages. This is almost always the right choice — a missing data point is honest, while a misleading 0 or inflated 100% erodes trust in the report.

    Mistake 3: Churn Score Is the Same for All Customers

    Symptom: Every customer shows a churn probability of 0.5 or some other constant.

    Cause: MAX(DimCustomer[CustomerID]) returns the same value regardless of which row is being evaluated in a table visual if CustomerID is numeric and the visual is showing a total row.

    Fix: Use SELECTEDVALUE instead of MAX when you need to identify a single customer in row context, and always test your measure on a table with a single customer filtered to verify it returns different values per row.

    Mistake 4: Performance Collapse on Large Datasets

    Symptom: The churn probability measure takes 30+ seconds to load in a table with 10,000 customers.

    Cause: Nested FILTER+CALCULATE inside COUNTROWS over a large table is O(n²) work — for each customer, you're scanning all other customers.

    Fix: Push recency calculations to calculated columns at refresh time, as we discussed with IsRepeatCustomer_90d. For churn ranking, pre-calculate LastPurchaseDate and DaysSinceLastPurchase as columns in DimCustomer, then your measure only needs to do the division, not the lookup. For detailed profiling guidance, see Performance Tuning DAX: Optimize Slow Measures with DAX Studio.

    Mistake 5: Acquisition Date vs First Order Date Mismatch

    Symptom: Some customers appear in your cohort grid in a month before they first appear in FactOrders.

    Cause: DimCustomer[AcquisitionDate] might be populated from a CRM or marketing system, while actual first order data lives in your transactional system. Customers sometimes register without purchasing, or data pipelines have timing lags.

    Fix: Decide explicitly which date defines your cohort. For revenue analysis, the first order date is almost always more reliable than a CRM signup date. Recalculate CohortMonth using MIN(FactOrders[OrderDate]) per customer:

    -- Alternative CohortMonth based on first order
    CohortMonth_Orders = 
        VAR FirstOrder = 
            CALCULATE(
                MIN(FactOrders[OrderDate]),
                FILTER(ALL(FactOrders), FactOrders[CustomerID] = DimCustomer[CustomerID])
            )
        RETURN DATE(YEAR(FirstOrder), MONTH(FirstOrder), 1)
    

    Warning

    If you use this in a calculated column in DimCustomer, be careful about circular dependency — DimCustomer referring to FactOrders which refers back to DimCustomer via a relationship. Test thoroughly and ensure the relationship direction supports this lookup.


    Summary & Next Steps

    You've now built a production-quality CLV analysis framework in DAX. Let's recap the key patterns and when to reach for each one:

    Cohort Revenue uses ALL + FILTER to override default filter context and re-apply it precisely on two dimensions simultaneously — acquisition month and time offset. The key insight is that you must hold cohort identity constant while varying the time axis.

    Repeat Purchase Rate uses iterator functions to evaluate a condition (did this customer buy again?) per customer, then aggregates the boolean result. For large datasets, pre-calculate as a calculated column and measure only does simple aggregation.

    Churn Probability uses recency-based normalization to convert raw days-since-purchase into a comparable 0–1 score. Window functions help rank customers relative to each other without leaving DAX.

    CLV Score combines these into a weighted composite, surfaced as a slicer-ready segment.

    The natural next step is to extend this model with predictive elements — specifically, projecting future revenue based on observed cohort decay curves. The DAX Predictive Patterns: Moving Averages, Exponential Smoothing, and Forecast Measures Without ML Tools article shows how to build forward-looking measures that extrapolate cohort trends. You might also want to explore DAX Cohort Analysis: Building Retention, Churn, and Lifetime Value Measures with GENERATE and Date-Based Segmentation for alternative approaches to the same problem space using GENERATE-based virtual tables.

    CLV analysis is one of those domains where DAX genuinely shines — the ability to slice a live cohort grid by acquisition channel or region in real time, with sub-second response, is something that takes significant engineering effort to replicate in other environments. Build this once, build it right, and it becomes one of the highest-value assets in your analytics portfolio.

    Work With Us

    From insight to implementation

    Reading is the start. When you're ready to build the data, automation, or AI systems behind it, our team turns strategy into shipped results.

    Let's Build

    DAX Mastery

    Previous

    DAX Date Table Patterns: Building a Reusable Calendar Table with CALENDAR, CALENDARAUTO, and Calculated Date Columns for Time Intelligence

    Related Insights

    Power BIFoundation

    Implementing Power BI Dataflow Refresh Orchestration with Power Automate to Enforce Dependency-Aware Scheduling Across Enterprise Workspaces

    19 min
    Power BIFoundation

    Saving and Organizing Your Power BI Desktop File: Best Practices for File Structure, Naming, and Version Control for Beginners

    16 min
    Power BIExpert

    Implementing Power BI Information Protection Policies with Microsoft Purview: Automating Sensitivity Label Inheritance, DLP Rules, and Compliance Reporting Across the Enterprise Semantic Layer

    31 min

    On this page

    • Introduction
    • Prerequisites
    • Setting Up Your Data Model
    • The Core Tables
    • The CohortMonth Calculated Column
    • Part 1: Acquisition Cohort Revenue
    • Building the Cohort Revenue Measure
    • Cumulative Cohort Revenue
    • Revenue Per Customer in Cohort
    • Part 2: Repeat Purchase Rate
    • Defining "Repeat" Correctly
    • The Repeat Purchase Rate Measure
    • A More Performant Approach Using Pre-Aggregation
    • Part 3: Churn Probability
    • Days Since Last Purchase
    • Churn Risk Tier — Calculated Column
    • Churn Probability Score Using WINDOW
    • Part 4: Combining the Metrics into a CLV Score
    • Part 5: Dynamic Segmentation Using CLV Score
    • Hands-On Exercise
    • Common Mistakes & Troubleshooting
    • Mistake 1: Cohort Revenue That Doesn't Add Up
    • Mistake 2: Repeat Rate = 100% for Recent Cohorts
    • Mistake 3: Churn Score Is the Same for All Customers
    • Mistake 4: Performance Collapse on Large Datasets
    • Mistake 5: Acquisition Date vs First Order Date Mismatch
    • Summary & Next Steps