PowerBI/PowerPivot(以下、総称してDAXと呼びます)で非常に重要な意味合いを持つCaluculate関数について、今回はより詳細に書いてみたいと思います。Calculateの動作を理解するには、処理の流れを把握し「現在どのコンテキストで評価されているか」「何を置き換えようとしているのか」を意識することが重要です。
簡単な例
式1---
[青色商品の売上合計] =
Calculate(
SUM('売上明細' [売上])、
'商品' [色] = "青"
)
この式は簡単ですね。商品テーブルの「色」列が青の商品にフィルタされたテーブルを評価し、売上を合計します。ここで注意しなければならないのは、スライサーなどで色を別途フィルタしていても、そのフィルタは無視される(置き換えられる)という点です。
処理を順に見ていきましょう。前提条件として、スライサーで商品の色を「黒」にフィルタしていたとします。この場合、Calculate式は色の値が黒であるフィルタコンテキストによってまずは評価されます。Calculate式の第二引数は、既存のフィルタを置き換えて新しいフィルタコンテキストに置き換えます。
ここでは、スライサーで指定している'商品'[色]='黒'と'商品'[色]='青'がバッティングし、Calculateで指定した'商品'[色]='青'によって置き換えられています。(そのほかのフィルタには影響が及ばない点に注意してください)
結果として、スライサーでどの色を選択しても、常に青色商品の売上合計を算出することができています。
もう少し複雑な例(FILTER関数と組み合わせる)
Calculateにもう少し複雑な条件を渡してみましょう。次のような式を考えてみます。'商品'[定価]>='商品'[原価] * 2で高収益商品の売上のみを集計するメジャーです。
式2---
[高収益商品] =
Calculate(
SUM('売上明細' [売上])、
'商品'[定価]>='商品'[原価] * 2
)
この式はエラーになります。Calculateの第二引数で指定するフィルタ条件には、一度に一つの列しか指定できません。この例では[定価]列と[原価]列を指定しているため、うまくいきません。
※フィルタ条件を下記のように変えるとエラーはなくなります。
商品[定価]>=1000
こうすると[定価]列のみの参照になるため、エラーにはなりません。
では、複雑なフィルタを指定したい場合はどうするべきでしょうか。
このような場合、FILTER関数と組み合わせて使用するとうまくいきます。
式3---
[高収益商品] =
Calculate(
SUM('売上明細' [売上])、
FILTER('商品','商品'[定価]>='商品'[原価] * 2)
)
FILTER関数はフィルタされたテーブルを返します。一方、Calculate関数の第二引数には、フィルタ条件としての列またはテーブルを取ることが許可されています。複数の列を直接フィルタ条件にすることはできませんが、引数としてテーブルを取ることはできます。そのため、FILTER関数でフィルタされたテーブルを引数として指定することで、より複雑なフィルタ条件も処理できるわけです。
ここで、先ほどの式1を振り返ってみましょう。式1では単一の列を指定して'商品' [色] = "青"としていました。その結果、ピボットテーブルにはすべての行に青色商品の売上が表示されました。
式3ではフィルタ関数で商品テーブルを引数としています。Calculateは既存のフィルタを置き換える処理を行うはずでした。引数でFILTER関数を指定し、FILTER関数の引数にテーブルを指定した場合はテーブル全体のフィルタがクリアされるのでしょうか?
答えは、NOです。式3の結果である「高収益商品の売上」を見てください。ここでは各色別に高収益商品の売上が合計されています。スライサーで色を指定した場合には、色のフィルタはきちんと反映されます。
つまり、CalculateでFILTER関数を使用した場合、Calculateは元のフィルターコンテキストで(スライサーなどのCalculateの外側でかかっているフィルタを残した状態で)FILTERを評価します。スライサーで色を黒に指定した場合、[色]=黒に該当する行のみ評価します。
元のフィルタコンテキストをFILTER関数で置き換える??
Calculateは元のフィルタを置き換えるのですが、置き換える対象がFILTER関数であるため、「元のフィルタをFILTER関数で置き換える」と考えたほうが良いかもしれません(かなりややこしいですが)
FILTER関数は引数としてテーブルを取ります。このテーブルは、元のフィルタコンテキストで評価されるテーブルです。式3の4行目FILTER('商品','商品'[定価]>='商品'[原価] * 2)でFILTER関数の第一引数となっている商品テーブルは元のフィルタコンテキストの影響を受けています。そのうえで、FILTER関数は第二引数のフィルタ条件を評価した結果をCalculateに返します。
一方、Calculateは既存のフィルターコンテキストを、FILTER関数の戻り値で置き換えます。つまり、既存のフィルタコンテキストをFILTER関数の結果で置き換えています。FILTER関数はそもそも元のフィルタコンテキストで評価されたテーブルを引数に取っています。
結果として、元のフィルタコンテキストの影響を受けた値が返されることになります。
…ややこしいですね。
繰り返しになりますが、Calculateの引数としてブール条件(特定の列を指定した場合)には、Calculateは既存のフィルターコンテキストを置き換えます。ただし、その影響範囲は指定した列のみです。
ここで、以下のような式を考えてみます。
式4---
[高収益商品] =
Calculate(
SUM('売上明細' [売上]),
FILTER(ALL('商品'),'商品'[定価]>='商品'[原価] * 2)
)
同じくFILTER関数を使って、Calculateによりフィルタの置き換えを行っています。先ほどとは異なり、ALL関数で全てのフィルタをクリアした状態のテーブルを引数に取っています。
こうすると常にすべての商品を反復処理し、Calculateが色のフィルターも置き換えるため、結果は常に高収益商品の売上合計が表示されることになります。スライサーで色を指定しても変わりません。ALL関数によってすべてのフィルタがクリアされています。
いかがでしょうか。構造がややこしいですが、要はCalculate関数は常にフィルタを置き換える処理を行います。その際に引数として渡す対象をFILTER関数として既存のフィルタコンテキストが生きた状態で渡すか、列をブーリアン演算させることで関係列のフィルタを明示的に置き換えるか、あるいはALL関数を間に挟むことで全てのフィルタをクリアさせるか、などの様々な置き換えができるということです。
概念として理解しようとすると大変ですが、DAX式を書きながら考えれば理解しやすいと思います。ぜひ試してみてください。