動機
Power Queryでデータのグループ化(SQLで言うところのGROUP BY)を行う際、「集計する列を増やしたい」「合計も見たいけど、平均も見たくなった」といった感じで、変更があるたびにクエリを編集してメンドクサイと思ってました。
ということで、「集計の定義(キー項目や計算方法)をExcelのテーブルに記述して、Power Query側は一切触らずに集計結果を変える」 ということにチャレンジしました。
Power QueryのM言語は「関数を値として扱えるという性質」があるので、これを活用しています。
やりたいこと
-
データソース:
T_売上というテーブル。 -
設定ファイル:
T_CONFIGというテーブルに、「どの列をキーにするか」「どの列をどう集計するか(SUM, MAXなど)」を記述して、Table.Groupを外から制御します。 -
Power Query: 設定ファイルを読み込み、動的に
Table.Groupを生成・実行する。
テスト用売上データ(T_売上)
集計対象のテスト用データ。
| 地域名 | 都道府県 | 売上金額 | 点数 | 来店回数 | 日付 |
|---|---|---|---|---|---|
| 東北 | 青森 | 1,000 | 80 | 1 | 2025/1/31 |
| 東北 | 青森 | 2,000 | 90 | 5 | 2026/2/7 |
| 東北 | 青森 | 1,500 | 85 | 3 | 2026/2/7 |
| 関東 | 東京 | 5,000 | 60 | 2 | 2025/1/31 |
| 関東 | 東京 | 3,000 | 70 | 1 | 2026/2/7 |
| 東海 | 名古屋 | 8,000 | 100 | 10 | 2025/1/31 |
| 東海 | 名古屋 | 1,000 | 40 | 1 | 2026/2/7 |
設定テーブル(T_CONFIG)のイメージ
これが今回の 「Table.Groupを外から制御するテーブル」 です。
| 新しい列名 | 対象列 | 集計タイプ | 型 |
|---|---|---|---|
| (指定しなくてよい) | 地域名 | KEY | text |
| (指定しなくてよい) | 都道府県 | KEY | text |
| 売上合計 | 売上金額 | SUM | number |
| 最高スコア | 点数 | MAX | number |
| 最低スコア | 点数 | MIN | number |
| 平均来店数 | 来店回数 | AVG | number |
| 取引件数 | (指定しなくてよい) | COUNT | Int64 |
| 最終取引日 | 日付 | MAX | date |
-
KEY: グループ化のキー項目(複数指定可) -
SUM,MAX等: 集計関数 -
COUNT: 行数のカウント
M言語コード
詳細エディタに記述するコードです。
let
// 1. データと設定の読み込み
Source = Excel.CurrentWorkbook(){[Name="T_売上"]}[Content],
Config = Excel.CurrentWorkbook(){[Name="T_CONFIG"]}[Content],
// 2. 設定を「キー」と「集計」に分離
KeyRows = Table.SelectRows(Config, each [集計タイプ] = "KEY"),
AggRows = Table.SelectRows(Config, each [集計タイプ] <> "KEY"),
// グループ化キーをリストとして取得(Table.Groupの第2引数用)
GroupKeys = Table.Column(KeyRows, "対象列"),
// ★関数を「辞書(Record)」で管理する
// 3. 文字列と関数のマッピング辞書
FuncMap = [
SUM = List.Sum,
MAX = List.Max,
MIN = List.Min,
AVG = List.Average,
// COUNTはテーブル全体を受け取る関数として定義(後述のif分岐で使用)
COUNT = Table.RowCount
],
// 4. 文字列と型のマッピング辞書
TypeMap = [
number = type number,
Int64 = Int64.Type,
text = type text,
date = type datetime,
any = type any
],
// 5. 動的な集計リストの生成(ここが心臓部!)
Aggregations = List.Transform(
Table.ToRecords(AggRows),
(row) => {
// 第1要素: 新しい列名
row[新しい列名],
// 第2要素: 集計ロジック(関数)
(groupedTable) =>
// ★COUNT(テーブル操作)とSUM等(リスト操作)の分岐
if row[集計タイプ] = "COUNT"
then
// List.Countを使えばif文で分けなくても良いのですが、List.Countはnull値をカウントしないため、このようにしてます。
FuncMap[COUNT](groupedTable)
else
// 文字列から関数を取り出し、列データを抽出して適用
Record.Field(FuncMap, row[集計タイプ])(Table.Column(groupedTable, row[対象列])),
// 第3要素: 型(文字列から型オブジェクトへ変換)
Record.FieldOrDefault(TypeMap, row[型], type any)
}
),
// 6. グループ化の実行
Grouped = Table.Group(
Source,
GroupKeys, // 動的キー
Aggregations // 動的集計定義
)
in
Grouped
実行結果
このコードのポイント
1. 関数を「辞書(Record)」で管理する
M言語では、関数も 1 や "A" と同じようにデータとして扱えます。
FuncMap というレコードを作り、SUM という文字列キーに対して List.Sum という関数そのものを値として持たせています。
これにより、Record.Field(FuncMap, "SUM") とすることで、動的に関数を呼び出せるようになります。
2. リスト操作 vs テーブル操作
集計関数には2種類あります。
-
List系:
List.Sum([売上])のように、列(リスト) を引数に取るもの。 -
Table系:
Table.RowCount(_)のように、テーブル を引数に取るもの。
Table.Group の内部では、グループ化された「サブテーブル(groupedTable)」が渡されてきます。
そのため、コード内の ★ の部分で条件分岐を行っています。
-
COUNTの場合: サブテーブルをそのまま
Table.RowCountに渡す。 -
それ以外:
Table.Columnを使って、サブテーブルから特定の列(リスト)を引き抜いてからList.Sumなどに渡す。
Note:
List.Countを使えばList系に統一できるのでif分岐は消せるんですが、その場合List.Countはnullをカウントしないため、NULLを含む列を数える際にTable.RowCountと結果がズレる可能性がありまして、あえてif分岐してます。
3. 型を動的に指定する
type number などの型情報は、Excelのセルには書けません(ただの文字列 "number" になってしまう)。
これも TypeMap を用意することで、Excel上の文字列を Power Query の正しい型オブジェクトに変換しています。
Record.FieldOrDefault を使うことで、未知の型が来た場合はとりあえず type any で逃げてます。
