Power Query へそのゴマ　第７章　エラーハンドリングとデバッグ戦略

この章では、Power Queryでデータ処理を行う際に不可欠なエラーハンドリングとデバッグのテクニックについて学びます。クエリの実行中にエラーが発生した場合、その原因を特定し、適切に対処することで、データの整合性やクエリの安定性を保つことができます。特に、エラーが多発しやすい大規模なデータセットや複雑なクエリにおいて、エラーハンドリングとデバッグは重要なスキルです。

Power Queryには、大きく2種類のエラーがあります。

# 7.1. ステップレベルのエラー

## 7.1.1 エラーの表示

ステップレベルのエラーが発生すると、エラーの内容が黄色で表示され、後続のステップの処理は行われません。

```
let
    Source = "0",
    AddOne = Source + 1,
    Result = AddOne & "X"
in
    Result
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/b38ae4f0-491a-d307-faac-e4f4746efd80.png)

この場合、原因は数値の１と文字の０を＋演算子で処理しようとしたためです。

エラーメッセージの右の「エラーに移動する」をクリックすると、エラーが発生しているステップに移動します。

## 7.1.2. 一般的なステップレベルのエラー

詳細エディタでコードを記述している段階で検出されるエラーがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/3b3aae6c-0351-1622-eeb5-09394b100043.png)

エラーが検出されない場合、「構文エラーが検出されませんでした」と表示されますが、詳細エディターが式のパターンや構造に問題を見つけられなかったというだけです。エラー検出の精度は高くありません。

その他、代表的なエラーには以下のようなものがあります。

- DataSource.Error : ユーザーがデータソースにアクセスできない場合発生
- 識別子が認識できない：参照している識別子がみつからない
- 不明な関数：関数の綴りが間違っているか、存在しない関数が使われている
- 不明な列参照：参照する列名の綴りが間違っているか、既に削除・変更されている
- 不明なフィールド参照：指定されたフィールドの綴りが間違っているか、既に削除・変更されている
- 列挙内の要素が不足：`{1,2,3}`のリストから4番目のアイテムを抽出しようとするような場合
- Formula.Firewall : 複数のデータを扱う時、データプライバシーレベルに関連してエラーが発生することがある
- Expression.Error: キーがテーブルのどの行とも一致しませんでした
```
let
    Source = Table.FromRows(
        {
            {"YourTable", "Table", #table(2,{{1,2}}) },
            {"YourTable2", "Table", #table(2,{{1,2}}) }
        }, 
        {"Item", "Kind", "Data"}
    ),
    Navigation = Source{[Item="myTable", Kind="Table]"]}[Data] 
    //                        ~~~~~~~~~ 
    // Item列に myTable は存在しない。YourTable に変更すればエラーは発生しない
in
    Navigation
```
- Expression.Error: キーがテーブルの複数の行に一致しました
```
let
    Source = Table.FromRows(
        {
            {"YourTable", "Table", #table(2,{{1,2}}) },
            {"YourTable2", "Table", #table(2,{{1,2}}) }
        }, 
        {"Item", "Kind", "Data"}
    ),
    Navigation = Source{[Kind="Table"]}[Data]
    //                   ~~~~~~~~~~~~
    // Kind列に Table は複数存在する
in
    Navigation
```
- Expression.Error: 評価でスタック オーバーフローが発生したため、続行できません
```
// 再帰関数に終了条件がないため、利用可能メモリが不足した
let
    NoEnd = (x) => @NoEnd (x + 1)
in
    NoEnd(0)
```

# 7.2. セルレベルのエラー

## 7.2.1. エラーの表示

セルレベルのエラーが発生するとセルに`Error`と表示されます。しかし、続くステップの処理は継続されます。つまり、エラーが発生した場合にできるだけトップレベルの士気に伝搬せず、その影響を限定的な範囲内にとどめる **エラーの包含（Error Containment）** が行われます。

```
let
    // サンプルデータの作成
    Source = Table.FromRows(
        {
            {"1", "123"},
            {"2", "ABC"},
            {"3", "456"}
        },
        {"ID", "Value"}
    ),

    // データ型を設定
    ChangedType = 
        Table.TransformColumnTypes(
            Source, 
            {
                {"ID", Int64.Type}, 
                {"Value", Int64.Type}
            }
        ),
    #"Added Index" = Table.AddIndexColumn(ChangedType, "インデックス", 1, 1, Int64.Type)
in
    #"Added Index"
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/1c934577-af46-123c-1cdb-dbbb9f4bed67.png)

`Error`と表示されたセルをクリックすると、エラーメッセージを確認することができます。

## 7.2.1. データプロファイルツール

データプロファイルツールを使用すると、列の品質機能でセルレベルのエラーを識別できます。

### 列の品質

「表示」メニューから「列の品質」のチェックボックスをオンにすると、データの列名の下にデータの品質に関する情報が表示されます。品質は、「有効」「エラー」「空」の3種類に分類され、それぞれ何パーセントあるか表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/a642692b-75d1-cd8c-eb15-7e7a79216a5f.png)

### 列の分布

「列の分布」をオンにすると、各列の値の頻度と分布を示すビジュアルが列名の下に表示されます。表示は、頻度が高い値から降順に並び替えされています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/f6a17234-b8d9-1840-00eb-5ffa6ba287ca.png)

ビジュアルの上にカーソルを置くと、更に情報が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/cfb4f568-1e33-8ed6-3e83-19c1f916ca8f.png)

