在 Power BI 中引入 VISUAL SHAPE 进行可视化计算

本文翻译自Marco Russo & Alberto Ferrari的文章—《Introducing VISUAL SHAPE for visual calculations in Power BI》来源:SQLBI 本文介绍了“VISUAL SHAPE”子句,该子句为用于视觉计算的表定义了一个层次结构。

视觉计算是Power BI在2024年2月发布时作为预览功能引入的,旨在简化与特定视觉相关联的计算的创建。对简单的计算使用可视化计算是容易的。然而,一旦开发人员创建更复杂的计算,他们就应该了解可视化计算实现的技术细节。这需要理解虚拟表的层次结构、新的可视化上下文、ROWS和COLUMNS的语义、CALCULATE的行为以及新的可视化上下文修饰符EXPAND和COLLAPSE。在关于可视化计算的第一篇文章中,我们将介绍VISUAL SHAPE和可视化计算实现的基础知识,其余的主题将留待以后的文章讨论。矩阵可视化显示以行和列组织的数据。例如,下面的矩阵显示了按品牌(在行上)和年份(在列上)切片的销售额。

尽管包含在行和列中组织良好的小计和数据,但矩阵是由返回平面表的查询生成的。平面表包含用于内部单元格的行以及用于小计的行。实际上,为矩阵检索数据的基本查询(简化)如下:

EVALUATE
SUMMARIZECOLUMNS (
    ROLLUPADDISSUBTOTAL ( 'Product'[Brand], "IsGrandTotalRowTotal" ),
    ROLLUPADDISSUBTOTAL ( 'Date'[Year], "IsGrandTotalColumnTotal" ),
    "Sales_Amount", 'Sales'[Sales Amount]
)

该查询的输出包含整个结果集。在下面的节选中,你可以很容易地识别出2019年Tailspin Toys的价值和同年的列小计:

Power BI 负责读取平面表,检测小计行(使用表示行是否为小计的附加列),并以我们习惯阅读矩阵的方式显示值。

重要的是要认识到,SUMMARIZECOLUMNS的结果是扁平化的:所有行都处于同一级别,包含原始值的行和小计行之间没有任何区别。Power BI知道如何区分小计和常规行。然而,从DAX的角度来看,所有行都是相似的。

我们可以很容易地创建一个可视化计算,以百分比计算前一个值和当前值之间的增长:

Growth =
VAR Curr = [Sales Amount]
VAR Prev = PREVIOUS ( [Sales Amount], COLUMNS )
VAR Result = DIVIDE ( Curr - Prev, Prev )
RETURN
    FORMAT ( Result, "0.00 %" )

计算使用返回前一列值的PREVIOUS函数。因为年份在列上,所以PREVIOUS ([Sales Amount], columns)返回前一年的值。计算产生以下结果。


2017年的增长是空白的,因为没有之前的列,而计算在所有其他年份产生正确的输出。有趣的问题是:如果SUMMARIZECOLUMNS返回的表只是一个平面表,DAX如何知道前一列是什么?答案在添加到SUMMARIZECOLUMNS返回的表中的visualshape子句中。

让我们看一下产生上述矩阵的查询的稍微简化的版本:

