Azure Data Factory の Mapping Data Flow で CSV ファイルの重複行を削除する

# 本記事のテーマ
タイトルの通り、Azure Data Factory の Mapping Data Flow で CSV ファイルの重複行を削除する方法について記載します。サンプルのユースケースは以下の通りです。

- 複数に分かれた顧客情報 (customers) の CSV ファイルがあり、それらを 1 つのファイルにマージする
- ただし、重複する `ID` を持つレコードが存在する可能性がある
- `ID` が重複する場合、更新日時が新しいレコードを残して他のレコードは削除したい

# 入出力ファイルのイメージ
本記事では `Input: Azure Data Lake Storage Gen2 (ADLS Gen2) -> Process: Mapping Data Flow -> Output: ADLS Gen2` という流れを例にしたいと思います。

## 入力ファイルのイメージ
あるディレクトリの配下に同じレイアウトの CSV ファイルが複数ある想定にしています。

* **customers1.csv**
    * ストレージアカウント: stnakazaxqiitadf (ADLS Gen2)
    * コンテナ: customers-container
    * ファイルパス: /input/customers1.csv

```
"ID","Email","Name","Updated At"
"1","yamada.hanako@example.com","Yamada Hanako","2019-08-31 00:00:00 +0900"
"2","tanaka.taro@example.com","Tanaka Taro","2019-08-31 00:00:00 +0900"
```

* **customers2.csv**
    * ストレージアカウント: stnakazaxqiitadf (ADLS Gen2)
    * コンテナ: customers-container
    * ファイルパス: /input/customers2.csv

```
"ID","Email","Name","Updated At"
"3","adam@contoso.com","Adam","2019-09-01 00:00:00 +0900"
"4","ben@contoso.com","Ben","2019-10-01 00:00:00 +0900"
```

* **customers3.csv**
    * ストレージアカウント: stnakazaxqiitadf (ADLS Gen2)
    * コンテナ: customers-container
    * ファイルパス: /input/customers3.csv

```
"ID","Email","Name","Updated At"
"5","chris@contoso.com","Chris","2020-01-01 00:00:00 +0900"
"6","david@contoso.com","David","2020-02-01 00:00:00 +0900"
```

* **customers4.csv**
    * ストレージアカウント: stnakazaxqiitadf (ADLS Gen2)
    * コンテナ: customers-container
    * ファイルパス: /input/customers4.csv

```
"ID","Email","Name","Updated At"
"1","tanaka.hanako@example.com","Tanaka Hanako","2020-04-15 00:00:00 +0900"
"3","adam@fabricam.com","Adam","2020-04-15 00:00:00 +0900"
"5","chris@fabricam.com","Chris","2020-04-15 00:00:00 +0900"
"7","elvis@fabricam.com","Elvis","2020-04-15 00:00:00 +0900"
```

## 出力ファイルのイメージ
* ストレージアカウント: stnakazaxqiitadf (ADLS Gen2)
* コンテナ: customers-container
* ファイルパス: /output/merged_customers.csv

```
"ID","Email","Name","Updated At"
"1","tanaka.hanako@example.com","Tanaka Hanako","2020-04-15 00:00:00 +0900"
"2","tanaka.taro@example.com","Tanaka Taro","2019-08-31 00:00:00 +0900"
"3","adam@fabricam.com","Adam","2020-04-15 00:00:00 +0900"
"4","ben@contoso.com","Ben","2019-10-01 00:00:00 +0900"
"5","chris@fabricam.com","Chris","2020-04-15 00:00:00 +0900"
"6","david@contoso.com","David","2020-02-01 00:00:00 +0900"
"7","elvis@fabricam.com","Elvis","2020-04-15 00:00:00 +0900"
```

# Mapping Data Flow での処理フロー例
Mapping Data Flow の処理フロー例は以下の通りです。色々なやり方があると思いますので、これが絶対の正解という訳ではなく方法の一つだと捉えて頂ければと思います。

