Power BI で知育アプリを作る ～けいさんクイズ編～

この記事は [Microsoft Power BI Advent Calendar 2024](https://qiita.com/advent-calendar/2024/powerbi) の12日目の記事です。

# はじめに

Power Apps での知育アプリ作りから派生して、**Power BI でも知育アプリを作ってみました**。今年の 1月に「★の色を変えるアプリ」を Power BI で作りましたが、今回はゲーム性(ランダム性や正誤判定) があるアプリを作りたいと思います。

https://qiita.com/AnPanda/items/edec1746a2ba1a1e69bb

https://qiita.com/AnPanda/items/8d38a001e6b7447ec68d

# 作るもの

**足し算の和を答える「けいさんクイズ」ゲーム**を作ります。

# 実際の動作画面

https://x.com/anpanda_44075/status/1864050962572476675

なお、先行して **Power Apps でも同様のアプリを作成済み**。今回は、このアプリを Power BI で再現するような形です。

# 必要な要素

* ランダム性
* 設問の切り替え
* 解答入力欄
* 正誤判定
* ヒント機能

# 諦めたこと

* 正解数の蓄積
* サウンド
* アニメーション
※GIF アニメなら導入できるようです。

https://qiita.com/k_maki/items/10785fcbb89bf488b8a8

# データ作成

**Power Query で、設問のベースになるテーブルを作成**します。

### レベルとインデックス

**レベルを 3段階に分けて、各レベル 20問用意**することにします。

**このリストの作成には、`List.Numbers`関数 を利用**しました。

https://learn.microsoft.com/ja-jp/powerquery-m/list-numbers

レベルを表すリストは、1 が 20回、2 が 20回、3 が 20回繰り返されます。要素数60のリスト・増分を 1/20(=0.05) として、あとで小数点以下を切り捨てることで実現しています。

`List.Numbers(1,60,1/20)`

各レベルごとの番号 (インデックス) は、1～20 の連番が 3回繰り返されるリストです。`List.Numbers(1,20)` を List.Repeat で 3回繰り返す形で実現しました。

`List.Repeat(List.Numbers(1,20),3)`

**`Table.FromColumns`関数で、この 2つのリストをテーブル化**します。

`= Table.FromColumns({List.Numbers(1,60,1/20),List.Repeat(List.Numbers(1,20),3)}, {"Level","Index"})`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/2c9092ab-c354-129b-0440-b33d26b7ceb4.png)

### レベルとインデックスを記号化

**レベルを★表記に、インデックスを ABC 表記にした列を作成**しておきます。これらを数値のままアプリで使うと、設問・解答の数値と合わせて画面上が数値だらけになってわかりにくかったため。

`Text.Repeat("★", [Level])`

`Character.FromNumber(64 + [Index])`

（`{"A".."T"}` でもいけたっぽい）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/02f1f169-23e1-34f8-e534-e75bd468c47a.png)


### 足し算する数値の列

つづいて、**設問の足し算に使う数値の列を作成**します。

繰り上がりの計算はちょっと難しくなるので、**レベル1 では設問の答えが最大でも 10 になるように**、足す数の最大値を 5 に設定します。レベル2 では繰り上がりがあったりなかったり(3～10)、レベル3 では確実に繰り上がりが発生する(5～15) ように設計しました。

なお、このあとの `Number.RandomBetween`関数の処理で小数点以下を切り捨てる前提で、式における最大値は [設定したい値 + 1] になっています。

最小値 MinRand：`each if [Level] = 1 then 1 else if [Level] = 2 then 3 else if [Level] = 3 then 5 else null)`

最大値 MaxRand：`each if [Level] = 1 then 6 else if [Level] = 2 then 11 else if [Level] = 3 then 16 else null)`

**MinRand ～ MaxRand の範囲で、ランダムな数値列を 2つ作成**します。最小値と最大値の間でランダムな値を返す `Number.RandomBetween`関数を利用しました。

Rand1：`each Number.RoundDown(Number.RandomBetween([MinRand], [MaxRand]), 0))`

