動機
Power Queryでデータを加工していると、連続した値をある程度の区間に分けて(「連続値の区分け(ビニング)」)、その区間でGROUP BYして集計したくなります。
- 「年齢」を「10代, 20代...」に分けたい
- 「点数」を「赤点, 合格...」に分けたい
- 「売上」を「ランクA, B...」に分けたい
で、これが起きるたびに、毎度詳細エディタを開いて、Table.AddColumn と if 文を書いたり、マスタとのマージ処理を書き足していてメンドクサイなぁと。
設定テーブル駆動によるアプローチ
「区分けのルールが増えるたびに、クエリ自体を修正しないですむ方法はあるか?」 と考えまして、先の【Power Query】集計定義をExcelシートに外出してTable.Groupをクエリの外から制御すると同じ考え方で、設定テーブルを軸にして、「設定」と「ロジック」を分離する方式を取りました。
この検討の中で、List.Accumulate という関数が今回の主役なのですが、結構面白いところがありまして、今回の投稿になりました。
1. 用意したテーブル構造
まず、以下のような「設定テーブル( T_BIN_CONFIG )」をExcel上に作ります。これが今回の司令塔です。
| 対象列 | マスタテーブル名 | 新しい列名 |
|---|---|---|
| 年齢 | T_M_年齢区分 | 年齢層 |
| 点数 | T_M_点数区分 | 成績 |
- 対象列: 元データのどの列を区分けするか
- マスタテーブル名: どの境界値テーブル(0~30は"小"など)を使うか
- 新しい列名: 結果を何という列名で出力するか
この行を増やすことで、Power Queryが自動的に列を追加してくれるようにします。
2. 実装したM言語( List.Accumulate )
今回の主役は List.Accumulate です。検索エンジンで検索すると、通常は「数値の合計」などの例で語られることが多い関数なのですが、今回は 「テーブル変数の更新」 に使います。
let
// 1. 各種データの読み込み
Source = Excel.CurrentWorkbook(){[Name="T_テスト結果"]}[Content],
Config = Excel.CurrentWorkbook(){[Name="T_BIN_CONFIG"]}[Content],
// 2. ループ処理の実行 (List.Accumulate)
// Configテーブルを1行ずつ読み込みながら、テーブル(state)を育てていく
ResultTable = List.Accumulate(
Table.ToRecords(Config), // ループするリスト(設定情報)
Source, // 初期状態(種となるテーブル)
// --- ループ処理の中身 (state:現在のテーブル, currentConfig:今の設定行) ---
(state, currentConfig) =>
let
// A. マスタテーブルを名前から動的に読み込む
// Excelブック内から "T_M_年齢区分" などの名前でテーブルを探す
RawRangeTable = Excel.CurrentWorkbook(){[Name=currentConfig[マスタテーブル名]]}[Content],
RangeTable = Table.Buffer(RawRangeTable), // 高速化のためバッファ必須
// B. 列の追加(ビニング処理)
AddedColumn = Table.AddColumn(state, "TempBin", (record) =>
// マスタから条件に合う行を探す (>= 下限 and < 上限)
Table.SelectRows(RangeTable, each
Record.Field(record, currentConfig[対象列]) >= [下限] and
Record.Field(record, currentConfig[対象列]) < [上限]
)
),
// C. テーブルを展開して値を確定させる
Expanded = Table.ExpandTableColumn(AddedColumn, "TempBin", {"区分名"}),
// D. 列名をコンフィグ指定の名前に変更 (例: "区分名" -> "年齢層")
Renamed = Table.RenameColumns(Expanded, {{"区分名", currentConfig[新しい列名]}})
in
Renamed
)
in
ResultTable
実行結果はこんな感じ
面白い発見:reduce List.Accumulateで「テーブルを育てる」という感覚
この実装を通じて面白かったのは、List.Accumulate の Accumulate は プリミティブな値だけ でなく テーブルでもよい という点です。
List.Accumulateは、javascriptなどの一般的な関数型プログラミング言語の reduce 関数(数値の合計など)に相当する関数です。
reduce関数は、こんな感じのバケツリレーするようなイメージです。
- 初期値
0に1を足す →1に2を足す → 結果は3(数値)
で、今回の実装ではバケツの中身(Accumulator)が 「テーブルそのもの」 なのです。
-
初期状態:
[名前, 年齢, 点数]の3列 -
1周目: 設定1行目(年齢)を読み込み、 列を追加
→[名前, 年齢, 点数, *年齢層*]に成長 -
2周目: 設定2行目(点数)を読み込み、 さらに列を追加
→[名前, 年齢, 点数, 年齢層, *成績*]に成長
状態(State)としてのテーブル
図にすると、前のループで作られたテーブルが、次のループの入力(currentTable)としてバケツリレーされていることがわかります。
M言語で関数型プログラミングするのって、結構面白い?!
出発点は「M言語を毎回修正したくない」という、毎度のメンドクサイのをどうにかしたいでしたが、 累積器 (イミュータブルなのに変数というのはおかしな感じがなので、累積器 と呼びます)に、数値などの単純な値を累積するだけでなく、テーブルそのものを累積できる という、関数型言語らしい発見があって、面白かったです。