No. | 処理内容 | 利用する組み込み機能
--- | --- | ---
1 | 入力ファイルを読み込み | [ソース変換](https://docs.microsoft.com/ja-jp/azure/data-factory/data-flow-source)
2 | `ID` と `Updated At` の最大値を出力 | [集計変換](https://docs.microsoft.com/ja-jp/azure/data-factory/data-flow-aggregate)
3 | 1 と 2 のストリームを `ID` と `Updated At` をキーにしてジョイン | [結合変換](https://docs.microsoft.com/ja-jp/azure/data-factory/data-flow-join)
4 | 結果ファイルを出力 | [シンク変換](https://docs.microsoft.com/ja-jp/azure/data-factory/data-flow-sink)

以下のキャプチャは Mapping Data Flow で処理フローを組み上げたときの完成形のイメージです。
以降では各フローの簡単なポイントについて紹介します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284925/464b5ab2-b2ad-665c-e659-b47145d927fc.png)

## ソース変換
ソース変換で入力ファイルを指定する際にワイルドカード `*` が使えます。
以下のように指定すると `/input` ディレクトリの `customers1.csv` `customers2.csv` ... `customersN.csv` のようにまとめて読み込めます。
ワイルドカードパスはオプションなので省略可能です。省略した場合はデータセットで指定したディレクトリ直下の全ファイル、またはデータセットで指定したファイルが読み込まれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284925/4567f1ea-821c-56ea-c27a-8c1cd7818b4d.png)

## 集計変換
[Aggregate setting] タブで [Group by] の [Columns] に `ID` を指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284925/c039cab0-c64a-f734-63f1-8fc07780c19e.png)

[Aggregates] では `first()` `last()` `min()` `max()` などの集計関数が使えます。ここでは `max({Updated At})` で最新の更新日時を選択しています。なお、 `{}` はカラム名が空白を含む場合に指定する必要があります。空白を含まない場合は不要です。
式の左側のテキストボックスで集計関数の結果を格納するカラム名を指定できるのですが、元々のカラム名と同じ `Updated At` を指定している理由は後述します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284925/a7e7614a-dded-2cb1-4ff0-9ff417e07f8b.png)

Data flow debug をオンにして [Data preview] タブを見ると以下のように一意な `ID` と `Updated At` の最大値のペアが出力されているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284925/56bd9198-2502-b84f-4501-59af2ee27468.png)

## 結合変換
1 と 2のストリームを入力として `ID` と `Updated At` が一致するレコード同士をジョインします。ジョインタイプは `Inner` を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284925/0d2a8f7b-d03c-34ca-3bc3-35f06e91160d.png)

Data flow debug をオンにして [Data preview] タブを見ると以下のように一意な `ID` かつ最新の `Updated At` を持つレコードのみが表示されるはずです。なお、 `ID` 列と `Updated At` 列が二重で表示されていますが、シンク変換で一列ずつに絞り込めるためここでは無視して OK です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284925/0a9d90a7-2a3a-5554-5a14-00a79fef2833.png)

## シンク変換
[Inspect] タブの [Input] のカラムを見ると `ID` と `Update At` が二重で表示されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284925/127227ca-d520-ad91-250b-9ddfccd5545a.png)

[Mapping] タブで [Skip duplicate input columns] にチェックを入れると、入力ストリームに重複するカラムが存在する場合、1 つのカラムのみに絞り込むことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284925/f1ea0524-fbc3-3315-0ba9-d515efa2cb23.png)

Data flow debug をオンにして [Data preview] タブを見ると以下のような結果が出力されているはずです。基本はこれで問題ないと思いますが、例えば `ID` 昇順で並べ替えたい場合は [並べ替え変換](https://docs.microsoft.com/ja-jp/azure/data-factory/data-flow-sort)、カラムの並び順を変更したい場合は [選択変換](https://docs.microsoft.com/ja-jp/azure/data-factory/data-flow-select) をシンク変換の前に行うと良いでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/284925/007eacae-3353-efd8-5368-b2232950695a.png)

# 参考サイト
- [Azure Data Factory Data Flow: Data Deduping - YouTube](https://www.youtube.com/watch?v=OLenvYwg__I)
- [marksadfrepo/Dedupe.json at collab · kromerm/marksadfrepo](https://github.com/kromerm/marksadfrepo/blob/collab/dataflow/Dedupe.json)