DEFINE
    COLUMN '__DS0VisualCalcs'[Growth] =
        (
            VAR Curr = [Sales Amount]
            VAR Prev =
                PREVIOUS ( [Sales Amount], COLUMNS )
            VAR Result =
                DIVIDE ( Curr - Prev, Prev )
            RETURN
                FORMAT ( Result, "0.00 %" )
        )
    VAR __DS0Core =
        SUMMARIZECOLUMNS (
            ROLLUPADDISSUBTOTAL ( 'Product'[Brand], "IsGrandTotalRowTotal" ),
            ROLLUPADDISSUBTOTAL ( 'Date'[Year], "IsGrandTotalColumnTotal" ), 
           "Sales_Amount", 'Sales'[Sales Amount]
        )
    VAR __DS0VisualCalcsInput =
        SELECTCOLUMNS (
            KEEPFILTERS (
                SELECTCOLUMNS (
                    __DS0Core,
                    "Brand", 'Product'[Brand],
                    "IsGrandTotalRowTotal", [IsGrandTotalRowTotal],
                    "Year", 'Date'[Year],
                    "IsGrandTotalColumnTotal", [IsGrandTotalColumnTotal],
                    "Sales_Amount", [Sales_Amount]
                )
            ),
            "Brand", [Brand],
            "Year", [Year],
            "IsGrandTotalRowTotal", [IsGrandTotalRowTotal],
            "IsGrandTotalColumnTotal", [IsGrandTotalColumnTotal],
            "Sales Amount", [Sales_Amount]
        )
    TABLE '__DS0VisualCalcs' = __DS0VisualCalcsInput
        WITH VISUAL SHAPE
        AXIS ROWS
            GROUP [Brand]
                TOTAL [IsGrandTotalRowTotal]
            ORDER BY [Brand] ASC
        AXIS COLUMNS
            GROUP [Year]
                TOTAL [IsGrandTotalColumnTotal]
            ORDER BY [Year] ASC
        DENSIFY "IsDensifiedRow"
    VAR __DS0RemoveEmptyDensified =
        FILTER (
            KEEPFILTERS ( '__DS0VisualCalcs' ),
            OR (
                NOT ( '__DS0VisualCalcs'[IsDensifiedRow] ),
                NOT ( ISBLANK ( '__DS0VisualCalcs'[Growth] ) )
            )
        )
    VAR __DS0RemoveContextOnlyColumns =
        SELECTCOLUMNS (
            KEEPFILTERS ( __DS0RemoveEmptyDensified ),
            "'__DS0VisualCalcs'[Brand]", '__DS0VisualCalcs'[Brand],
            "'__DS0VisualCalcs'[Year]", '__DS0VisualCalcs'[Year],
            "'__DS0VisualCalcs'[IsGrandTotalRowTotal]",
                '__DS0VisualCalcs'[IsGrandTotalRowTotal],
            "'__DS0VisualCalcs'[IsGrandTotalColumnTotal]",
                '__DS0VisualCalcs'[IsGrandTotalColumnTotal],
            "'__DS0VisualCalcs'[Growth]", '__DS0VisualCalcs'[Growth],
            "'__DS0VisualCalcs'[IsDensifiedRow]", '__DS0VisualCalcs'[IsDensifiedRow]
        )
EVALUATE
    __DS0RemoveContextOnlyColumns

查询中包含大量需要处理的信息:

使用DEFINE COLUMNS将可视化计算创建为查询列。
基本查询与前面的矩阵相同(参见__Ds0Core)。
基本查询经历了几个处理步骤:
重命名列。
添加视觉形状和致密化过程。
在视觉计算中移除没有值的密集行。
删除用于计算但不需要用于可视化的列。
这些主题中的每一个都很有趣,值得单独写一篇文章。在本文中,我们主要关注VISUAL SHAPE部分。VISUAL SHAPE子句将一个层次结构添加到一个扁平的表中:

TABLE '__DS0VisualCalcs' = __DS0VisualCalcsInput
    WITH VISUAL SHAPE
    AXIS ROWS
        GROUP [Brand]
            TOTAL [IsGrandTotalRowTotal]
        ORDER BY [Brand] ASC
    AXIS COLUMNS
        GROUP [Year]
            TOTAL [IsGrandTotalColumnTotal]
        ORDER BY [Year] ASC
    DENSIFY "IsDensifiedRow"

DS0VisualCalcs的来源是DS0VisualCalcsInput,这只不过是原始查询在部分列被重命名以匹配本地名称之后的结果。实际上,视觉计算是通过使用矩阵中的列名或度量名称,而不是模型中的名称来工作的。这就是重命名步骤的原因。然而,原始查询并不包含作为视觉计算添加的新列。另一方面,__Ds0VisualCalcs包含了这些新列。

VISUAL SHAPE定义一个或两个轴:ROWS、COLUMNS或两者都定义。在本例中,我们同时定义了ROWS和COLUMNS。对于每个轴,Power BI定义分组列(在本例中为Brand和Year)、列的名称(指示一行是否为小计)以及它们的排序顺序。VISUAL SHAPE的最后一个子句是Densification列的名称,这是添加到表中的列,用于指示是否由于Densification而创建了行。