**`Number.RandomBetween`関数で返されるのは、整数ではなく小数**です。`Number.RoundDown`関数で小数点以下を切り捨てることで値を整数にしています。範囲内の整数を返す Excel の `RANDBETWEEN`関数とは挙動が異なるんですね。

https://learn.microsoft.com/ja-jp/powerquery-m/number-randombetween

https://support.microsoft.com/ja-jp/office/randbetween-%E9%96%A2%E6%95%B0-4cc7f0d1-87dc-4eb7-987f-a469ab381685

### ランダムな数値の和

**ランダムに作った数値の 2列を足し算**します。こういった単純な計算の場合は、対象の 2列を選択した状態で 列の追加 ＞ 標準 ＞ 加算 で作成できます。こういうときになぜかカスタム列の追加から手入力しちゃってたので、GUI も有効活用しないとです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/bcfd3a6c-f478-921e-0337-d10549fbe23a.png)

**解答の仕様の都合で、十の位と一の位の列を分けて持つ**必要があります。ここもいくつかの方法が考えられそうですが、下記のとおり実装。

十の位 AnswerSliced10：`each Number.IntegerDivide([AnswerAdd], 10)`
※列の追加 ＞ 標準 ＞ 除算 (整数)

一の位 AnswerSliced1：`each Number.Mod([AnswerAdd], 10)`
※列の追加 ＞ 標準 ＞ 剰余

### 正解時に表示されるメッセージ

**正解時に表示されるメッセージは、複数パターンを用意**しました。先ほどのように `Number.RandomBetween`関数を使ってランダムにしてもいいんですが、ここでは `Number.Mod`関数を使ってみました。正解の値を 4で割った時の余り(0～3)によって、4種類の異なるメッセージが表示されます。

Correct：
`each let mod4 = Number.Mod([AnswerAdd], 4) in if mod4 = 0 then "せいかい！" else if mod4 = 1 then "やったね！" else if mod4 = 2 then "いえーい！" else if mod4 = 3 then "はなまる！" else "")`

**`let` を使って「正解の値を 4で割った時の余り」の結果に名前をつける**形で、式を簡略化しました。Excel の `LET`関数や `LAMBDA`関数、Power Apps の `With`関数と同様、一時的な変数を作れるような機能です。

https://learn.microsoft.com/ja-jp/powerquery-m/expressions-values-and-let-expression

### メインのテーブル完成

ETL ツールでひたすらデータを「作る」という謎作業を終えて、こんな感じのテーブルが完成しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/bbeaa96c-29c7-ae37-e3a6-a72efe6dd6d5.png)

# 解答選択用のテーブル作成

解答するためのスライサーに用いるため、**十の位・一の位それぞれについて 0～9 の値を持ったテーブルを作成**します。選択肢のマスタのようなものです。中身は全く同じですが、十の位・一の位で別のスライサーとして機能する必要があるので、十の位・一の位それぞれ作成しています。

十の位解答用 Select10：`Table.FromColumns({List.Numbers(0,10)}, {"Select10"})`

一の位解答用 Select1：`Table.FromColumns({List.Numbers(0,10)}, {"Select1"})`

### リレーション作成

**メインのテーブルの `AnswerSliced`列とリレーション**を張ります。スタースキーマっぽくなってる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/dd32a478-49b4-0194-7487-2620a0c283ee.png)


# 設問選択

### レベル・設問スライサー

**表示対象のレコードを 1つに絞るために、レベルとインデックスのスライサーを配置**します。いずれも選択必須・単一選択です。右に置いた確認用のテーブルで、レコードが絞られていることを確認。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/f31059dd-e618-e9f2-1ff4-818b5d97c0c7.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/7d9e747b-cc2c-23ab-c215-2ecd3c221466.png)

### 設問の数値表示

**カードで Rand1・Rand2 を表示**しているだけです。＋と＝はテキストボックスで。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/32a48057-fe56-f7a3-2740-94266c55ae6b.png)

