LighitGBM で変数重要度を出すと列名が文字化けするという相談を受けたときの調査メモ。
解決策は3つある。
現象を再現
まずは文字化け現象を再現してみる。データとしてはこんな感じ。
Sys.setlocale(locale = "Japanese_Japan.932") df_all <- iris colnames(df_all) <- c("あ", "い", "う", "え", "お") colnames(df_all)[1] <- iconv(colnames(df_all)[1], from = "SJIS", to = "UTF-8") head(df_all)
あ い う え お 1 5.1 3.5 1.4 0.2 setosa 2 4.9 3.0 1.4 0.2 setosa 3 4.7 3.2 1.3 0.2 setosa 4 4.6 3.1 1.5 0.2 setosa 5 5.0 3.6 1.4 0.2 setosa 6 5.4 3.9 1.7 0.4 setosa
データを表示したときには列名は文字化けしていないのがポイント。
これを LightGBM にかけて変数重要度を出す。
library(lightgbm) train_data <- as.matrix(df_all[, 1:4]) label <- as.integer(df_all[, 5]) - 1 lgb_train <- lgb.Dataset(data = train_data, label = label) params <- list(objective = "multiclass", num_class = 3, metric = "multi_logloss") model <- lgb.train(params = params, data = lgb_train) lgb.importance(model)
Feature Gain Cover Frequency 1: 縺<86> 0.62786358 0.3758313 0.3721847 2: 縺<88> 0.33334783 0.2347444 0.2139640 3: 縺<84> 0.02101790 0.2002376 0.2246622 4: 縺<82> 0.01777069 0.1891867 0.1891892
列名 (Feature) が文字化けしてしまっている。
この原因は、列名の文字エンコーディングに UTF-8 と Shift_JIS が混在しているためである。
encoding <- colnames(df_all) |> stringi::stri_enc_detect() |> purrr::map_chr(~ .x$Encoding[1]) encoding
[1] "UTF-8" "Shift_JIS" "Shift_JIS" "Shift_JIS" "Shift_JIS"
現実のデータ分析では、データソースの異なるデータを結合して使用することも多いため、このような現象が起こる場合がある。
ひとつでも混じり物があると、すべての列名が文字化けしてしまうというのは興味深い。
解決策1. ロケールを UTF-8 にする
最もおすすめしたい解決策は、ロケールの文字エンコーディングを UTF-8 に変更することだ。 これができるなら一番早い。
Sys.setlocale(locale = "Japanese_Japan.utf8") lgb.importance(model)
Feature Gain Cover Frequency 1: う 0.62786358 0.3758313 0.3721847 2: え 0.33334783 0.2347444 0.2139640 3: い 0.02101790 0.2002376 0.2246622 4: あ 0.01777069 0.1891867 0.1891892
しかし、様々な事情があって、これが常に可能とは限らない。
解決策2. 学習時に列名を指定する
2番目におすすめなのは、学習時に列名を指定する方法だ。
lgb.train()
関数には colnames
という引数がある。
この引数に文字エンコーディングが統一された列名を入力する。
列名をエディタ上で定義してしまえば文字エンコーディングは自然に統一される。
Sys.setlocale(locale = "Japanese_Japan.932") col_names <- c("あ", "い", "う", "え") model <- lgb.train(params = params, data = lgb_train, colnames = col_names) lgb.importance(model)
Feature Gain Cover Frequency 1: う 0.62786358 0.3758313 0.3721847 2: え 0.33334783 0.2347444 0.2139640 3: い 0.02101790 0.2002376 0.2246622 4: あ 0.01777069 0.1891867 0.1891892
この方法のデメリットは、列数が多いときになかなか面倒なことである。
解決策3. 列名の文字エンコーディングを統一する
最もおすすめしない解決策は、列名の文字エンコーディングを統一する方法だ。
まずは、それぞれの列名の文字エンコーディングを調べる。
colnames(df_all) |> stringi::stri_enc_detect() |> purrr::set_names(colnames(df_all)) |> head(4)
$あ Encoding Language Confidence 1 UTF-8 0.8 $い Encoding Language Confidence 1 Shift_JIS ja 0.1 2 GB18030 zh 0.1 3 Big5 zh 0.1 $う Encoding Language Confidence 1 Shift_JIS ja 0.1 2 GB18030 zh 0.1 3 Big5 zh 0.1 $え Encoding Language Confidence 1 Shift_JIS ja 0.1 2 GB18030 zh 0.1 3 Big5 zh 0.1
1番目の列名「あ」が UTF-8 であるのが文字化けの原因なので、これを Shift_JIS に変換すればよい。
Sys.setlocale(locale = "Japanese_Japan.932") colnames(df_all)[1] <- iconv(colnames(df_all)[1], from = "UTF-8", to = "SJIS") model <- lgb.train(params = params, data = lgb_train) lgb.importance(model)
Feature Gain Cover Frequency 1: う 0.62786358 0.3758313 0.3721847 2: え 0.33334783 0.2347444 0.2139640 3: い 0.02101790 0.2002376 0.2246622 4: あ 0.01777069 0.1891867 0.1891892
一見するとスマートな方法に思える。
しかし、列名の文字エンコーディングを見抜くのは割と難しい。 ちょっとしたクイズを出してみよう。 次の列名のうち、文字エンコーディングが異なるのは何番目だろうか?
colnames(df_all) |> stringi::stri_enc_detect() |> purrr::set_names(colnames(df_all))
$スペード Encoding Language Confidence 1 windows-1252 es 0.42 2 UTF-16BE 0.10 3 UTF-16LE 0.10 4 Shift_JIS ja 0.10 5 GB18030 zh 0.10 6 Big5 zh 0.10 $ハート Encoding Language Confidence 1 UTF-16BE 0.1 2 UTF-16LE 0.1 3 Shift_JIS ja 0.1 4 GB18030 zh 0.1 5 Big5 zh 0.1 $ダイヤ Encoding Language Confidence 1 UTF-8 0.8 2 UTF-16BE 0.1 3 UTF-16LE 0.1 4 Shift_JIS ja 0.1 5 GB18030 zh 0.1 $クラブ Encoding Language Confidence 1 UTF-16BE 0.1 2 UTF-16LE 0.1 3 Shift_JIS ja 0.1 4 GB18030 zh 0.1 5 Big5 zh 0.1 $ジョーカー Encoding Language Confidence 1 windows-1250 pl 0.5 2 UTF-16BE 0.1 3 UTF-16LE 0.1 4 Shift_JIS ja 0.1 5 GB18030 zh 0.1 6 EUC-JP ja 0.1 7 EUC-KR ko 0.1 8 Big5 zh 0.1
正解は3番目の「ダイヤ」である。このデータは次のコードで作成した。
df_all <- iris colnames(df_all) <- c("スペード", "ハート", "ダイヤ", "クラブ", "ジョーカー") colnames(df_all)[3] <- iconv(colnames(df_all)[3], from = "SJIS", to = "UTF-8") head(df_all)
スペード ハート ダイヤ クラブ ジョーカー 1 5.1 3.5 1.4 0.2 setosa 2 4.9 3.0 1.4 0.2 setosa 3 4.7 3.2 1.3 0.2 setosa 4 4.6 3.1 1.5 0.2 setosa 5 5.0 3.6 1.4 0.2 setosa 6 5.4 3.9 1.7 0.4 setosa
このクイズが解けた人でも、現実のデータのぐちゃぐちゃな文字エンコーディングを見ると腰が引けるだろう。
また、最初に見たように、「ひとつでも異なる文字エンコーディングが混在していると、すべての列名が文字化けする」というのも、この方法での解決を困難にする。
おわりに
LighitGBM で変数重要度を出すと列名が文字化けするときの解決策を3つ紹介した。
ここまで読んでくれた人に、ハドリー・ウイッカムの次の言葉を送ろう。
Why are you using SJIS ?
https://github.com/tidyverse/dplyr/issues/339#issuecomment-38159109
参考文献
Rにおける文字列処理について詳しく書かれている(第8章)