"Reading about DAX is theory. Writing DAX against real data is the skill that gets you hired."

This is the hands-on companion to Comparative Analytics, Business Algorithms & Advanced Analytics Concepts. Every concept from that article is now implemented step by step — with a real demo dataset, exact DAX to copy, and expected outputs to validate against. Work through this in Power BI Desktop and by the end you will have a complete analytics solution built from scratch.

📦 Step 0 — Download the Demo Dataset

We use a fictional retail company — Northwind Electronics — with 3 years of sales data across 5 product categories, 8 regions, and 1,200 customers. The dataset is designed to contain intentional anomalies, seasonality, and RFM patterns so every analytics technique produces interesting, non-trivial results.

The dataset contains these tables:

Table
Rows
Key Columns
Sales
18,450
OrderID, CustomerID, ProductID, OrderDate, Quantity, UnitPrice, Discount, Revenue
Products
120
ProductID, ProductName, Category, SubCategory, UnitCost, MarketBenchmarkPrice
Customers
1,200
CustomerID, CustomerName, Region, Country, JoinDate, Segment
Dates
1,096
Date, Year, Quarter, Month, MonthName, Weekday, IsWeekend, FiscalYear
Targets
96
Region, Year, Month, RevenueTarget, UnitTarget
📥 Northwind Electronics Analytics Dataset ⬇ Download Dataset (.xlsx · 855 KB) 7 sheets · 18,450 rows · RR Skillverse branded
📥 Setup Instructions Create a new Power BI Desktop file. Open the downloaded .xlsx or use Enter Data to paste each table manually using the column structures above. Mark the Dates table as a Date Table (Table Tools → Mark as date table → Date column). Build relationships: Sales[CustomerID] → Customers[CustomerID], Sales[ProductID] → Products[ProductID], Sales[OrderDate] → Dates[Date]. All relationships one-to-many from the dimension side.

🧱 Step 1 — Foundation Measures (Build These First)

Create a dedicated Measures table (Enter Data → single row, name column "Placeholder", hide the column). All measures go here to keep the model clean.

1.1 Total Revenue

Total Revenue =
SUM(Sales[Revenue])

Validate: Add to a Card visual. Expected: approximately $4.2M–$6.8M depending on your data range.

1.2 Total Units Sold

Total Units =
SUM(Sales[Quantity])

1.3 Total Orders

Total Orders =
DISTINCTCOUNT(Sales[OrderID])

1.4 Average Order Value

Avg Order Value =
DIVIDE([Total Revenue], [Total Orders])

Why DIVIDE not /: DIVIDE returns BLANK instead of an error when Total Orders is zero — critical for filtered views with no data.

1.5 Gross Margin

Gross Margin =
SUMX(
    Sales,
    Sales[Revenue] - (RELATED(Products[UnitCost]) * Sales[Quantity])
)

Why SUMX not SUM: Margin requires a row-by-row calculation (revenue minus cost per row) before summing. SUM cannot do this — it only sums a single column.

1.6 Gross Margin %

Gross Margin % =
DIVIDE([Gross Margin], [Total Revenue])

Format as Percentage with 1 decimal place.

📅 Step 2 — Comparative Analytics: Period vs Period

2.1 Revenue Last Year (SAMEPERIODLASTYEAR)

Revenue LY =
CALCULATE(
    [Total Revenue],
    SAMEPERIODLASTYEAR(Dates[Date])
)

2.2 Year-over-Year Growth Amount

YoY Growth =
[Total Revenue] - [Revenue LY]

2.3 Year-over-Year Growth %

YoY Growth % =
DIVIDE(
    [Total Revenue] - [Revenue LY],
    [Revenue LY]
)

Format as Percentage. Validate: In a matrix with Year on rows and this measure in values — 2023 should show BLANK (no prior year), 2024 shows growth vs 2023, 2025 shows growth vs 2024.

2.4 Revenue Previous Month (DATEADD)

Revenue PM =
CALCULATE(
    [Total Revenue],
    DATEADD(Dates[Date], -1, MONTH)
)

