PowerBI DAX MVC 設計模式 導論 續 – 案例:競爭交叉分析(深度購物籃)
- 2019 年 10 月 6 日
- 筆記

繼 PowerBI DAX MVC 設計模式 導論 引發了很多會員夥伴的詢問,希望羅叔給出一個相對完整和複雜的案例來體會 MVC 架構和設計模式的作用。
本文將結合設計模式與 MVC 架構設計演示一個真實的案例:競爭交叉分析。用戶任選兩個對比實體,來看兩個參與對比實體的某種度量值表現。例如:
- 對於辦公用品大類,其中的紙張和裝訂機同時出現在不同類型客戶的訂單中的概率是怎樣的?
- 對於辦公用品大類,其中的紙張和裝訂機出現在不同地區的銷售是怎樣的?
- …
效果
為了更加清楚的理解這種對比,羅叔先和大家一起看看效果:

如上圖所示,其功能包括:
- 分為兩個對比項切片器,且該切片器按照頂部切片器(類別)進行聯動;
- 交叉訂單數,用於顯示同時滿足左右對比項交叉(同時包括)時的訂單數;
- 交叉銷售額按地域,用於顯示按地域且同時考慮兩個對比項的四種可能模式:
- 僅包括左邊的選擇,不包括右邊的選擇的訂單銷售額;
- 僅包括右邊的選擇,不包括左邊的選擇的訂單銷售額;
- 同時包括左右兩邊的選擇的訂單銷售額;
- 不包括任何一邊的訂單銷售額。
不難看出,本案例是購物籃分析的深度增強版。處於教學目的,羅叔故意增加了分析的靈活性和動態性,問題是如何實現上述的分析?
難點分析
在羅叔給出正確設計方案前,我們先一起來看看其中的難點以及你是否已經想到這些:
- 如何構建兩個對比切片器?雖然數據都是產品子類別,但應該如何構建?
- 構建的兩個切片器是否應該與原有模型建立關係?
- 如果構建的兩個切片器與原有模型沒有關係,那類別切片器如何影響這兩個切片器聯動?
- 如何實現交叉分析的計算?
- 如何實現四種模式下交叉銷售額的計算?
對於初學者,為了讓可視化效果產生聯動,會構建子類別並與數據模型進行關聯,這是很自然的想法,雖然這個思路確實可以實現最終效果,但這個思路是錯誤的。在真正的複雜項目中,這種類似交叉分析的分析主題可能會非常多,多到幾十個頁面甚至需要上百個度量值,如果使用這個思路,必然會使得模型變得非常複雜。
下面羅叔基於 MVC 架構設計給出標準的實現並指出我們應該遵守的設計思想和設計模式。
非侵入式設計
這裡正式提出重要的設計思想:非侵入式設計。羅叔並不記得這個思路來自哪裡,在 PowerBI DAX 領域,該思想由我們首次提出,其內涵為:不應該為了展現而破壞業務數據模型。
由於我們整體採用了 MVC 架構設計,在導論中我們指出數據模型包括:數據模型和視圖模型,由於這裡是以分析和展現為目的的,並沒有引入任何新的業務邏輯,因此,我們在完全不影響數據模型的前提下完成所有設計。
視圖模型
首先給出滿足非侵入式設計的視圖模型:

可以看出,這由三個遊離的表構成,它們均由 DAX 構造,如下:
View.Competitor.LeftItem = VALUES( Model_Product[子類別] ) View.Competitor.RightItem = VALUES( Model_Product[子類別] ) View.Competitor.Legend = VAR X = { ( "L1R0" , "LeftOnly" , 1 ), ( "L0R1" , "RightOnly" , 2 ), ( "L1R1" , "Both" , 3 ), ( "L0R0" , "None" , 4 ) } RETURN SELECTCOLUMNS( X , "ID" , [Value1] , "Name" , [Value2] , "OrderBy" , [Value3] )
這三個表(或者稱列表)與主數據模型(或者稱業務數據模型)沒有任何篩選關係,也就不會影響業務模型的計算或變更。
展現邏輯 – 交叉訂單數計算
在進行圖表展現時,一個最佳實踐是:
- 第一步,將你希望呈現的最終效果用維度和度量值來表示,其中度量值可以是佔位符;
- 第二步,實現這個度量值。
可視化大概的效果為:

現在給出這個度量值的 DAX 表達式:
View.Competior.SharedOrderNumber = // 共同出現的訂單數 VAR vOrdersFromLeft = CALCULATETABLE( VALUES( Model_Order[訂單ID] ) , TREATAS( { SELECTEDVALUE( 'View.Competitor.LeftItem'[子類別] ) } , Model_Product[子類別] ) ) VAR vOrdersFromRight = CALCULATETABLE( VALUES( Model_Order[訂單ID] ) , TREATAS( { SELECTEDVALUE( 'View.Competitor.RightItem'[子類別] ) } , Model_Product[子類別] ) ) RETURN COUNTROWS( INTERSECT( vOrdersFromLeft , vOrdersFromRight ) )
其思路如下:
- vOrdersFromLeft – 將左側切片器所選內容動態掛載到數據模型,以篩選出相應的訂單集合;
- vOrdersFromRight – 將右側切片器所選內容動態掛載到數據模型,以篩選出相應的訂單集合;
- 求上述兩個集合的交集的行數即可;
- 注意,在這個過程數據模型始終保持被細分或行業篩選。
展現邏輯 – 交叉銷售額的計算
類似地,不同類型的交叉銷售額也需要得到展現時的計算,最終效果:

按照展現的最佳實踐:
- 第一步,將你希望呈現的最終效果用維度和度量值來表示,其中度量值可以是佔位符;
- 第二步,實現這個度量值。
這裡涉及一個圖例維度,如下:
View.Competitor.Legend = VAR X = { ( "L1R0" , "LeftOnly" , 1 ), ( "L0R1" , "RightOnly" , 2 ), ( "L1R1" , "Both" , 3 ), ( "L0R0" , "None" , 4 ) } RETURN SELECTCOLUMNS( X , "ID" , [Value1] , "Name" , [Value2] , "OrderBy" , [Value3] )
該圖例給出了四種可能的交叉情況,進而繼續實現度量值,如下:
View.Competior.SalesByLegend = VAR vLeftItem = SELECTEDVALUE( 'View.Competitor.LeftItem'[子類別] ) VAR vRightItem = SELECTEDVALUE( 'View.Competitor.RightItem'[子類別] ) // 計算當前圖例 VAR vLegendItem = SELECTEDVALUE( 'View.Competitor.Legend'[ID] ) // 左右元素對應的訂單集合 VAR vOrdersFromLeft = CALCULATETABLE( VALUES( Model_Order[訂單ID] ) , TREATAS( { vLeftItem } , Model_Product[子類別] ) ) VAR vOrdersFromRight = CALCULATETABLE( VALUES( Model_Order[訂單ID] ) , TREATAS( { vRightItem } , Model_Product[子類別] ) ) // 四種交叉的集合可能 VAR vSetL1R0 = EXCEPT( vOrdersFromLeft , vOrdersFromRight ) VAR vSetL0R1 = EXCEPT( vOrdersFromRight , vOrdersFromLeft ) VAR vSetL1R1 = INTERSECT( vOrdersFromLeft , vOrdersFromRight ) VAR vSetL0R0 = EXCEPT( ALL( Model_Order[訂單ID] ) , UNION( vOrdersFromLeft , vOrdersFromRight ) ) // 對應不同圖例,計算與該圖例一致的交叉銷售額 RETURN SWITCH( TRUE() , vLegendItem = "L1R0" , CALCULATE( [KPI.Sales] , vSetL1R0 ) , vLegendItem = "L0R1" , CALCULATE( [KPI.Sales] , vSetL0R1 ) , vLegendItem = "L1R1" , CALCULATE( [KPI.Sales] , vSetL1R1 ) , vLegendItem = "L0R0" , CALCULATE( [KPI.Sales] , vSetL0R0 ) , BLANK() )
對該 DAX 表達式的註解參見上述表達式注釋。
MVC 架構設計
上述設計按照非侵入式設計思想構建,在構建的過程中,我們始終是在 MVC 框架下進行的,我們整理這個框架,視圖如下:

視圖的展現邏輯:

視圖模型:

我們再回顧一下 MVC 架構的模型如下:

不難看出這裡的設計完全嚴格遵守了 MVC 架構設計,具體說來:
- 視圖,依賴於視圖模型與展現度量值;
- 視圖模型,是從數據模型導出的,在展現度量值計算時,動態掛載到數據模型以產生篩選效應;
- 展現度量值,完全按照展現效果設計,將視圖模型與數據模型實現動態掛載。
可以看出,這樣的 MVC 架構設計與非侵入式設計思想融為一體。要實現非侵入式設計,採用 MVC 架構設計是通用的思路;而採用 MVC 架構設計便實現了非侵入式設計。
數據模型與視圖模型的聯動
至此,我們仍然有一個問題沒有給出答案,那就是:
- 子類別來自於孤立的視圖模型表;
- 類別來自於數據模型;
- 它們之間沒有任何關係是如何實現聯動的?
這要得益於 PowerBI 最近幾個月更新所支援的用度量值控制切片器的元素,這樣就具有了動態性。我們構造了一個度量值如下:
View.Competitor.LeftItem.Check = IF( CALCULATE( SELECTEDVALUE( Model_Product[類別] ) , TREATAS( { SELECTEDVALUE( 'View.Competitor.LeftItem'[子類別] ) } , Model_Product[子類別] ) ) = SELECTEDVALUE( Model_Product[類別] ) , "有效" )
並將其置於切片器的篩選中,如下:

這個有效性由度量值給出,而該度量值是與數據模型動態計算關聯的「橋樑」。
總結
羅叔正式提出 MVC 架構設計以及非侵入式設計其實已經等候多時,它需要幾個 PowerBI 的構件做支撐,具體包括:
- 度量值可以用文件夾組織,用於分類;
- 切片器可以被度量值篩選,以實現視圖模型與數據模型的橋接聯動效應;
- 可視化元素可以被編組以實現視圖級可視化元素與展現度量值的對應關係;
- 模型可以創建新的布局以區分數據模型和視圖模型;
- DAX 可以驅動更多視覺元素的可視化以便形成強大的展現計算能力。
在 2019 年幾個月來 PowerBI 的更新後,我們終於迎來了正式推出這套思想,並給出案例以明顯體會到這種模式的優越性。
本文給出了一個基於 MVC 架構的典型案例,該案例要求複雜的展現分析,而我們的設計不但可以實現目的,還完全不影響數據模型本身,這便是我們需要的。
值得注意的是:我們在設計視圖模型時,對維度的命名為:View.Competitor.RightItem,這個命名根本沒有提及子類別,而子類別是蘊含在其中的,也就是說這個命名是抽象的,我們完全可以繼續擴展這種設計,以實現按產品子類別分析或者其他實體(如:產品)來分析。也就是這是依賴於抽象,而不依賴於具體的,這使得我們的設計有最大化的可復用潛力。這在設計模式中叫做面向介面的設計。我們真正打開了 PowerBI DAX 通用設計模式的大門,我們會在後續的文章中不斷給出通用設計模式,以使得我們的 PowerBI 設計更加完美,無懈可擊。