(Power BI) マトリックスで空白にゼロを表示させたい

（この記事は2022/03/23に作成しました）

　値を表示する場合に、ゼロや空白はそれぞれ意味があります。計算する値が存在しない場合は空白になり、計算した結果が数値やゼロになります。それを踏まえたうえで、ゼロと空白についての扱い方を見ていきます。

# 空白をゼロにしたい

https://www.youtube.com/watch?v=_cT9PB72fu8

https://www.youtube.com/watch?v=ALBrFCIaQqQ&t=235s

## 発端
　以下のようなデータがあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/3caee7f3-899d-ac57-283d-d684eb11e7aa.png)
　値は、以下のDAX式です。

```text
SalesAmount = SUM(Sales[Amount])
```

　この空白部分は、売上データデータが存在しないためです。
　ユーザー側から「いいから、データが無いときはゼロが表示されるようにしてくれ」とデータエンジニアに依頼が出されることがあります。ユーザーは **ゼロが表示されるが当然** だと思っていますが、データエンジニアは **値がない** ことと **値がゼロ** であることの意味は違う！と考えていますが説得できず、ユーザーとデータエンジニアの間で短い睨みあいになる場面が往々にして発生します。

## +0 で簡単にゼロ表示
　いやいやながら、以下のようにDAXを書き直します。

```text
SalesAmount_1 = SUM(Sales[Amount]) +0
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/8db27dd5-919d-c8b2-0def-5aff83b41b95.png)
　空白の部分にゼロが入りますが、右に列が追加されてしまいました。
　対象外の年までゼロが表示されてしまうのは、 **日付テーブルが開始年から現在の年までの日付を持っているため** 、2021年5がから現在までの年までがゼロで表示されてしまうのです。しかし、明らかに必要のない年までゼロが表示されてしまうままで提出したのでは、嫌みととられかねません。解決策としては、日付テーブルを2021年1月から2021年4月までに設定してやればいいのですが、その他の製品のデータもレポートの中にあると影響が出る可能性があるので、ここでは日付テーブルの修正を行わないこととします。

## 対象製品の販売実績の開始と終了の日付を使う

　相手の要件に過剰不要なゼロ表示を消すための方法として、製品の取り扱い開始、終了の日付を調べ、その間で売り上げのない期間をゼロとすることが考えられます。

```text
SalesAmount_5 = 
// 全商品の売上データ最初の日付
VAR FirstSaleEver =
    CALCULATE ( MIN ( Sales[SalesDate] ), REMOVEFILTERS () )
// 全商品の売上データ最後の日付の月末
VAR LastSaleEver =
    CALCULATE ( EOMONTH( MAX( Sales[SalesDate] ), 0 ) , REMOVEFILTERS () )
// 計算対象日付
VAR CurrentDate =
    MAX ( 'DateTable'[Date] )
// ゼロ表示するか判定
VAR ForceZero = FirstSaleEver <= CurrentDate
    && CurrentDate <= LastSaleEver
// ゼロ表示する場合は０を加える
VAR Amt =
    [SalesAmount] + IF ( ForceZero, 0 )
RETURN
    Amt
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/87ecbfdf-3cf1-be8f-2d88-e8213476368f.png)

　Muffinは、まったく売上がないのに表に表示されています。これを外したい場合は、全期間の製品の売り上げが０以上であるか判定を入れるようにします。

```text
SalesAmount_5 =
// 全商品の売上データ最初の日付
VAR FirstSaleEver =
    CALCULATE ( MIN ( Sales[SalesDate] ), REMOVEFILTERS () ) 
// 全商品の売上データ最後の日付の月末
VAR LastSaleEver =
    CALCULATE ( EOMONTH ( MAX ( Sales[SalesDate] ), 0 ), REMOVEFILTERS () ) 
// 計算対象日付
VAR CurrentDate =
    MAX ( 'DateTable'[Date] ) 
// ゼロ表示するか判定
VAR ForceZero =
    FirstSaleEver <= CurrentDate
        && CurrentDate <= LastSaleEver
        && CALCULATE ( Sales[SalesAmount], ALLEXCEPT ( 'Sales', 'Products' ) ) > 0 
// ゼロ表示する場合は０を加える
VAR Amt =
    [SalesAmount] + IF ( ForceZero, 0 )
RETURN
    Amt
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/89913b76-f941-e347-4e41-7dffd53f6ee6.png)

### 製品ごとの販売開始、終了をみる

　前のものは、製品が販売開始されていなくてもゼロ表示されてしまいます。販売開始されているかどうかは、製品ごとの取り扱い開始、終了の日付を持つテーブルを使用することが望ましいのですが、ここでは売上テーブル上で値が最初に入っている年を取り扱い開始、最後に値が入っている年を取り扱い終了として計算してみます。

```text
SalesAmount_2 =
// 製品ごとの売り上げ初日
VAR FirstDateWithSales =
    CALCULATE ( MIN ( Sales[SalesDate] ), ALLEXCEPT ( sales, 'Products' ) ) 