2.5 Month-over-Month Growth %

MoM Growth % =
DIVIDE(
    [Total Revenue] - [Revenue PM],
    [Revenue PM]
)

2.6 Revenue vs Target

Revenue vs Target =
VAR ActualRevenue = [Total Revenue]
VAR TargetRevenue =
    CALCULATE(
        SUM(Targets[RevenueTarget]),
        TREATAS(VALUES(Dates[Year]), Targets[Year]),
        TREATAS(VALUES(Dates[Month]), Targets[Month]),
        TREATAS(VALUES(Customers[Region]), Targets[Region])
    )
RETURN
    DIVIDE(ActualRevenue - TargetRevenue, TargetRevenue)

Key concept: TREATAS maps a column from one table to another without a physical relationship — used here because Targets links on Year+Month+Region composite, not a single key.

✅ Build This Visual Now Create a Matrix: Rows = Region, Columns = Year, Values = Total Revenue, YoY Growth %. Apply conditional formatting on YoY Growth % — green for positive, red for negative. This is a standard executive dashboard pattern.

🏆 Step 3 — Ranking Models with RANKX

3.1 Global Product Rank by Revenue

Product Revenue Rank =
RANKX(
    ALL(Products[ProductName]),
    [Total Revenue],
    ,
    DESC,
    Dense
)

Arguments explained:

  • ALL(Products[ProductName]) — ranks across ALL products, ignoring any visual filter
  • [Total Revenue] — the value to rank on
  • Third argument (blank) — uses current context value, not a fixed value
  • DESC — highest revenue = rank 1
  • Dense — no gaps (tied items get same rank, next rank is consecutive)

3.2 Rank Within Category

Category Rank =
RANKX(
    ALLEXCEPT(Products, Products[Category]),
    [Total Revenue],
    ,
    DESC,
    Dense
)

ALLEXCEPT vs ALL: ALLEXCEPT removes all filters EXCEPT the ones specified — so ranking resets per category but ranks globally within each category.

3.3 Top 10 Flag

Is Top 10 =
IF([Product Revenue Rank] <= 10, "Top 10", "Others")

3.4 Customer Revenue Rank

Customer Revenue Rank =
RANKX(
    ALL(Customers[CustomerName]),
    [Total Revenue],
    ,
    DESC,
    Dense
)
🔨 Build This Visual Table visual: ProductName, Category, Total Revenue, Product Revenue Rank, Category Rank, Is Top 10. Add a visual-level filter: Is Top 10 = "Top 10". Sort by Product Revenue Rank ascending. This is a standard product performance table pattern for retail dashboards.

👥 Step 4 — RFM Customer Segmentation

RFM (Recency, Frequency, Monetary) is calculated as calculated columns on the Customers table — not measures — because each customer has a fixed score that does not change with visual filters.

4.1 Last Purchase Date (Calculated Column on Customers)

Last Purchase Date =
CALCULATE(
    MAX(Sales[OrderDate]),
    RELATEDTABLE(Sales)
)

4.2 Recency Score (Calculated Column)

Recency Score =
VAR DaysSince =
    DATEDIFF(Customers[Last Purchase Date], TODAY(), DAY)
RETURN
    SWITCH(TRUE(),
        DaysSince <= 30,  5,
        DaysSince <= 60,  4,
        DaysSince <= 90,  3,
        DaysSince <= 180, 2,
        1
    )

4.3 Frequency Score (Calculated Column)

Frequency Score =
VAR OrderCount =
    CALCULATE(DISTINCTCOUNT(Sales[OrderID]), RELATEDTABLE(Sales))
RETURN
    SWITCH(TRUE(),
        OrderCount >= 20, 5,
        OrderCount >= 10, 4,
        OrderCount >= 5,  3,
        OrderCount >= 2,  2,
        1
    )

4.4 Monetary Score (Calculated Column)

Monetary Score =
VAR LifetimeRevenue =
    CALCULATE(SUM(Sales[Revenue]), RELATEDTABLE(Sales))