### 列のプロファイル

「列のプロファイル」をオンにすると、列内のデータの詳細を確認できます。「列の品質」「列の分布」の情報に加え、詳しい統計情報がデータプレビューセクションの下に表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/d7f1d000-eee6-1f34-d096-08517a91470b.png)

値分布グラフをクリックし、値をフィルターするステップを追加することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/57e4d3da-363c-7433-ce9a-e05c4d498332.png)

右クリックして、更に詳しい処理を追加することもできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/1b80a3bf-d65b-907d-6d95-3fdc13360b74.png)

値分布グラフの右上隅にある省略記号ボタンをクリックすると、「コピー」と「グループ化」を選択することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/8bca3173-a6b8-36c1-6932-f7e1ac88f7c0.png)

::: note warn
ただし、このプロファイリングは、データの上位1000行に対してのものです。パフォーマンスを犠牲にしてもデータ全体のプロファイリングを見たい場合は、左下の「上位1000行に基づく列のプロファイリング」をクリックし、「データセット全体に基づく列のプロファイリング」に変更します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/8fd81901-82e4-0fba-b832-32bb56bc8f41.png)
:::

# 7.3. エラーハンドリングの基本テクニック

エラーが発生した際に、適切に対処するためのエラーハンドリングテクニックを学びます。これにより、エラー発生時のクエリの挙動をコントロールし、データ処理をスムーズに進めることができます。

## 7.3.1. `try`と`otherwise`の活用

`try`を用いることで、特定の処理でエラーが発生した場合にエラー内容をキャプチャし、`otherwise`で設定したデフォルト値を使って処理を続行できます。これにより、エラー発生時の処理を制御可能になります。

以下の例では、日付変換に失敗した場合、デフォルトの日付（例: "2024-01-01"）を設定することで、データの一貫性を保ちながらエラーハンドリングを行います。

```
let
    Source = 
        Text.Combine(
            {
                "ID,Name,Date",
                "1,Alice,2024-11-25",
                "2,Bob,2024-8-5",
                "3,Charlie,2024/6/2",
                "4,Chris,""25th November 2024"""
            },
            "#(cr)#(lf)"
        ),
    CsvData = Csv.Document(Source),
    #"Promoted Headers" = Table.PromoteHeaders(CsvData, [PromoteAllScalars=true]),
    AddColumn = 
        Table.AddColumn(
            #"Promoted Headers",
            "Change Type",
            each 
            // try - otherwise を使った処理
                try
                    Date.FromText(_[Date])
                otherwise
                    #date(2024,1,1),
            type date
        )
in
    AddColumn
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/83bb546b-82cd-16ea-4b71-b12f800f97c2.png)

## 7.3.2. `try` と `catch` の活用

`catch` を使うと、エラー情報を更に詳細に捕まえることができます。

`catch`の後に続くカッコ内に識別子を持つ場合と、持たない場合の２通りの書き方があります。識別子を持たない場合、上記 try otherwise と同様の処理になります。

```
let
    Source = 
        try "foo" + 1
        catch() => 0