### (ボツ案：ブックマークで設問選択)

**ひとつの設問だけを表示するブックマークを作って、画面移動を実現しようとしましたが、断念**しました。ブックマークを 20個作るのがしんどかったので。

# 解答選択

### 解答選択スライサー

**解答選択用テーブルに作った Select10・Select1 列を使って、スライサーを 2つ配置**します。設問選択と同じく、選択必須・単一選択です。

2×5 になるようにサイズを調整。文字がこれ以上大きくできなかったり、選択時の色が指定できなかったり、普段はあまりこだわらないところですが、見た目の制約が意外と多いことに気づきますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/7ff4acce-a69f-6f2b-e94c-8045a577dd64.png)

### 選択した数字の表示

**選択した 2つの数字が 2ケタの数値に見えるように、カードを配置**します。この数値は常に表示されるものなので、マスタ側である Select10・Select1 を表示しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/b154db7f-c86c-7f50-acbe-1103058839fd.png)

**十の位については、0 が選択されているときは何も表示されない**ようにしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/a6e5ce21-9267-be72-2e07-a372314292fd.png)

**吹き出しの値のカラーの設定で、0 (か空白) のときは背景色と同じ色にする**ことで、表示されていないように見せています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/7ba13b6d-5d84-c4cc-03ef-5172329e7692.png)

### 正解判定メッセージ

**解答選択スライサーで選択した数字が、レコードの十の位・一の位とそれぞれ一致していれば、そのレコードの正解メッセージを表示**します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/262d0b25-d109-2c8c-8ffa-0f4b8936ecf8.png)

**スライサーの選択が間違っている場合、返されるレコードがない**ため、(空白)が表示されてしまいます。先ほどと同様、吹き出しの値のカラー設定で、空白の場合は背景色と同じ色に設定することで、非表示に見せています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/ae753078-8192-445a-25b7-63955edc5516.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/3cd29085-60d4-e743-4124-53cc8f71b609.png)

### 要改善：設問変更時に、解答がリセットされない

表題のとおりのため、**現在選択されている解答＝変更後の設問の答え になっている場合、設問変更した時点で正解メッセージが表示**されてしまいます。イケてない仕様ですが、解消方法が思いつかず…

### (ボツ案：Text Filterで解答)

**当初、Text Filter で解答する形にしようとしていましたが、断念**しました。

**Text Filterは完全一致ではなく部分一致**なので、たとえば 1 を入力すると、設問の答えが 10 でも 15 でも 21 でも正解扱いになってしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/3946129a-ca43-9d37-4961-d5495ecae305.png)

※Text Filter で扱えるように、数値型ではなくテキスト型にしています

完全一致に変更するオプションもなさそうだったので、数値入力の UI を作った次第です。

# ビジュアル

### ヘッダーアイコン

**すべてオフ**にします。好奇心旺盛な 3～5歳程度の子どもを想定ユーザーにしているので、余計な要素は表示させないようにしています。

### 相互作用

**スライサー操作時の相互作用を正しく設定する**必要があります。ここが一番の学びになったところかも。

**設問選択スライサーは全体の 60問から 1問をフィルターする役割**なので、他のほとんどのビジュアルをフィルターします。

**解答選択スライサーはその 1問(の正解メッセージ)を表示するかしないかを決める役割**です。そのため、フィルターする範囲は限定的。例えば、解答選択スライサー → 足す数の 1つ目 の相互作用を無効にしないと、フィルターされて該当レコードがない(空白) と表示されることになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/675e2113-0391-fbbc-d6dc-1935217d9f84.png)

また、**スライサーで選択できる項目はすべて固定**で、他のスライサーの選択に影響を受けません。

まとめると、下記のとおり。

* 設問選択スライサーは、他のスライサー以外をフィルターする
* 解答選択スライサーは、解答と正解メッセージのみフィルターする
* スライサー同士は、お互いをフィルターしない