// 売り上げ全体の最終日
VAR LastDateEver =
    CALCULATE (
        EOMONTH ( MAX ( Sales[SalesDate] ), 0 ),
        ALLEXCEPT ( sales, 'Products' )
    )
VAR CurrentDate =
    MAX ( DateTable[Date] ) 
// ゼロ表示の判定
VAR ForceZero = FirstDateWithSales <= CurrentDate
    && CurrentDate <= LastDateEver
RETURN
    [SalesAmount] + IF ( ForceZero, 0 )
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/2abcc791-37cc-8aff-00c4-1cfd8fc057a1.png)

　最終日を製品ごとではなく、全製品の最終日に揃える場合は以下のようになります。

```text
SalesAmount_2 = 
// 製品ごとの売り上げ初日
VAR FirstDateWithSales =
    CALCULATE(
        MIN(Sales[SalesDate]),
        ALLEXCEPT(sales,'Products')
    )
// 売り上げ全体の最終日
VAR LastDateEver =
    CALCULATE(
        EOMONTH( MAX(Sales[SalesDate]), 0 ),
        REMOVEFILTERS()
    )
VAR CurrentDate = MAX(DateTable[Date])
// ゼロ表示の判定
VAR ForceZero = FirstDateWithSales <= CurrentDate && CurrentDate <= LastDateEver
RETURN
    [SalesAmount] + IF(ForceZero,0)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/73e13c14-5ad2-0b21-ca16-389cef4c9077.png)

　ここまでやるのは、やりすぎかもしれません。

## ゼロ表示判定を別のレイヤにしたい
　先の例では、ファクトテーブル上にDAXでゼロ表示判定を書き込んでしまったため、データが汚れた状態になっています。ファクトテーブルに余計なものを追加せず、別テーブルでゼロ表示する・しないの判定を行うようにしてみます。

```text:ZeroGain
ZeroGrain = 
// 売り上げ全体の最終日
VAR MaxSale = MAX(Sales[SalesDate])
// 製品ごとの売り上げ初日
VAR ProdsAndDate =
    GENERATE(
        FILTER('Products', [SalesAmount] > 0),  // 全期間で売り上げがあるかどうか判定
        VAR FirstSaleOfProduct = CALCULATE(MIN('Sales'[SalesDate]))
        VAR Dates = DATESBETWEEN('DateTable'[Date], FirstSaleOfProduct, MaxSale)
        RETURN 
            Dates
    )
// 
VAR Result = 
    SELECTCOLUMNS(
        ProdsAndDate,
        "ProductID", 'Products'[ProductID],
        "Date", 'DateTable'[Date]
    )
RETURN
    Result
```

　このテーブルを使って、ゼロ表示するかどうかDAX式で判定します。

```text:SalesZeroWith
SalesAmount_4 = 
VAR ForceZero = COUNTROWS(ZeroGrain) > 0
RETURN
    [SalesAmount] + IF(ForceZero,0)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/83406057-9c5b-755a-4d90-8613e14de111.png)

## CalculationGroupを使う
　外部ツール Tabular Editor を使って CalculationGroup を作成してゼロ表示させることもできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/95b595dd-9f13-3b9c-1581-44a78195c187.png)

```text:Show Blanks
SELECTEDMEASURE()
```

```text:Show zeros
VAR ForceZero = COUNTROWS(ZeroGrain) > 0
VAR Result = SELECTEDMEASURE() + IF(ForceZero,0)
RETURN
    Result
```

　スライサーの選択で、ゼロ表示する・しないの選択ができるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/91b0ac13-449a-e72e-ad88-5d0a4b125619.png)