RETURN
    SWITCH(TRUE(),
        LifetimeRevenue >= 50000, 5,
        LifetimeRevenue >= 20000, 4,
        LifetimeRevenue >= 10000, 3,
        LifetimeRevenue >= 2000,  2,
        1
    )

4.5 Combined RFM Score (Calculated Column)

RFM Score =
Customers[Recency Score] + Customers[Frequency Score] + Customers[Monetary Score]

4.6 Customer Segment Label (Calculated Column)

Customer Segment =
SWITCH(TRUE(),
    Customers[RFM Score] >= 13, "Champions",
    Customers[RFM Score] >= 10, "Loyal Customers",
    Customers[RFM Score] >= 7,  "Potential Loyalists",
    Customers[RFM Score] >= 5,  "At Risk",
    "Lost"
)
✅ Build This Visual Donut chart: Legend = Customer Segment, Values = DISTINCTCOUNT(Customers[CustomerID]). Add a second visual — Bar chart: X = Customer Segment, Y = Total Revenue. Champions should show the highest average revenue per customer despite potentially lower count.

💰 Step 5 — Price Comparison Analytics

5.1 Average Selling Price

Avg Selling Price =
DIVIDE(
    SUMX(Sales, Sales[Revenue]),
    SUMX(Sales, Sales[Quantity])
)

This calculates weighted average price — not a simple average of unit prices. SUMX iterates row by row.

5.2 Market Benchmark Price (from Products table)

Avg Market Price =
AVERAGEX(
    VALUES(Products[ProductID]),
    RELATED(Products[MarketBenchmarkPrice])
)

5.3 Price vs Market Variance %

Price vs Market % =
DIVIDE(
    [Avg Selling Price] - [Avg Market Price],
    [Avg Market Price]
)

Interpretation: Positive = we charge above market (premium positioning or overpriced). Negative = we charge below market (competitive or underpriced).

5.4 Price Band (Calculated Column on Products)

Price Band =
SWITCH(TRUE(),
    Products[UnitPrice] < 500,    "Economy (< ₹500)",
    Products[UnitPrice] < 2000,   "Mid-Range (₹500–2K)",
    Products[UnitPrice] < 10000,  "Premium (₹2K–10K)",
    "Enterprise (> ₹10K)"
)
🔨 Build This Visual Clustered bar chart: X = Category, Values = Avg Selling Price and Avg Market Price side by side. Add conditional formatting to Price vs Market % — red if below -10%, green if above +10%, yellow in between. This immediately shows which categories are priced strategically vs competitively.

📈 Step 6 — Trend Analysis and Moving Averages

6.1 Revenue YTD

Revenue YTD =
TOTALYTD(
    [Total Revenue],
    Dates[Date]
)

6.2 Revenue MTD

Revenue MTD =
TOTALMTD(
    [Total Revenue],
    Dates[Date]
)

6.3 3-Month Moving Average

3M Moving Avg =
AVERAGEX(
    DATESINPERIOD(
        Dates[Date],
        LASTDATE(Dates[Date]),
        -3,
        MONTH
    ),
    [Total Revenue]
)

How it works: DATESINPERIOD returns the last 3 months of dates ending at the current date in context. AVERAGEX then iterates those months and averages the revenue — producing a rolling average that smooths out monthly volatility.

6.4 12-Month Moving Average

12M Moving Avg =
AVERAGEX(
    DATESINPERIOD(
        Dates[Date],
        LASTDATE(Dates[Date]),
        -12,
        MONTH
    ),
    [Total Revenue]
)

6.5 Revenue Trend Direction

Trend Direction =
VAR Current3M = [3M Moving Avg]
VAR Prior3M =
    CALCULATE(
        [3M Moving Avg],
        DATEADD(Dates[Date], -3, MONTH)
    )
RETURN
    IF(
        ISBLANK(Prior3M), "—",
        IF(Current3M > Prior3M, "▲ Growing", "▼ Declining")
    )
