ほくそ笑む

R言語と統計解析について

LighitGBM で変数重要度を出すと列名が文字化けするときの解決策

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章)