在第一个简单的例子中,每个轴都有一个级别。但是,如果在行上添加Category和Color,在列上添加月份,结果就比较复杂了。


更复杂的层次结构带来了更复杂的VISUAL SHAPE定义:

TABLE '__DS0VisualCalcs' = __DS0VisualCalcsInput
    WITH VISUAL SHAPE
    AXIS ROWS
        GROUP [Brand]
            TOTAL [IsGrandTotalRowTotal]
        GROUP
            [Category],
            [Category_Code]
            TOTAL [IsDM1Total]
        GROUP [Color]
            TOTAL [IsDM3Total]
        ORDER BY
            [Brand] ASC,
            [Category_Code] ASC,
            [Category] ASC,
            [Color] ASC
    AXIS COLUMNS
        GROUP [Year]
            TOTAL [IsGrandTotalColumnTotal]
        GROUP
            [Month],
            [Month_Number]
            TOTAL [IsDM6Total]
        ORDER BY            
[Year] ASC,            
[Month_Number] ASC,
[Month] ASC
    DENSIFY "IsDensifiedRow"

轴的每一组都包含定义层次结构级别的列,如果需要,还包含按列排序的列,在Category和Month级别中都可见。

VISUAL SHAPE添加到表中的元数据是大多数可视化计算函数的关键。实际上,Growth代码使用关键字COLUMNS作为PREVIOUS的第二个参数:

Growth =
VAR Curr = [Sales Amount]
VAR Prev = PREVIOUS ( [Sales Amount], COLUMNS )
VAR Result = DIVIDE ( Curr - Prev, Prev )
RETURN
    FORMAT ( Result, "0.00 %" )

在本例中,COLUMNS是一个关键字,它告诉PREVIOUS使用COLUMNS的轴定义来导航虚拟表。根据可视化上下文所处的级别,引擎知道它需要使用轴中定义的排序顺序和分组导航到虚拟表的前一行。

ROWS和COLUMNS是只能在可视化计算中使用的关键字。原因是VISUAL SHAPE是在Power BI的查询中创建的。常规模型度量既不能访问ROWS,也不能访问COLUMNS,因为模型度量可以添加到没有可视化计算的矩阵中。因此,它没有定义轴。

总之,创建可视化计算环境的第一步是通过VISUAL SHAPE向SUMMARIZECOLUMNS的原始结果添加元数据。值得注意的是,带有VISUAL SHAPE的表不是一个变量;它是一个查询定义的表。因为它是一个表,所以查询可以定义添加到表中的新列(列不能添加到变量中)。每个可视化计算都是添加到表中的一个新列,这些列可以引用ROWS和COLUMNS,因为轴的定义是表定义的一部分。

理解VISUAL SHAPE语法是理解如何计算可视化计算的第一步。接下来,理解由轴线定义的关卡格子以及EXPAND和COLLAPSE如何让开发者在格子中导航是很重要的。

然而,VISUAL SHAPE已经足够弄清楚像PREVIOUS和NEXT这样的函数是如何工作的。PREVIOUS和NEXT不导航晶格。PREVIOUS相当于OFFSET (-1, ROWS)。引用ROWS的能力让用户避免了使用OFFSET所必需的PARTITIONBY和ORDERBY的复杂性。ROWS基本上是窗口函数的group-by、partition-by和order-by部分的快捷方式。

在本文中,我们只触及了可视化计算的表面。我们使用了一些需要进一步描述的术语:视觉环境、当前水平和致密化过程。我们将在以后的文章中讨论这些主题。


如果您想深入学习微软Power BI,欢迎登录网易云课堂试听学习我们的“从Excel到Power BI数据分析可视化”系列课程。或者关注我们的公众号(PowerPivot工坊)后猛戳”在线学习”。




长按下方二维码关注“Power Pivot工坊”获取更多微软Power BI、PowerPivot相关文章、资讯,欢迎小伙伴儿们转发分享~


Power Pivot工坊