✅ Build This Visual Line chart: X = Dates[Date] (Month granularity), Y = Total Revenue, 3M Moving Avg, 12M Moving Avg as three lines. The 12M line should be the smoothest — revealing the true long-term trend while the monthly line shows seasonality peaks. Enable the Analytics pane Forecast with 3-month horizon on this chart.

🚨 Step 7 — Anomaly Detection

7.1 Monthly Revenue Average (for baseline)

Monthly Revenue Avg =
AVERAGEX(
    ALL(Dates[Year], Dates[Month]),
    [Total Revenue]
)

7.2 Standard Deviation of Monthly Revenue

Revenue Std Dev =
VAR Avg = [Monthly Revenue Avg]
RETURN
SQRT(
    AVERAGEX(
        ALL(Dates[Year], Dates[Month]),
        POWER([Total Revenue] - Avg, 2)
    )
)

7.3 Anomaly Z-Score

Revenue Z-Score =
DIVIDE(
    [Total Revenue] - [Monthly Revenue Avg],
    [Revenue Std Dev]
)

Interpretation: Z-Score > 2 or < -2 indicates a statistical anomaly (more than 2 standard deviations from the mean).

7.4 Anomaly Flag

Anomaly Flag =
SWITCH(TRUE(),
    [Revenue Z-Score] >  2, "⚠️ Spike",
    [Revenue Z-Score] < -2, "⚠️ Drop",
    "✅ Normal"
)

7.5 Built-in Anomaly Detection (No DAX Required)

On your monthly revenue line chart:

  1. Click the chart → open Analytics pane (magnifying glass icon)
  2. Expand Find anomalies → toggle On
  3. Set Sensitivity to 80
  4. Anomaly dots appear on the line — hover each to see the expected range vs actual
  5. Click an anomaly dot → select Explain anomaly → Power BI runs a decomposition to identify which dimension (region, category, product) contributed most to the deviation
⚠️ Common Mistake Anomaly detection only works on line charts with a continuous date axis. If you use a categorical axis (e.g. Month Name as text), the option will be greyed out. Always use the Dates[Date] field — not a text month name — on the X axis.

🎯 Step 8 — Dynamic Segmentation with What-If Parameter

8.1 Create the What-If Parameter

  1. Modeling tab → New Parameter
  2. Name: Top N Products
  3. Data type: Whole Number
  4. Minimum: 1, Maximum: 50, Increment: 1, Default: 10
  5. Tick Add slicer to this page

Power BI auto-creates a table called Top N Products with a column Top N Products Value and a measure Top N Products.

8.2 Dynamic Top N Measure

Is In Top N =
IF(
    [Product Revenue Rank] <= [Top N Products],
    "In Top N",
    "Others"
)

8.3 Performance Band — Dynamic Threshold

Performance Band =
VAR CurrentRevenue = [Total Revenue]
VAR AllRevenue     = CALCULATE([Total Revenue], ALL(Products))
VAR Pct            = DIVIDE(CurrentRevenue, AllRevenue)
RETURN
    SWITCH(TRUE(),
        Pct >= 0.05, "High Contributor (≥5%)",
        Pct >= 0.01, "Mid Contributor (1–5%)",
        "Low Contributor (<1%)"
    )
✅ Build This Interactive Visual Bar chart filtered to Is In Top N = "In Top N". Connect it to the Top N slicer. Moving the slicer from 10 to 25 immediately shows the top 25 products — no refresh, no filter changes. This is the What-If pattern for dynamic analysis.

📊 Step 9 — Final Dashboard: Put It All Together

Build a 3-page report using all the measures you created:

Page 1 — Executive Overview

  • KPI Cards: Total Revenue, YoY Growth %, Gross Margin %, Total Orders
  • Line chart: Monthly Revenue + 3M Moving Avg + Forecast (3 months) with Anomaly Detection enabled
  • Matrix: Region × Year with Total Revenue and YoY Growth % + conditional formatting
  • Slicer: Year

