CSV データを danfo.js の Data Frame に読み込むベストプラクティス

# CSV データの読み込みは簡単だが意外と奥が深いです

日本語を含む CSV をデータセットとして読み込むうえで、以下の2点が問題となります。
- 日本語コードに自動対応したい
- カラム名が空白だったり重複していたり

これらを回避する方法について述べます。
私なりにはベストプラクティスと考えています。

## ローカルの CSV を danfo.js の Data Frame に読み込む際の問題点

まずは、[danfo.js 公式](https://danfo.jsdata.org/api-reference/input-output/danfo.read_csv)に掲載のコードを実行してみます。

ライブラリ込みの html にしてありますので、
[jsfiddle](https://jsfiddle.net/) などにコピペして簡単に試せます。

```HTML:HTML
<script src="https://cdn.jsdelivr.net/npm/danfojs@1.1.2/lib/bundle.min.js"></script>

<body>
    <input type="file" id="file" name="file">
    <script>
     	const inputFile = document.getElementById('file');
        inputFile.addEventListener("change", async () => {
            const csvFile = inputFile.files[0]
            dfd.readCSV(csvFile, { header: true }).then((df) => {
                df.print()
            })
        })
    </script>
</body>
```

### 空白カラムや重複カラムのあるデータを読み込んでみる

これで、次の CSV (UTF-8) を読み込んでみましょう。
空白のカラムが2個と、そのほかに重複するカラムが2種類あります。

```CSV:CSV
"","","あ","あ","あ","い","い","う"
"11","12","あ11","あ12","あ13","い11","い12","う11"
"21","22","あ21","あ22","あ23","い21","い22","う21"
```

#### 結果 (一番左の列は行番号)
```text:CONSOLE
╔════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╗
║            │                   │ あ                │ い                │ う                ║
╟────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 0          │ 12                │ あ13              │ い12              │ う11              ║
╟────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 1          │ 22                │ あ23              │ い22              │ う21              ║
╚════════════╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╝
```

空白も含めて、カラム名が重複していると、先のデータが後のデータで上書きされてしまいます。

### SHIFT-JIS にして読み込んでみる

次に、先ほどのファイルを SHIFT-JIS で保存してから読み込んでみましょう。

#### 結果
```text:CONSOLE
╔════════════╤═══════════════════╤═══════════════════╗
║            │                   │ ��                ║
╟────────────┼───────────────────┼───────────────────╢
║ 0          │ 12                │ ��11              ║
╟────────────┼───────────────────┼───────────────────╢
║ 1          │ 22                │ ��21              ║
╚════════════╧═══════════════════╧═══════════════════╝
```

文字化けしています。しかも、"あ", "い", "う" は全て同じカラム名とみなされているようです。

### 問題点のまとめ

公式のコードだと簡単ですが、以下のような問題が生じています。

- 日本語が文字化けする
- カラム名が重複していると、先のデータが後のデータで上書きされてしまう

### エクセルを読む場合はよしなに修正してくれる

danfo.js には エクセルファイルを読み込む機能がありますが、こちらでは、CSV での問題点は回避されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3781632/948ed475-48ad-4bdb-2fca-ab0abd667198.png)

```HTML:HTML
<script src="https://cdn.jsdelivr.net/npm/danfojs@1.1.2/lib/bundle.min.js"></script>

<body>
    <input type="file" id="file" name="file">
    <script>
     	const inputFile = document.getElementById('file');
        inputFile.addEventListener("change", async () => {
            const excelFile = inputFile.files[0]
            dfd.readExcel(excelFile).then((df) => {
                df.print()
            })
        })
    </script>
</body>
```

#### 結果
```text:CONSOLE
╔════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╗
║            │ __EMPTY           │ __EMPTY_1         │ あ                │ あ_1              │ あ_2              │ い                │ い_1              │ う                ║
╟────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 0          │ 11                │ 12                │ あ11              │ あ12              │ あ13              │ い11              │ い12              │ う11              ║
╟────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 1          │ 21                │ 22                │ あ21              │ あ22              │ あ23              │ い21              │ い22              │ う21              ║
╚════════════╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╝
```
- 日本語のデータでも問題ない
- 空白のカラム名があると __EMPTY にしてくれる
- カラム名が重複していると、_1, _2, ... の suffix を付けてくれる

なぜか CSV には優しくないんです。
ですので、CSV を読み込む場合にも同じように処理させるようにしてみました。

## まずは日本語を自動判定

danfo.js には日本語のコードの変換の機能はありませんので、encoding と PapaParse を使うことにします。

```HTML:HTML
<script src="https://cdn.jsdelivr.net/npm/danfojs@1.1.2/lib/bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/encoding-japanese/2.0.0/encoding.min.js"></script>
<script async src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script>

<body>
    <input type="file" id="file" name="file">
    <script>
        const inputFile = document.getElementById('file');
        inputFile.addEventListener("change", async () => {
            const csvFile = inputFile.files[0];
            
            var reader = new FileReader();
            reader.onload = (e) => {
                const codes = new Uint8Array(e.target.result);
                const unicodeString = Encoding.convert(codes, {
                    to: 'unicode',
                    from: Encoding.detect(codes),
                    type: 'string'
                }).trim();
                            
                Papa.parse(unicodeString, {
                    header: true,
                    complete: function(results) {
                    	const df = new dfd.DataFrame(results.data);
                        df.print();
                    }
                });
            };
            reader.readAsArrayBuffer(csvFile);
        });
    </script>
</body>
```

#### 結果

これで文字化けは解消しましたが、PapaParse でもカラム名の重複については同じです。
danfo.js のときと同じなので、もしかして内部コードは同じなのかしら？

```text:CONSOLE
╔════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╗
║            │                   │ あ                │ い                │ う                ║
╟────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 0          │ 12                │ あ13              │ い12              │ う11              ║
╟────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 1          │ 22                │ あ23              │ い22              │ う21              ║
╚════════════╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╝
```

## カラム名をエクセルの場合に準じて変換

カラム名の変換を実装します。

1. ```header: false``` にしてデータをパース
2. ヘッダー (1行目) を修正
3. 修正したヘッダーと元のデータを結合
4. CSV文字列に戻して再度パース

```HTML:HTML
<script src="https://cdn.jsdelivr.net/npm/danfojs@1.1.2/lib/bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/encoding-japanese/2.0.0/encoding.min.js"></script>
<script async src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script>

<body>
    <input type="file" id="file" name="file">
    <script>
        const modifyHeaders = (headers) => {
          const seen = {};
          return headers.map((header) => {
            if (header === "") {
              seen[header] = (seen[header] || 0) + 1;
              return seen[header] === 1 ? "__EMPTY" : `__EMPTY_${seen[header] - 1}`;
            }
            if (header in seen) {
              seen[header]++;
              return `${header}_${seen[header] - 1}`;
            }
            seen[header] = 1;
            return header;
          });
        };
                
        const inputFile = document.getElementById('file');
        inputFile.addEventListener("change", async () => {
            const csvFile = inputFile.files[0];
            
            var reader = new FileReader();
            
            reader.onload = (e) => {
                const codes = new Uint8Array(e.target.result);
                const unicodeString = Encoding.convert(codes, {
                    to: 'unicode',
                    from: Encoding.detect(codes),
                    type: 'string'
                }).trim();
                
                // Step 1: データをパース
                const parsedData = Papa.parse(unicodeString, {
                    header: false // まずは全てを data として扱う
                });

                // Step 2: ヘッダー (1行目) を修正
                const modifiedHeaders = modifyHeaders(parsedData.data[0]);

                // Step 3: 修正したヘッダーと元のデータを結合
                const newData = [modifiedHeaders, ...parsedData.data.slice(1)];

                // Step 4: CSV文字列に戻す
                const newUnicodeString = Papa.unparse(newData);

                // 変換結果を確認
                Papa.parse(newUnicodeString, {
	                header: true,
                    complete: function(results) {
                    	const df = new dfd.DataFrame(results.data);
                        df.print();
                    }
                });
            };
            reader.readAsArrayBuffer(csvFile);
        });
    </script>
</body>
```

#### 結果

これで文字化けとカラム名の重複の問題が解消しました。

```text:CONSOLE
╔════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╗
║            │ __EMPTY           │ __EMPTY_1         │ あ                │ あ_1              │ あ_2              │ い                │ い_1              │ う                ║
╟────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 0          │ 11                │ 12                │ あ11              │ あ12              │ あ13              │ い11              │ い12              │ う11              ║
╟────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 1          │ 21                │ 22                │ あ21              │ あ22              │ あ23              │ い21              │ い22              │ う21              ║
╚════════════╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╝
```

## 改行コード

上記コードでは、改行が ```CR+LF```, ```CR```, ```LF``` のいずれの場合でも問題なく読み込めます。

## Reactive stat では、できるだけユーザーがエラーで困ることがないようにしています

**[Reactive stat](https://www.emuyn.net/stats)** は、ブラウザだけで使える無料統計ソフトです。信頼性の高い R で統計解析を行い、その結果を AI が解説します。PC にソフトウェアをインストールする必要がなく、インターネット接続があればどこでも利用できます。

ぜひご利用ください。

![Reactive stat バナー](https://www.emuyn.net/_media/stats/reactive_stat_banner_compressed.png)