CSV データの読み込みは簡単だが意外と奥が深いです
日本語を含む CSV をデータセットとして読み込むうえで、以下の2点が問題となります。
- 日本語コードに自動対応したい
- カラム名が空白だったり重複していたり
これらを回避する方法について述べます。
私なりにはベストプラクティスと考えています。
ローカルの CSV を danfo.js の Data Frame に読み込む際の問題点
まずは、danfo.js 公式に掲載のコードを実行してみます。
ライブラリ込みの html にしてありますので、
jsfiddle などにコピペして簡単に試せます。
<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種類あります。
"","","あ","あ","あ","い","い","う"
"11","12","あ11","あ12","あ13","い11","い12","う11"
"21","22","あ21","あ22","あ23","い21","い22","う21"
結果 (一番左の列は行番号)
╔════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╗
║ │ │ あ │ い │ う ║
╟────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 0 │ 12 │ あ13 │ い12 │ う11 ║
╟────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 1 │ 22 │ あ23 │ い22 │ う21 ║
╚════════════╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╝
空白も含めて、カラム名が重複していると、先のデータが後のデータで上書きされてしまいます。
SHIFT-JIS にして読み込んでみる
次に、先ほどのファイルを SHIFT-JIS で保存してから読み込んでみましょう。
結果
╔════════════╤═══════════════════╤═══════════════════╗
║ │ │ �� ║
╟────────────┼───────────────────┼───────────────────╢
║ 0 │ 12 │ ��11 ║
╟────────────┼───────────────────┼───────────────────╢
║ 1 │ 22 │ ��21 ║
╚════════════╧═══════════════════╧═══════════════════╝
文字化けしています。しかも、"あ", "い", "う" は全て同じカラム名とみなされているようです。
問題点のまとめ
公式のコードだと簡単ですが、以下のような問題が生じています。
- 日本語が文字化けする
- カラム名が重複していると、先のデータが後のデータで上書きされてしまう
エクセルを読む場合はよしなに修正してくれる
danfo.js には エクセルファイルを読み込む機能がありますが、こちらでは、CSV での問題点は回避されています。
<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>
結果
╔════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╗
║ │ __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 を使うことにします。
<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 のときと同じなので、もしかして内部コードは同じなのかしら?
╔════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╗
║ │ │ あ │ い │ う ║
╟────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 0 │ 12 │ あ13 │ い12 │ う11 ║
╟────────────┼───────────────────┼───────────────────┼───────────────────┼───────────────────╢
║ 1 │ 22 │ あ23 │ い22 │ う21 ║
╚════════════╧═══════════════════╧═══════════════════╧═══════════════════╧═══════════════════╝
カラム名をエクセルの場合に準じて変換
カラム名の変換を実装します。
-
header: false
にしてデータをパース - ヘッダー (1行目) を修正
- 修正したヘッダーと元のデータを結合
- CSV文字列に戻して再度パース
<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>
結果
これで文字化けとカラム名の重複の問題が解消しました。
╔════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╤═══════════════════╗
║ │ __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 にソフトウェアをインストールする必要がなく、インターネット接続があればどこでも利用できます。
ぜひご利用ください。