Page 2 — Product & Pricing Intelligence

  • Table: ProductName, Category, Total Revenue, Product Revenue Rank, Category Rank, Price Band, Price vs Market %
  • Clustered bar: Category vs Avg Selling Price vs Avg Market Price
  • What-If slicer for Top N + filtered bar chart
  • Slicer: Category, Price Band

Page 3 — Customer Analytics

  • Donut: Customer Segment distribution
  • Bar: Revenue by Customer Segment
  • Table: CustomerName, Region, RFM Score, Customer Segment, Total Revenue, Customer Revenue Rank
  • Card: Count of Champions, Count of At Risk customers
  • Slicer: Region, Customer Segment

🧠 Knowledge Quiz — Test Your Understanding

10 questions. No peeking at the DAX above. Write your answer before revealing.

  1. Q1 — Why use DIVIDE() instead of the / operator in DAX?
    Reveal Answer

    DIVIDE(a, b) returns BLANK (or a custom alternate result) when b is zero, avoiding a division-by-zero error. The / operator throws an error. In filtered visuals where no data exists, DIVIDE keeps the report clean — the cell shows blank instead of an error symbol.

  2. Q2 — What is the difference between ALL() and ALLEXCEPT() in RANKX?
    Reveal Answer

    ALL(Products[ProductName]) ranks across every product ignoring all filters — giving a global rank. ALLEXCEPT(Products, Products[Category]) removes all filters EXCEPT Category — so the rank resets within each category, giving a category-level rank.

  3. Q3 — Why is RFM scoring implemented as calculated columns rather than measures?
    Reveal Answer

    Each customer has a fixed score based on their full purchase history — it does not change when a visual filter is applied. Calculated columns evaluate at data refresh time and store the result per row, making them appropriate for stable per-entity attributes like RFM scores. Measures re-evaluate dynamically on every visual interaction.

  4. Q4 — What does SAMEPERIODLASTYEAR() require to work correctly?
    Reveal Answer

    A Date table that is marked as a Date Table (Table Tools → Mark as date table) and a continuous date range with no gaps. Without a marked Date table, Power BI cannot correctly interpret time intelligence functions like SAMEPERIODLASTYEAR, TOTALYTD, or DATEADD.

  5. Q5 — What does a Z-Score of -2.5 mean for a monthly revenue value?
    Reveal Answer

    The month's revenue is 2.5 standard deviations below the historical average — a statistically significant downward anomaly. Values beyond ±2 standard deviations typically represent unusual events worth investigating (promotions, supply issues, external shocks).

  6. Q6 — Why does the 3-Month Moving Average use AVERAGEX with DATESINPERIOD rather than a simple AVERAGE?
    Reveal Answer

    AVERAGE(Sales[Revenue]) would average individual transaction amounts — not monthly totals. AVERAGEX iterates over each month in the 3-month window and evaluates [Total Revenue] per month, then averages those monthly totals. DATESINPERIOD generates the correct rolling 3-month date range relative to the current context date.

  7. Q7 — In the Price vs Market % measure, what does a result of +0.15 (15%) mean and what business decision might it trigger?
    Reveal Answer

    The average selling price is 15% above the market benchmark. This could indicate strong brand premium (positive) or potential overpricing causing volume loss (negative). The business decision depends on whether units sold are growing or shrinking — combine with YoY Units to distinguish premium positioning from demand-killing overpricing.

  8. Q8 — What is the key difference between TOTALYTD() and using CALCULATE() with DATESYTD()?
    Reveal Answer

    They produce the same result. TOTALYTD([Total Revenue], Dates[Date]) is syntactic sugar for CALCULATE([Total Revenue], DATESYTD(Dates[Date])). TOTALYTD is simpler to write; CALCULATE + DATESYTD gives more flexibility — for example, you can add additional filter arguments to CALCULATE that TOTALYTD does not support.

  9. Q9 — A product has a Category Rank of 1 but a Global Product Revenue Rank of 47. What does this tell you?
    Reveal Answer

    The product is the top seller within its own category, but that category generates relatively modest revenue compared to others. The category itself is a smaller contributor globally. This insight guides where to invest — the product is a category leader but the category may need development, or the business may choose to deprioritise it in favour of higher-revenue categories.

  10. Q10 — Why does built-in Power BI Anomaly Detection require a continuous date axis and not work with a categorical month name column?
    Reveal Answer

    The SR-CNN algorithm that powers Power BI anomaly detection requires temporal ordering — it models expected values based on time sequence and seasonality patterns. A categorical text axis (January, February…) does not communicate temporal order or spacing to the algorithm. Only a proper date/time column on the axis enables Power BI to interpret the sequence correctly and calculate expected value ranges.

