LoginSignup
0
0
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

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

Last updated at Posted at 2024-06-25

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

日本語を含む CSV をデータセットとして読み込むうえで、以下の2点が問題となります。

  • 日本語コードに自動対応したい
  • カラム名が空白だったり重複していたり

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

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

まずは、danfo.js 公式に掲載のコードを実行してみます。

ライブラリ込みの html にしてありますので、
jsfiddle などにコピペして簡単に試せます。

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
"","","あ","あ","あ","い","い","う"
"11","12","あ11","あ12","あ13","い11","い12","う11"
"21","22","あ21","あ22","あ23","い21","い22","う21"

結果 (一番左の列は行番号)

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

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

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

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

結果

CONSOLE
╔════════════╤═══════════════════╤═══════════════════╗
║            │                   │ ��                ║
╟────────────┼───────────────────┼───────────────────╢
║ 0          │ 12                │ ��11              ║
╟────────────┼───────────────────┼───────────────────╢
║ 1          │ 22                │ ��21              ║
╚════════════╧═══════════════════╧═══════════════════╝

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

問題点のまとめ

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

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

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

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

image.png

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>

結果

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
<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 のときと同じなので、もしかして内部コードは同じなのかしら?

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

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

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

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

結果

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

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 は、ブラウザだけで使える無料統計ソフトです。信頼性の高い R で統計解析を行い、その結果を AI が解説します。PC にソフトウェアをインストールする必要がなく、インターネット接続があればどこでも利用できます。

ぜひご利用ください。

Reactive stat バナー

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0