in
    Source
```

文字列と数値を足し算しようとしてエラーになりますが、`catch() =>` で設定された値 `0` が返されます。

`catch`を使って、エラーの情報を取得したい場合は、以下のように書きます。

```
let
    Source = 
        try "foo" + 1
        catch(e) =>
            e[Message]
in
    Source
```

結果は「演算子 + を型 Text および Number に適用できません。」と表示されます。

## 7.3.3 HasErrorを使ってエラーを検知する

エラーになるかどうかを判定するには`HasError`を使って調べます。

以下のコードでは、文字列を数値に変換を行いますが、4番目がエラーになります。

```
let
    Source = {"1", "2", "3", "a"},
    Result = List.Transform(
        Source,
        Number.FromText
    )
in
    Result
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/df0151d8-6868-71ff-5e60-bfa081586317.png)

値が数値かどうか判定する処理は、以下の様になります。

```
let
    Source = {"1", "2", "3", "a"},
    Result = List.Transform(
        Source,
        each (try Number.FromText(_) )[HasError]
    )
in
    Result
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/e7432713-a8e5-fa37-64f3-1150471b79cc.png)

## 7.3.4 エラーメッセージの表示とカスタムエラーメッセージ

### エラー時にメッセージを表示する

クエリ内でエラーが発生した場合に、標準のエラーメッセージをカスタマイズして表示することで、原因の特定が容易になります。

```
let
    Source = Table.FromRows(
        {
            {"1", "2024/11/1"},
            {"2", "2024/12/1"},
            {"3", "2024/13/1"}
        },
        type table [ID = text, Date = text]
    ),
    Result = Table.TransformColumns(
        Source,
        {
            {
                "Date",
                each try Date.FromText(_) otherwise "日付に変換できません"
            }
        }
    )
in
    Result
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/08d66575-bbd3-3edc-6fe5-5626716fff34.png)

上記は、エラーが発生した理由を文字列で返すようになっています。

### `Error.Record` 関数を使う

次の例は、`Error.Record`関数を使用し、カスタムエラーメッセージを含むエラーオブジェクトを生成します。

```
let
    Source = Table.FromRows(
        {
            {"1", "2024/11/1"},
            {"2", "2024/12/1"},
            {"3", "2024/13/1"}
        },
        type table [ID = text, Date = text]
    ),
    Result = Table.TransformColumns(
        Source,
        {
            {
                "Date",
                each 
                    try Date.FromText(_) 
                    otherwise 
                        Error.Record(
                            "カスタムエラー", 
                            "日付に変換できません", 
                            [Details = "詳細情報：日付書式エラー"]
                        )
            }
        }
    )
in
    Result
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/c99cf144-d7da-5304-f242-47d6a8875499.png)

## 7.3.5 意図的にエラーを発生させる

`error` を呼び出し、表示したエラーメッセージをつけます。

```
let
    Source = Table.FromRows(
        {
            {"1", "2024/11/1"},
            {"2", "2024/12/1"},
            {"3", "2024/13/1"}
        },
        type table [ID = text, Date = text]
    ),
    Result = Table.TransformColumns(
        Source,
        {
            {
                "Date",
                each 
                    try Date.FromText(_) 
                    otherwise error "日付に変換できません"
            }
        }
    )
in
    Result
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/22d997f2-6e20-91a7-206e-f578e491f4a1.png)

`error`は、実際にエラーが発生しない条件においてもエラーを発生させることができます。