🏆 Score Yourself 9–10 correct: Ready for PL-300 analytics questions · 6–8: Solid understanding, review the ones you missed · Below 6: Re-read the concepts article and rebuild the measures before attempting the exam.

Quick Knowledge Check

Q1. You write Sales MTD = TOTALMTD([Sales], Dates[Date]). On June 15, this measure shows the sum of sales from June 1 to June 15. What happens to this measure's value on June 30?

Show Answer

It shows the cumulative total from June 1 to June 30 — month-to-date resets at the start of each month. TOTALMTD uses DATESYTD internally but scoped to the current month. On July 1, it resets to just July 1's sales. Use this for running totals within a month (e.g., monthly sales progress dashboards). It requires a properly marked Date table with continuous dates and no gaps.

Q2. A colleague writes Revenue LY = CALCULATE([Revenue], SAMEPERIODLASTYEAR(Dates[Date])). The measure works in a bar chart but shows BLANK in a card visual filtered to a single date. Why?

  • A) SAMEPERIODLASTYEAR only works with month-level granularity, not individual dates
  • B) The card is filtered to a single date; SAMEPERIODLASTYEAR returns the prior-year date, but if that date has no transaction data, the measure returns BLANK
  • C) CALCULATE cannot be used inside SAMEPERIODLASTYEAR
  • D) The Date table must be disconnected for SAMEPERIODLASTYEAR to work
Show Answer

B. SAMEPERIODLASTYEAR shifts the filter context to the same period one year earlier. If June 15 last year had no transactions (weekend, holiday, or simply no data), [Revenue] returns BLANK for that single date. In a bar chart aggregated by month, this is averaged out. The fix is to use a different grain for the comparison or handle BLANK with IF(ISBLANK(...), 0, ...) or COALESCE.

Q3. What is a "disconnected table" in Power BI and how is it used for what-if comparative analytics?

Show Answer

A disconnected table has no relationship to other model tables — it is used as a slicer input to a SELECTEDVALUE() or VALUES() DAX expression. For example, a table with metric names ("Revenue", "Units", "Margin") has no relationship to the Sales table. A measure uses SWITCH(SELECTEDVALUE(MetricTable[Metric]), "Revenue", [Revenue], "Units", [Units], ...) to return the right measure based on slicer selection. This creates a dynamic metric selector — the user chooses which KPI the chart displays, all from one visual. It is a powerful pattern for space-efficient comparative dashboards.

5 Things to Remember
  • TOTALMTD resets at month start — gives cumulative month-to-date. Requires a continuous Date table with no gaps.
  • SAMEPERIODLASTYEAR shifts context by 365 days — BLANK if the prior-year period has no data. Handle with COALESCE or an IF/ISBLANK wrapper.
  • RANKX in a measure ranks dynamically by filter context — rank changes when slicers filter. Use a calculated column for a fixed global rank that doesn't change.
  • Disconnected tables power dynamic metric selectors — no relationship needed. Use SELECTEDVALUE() + SWITCH() to let users pick which KPI to display in a single visual.
  • Always test time intelligence with a proper Date table — Mark the table as Date table in Desktop. Time intelligence functions (TOTALYTD, SAMEPERIODLASTYEAR, etc.) require a complete, gap-free calendar column.