Two dashboard pages where every number comes from a measure you write from scratch, with automatic color indication for region status. Working on Telecom_PowerBI_Star.xlsx — a pure star schema: one fact table, seven dimensions, 30,000 rows, and seven relationships. 29 exercises, each on its own page with a full solution. Note: the download and practice are On the computer only — Power BI Desktop doesn't exist on phone.
2,594,253.85USD billed revenue — the anchor number
30,000Rows in the fact table
1,236Active customers
14Measures to write
The data is synthetic. The country names are real, but every customer, package, device, channel and number in the file was artificially generated with a fixed random seed 20260821. Nothing about the real telecom market should be inferred from them.
Before we start — building the model. The file is a pure star schema: a single fact table Fact_Usage and seven dimensions, and every dimension connects directly to it. There's no dimension-to-dimension relationship and no concatenation. Mark Dim_Date As a date table (Mark as date table on Full_Date), turn off Autodetect and build the seven relationships manually, all of them Single From the dimension to the fact. Both here is completely unnecessary.
The trap of this file — rows with intentionally negative spacing. in the table of Fact_Usage there is 2,824 rows (9.41%) where Gross_Margin_USD negative — subsidized devices and roaming packages bought at a wholesale cost higher than the price to the customer. They carry 241,146.59 USD revenue and delete 50,484.13 USD margin. A total-margin measure silently offsets them and shows 889,705.65 instead 940,189.78. The loss doesn't disappear — it just hides in the schema. Exercise 3.3 breaks it down.
A column that looks additive but isn't.Fact_Usage[Revenue_Per_GB] It's a ratio, not a sum. Dragging it with Sum returns 1,374,449.63 — a number with no meaning at all. Exercise 3.2 explains exactly why, and what to do instead.
Matching brackets.Table[Column] = column · [measure] = square brackets without a table name. For each measure: right-click on Fact_Usage in the panel Data ← New measure ← the formula ← Enter. The name must be exactly as it appears in the exercise.
Assignment structure
Part 1
14 DAX measures
Two SUMs, two DISTINCTCOUNTs, DIVIDE, two CALCULATEs, ALL, IF, two SWITCHes — and three window functions: RANK, INDEX and WINDOW.
Part 2
Eleven dashboard pages
A KPI tray, donut vs. bar chart, small multiples, scatter, gauge vs. target, drill-through, tooltip, bookmark, waterfall, treemap, and navigation.
Part 3
Color, trap, and insight
Conditional formatting with two techniques, two thinking exercises about misleading numbers, and the closing insight about channel profitability.
The seven relationships — exactly how they should be built
The seven relationships in the model
dimension
key
Cardinality
Filter direction
Dim_Date
Date_ID
1 : Many
Single
Dim_Customer
Customer_ID
1 : Many
Single
Dim_Plan
Plan_ID
1 : Many
Single
Dim_Region
Region_ID
1 : Many
Single
Dim_Device
Device_ID
1 : Many
Single
Dim_Channel
Channel_ID
1 : Many
Single
Dim_Service_Type
Service_Type_ID
1 : Many
Single
The regions — the verification board for Chapter I
Revenue, tier, and customers per region
area
Revenue USD
layer
customers
rows
North Metro
367,098.76
Top
1,087
4,277
South Metro
335,335.68
Top
1,070
3,772
Thames Corridor
282,052.63
Top
1,030
3,311
West Valley
268,760.12
Top
993
2,991
Rhine Belt
253,778.56
Top
994
2,989
East Coastal
250,554.98
Top
980
2,942
Pacific Rim
216,630.96
Mid
935
2,542
Northern Lakes
188,973.52
Mid
861
2,061
Iberian Coast
155,388.98
Mid
825
1,794
Central Plains
125,991.31
Low
783
1,560
Alpine South
81,044.73
Low
578
893
Highland West
68,643.62
Low
559
868
All the numbers were verified in pandas against the file
1.1 · Total Billed Revenue — the anchor measure
Create a measure named Total Billed Revenue that summarizes the Fact_Usage[Billed_Amount_USD]. Format with a thousands separator and two decimal digits.
Total Billed Revenue =
SUM ( Fact_Usage[Billed_Amount_USD] )
creationRight-click on Fact_Usage ← New measure
designFormat ← Currency ← two digits
checkDrag onto an empty card to make sure the number appears
Why this is a measure and not a column: A calculated column would be stored once per row out of 30,000 and wouldn't know how to respond to a slicer. A measure is recalculated at every cell of every visual, according to the filter bubble that cell represents. That's the only difference that really matters.
result: 2,594,253.85 USD — the anchor number of the whole assignment.
1.2 · Total Gross Margin — the second SUM
Create a measure named Total Gross Margin that summarizes the Fact_Usage[Gross_Margin_USD]. Note that this is a column otherwise than that of exercise 1.1.
Total Gross Margin =
SUM ( Fact_Usage[Gross_Margin_USD] )
The column is derived, not invented:Gross_Margin_USD = Billed_Amount_USD - Service_Cost_USD in each of the 30,000 rows. Therefore 2,594,253.85 less 1,704,548.20 Exactly equal 889,705.65. If you get something different — the load broke something, not the model.
Early warning: This total margin already offsets within it 2,824 loss rows. Exercise 3.3 breaks them down.
result: 889,705.65 USD — 34.3% from the income.
1.3 · Active Customers — how many customers, really
Create a measure named Active Customers that counts customers unique based on Fact_Usage[Customer_ID].
Active Customers =
DISTINCTCOUNT ( Fact_Usage[Customer_ID] )
Why not COUNT:COUNT(Customer_ID) would return 30,000 — it counts rows that have a value, not distinct values. Every customer carries 24.3 usage rows in the average, so 30,000 ÷ 1,236 ≈ 24.3.
result: 1,236 Active customers.
2.4 · Devices In Use — the second DISTINCTCOUNT
Create a measure named Devices In Use that counts unique device models based on Fact_Usage[Device_ID] — a different column than exercise 1.3's. The measure serves the Treemap in exercise 2.10.
Devices In Use =
DISTINCTCOUNT ( Fact_Usage[Device_ID] )
Note the level of detail: The measure counts models, not physical units. Dim_Device contains 48 Models and everyone appear in the fact table, so at the grand-total level the measure returns exactly the number of rows in the dimension. As soon as you filter a region or period the number drops — and that's exactly what makes it a useful measure.
result: 48 Device models.
1.5 · Revenue per Customer — the Lego principle
Create a measure named Revenue per Customer — strictly from the two existing measures, without the division sign / and without AVERAGE. Format to two decimal places.
The average of what:AVERAGE(Billed_Amount_USD) would return the average for a usage row (86.48 USD), not per customer. When the measure is built from two existing measures the denominator is defined explicitly and you know exactly what you divided. DIVIDE Preferable to / because it returns blank instead of an error when the denominator is zero.
The measure automatically rolls up to any context: Drag it onto the regions table and it will return the ARPU of each region separately — without writing another line.
result: 2,098.91 USD per customer.
1.6 · Postpaid Revenue — first CALCULATE
Create a measure named Postpaid Revenue that returns revenue from postpaid plans only. Use CALCULATE with [Total Billed Revenue] As an expression, and a condition on Dim_Plan[Plan_Type].
What happens here: CALCULATE adds the condition to the filter context and only then runs the [Total Billed Revenue]. The filter sits on a text column in the dimension table, flows through the relationship Dim_Plan[Plan_ID] → Fact_Usage[Plan_ID] and from there to the fact rows — without any JOIN.
Revenue by package type
Package type
Revenue USD
percent
Postpaid
1,742,988.22
67.19%
Business
681,426.86
26.27%
Prepaid
169,838.77
6.55%
result: 1,742,988.22 USD — 67.19% of total revenue.
1.7 · Core Services Revenue — CALCULATE with IN
Create a measure named Core Services Revenue that returns the revenue from just three core services: Mobile Data, Fiber Broadband andIPTV. Use the operator IN { } and not in three OR conditions.
Why IN and not OR: Three conditions || work on the same column, but they expand into three separate filters that the engine has to unify. IN { } produces a single list of values — shorter, faster, and easy to add a fourth value to without touching the logic.
The trap with curly braces:{ } In DAX it’s a table constructor, not a list. If you write a value there that doesn’t exist in the column — you won’t get an error, you’ll simply get less revenue. Check the spelling against Dim_Service_Type before you blame the measure.
The three core services
service
Revenue USD
Mobile Data
768,275.52
Fiber Broadband
437,010.40
IPTV
260,392.29
Core Services Revenue
1,465,678.21
result: 1,465,678.21 USD — 56.5% of total revenue.
1.8 · Pct of All Usage — ALL and percent of all facts
Create a measure named Pct of All Usage that returns the current row's share out of all the revenue in the fact table. Display it in a table by Dim_Service_Type[Service_Name] and check that the column sums to 100%.
Pct of All Usage =
DIVIDE (
[Total Billed Revenue],
CALCULATE (
[Total Billed Revenue],
ALL ( Fact_Usage ),
ALL ( Dim_Service_Type )
)
)
Why two ALLs and not one — that's the point of the exercise:ALL ( Fact_Usage ) Clears filters sitting on fact table columns. It no Clears a filter that came fromDim_Service_Type and flows in through the relationship. If you leave only the first-ALL — the numerator and denominator will be identical in every row and the measure will return 100.00% in every row — and this is the most common mistake with this measure.
Immediate sanity check: If the sum of the column in the table isn't exactly 100.00% — the denominator wasn't really released. That's the only check you need.
Percent of revenue per service
service
Revenue USD
Percent of the total
Mobile Data
768,275.52
29.61%
Roaming Package
513,156.72
19.78%
Fiber Broadband
437,010.40
16.85%
IPTV
260,392.29
10.04%
Device Instalment
228,696.21
8.82%
Cloud Storage
149,152.69
5.75%
Voice Domestic
125,399.74
4.83%
SMS Messaging
58,374.38
2.25%
Voice International
53,795.90
2.07%
Total
2,594,253.85
100.00%
Result: the leader Mobile Data with 29.61%; the column sums to100.00%.
1.9 · Margin Status — IF against a computed break-even point
Create a text measure named Margin Status that compares revenue to the break-even point — that is, to the cost of service in the same context — and returns Above Break-Even or Below Break-Even. The break-even point must be calculated inside the measure, not a hardcoded number.
Margin Status =
VAR BreakEven =
SUM ( Fact_Usage[Service_Cost_USD] )
VAR Revenue = [Total Billed Revenue]
RETURN
IF (
Revenue >= BreakEven,
"Above Break-Even",
"Below Break-Even"
)
Why VAR and not writing the sum twice: VAR is calculated once per cell and then reused. It's both faster and prevents the classic mistake where someone changes the formula in one place and forgets the other.
The break-even point shifts with the context: At the grand total level it is 1,704,548.20 USD, and in every service row it's the cost of only that service. This is exactly the difference between a measure and a hardcoded number — a hardcoded number would mark everything as profitable or everything as a loss.
Break-even point for each service
service
Revenue USD
cost USD
Margin Status
Mobile Data
768,275.52
414,499.34
Above Break-Even
Roaming Package
513,156.72
411,929.20
Above Break-Even
Fiber Broadband
437,010.40
233,766.61
Above Break-Even
IPTV
260,392.29
186,268.21
Above Break-Even
Device Instalment
228,696.21
264,819.00
Below Break-Even
Cloud Storage
149,152.69
68,839.97
Above Break-Even
Voice Domestic
125,399.74
60,184.93
Above Break-Even
SMS Messaging
58,374.38
25,012.54
Above Break-Even
Voice International
53,795.90
39,228.40
Above Break-Even
result: 8 services above the break-even point and 1 below it (Device Instalment). At the channel level: 8 above, 0 below.
1.10 · Region Tier — SWITCH ( TRUE ( ) )
Create a measure named Region Tier that divides regions into three tiers by revenue: Top From 250,000 USD and up, Mid From 150,000 and up, and everything else Low. Use SWITCH ( TRUE ( ) ) and not a nested IF.
Why SWITCH ( TRUE ( ) ): It reads the conditions top to bottom and stops at the first one that's true, exactly like a nested IF — but without the nesting. Adding a fourth layer is one line, not re-parsing parentheses.
The order is the logic: If you flip the two rows and put 150000 First, every region above 250,000 will get Mid — because it also satisfies the looser condition. There's no error, just a silent, wrong result.
Revenue layer per region
area
Revenue USD
layer
North Metro
367,098.76
Top
South Metro
335,335.68
Top
Thames Corridor
282,052.63
Top
West Valley
268,760.12
Top
Rhine Belt
253,778.56
Top
East Coastal
250,554.98
Top
Pacific Rim
216,630.96
Mid
Northern Lakes
188,973.52
Mid
Iberian Coast
155,388.98
Mid
Central Plains
125,991.31
Low
Alpine South
81,044.73
Low
Highland West
68,643.62
Low
result: 6 regions Top · 3Mid · 3Low.
1.11 · Tier Color — SWITCH on a value
Create a measure named Tier Color That receives the result of [Region Tier] and returns a hash code: Top ← #132449, Mid ← #F5C518, everything else ← #A9B4CC. This is a SWITCH on value, not on TRUE ( ).
Two forms of the same function: In 1.10 the first argument was TRUE () and each pair was a condition-and-result. Here the first argument is an expression, and each pair is a value-and-result - a simple equality comparison. The last, unpaired argument is always the default.
This measure is not meant to be seen: It's meant forConditional formatting ← Field value. Drag it into a table to confirm it returns valid strings, then hide the column. Exercise 3.1 connects it for real.
Create a measure named Region Rank by Customers That ranks regions by Number of customers (not by revenue), from high to low, in dense ranking. Use RANK with ORDERBY.
Region Rank by Customers =
RANK (
DENSE,
ALLSELECTED ( Dim_Region ),
ORDERBY ( [Active Customers], DESC )
)
The three parts of RANK:DENSE Ensures there are no gaps in the numbering, ALLSELECTED ( Dim_Region ) defines the comparison group — all the regions the slicer left on screen — andORDERBY Determines what to sort by. Without the second argument, every row will rank itself against itself and get 1.
Why ALLSELECTED and not ALL:ALL It would also ignore the slicer, so a region filtered out would still take up a spot in the ranking. ALLSELECTED Ranks within what the user actually sees — and that's almost always what you want in a dashboard.
Ranking by number of customers
area
customers
rating
North Metro
1,087
1
South Metro
1,070
2
Thames Corridor
1,030
3
Rhine Belt
994
4
West Valley
993
5
East Coastal
980
6
Pacific Rim
935
7
Northern Lakes
861
8
Iberian Coast
825
9
Central Plains
783
10
Alpine South
578
11
Highland West
559
12
result: North Metro instead 1 with 1,087 customers · Highland West instead 12 with 559.
1.13 · Top Margin Channel Rev — INDEX
Create a measure named Top Margin Channel Rev that returns the revenue of the channel with The highest margin — the same number in every row of the table, so it can be compared against. Use INDEX with ORDERBY.
INDEX returns a row, not a number:INDEX ( 1, ... ) It picks the first row in the sort you defined and returns a single-row table. To get a number from it you need to feed it as a filter into CALCULATE - which is why the measure is built in two steps.
The sort is by interval, the output is revenue: And that's exactly the point. The channel leading by a margin is Web Self Service with 203,233.89 USD margin, while the channel that leads in revenue is Authorised Dealer with 518,340.19 USD. Two different leaders — exercise 3.4 comes back to this.
Revenue and margin per channel
channel
Revenue USD
USD margin
Margin lesson
Web Self Service
409,216.29
203,233.89
49.66%
Mobile App
369,873.34
179,382.20
48.5%
Authorised Dealer
518,340.19
136,924.32
26.42%
Own Retail Store
353,217.67
130,295.07
36.89%
Inbound Call Center
240,639.08
95,938.14
39.87%
Retail Chain
289,874.03
65,925.50
22.74%
Online Marketplace
256,111.85
54,088.01
21.12%
Field Sales Agency
156,981.40
23,918.52
15.24%
result: 409,216.29 USD in each row — the revenue of Web Self Service.
1.14 · Trailing Avg Excl Current — WINDOW
Create a measure named Trailing Avg Excl Current that returns the average revenue of The three months preceding for the current month — without including the current month itself. Use WINDOW with ORDERBY on Dim_Date[Month_Key].
Reading the boundaries:-3, REL it is three rows back from the current row, -1, REL it's one row back. The window stops before the current month, so the average doesn't include it. If you write 0, REL At the upper bound — you got a regular four-month moving average, and that's a different exercise.
Why Month_Key and not Month_Label: WINDOW sorts the rows by the column you gave to ORDERBY, and it must be the same column that appears on the visual's axis. Month_Key it's a number (202401) so it also sorts correctly across years. Put it on the axis.
The first month returns empty: There are no rows before it, and the window is empty. This is correct, not a bug — don't wrap it inCOALESCE Just to see zero.
Monthly revenue vs. the running average
month
Revenue USD
Trailing Avg
2024-01
82,029.81
BLANK
2024-02
72,960.34
82,029.81
2024-03
95,505.01
77,495.07
2024-04
97,030.72
83,498.39
2024-05
102,307.88
88,498.69
2024-06
95,712.73
98,281.20
2024-07
151,134.88
98,350.44
2024-08
144,246.34
116,385.16
2024-09
102,500.84
130,364.65
2024-10
101,186.78
132,627.35
2024-11
107,624.55
115,977.99
2024-12
133,989.19
103,770.72
2025-01
88,243.35
114,266.84
2025-02
87,518.17
109,952.36
2025-03
115,544.53
103,250.24
2025-04
92,545.61
97,102.02
2025-05
106,137.18
98,536.10
2025-06
97,856.65
104,742.44
2025-07
135,264.34
98,846.48
2025-08
160,572.15
113,086.06
2025-09
96,813.81
131,231.05
2025-10
101,957.78
130,883.43
2025-11
95,232.77
119,781.25
2025-12
130,338.44
98,001.45
result: 2025-12 = 130,338.44 USD vs. rolling average 98,001.45 USD. 2024-01 Returns BLANK.
2.1 · Background, title, and KPI tray
Build the first dashboard page: dark background, top header, and a tray with four cards showing the [Total Billed Revenue], [Total Gross Margin], [Active Customers] and[Revenue per Customer].
backgroundCanvas background ← color #132449 ← transparency 0%
titleText box full width at the top of the page — Telecom Performance, font 28, white
trayOne rectangle, width 1240 and height 130, below the title, fill #1B2E5A, rounded corners 8
Four cardsCard ← one measure per card ← arrange them within the tray with equal spacing
alignmentSelect all four ← Format ← Align top ← Distribute horizontally
categoryIn every card: Category label On, font 11, color #A9B4CC
Four cards: 2,594,253.85 · 889,705.65 · 1,236 · 2,098.91.
2.2 · Donut vs. columns — the same data, two forms
Build two visuals side by side on the exact same data: [Total Billed Revenue] by Dim_Service_Type[Service_Name] — once as a donut and once as staggered columns. Write one sentence in a text box beneath them explaining which one answers the question "who's second" faster.
labelsDetail labels ← Label style = Category, percent of total
columnsClustered bar chart ← Y axis = Service_Name ← X axis = the same measure
sortingIn the column chart: three points ← Sort axis ← the measure ← Descending
solid colorBoth visuals with the same palette, so the comparison is about shape, not color
the conclusionIn a donut you need to compare angles; in columns the answer is the second row from the top
The top five services
service
Revenue USD
percent
Mobile Data
768,275.52
29.61%
Roaming Package
513,156.72
19.78%
Fiber Broadband
437,010.40
16.85%
IPTV
260,392.29
10.04%
Device Instalment
228,696.21
8.82%
The leader Mobile Data with 29.61%; the second Roaming Package with 19.78%.
2.3 · Small multiples — a different region in each cell
Build a bar chart of [Total Billed Revenue] by Dim_Date[Month_Key], and split it into Small multiples by Dim_Region[Market_Cluster]. Three cells, one for each market cluster.
base chartClustered column chart ← X axis = Month_Key ← Y axis = [Total Billed Revenue]
splitdrag the Market_Cluster to the field Small multiples
Shared axisY axis ← Shared axis on — without it, comparing between cells is meaningless
titlesHeader ← font 12 ← color #F5C518
Three cells: Urban Core, Suburban, Rural — with a shared Y axis.
2.4 · Scatter — revenue vs. margin by channel
Build a scatter plot where each point is a channel: X axis = [Total Billed Revenue], Y axis = [Total Gross Margin], size = [Active Customers]. Add a trend line and mark the outlier.
trend lineAnalytics ← Trend line ← dashed ← #A9B4CC
the exceptionThe point furthest below the line is the channel generating revenue that's too expensive
the exception: Authorised Dealer — 518,340.19 USD revenue but only 26.42% gap.
2.5 · Gauge vs. target — Gauge on the margin
built Gauge that shows the [Total Gross Margin] Against an annual target of 900,000.00 USD. Set minimum 0, maximum 1,200,000.00 and a fixed target.
visualGauge ← Value = [Total Gross Margin]
boundariesFormat ← Gauge axis ← Min = 0 ← Max = 1200000
targetTarget value = 900000 — manual, since there's no targets table in the file
colorfilling #F5C518 ← target line #A9B4CC
labelCallout value on, two digits after the decimal point
the gap 889,705.65 USD vs target 900,000.00 — a gap of -10,294.35 USD (98.9% from the target).
2.6 · Drill-through — from the channel to the service details
Build a third page called Channel Detail which is set as a Drill-through target by Dim_Channel[Channel_Name]. On the destination page: a card with the channel name, a services table, and a monthly chart — all automatically filtered to the channel they came from.
target pageCreate a new page ← Format page ← Page information ← Allow use as drill-through
Diving fielddrag the Channel_Name to Add drill-through fields here
Title cardCard with Channel_Name — so it's clear where you dove into
chartLine chart ← Month_Key against [Total Billed Revenue]
Back buttonPower BI automatically adds a back arrow — style it, don't delete it
checkOn the main page: right-click the channel column ← Drill through ← Channel Detail
Drilling into Authorised Dealer shows 518,340.19 USD and nine services.
2.7 · Tooltip Page — a panel that appears on hover
Build a dedicated Tooltip page at a size 320x240 Shows three numbers for the region you're hovering over: revenue, margin and tier. Connect it to the regions' bar chart.
new pageFormat page ← Page information ← Allow use as tooltip
BreakdownFormat ← Breakdown ← Max breakdowns = 9 — so all nine services appear and aren't grouped into Other
readingThe Total column at the end must be exactly equal to[Total Gross Margin]
Each service's contribution to the margin
service
USD margin
Mobile Data
353,776.18
Roaming Package
101,227.52
Fiber Broadband
203,243.79
IPTV
74,124.08
Device Instalment
-36,122.79
Cloud Storage
80,312.72
Voice Domestic
65,214.81
SMS Messaging
33,361.84
Voice International
14,567.50
One column goes down: Device Instalment with -36,122.79 USD. The Total sums to889,705.65.
2.10 · Treemap — share by device type
built Treemap of [Total Billed Revenue] by Dim_Device[Device_Class], with Dim_Device[Device_Vendor] as internal detail. Add a card with [Devices In Use] next to it.
breakdownDetails = Device_Vendor — creates inner boxes inside each rectangle
labelsData labels on ← Value lit up — without it the Treemap is just a drawing
card next toCard with [Devices In Use] — the context changes with the slicer
warningTreemap is good at showing who's big and bad at showing by how much. If you need to compare numbers — columns
Revenue by device type
Device type
Revenue USD
Smartphone
522,228.62
Mobile Hotspot
404,352.95
Feature Phone
384,515.09
Fiber Router
374,095.97
Tablet
315,831.16
IoT Module
298,428.47
Fixed Modem
294,801.59
The big rectangle: Smartphone with 522,228.62 USD. The card: 48 models.
2.11 · Navigation and polish — the page that turns this into a dashboard
Close out the dashboard: a shared regions slicer, navigation between pages, consistent alignment, and turning off anything unnecessary. This is the page that distinguishes a collection of visuals from a product.
slicerSlicer ← Dim_Region[Region_Name] ← Format ← Dropdown
synchronizationView ← Sync slicers ← mark Sync andVisible On both pages
alignmentSelect all ← Format ← Align — uneven spacing is what the eye picks up on first
cleaningIn every visual: Title short, Background off, Border off, Shadow off
Focus modeturn off Visual header On decorative visuals only
One last checkFile ← Options ← Report settings ← make sure thatPersistent filters on
Two pages + a Drill-through page + a Tooltip page, a synced slicer and working navigation. Every number on the screen comes from one of the 14 measures.
3.1 · Same split, two coloring techniques
Color the regions table by layer — once with Rules inside the visual, and once with the measure [Tier Color] way Field value. Compare the two and write one sentence about when each is preferable.
technique BThe same screen ← Format style = Field value ← [Tier Color]
the checkThe two techniques must give exactly the same picture — 6 dark, 3 yellow, 3 gray
the differenceRules lives inside the visual; the measure lives in the model and serves every visual in the file
When each one:Rules Fast for a single visual and doesn't require a measure. Field value It's the only thing that guarantees every visual in the report tells the same color story - and when someone changes the threshold from 250,000 to 260,000, it's one line in the measure instead of editing every visual separately.
The two techniques: 6 cells #132449 · 3#F5C518 · 3#A9B4CC.
3.2 · Why the naive answer is wrong — and not writing a measure
This exercise does not ask for a measure. A manager asks, "How much do we charge on average per GB?". An analyst drags the Fact_Usage[Revenue_Per_GB] to the card, choose Average and gets 45.8150 USD. The correct answer is 1.2057 USD — times 38.0 less. Write in two-three lines why the naive approach is wrong, and what to do instead.
The answer: the average of ratios is not the ratio of the sums.Revenue_Per_GB calculated for each row separately — Billed_Amount_USD / Data_Volume_GB — and therefore a row that consumed 0.03 GBP and paid 6.00 USD contributes a ratio of 200 Just like a row that consumed 400 etc. AVERAGE gives both of them equal weight, and the small rows — which are the majority — pull the result up.
What to do instead: Calculate the ratio once at the aggregate level, don't average ratios:
True Revenue per GB =
DIVIDE (
[Total Billed Revenue],
SUM ( Fact_Usage[Data_Volume_GB] )
)
and why the column shouldn't be summed at all:SUM(Revenue_Per_GB) returns 1,374,449.63 — a number that has no unit and no meaning. Any column that has already been divided isn't additive, so it needs to be hidden in the model (Hide in report view) so no one drags it by mistake.
Three numbers, only one is correct
calculation
result
valid?
AVERAGE ( Revenue_Per_GB )
45.8150
No — an average of ratios
SUM ( Revenue_Per_GB )
1,374,449.63
No — a non-additive column
DIVIDE ( Revenue, GB )
1.2057
Yes — a ratio at the aggregate level
The correct answer: 1.2057 USD to GBP (2,594,253.85 ÷ 2,151,653.92 etc.).
3.3 · The trap — the loss the schema hides
Write two helper measures that break the total margin into two parts: what we earn and what gets burned. The goal is to show that the reported margin is The result of offsetting, not of uniform profitability.
Margin From Loss Rows =
CALCULATE (
[Total Gross Margin],
Fact_Usage[Gross_Margin_USD] < 0
)
What the numbers say:2,824 Rows out of 30,000 (9.41%) carry a negative margin. They generate 241,146.59 USD revenue — and delete 50,484.13 USD margin. Without them the margin would be 940,189.78 instead 889,705.65.
Why this matters specifically here:9.41% From the rows it looks negligible, but they are concentrated in one service — Device Instalment — which is entirely below the break-even point. Summing across the whole file completely hides this; only breaking it down by service reveals it. That is exactly why exercise 2.9 is built as a Waterfall and not as bars.
Breaking the interval into two parts
part
rows
Revenue USD
USD margin
Profitable rows
27,176
2,353,107.26
940,189.78
Loss rows
2,824
241,146.59
-50,484.13
total
30,000
2,594,253.85
889,705.65
the offset: 940,189.78 minus 50,484.13 equals 889,705.65 USD.
3.4 · Closing insight — the channel that sells the most isn't the most profitable
The closing question: Which channel is really making us money? Build a single table with four columns — channel, revenue, margin, and margin rate — sort by revenue, then sort again by margin. Write three lines: what you saw, what it means, and what you'd recommend.
Margin lessonhelper measure: DIVIDE ( [Total Gross Margin], [Total Billed Revenue] ) In percent format
First sortsorted by revenue descending — and note who's on top
second sortby interval descending — and note who's at the top now
comparisonAdd the [Top Margin Channel Rev] From exercise 1.13 as a reference column
What we see:Authorised Dealer Leads in revenue with 518,340.19 USD — but its margin rate is only 26.42%, and the actual spacing 136,924.32 USD. In comparison, Web Self Service Generates less revenue (409,216.29) but More spacing — 203,233.89 USD at a rate 49.66%.
What this means: The gap isn't in the price the customer pays — it's in the cost of acquisition. Direct digital channels don't pay a commission, and partner channels do. Therefore Field Sales Agency ends with 15.24% only. Ranking channels by revenue is a ranking of turnover, not of contribution.
What's recommended: Don't close the Partners channel — it's still generating 280,856.35 USD positive margin. The recommendation is to move existing customers to digital renewal and keep the partners for new acquisition only. And every channel target metric should be on margin, not on revenue.
Direct channels vs. partners
Channel group
Revenue USD
USD margin
Margin lesson
Direct
1,372,946.38
608,849.30
44.35%
Partner
1,221,307.47
280,856.35
23.0%
All eight channels by interval
channel
Revenue USD
USD margin
Margin lesson
Web Self Service
409,216.29
203,233.89
49.66%
Mobile App
369,873.34
179,382.20
48.5%
Authorised Dealer
518,340.19
136,924.32
26.42%
Own Retail Store
353,217.67
130,295.07
36.89%
Inbound Call Center
240,639.08
95,938.14
39.87%
Retail Chain
289,874.03
65,925.50
22.74%
Online Marketplace
256,111.85
54,088.01
21.12%
Field Sales Agency
156,981.40
23,918.52
15.24%
Leads in revenue Authorised Dealer; the leader by a margin Web Self Service. Margin percentage gap: 49.66% against 15.24%.
Summary — all the results in one place
A consolidated verification table for all 14 measures and every number in the assignment. Open it only after you are done — it gives away all the answers.
All verified results in one place
Measure / component
Verified result
Total Billed Revenue
2,594,253.85 USD
Total Gross Margin
889,705.65 USD (34.3%)
Active Customers
1,236 (COUNT would have returned 30,000)
Devices In Use
48
Revenue per Customer
2,098.91 USD
Postpaid Revenue
1,742,988.22 USD (67.19%)
Core Services Revenue
1,465,678.21 USD (56.5%)
Pct of All Usage
leading 29.61% · Total 100.00%
Margin Status
8 above · 1 Below · break-even point 1,704,548.20
Region Tier
6 Top · 3 Mid · 3 Low
Tier Color
#132449 · #F5C518 · #A9B4CC
Region Rank by Customers
North Metro = 1 · Highland West = 12
Top Margin Channel Rev
409,216.29 in every row (Web Self Service)
Trailing Avg Excl Current
2025-12 = 98,001.45 · 2024-01 = BLANK
True Revenue per GB
1.2057 USD to GBP
The top channel by revenue
Authorised Dealer — 518,340.19 USD
The top channel by margin
Web Self Service — 203,233.89 USD
The leading region
North Metro — 367,098.76 USD
Peak month
2025-08 — 160,572.15 USD
The weak month
2024-02 — 72,960.34 USD
The losing service
Device Instalment — -36,122.79 USD
Rows with negative margin
2,824 (9.41%)
All results were verified in pandas against Telecom_PowerBI_Star.xlsx (30,000 fact rows), and all seven relationships were checked programmatically — 0 orphan keys, 0 blank keys, 0 circular dependencies and 0 duplicate filter paths.
You can also navigate with the buttons at the top of the screen, keyboard arrows, and on phone with a swipe