**ビジュアルを追加したときは、上記のルールに従って各スライサーからの相互作用を編集する**必要があります。既存ビジュアルをコピーする方が楽ではありますが、思わぬところで不要な設定が残るリスクもあるので、一長一短なのかなと感じました。

### ヒント表示

**足す数と同じ分の丸を表示させ、丸を数えることで計算の助けにしよう**という意図です。

**必要に応じて表示・非表示を切り替えられるように、ブックマーク機能**を利用します。ヒントを表示した状態のブックマークと、ヒントが非表示のブックマークを作成し、ボタンで遷移できるようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/c203f116-d89a-b176-706e-f9c95cfb6276.png)

↑↓

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/ab4a607c-dd49-e9c2-2781-f40b474cd79e.png)

また、**遷移時に設問が変更されないように、ブックマークの「データ」のチェックは外しておきます**。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/ccb03d3c-0236-5e19-60b2-38c04417d950.png)

# モバイルレイアウト

**スマホで閲覧できるように、モバイルレイアウトを作成**します。レイアウトセンスは修業が足りん… ブックマーク機能でレベル・設問選択を常時表示させない形も検討しましたが、ユーザーの操作数を減らすことを優先して常時表示に戻しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/3417d74e-4844-caa0-b4d0-416efa0ba404.png)

# 長女(4歳)のテストプレイより

### 表示速度は問題ない

**集中力が途切れるようなロード時間は発生していない**ようでした。

### ヒント小さすぎ

ヒント表示ボタンは直感で理解してもらえたものの、**表示される丸が小さすぎて数え間違え**が起きていました。サイズをもっと大きくしたり、5・10 単位で区切ったり、といった工夫が必要そうです。
### ビジュアルのダブルタップで拡大される

Power BI モバイルアプリの標準機能で、**ビジュアルをダブルタップするとフォーカスモードになる**ようです。そのビジュアルだけが拡大表示され、元の画面に戻るには、左上の矢印ボタンか、端末の戻るボタンを押す必要があります。

![Screenshot_2024-12-11-04-06-13-79_5957ebf05589ce547f0347b86fd73533.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464218/9a347924-cfe9-6e25-69ee-2fd619e85936.jpeg)

さすがにこの機能をオフにすることはできなさそうなので、「変な画面になったら左上の矢印を押して」と長女に伝えて、運用でカバーすることにしました。

### 下方向への謎スクロール

スマホの画面内に収まるように余裕を持ってレイアウトしたはずなんですが、**下方向にちょっとだけスクロール**する余地あります。レベル選択ボタンの上半分が隠れるくらい。こういう余地を見つけると、すぐ子どもは上下に高速小刻みスクロールして遊んじゃうんですよね… 大きな影響はないものの、何が原因でスクロール余地ができているのかが気になります。

# おわりに

いろいろと諦めた点はあったものの、**とりあえず遊べるクイズアプリが作れました**。やってやれないことはない。

**今まであまり使っていなかった機能を触るきっかけ**になりました。相互作用の編集を細かくいじってみたり、モバイルレイアウトを作成してスマホで開いてみたりですね。

**Power Query でゼロからデータを作成する**、というあまり使わなさそうな技術も学べました。外部データソースに頼らずにデータが作れるようになれば、ちょっとした検証用途には役立つかも。

Power Query でのデータ作成では、**やりたいことを実現する方法はひとつではない**ことを実感できます。パフォーマンスを追求するなら、複数のやり方を考えて比較する必要も出てきますね。

一方で、**データ作成の部分は、メジャーでやるべきところもあったのかも**、と思っています。`ISSELECTEDMEASURE` とか `SELECTEDVALUE` とか `HASONEFILTER` とか、DAX でも担えそうな関数はあるので。

知育アプリはおとなしく Power Apps で作るとして、**Power BI はデータ可視化・分析のツールとして使う**べきですね(何をいまさら)。「時間の家計簿」に新しい項目(体重・ながらバイクの累積距離など)を追加しているので、それらを反映したアップデートもしたいなと。

https://qiita.com/AnPanda/items/9ec3964473a3215bcd49