```
let
    Source = Table.FromRows(
        {
            {"1", "2024/11/1"},
            {"2", "2024/12/1"},
            {"3", "2024/12/24"}
        },
        type table [ID = text, Date = text]
    ),
    Result = Table.TransformColumns(
        Source,
        {
            {
                "Date",
                each 
                    if _ = "2024/12/24"
                    then error "クリスマス"
                    else Date.FromText(_)
            }
        }
    )
in
    Result
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/72cc5646-2c21-9298-f03e-4b7471409d66.png)

また、エラー記録には `Reason` `Message` `Detail` などの詳細情報を含めることができます。

```
let
    Source = Table.FromRows(
        {
            {"1", "2024/11/1"},
            {"2", "2024/12/1"},
            {"3", "2024/12/24"}
        },
        type table [ID = text, Date = text]
    ),
    Result = Table.TransformColumns(
        Source,
        {
            {
                "Date",
                each 
                    if _ = "2024/12/24"
                    then error 
                        [
                            Reason = "クリスマス",
                            Message = "クリスマスを指定してはいけません",
                            Detail = [Date = "2024/12/24"]
                        ]
                    else Date.FromText(_)
            }
        }
    )
in
    Result
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/b42f9b74-2429-bd16-e11b-fca03786fb40.png)

更に詳しい内容は、以下も参照してください。

https://qiita.com/spumoni/items/cd5843e0104fed9b4874

# 7.4. デバッグツールの活用方法

Power Queryには、クエリのステップごとに処理結果を確認し、エラーの原因を特定するためのデバッグツールが備わっています。

## 7.4.1. ステップごとのエラー確認

  Power Queryエディター内のステップバーで、各ステップの処理結果を確認し、エラーが発生した場所を特定できます。複雑な処理を複数のステップに分割し、処理結果をステップごとに確認することで、エラー原因を細かく調査します。

### ステップの名前を変更する

ステップが作成されると、デフォルトで汎用的な名前が割り当てられます。この名前は、そのステップが具体的に何をしているのかわかりにくいものになっています。より説明的な名前に変更することで、全体的な処理の流れをつかむことができ、コードの読みやすさと理解しやすさが向上します。

将来クエリを修正する必要がある場合、ステップが適切に命名されていると、修正箇所を見つけるのがずっと簡単になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/419b95e9-5767-8365-2afa-bb6b0722f162.png)

### ステップにコメントをつける

各ステップにコメントをつけておくと、問題の解決が速くなります。

例えば、ステップのプロパティを設定することで以下のようにコードにコメントを付けることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/0e0eddb5-3394-154c-0af3-44412b417cb3.png)

```
let
    Source = 1,
    // 1を加える
    Second = Source + 1,
    // 2を加える
    Third = Second + 2,
    // 結果
    Result = Third
in
    Result
```

詳細エディターで直接記載することもでき、書かれたコメントは以下のようにステップ名右のアイコンにカーソルを置くと表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/935f4e16-b850-dffe-c8d9-e98155a446ba.png)

### 複雑な式を管理しやすい単位に分解する

M言語は、関数型の言語なので、複雑なネストした式を作成することができます。これは、非常に強力な機能ですが、問題が発生したときにデバッグするのが難しい場合があります。

複雑な式は、各部分を個別に隔離し、テストできるようにしておくと、問題の根本原因を特定しやすくなります。

その方法としては、別のクエリで関数を作成したり、同一コード中に関数を作成するなどの方法があります。

