はじめに
内容
Power BIで集計していると、「あれなんかおかしいぞ??」ということがある。特に、単価×数量を合計するときとか。 でも変な結果は実はラッキー。評価コンテキストをちゃんと理解するきっかけになるから。 人間はモチベーションがあれば新しいことを覚えられるもの。
間違うケースを2つ紹介するが、ひとつめはフィルター コンテキストの、ふたつめはコンテキスト トランジションの理解不足が原因。後者は中級者以上でもうっかり間違えやすいので注意!(実際、本でも間違っていたりする)
対象読者
対象読者はPower BIのセマンティック モデル開発者で、上級者を目指す、次のような人
- 行コンテキストを理解している人
- フィルター コンテキストを理解している人
- それらを組み合わせたコンテキスト トランジションを理解したい人
ちょっとそれは難しいと感じる人は、ひとまず次を覚えておけばOK。ちゃんと理解しようとすると難しいので、全員が知らなければならないかというと、微妙なんだよね。。
行コンテキスト(SUMX
関数のようなイテレーター関数や計算列)で安易にメジャーを使用しないこと
サンプル データと欲しい結果
以下を'売上'
テーブルとして読み込む。
日付 | 顧客 | 品目 | 単価 | 数量 |
---|---|---|---|---|
2023/12/1 | 山岡 | うまい棒 | 10 | 10 |
2023/12/1 | 栗田 | ヤングドーナツ | 30 | 2 |
2023/12/2 | 山岡 | あわ玉 | 10 | 1 |
2023/12/2 | 山岡 | ポテトフライ | 30 | 3 |
2023/12/2 | 栗田 | チロルチョコ | 20 | 2 |
2023/12/2 | 山岡 | あわ玉 | 10 | 1 |
ここから、人別品目別の売上合計をマトリックス ビジュアルで表す。ちなみに、正解は以下(マトリックス ビジュアル風に):
顧客 | 品目 | 売上合計 |
---|---|---|
山岡 | 210 | |
あわ玉 | 20 | |
うまい棒 | 100 | |
ポテトフライ | 90 | |
栗田 | 100 | |
チロルチョコ | 40 | |
ヤングドーナツ | 60 | |
合計 | 310 |
正しい方法
以下で計算した人はOKです。おつかれさまでした。また会いましょう。
SUMX関数の場合
以下のメジャーを作成
売上合計 = SUMX('売上', '売上'[単価] * '売上'[数量])
これで行見出しに'売上'[顧客]
列、'売上'[品目]
列、値に[売上合計]
メジャーをセットすれば良い。
SUMX
関数のようにXで終わる関数をイテレーター関数と呼ぶが、その説明はまた別の機会に。
計算列の場合
'売上'
テーブルに'売上'[売上合計_計算列]
列として、以下の計算列を定義。
売上合計_計算列 = '売上'[単価] * '売上'[数量]
これで行見出しに'売上'[顧客]
列、'売上'[品目]
列、値に'売上'[売上合計_計算列]
列をセット(&暗黙のメジャーで合計)すれば良い。
誤った方法その1 - メジャー同士の掛け算
よくある話。メジャーを覚えたてのころにやりがち。しかし、フィルター コンテキストの理解にはちょうどよい。
単価、数量の合計をメジャーで定義して、その掛け算で売上合計を定義する。
単価合計 = SUM('売上[単価])
数量合計 = SUM('売上[数量])
売上合計(誤1) = [単価合計] * [数量合計]
正しい結果を表すマトリックス ビジュアルの値に[売上合計(誤1)]
メジャーを追加すると、次のようになる。
顧客 | 品目 | 売上合計 | 売上合計(誤1) |
---|---|---|---|
山岡 | 210 | 900 | |
あわ玉 | 20 | 40 | |
うまい棒 | 100 | 100 | |
ポテトフライ | 90 | 90 | |
栗田 | 100 | 200 | |
チロルチョコ | 40 | 40 | |
ヤングドーナツ | 60 | 60 | |
合計 | 350 | 2,090 |
Oops!赤字のところが違う!!
何が起きていたかを確認するために、[単価合計]
メジャー、[数量合計]
メジャーを追加してみる。
顧客 | 品目 | 売上合計 | 売上合計(誤1) | 単価合計 | 数量合計 |
---|---|---|---|---|---|
山岡 | 250 | 900 | 60 | 15 | |
あわ玉 | 20 | 40 | 20 | 2 | |
うまい棒 | 100 | 100 | 10 | 10 | |
ポテトフライ | 30 | 30 | 30 | 1 | |
栗田 | 100 | 200 | 50 | 4 | |
チロルチョコ | 40 | 40 | 20 | 2 | |
ヤングドーナツ | 60 | 60 | 30 | 2 | |
合計 | 490 | 2,090 | 110 | 19 |
おわかりいただけただろうか。1行目の売上合計(誤1)
の900は、単価合計
の60と数量合計
の15の積となっている。そして、単価合計
は山岡さんが買ったものの単価の合計となっている。
また、合計(2,090) ≠ 山岡(900)+栗田(200)のように、列内での整合性も取れていない。
どうしてそうなるかは、フィルター コンテキストの理解が必要。フィルター コンテキストを理解するにはこちらを参照すること。
誤った方法その2 - 意図せぬコンテキスト トランジション
実はこの記事のメインテーマ。
フィルター コンテキストも行コンテキストもイテレーター関数も知っているけど、コンテキスト トランジションは知らんという人は、意図せぬコンテキスト トランジションで誤った数字を計算してしまうことがある。そして、先ほどと違って地味にずれるので最初は気づかないなんてこともある(そのまま最後まで?)。
先に述べておくが、誤った集計の原因としてコンテキスト トランジションを挙げるが、コンテキスト トランジションは悪い機能なので使わないようにしようという趣旨ではない。それが必要な場面もあるが、いつのまにか使っていたために、誤集計をしてしまうので、注意しましょうという話。
数字を確認
次のメジャーを定義する。
売上合計(誤2) = SUMX('売上', [単価合計] * [数量合計])
正しい結果を表すマトリックス ビジュアルの値に'売上'[売上合計(誤2)]
メジャーを追加すると、次のようになる。
顧客 | 品目 | 売上合計 | 売上合計(誤2) |
---|---|---|---|
山岡 | 210 | 270 | |
あわ玉 | 20 | 80 | |
うまい棒 | 100 | 100 | |
ポテトフライ | 90 | 90 | |
栗田 | 100 | 100 | |
チロルチョコ | 40 | 40 | |
ヤングドーナツ | 60 | 60 | |
合計 | 310 | 370 |
栗子は正しいが、山岡さんが誤っている。あわ玉のところが。しかし、合計=山岡+栗田なので、列内での整合性は取れていそう。
そして、メジャーはデータによっては正しい結果を返すことも。試しに、元のデータから最後の行を除くと一致する。なぜだ!?
理由はコンテキスト トランジション
誤った訳は、意図せぬコンテキスト トランジションが起きたため。
もう一度、正しいメジャーと誤ったメジャーを比較してみよう。
売上合計 = SUMX('売上', [単価] * [数量])
//単価合計 = SUM('売上[単価])
//数量合計 = SUM('売上[数量])
売上合計(誤2) = SUMX('売上', [単価合計] * [数量合計])
両者ともSUMX
関数を使用しているが、違いは第2引数が、列かメジャーかという点。
SUMX
関数のようなイテレーター関数の第2引数は行コンテキストで評価されるが、メジャーが与えられた場合は、行コンテキストをフィルター コンテキストに変換するコンテキスト トランジションが発生する。コンテキスト トランジションは、行コンテキストからフィルター コンテキストを生成すること。行コンテキストでイテレーションしている行のすべての列とその値の組み合わせとして、フィルター コンテキストが生成される(キー列が設定されている場合はその列を使う(後述))。
次の節で具体的に説明しよう。
計算列でコンテキスト トランジションを確認
行コンテキストなので、計算列でも再現可能。
'売上'
テーブルに以下の計算列を追加してみる。右辺は[売上合計(誤2)]
メジャー内SUMX
関数の第2項。
売上合計_計算列(誤2) = [単価合計] * [数量合計]
'売上'
テーブルは次のようになる。
日付 | 顧客 | 品目 | 単価 | 数量 | 売上合計_計算列 | 売上合計_計算列(誤2) |
---|---|---|---|---|---|---|
2023/12/1 | 山岡 | うまい棒 | 10 | 10 | 100 | 100 |
2023/12/1 | 栗田 | ヤングドーナツ | 30 | 2 | 60 | 60 |
2023/12/2 | 山岡 | あわ玉 | 10 | 1 | 10 | 40 |
2023/12/2 | 山岡 | ポテトフライ | 30 | 3 | 90 | 90 |
2023/12/2 | 栗田 | チロルチョコ | 20 | 2 | 40 | 40 |
2023/12/2 | 山岡 | あわ玉 | 10 | 1 | 10 | 40 |
この40
がどのように計算されているか?売上合計_計算列(誤2)
の値は[単価合計]
と[数量合計]
の積なので、それぞれ確認する。
行コンテキストでイテレーションしている行のすべての列とその値の組み合わせとして、フィルター コンテキストが生成されるため、3行目の[単価合計]
は次のフィルター コンテキストで評価されている。
3行目の[単価合計]の値 = CALCULATE(
[単価合計],
'売上'[日付] = DATE(2023,12,2),
'売上'[顧客] = "山岡",
'売上'[品目] = "あわ玉",
'売上'[単価] = "10",
'売上'[数量] = "1",
'売上'[売上合計_計算列] = "10" //計算列を追加している場合
)
CALCULATE
関数の第2引数以降がやかましい感じ。ここで、3行目と6行目の内容が全く同じであるため、これら2行がフィルター コンテキストで抽出され、[単価合計]
の値が20となる。[数量合計]
についても同様に、2となる。したがって、3行目の売上合計_計算列(誤2)
の値はそれらをかけ合わせた40となるわけ。
おわかりいただけただろうか(2回目)。地味にずれると書いたのは、ファクト テーブルに重複行がある場合のみずれるため。そのため、データ数が少ないときには誤りが顕在化せず、データが増えてから顕在化することもあり見落としがち。こういうことがあるので、行コンテキスト(主にイテレーター関数)を使うときは気を付けよう。
なお、コンテキスト トランジションについては、こちらでも説明しているので参考にしてほしい。
やっちゃいけないこと
ファクト テーブルに行を特定する一意のID列(例えば連番)を作成すれば、意図しないコンテキスト トランジションによる誤集計は発生しない。が、これは避けるべきバッド プラクティス。「やっちゃいけなかったんだよ。そんなこともわからないから、大人って、地球だって平気で消せるんだ」と言える。
なぜなら、ファクト テーブルにID列を用意すると、カーディナリティが激増し、列圧縮が効かず、セマンティック モデルのサイズが爆発してしまうため(ID列だけでセマンティック モデルのサイズの半分以上を占めることも)。そうまでして、行コンテキストでメジャーを使用して起きるコンテキスト トランジションを使いたいかというと、答えはNOでしょう。そもそも注意深くメジャーを作成すれば回避できる話なので。
宿題:もうちょっと考えてみる
次のメジャーを定義してみる。
売上合計(誤3) = SUMX('売上', [単価合計] * '売上'[数量])
[売上合計(誤2)]
メジャーとの違いは、[数量合計]
メジャーではなく、'売上'[数量]
列を使用していること。
結果は以下となる。なぜこうなるか考えてみよう。
顧客 | 品目 | 売上合計 | 売上合計(誤2) | 売上合計(誤3) |
---|---|---|---|---|
山岡 | 210 | 270 | 230 | |
あわ玉 | 20 | 80 | 40 | |
うまい棒 | 100 | 100 | 100 | |
ポテトフライ | 90 | 90 | 90 | |
栗田 | 100 | 100 | 100 | |
チロルチョコ | 40 | 40 | 40 | |
ヤングドーナツ | 60 | 60 | 60 | |
合計 | 310 | 370 | 330 |
わかったらコメント欄に投稿してください。
補足:CALCULATE関数がフィルター コンテキストを生成するという話
[売上合計(誤2)]
メジャーの定義は以下であった。
//単価合計 = SUM('売上[単価])
//数量合計 = SUM('売上[数量])
売上合計(誤2) = SUMX('売上', [単価合計] * [数量合計])
これを、
売上合計(誤4) = SUMX('売上', SUM('売上[単価]) * SUM('売上[数量]))
とすると、両者に違いはないように思えるが、次のように異なる結果となる。
顧客 | 品目 | 売上合計 | 売上合計(誤2) | 売上合計(誤4) |
---|---|---|---|---|
山岡 | 210 | 270 | 3,600 | |
あわ玉 | 20 | 80 | 80 | |
うまい棒 | 100 | 100 | 100 | |
ポテトフライ | 90 | 90 | 90 | |
栗田 | 100 | 100 | 400 | |
チロルチョコ | 40 | 40 | 40 | |
ヤングドーナツ | 60 | 60 | 60 | |
合計 | 310 | 370 | 12,540 |
実は[単価合計]
メジャーと[数量合計]
メジャーは、メジャー定義時に自動的にCALCULATE
関数でラップされている点が異なっている。CALCULATE
関数には、フィルター コンテキストを生成して、第1引数の集計関数に渡すという超重要な機能があるのだ。(詳細はこちら)
備忘:わかりにくい循環依存
計算列の定義でメジャーを使用すると、コンテキスト トランジションが発生する。コンテキスト トランジションは行に含まれるすべての列と値の組み合わせで、フィルター コンテキストを生成する。
ここで疑問。2つ以上の計算列の定義でメジャーを使用した場合にどうなるのだろうか?
答えは、お互いの列がコンテキスト トランジション時に参照し合うため、循環依存が発生する。
まあ、こんな感じなんで計算列でメジャーを使うのはおすすめしませんよと。
備忘:キー列を使用したコンテキスト トランジション
コンテキスト トランジションにより、行コンテキストでイテレーションしている行のすべての列とその値の組み合わせとして、フィルター コンテキストが生成されると述べたが、テーブルにキー列が設定されている場合は動作が異なる。
キー列の設定方法は、[モデル ビュー] ->> 該当のテーブルを選択 ->> [プロパティ ウィンドウ] ->> [全般] ->> [キー列] から。設定するキー列は一意の値を含む列でなければならない。
キー列を設定すれば、以下のように動作が変化する。
- コンテキスト トランジションが変化。キー列とその値の組み合わせで、フィルター コンテキストが生成される。これにより:
- 複数の計算列でコンテキスト トランジションを使用しても、循環依存が発生しない
- フィルター コンテキストが単純になる分、ちょびっとメジャーのパフォーマンスが良くなる(だろう)
- インポート時にキー列に重複があると、インポート エラーが発生するようになる
正直、キー列設定の効果は小さいが、モデルがよりセマンティックになるので、やっておいた方が丁寧だろう。
なお、先述のとおり、ファクト テーブルで一意の値となる列をを作成することはバッド プラクティスなので、使うケースはディメンション テーブルとなるだろう。
おわりに
コンテキスト トランジションを知らないと、何かができなくて困るというよりも、思わぬところで誤ってしまかもよという話。ということなんで、コンテキスト トランジションもちゃんと勉強しとかないとね。ちゃんと使えれば強力な機能なんだし💖