```
let
    // カスタム関数：文字列を数値に変換して＋１する
    MyFunction = (n as text) as number =>
        let
            Value = Int64.From(n),
            Result = Value + 1
        in
            Result,

    // テーブルを作成
    Source = Table.FromRows(
        {
            {"1", "123"},
            {"2", "456"},
            {"3", "789"}
        },
        type table [ID = text, Value = text]
    ),

    // テーブルの Value 列にカスタム関数を適用
    TableTransform = 
        Table.TransformColumns(
            Source,
            {
                {
                    "Value",
                    each MyFunction(_)
                }
            }
        )
in
    TableTransform
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/d34490b1-234d-48c4-2dbd-2e89a9f0b40c.png)

## 7.4.2. 「データのプレビュー」を使ったデバッグ

データのプレビューを使用して、クエリの各ステップでどのような変換が行われたかを視覚的に確認します。これにより、データの変換が期待通りに行われているかを検証します。

データのプレビューでエラーデータを迅速に見つけ出し、詳細なデバッグが可能です。

## 7.4.3. クエリの実行時間の確認

クエリの実行時間を確認し、処理が重いステップを特定することで、パフォーマンス最適化の候補を見つけます。

複数の集計処理が行われるクエリで、特に時間がかかるステップを特定し、処理の順序を再編成して最適化します。

# 7.5. エラーログの作成と管理

Power Queryで発生したエラーを記録し、後で確認できるようにするためにエラーログを作成します。エラーログを用いることで、定期的にエラーログを確認し、繰り返し発生するエラーの修正やクエリの改善を行います。

また、必要に応じてエラーログを整理し、古いエラー記録を削除することで、ログの可読性と管理性を向上させます。

## 7.5.1. エラーログ用のクエリ作成

```
let
    // サンプルデータ（数値として変換できない値を含む）
    Source = Table.FromRows({
        {"123"},
        {"45.67"},
        {"abc"},   // 数値に変換できない値
        {"789"},
        {"1.23e5"},
        {"error"}  // 数値に変換できない値
    }, {"Input"}),

    // 文字列を数値に変換する関数
    ConvertToNumber = (input as text) =>
        try Number.FromText(input) 
        otherwise Error.Record("Conversion Error", "数値に変換できません", [Value = input]),

    // 数値に変換する列を追加
    AddConvertedColumn = Table.AddColumn(Source, "Converted", each ConvertToNumber([Input])),

    // エラー行のみを抽出
    ErrorRows = Table.SelectRows(AddConvertedColumn, each Value.Is([Converted], type record)),

    // エラーログを整形
    ErrorLog = Table.TransformColumns(ErrorRows, {"Converted", each Record.ToTable(_)})
in
    ErrorLog
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/f34eb71a-874e-1529-05b5-bbccc5d60382.png)


## 7.5.2. エラーの種類に応じたログ分類

エラーログ内でエラーを種類別に分類し、分析や原因特定を効率化します。たとえば、型エラーや参照エラーなど、発生原因ごとに分類します。

```
let
    // サンプルデータ
    Source = Table.FromRows({
        {"123"},   // 正常データ
        {"abc"},   // 数値変換エラー
        {null},    // NULLデータエラー
        {"45.67"}, // 正常データ
        {"error"}, // 数値変換エラー
        {"1.2e3"}  // 正常データ
    }, {"Input"}),

    // カスタムエラーチェック関数
    ProcessData = (input as nullable text) as any =>
        try
            if input = null then 
                Error.Record("Null Value Error", "値がNULLです", [Value = input])
            else if not Text.Contains(input, "error") and Number.FromText(input) = null then
                Error.Record("Conversion Error", "数値に変換できません", [Value = input])
            else
                Number.FromText(input)
        otherwise 
            Error.Record("General Error", "予期しないエラー", [Value = input]),

    // データを変換しつつエラー処理
    ProcessedTable = Table.AddColumn(Source, "Processed", each ProcessData([Input])),

    // エラー行を抽出
    ErrorRows = Table.SelectRows(ProcessedTable, each Value.Is([Processed], type record)),

    // エラー分類列を追加
    ErrorWithType = Table.AddColumn(ErrorRows, "ErrorType", each [Processed][Reason]),

    // エラーログを整形
    ErrorLog = Table.TransformColumns(ErrorWithType, {"Processed", each Record.ToTable(_)})
in
    ErrorLog
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/864075/94735ce3-76ed-325f-aef2-57f7438c44c8.png)

https://learn.microsoft.com/ja-jp/power-query/dealing-with-errors

https://qiita.com/spumoni/items/d3b127c7b326497e20df