本文へスキップ
からだにいいもの

Rのトピックスを中心に『まだ、まだ、知らない、役に立つ情報?』を発信します。

Rで解析:文字コード変換とテキストカウントを行うtauパッケージの紹介

Shift-JISやCP932でエンコードされたファイルを読み込んだ際の文字化けや、宣言エンコーディングの不一致といったことに直面することは少なくありません。「tau」パッケージは、文字ベクトルのエンコーディング(文字コード)の判定・変換、C実装のプレフィックス木による高速な単語・n-gramの出現頻度カウント、トークン分割やストップワード除去まで、テキスト分析の前処理に必要な機能をコマンドでまとめて提供してくれます。大規模な文書集合を逐次処理できるインクリメンタルカウントに対応し、テキストマイニング定番のtmパッケージの作者を含む開発陣によるユーティリティ集という信頼感も魅力です。ただしtokenize・remove_stopwordsは英語のような空白区切りの言語を前提としており、日本語テキストを扱う際は分かち書きや文字コード変換など一手間必要になる点も踏まえて紹介します。

パッケージバージョンは0.0.29。Windows 11 x64 (build 26200)のR version 4.6.1で確認しています。

パッケージのインストール

下記コマンドを実行してください。

# パッケージのインストール
install.packages("tau")
# パッケージの読み込み
library("tau")
スポンサーリンク

コマンド例

詳細はコメント、パッケージのヘルプを確認してください。

tauパッケージは、文字ベクトルのエンコーディング(文字コード)を判定・変換する「encoding」、単語やパターンの出現回数をカウントする「textcnt」、そしてトークン分割やストップワード除去といった前処理を行う「util」という、独立した3つの機能群で構成されています。以下ではそれぞれの代表的なコマンドを紹介します。

エンコーディングの判定と明示的な変換:is.utf8・translate・fixEncodingコマンド

実行すると、文字ベクトルの宣言エンコーディングの種別が確認できたり、現在のロケールへ変換・修正した結果が確認できます。

## RのロケールがUTF-8であることを前提
# UTF-8の日本語テキストを定義
text_utf8 <- "こんにちは"

# CP932(Shift-JIS系)にエンコードしたバイト列を作成し、宣言をunknownに設定
text_sjis <- rawToChar(iconv(text_utf8, from = "UTF-8", to = "CP932", toRaw = TRUE)[[1]])
Encoding(text_sjis) <- "unknown"

# 有効なUTF-8文字列かどうかを確認
is.utf8(text_sjis)
[1] FALSE

# ASCII文字列かどうかを確認
is.ascii(text_sjis)
[1] FALSE

# 現在のロケールのエンコーディングかどうかを確認
is.locale(text_sjis)
[1] FALSE

## 大文字"UTF-8"で正しく変換
# CP932からUTF-8へ明示的に変換
t1 <- iconv(text_sjis, from = "CP932", to = "UTF-8")

# t1の宣言エンコーディングを確認
Encoding(t1)
[1] "UTF-8"

# t1の内容を表示
t1
[1] "こんにちは"

## 見落としがちな例
# to引数を小文字の"utf-8"で指定(変換はされるが宣言エンコーディングが設定されない)
t2 <- iconv(text_sjis, from = "CP932", to = "utf-8")

# t2のエンコーディングを確認(unknownのままになる)
Encoding(t2)
[1] "unknown"

# t2の内容を表示(変換自体は成功している)
t2

# t2が現在のロケールのエンコーディングかどうかを確認
is.locale(t2)
[1] "こんにちは"

# 宣言エンコーディングを正しい値に修正(再宣言)
t2 <- fixEncoding(t2)

# 修正後の宣言エンコーディングを確認
Encoding(t2)
[1] "UTF-8"

## translateの限界
# text_sjisはCP932のバイト列だが宣言は"unknown"のまま
# translateはEncoding()の宣言("latin1"・"UTF-8"等)を頼りに変換するため、
# 宣言が"unknown"だと正しく変換できない
t3 <- translate(text_sjis)

# t3の宣言エンコーディングを確認
Encoding(t3)
[1] "unknown"

# t3の内容を表示(文字化けしたままになる)
t3
[1] "\x82\xb1\x82\xf1\x82ɂ\xbf\x82\xcd"

CP932のようにEncoding()が扱えないエンコーディングは、宣言を頼りに変換するtranslateでは対応できません。実際の文字コードが分かっている場合はiconvfrom引数に直接指定するのが確実です。

単語やn-gramの出現回数をカウントする:textcntコマンド

実行すると、テキスト内の単語や文字n-gram、接頭辞・接尾辞の出現回数がカウントされ、頻度分布や統計情報が取得できます。

オプション意味初期値
x1つ以上の文書を表す文字ベクトル(またはそのリスト)を指定
nn-gram・接頭辞・接尾辞カウントで考慮する最大文字数を指定(stringメソッドでは単語シーケンスの長さ)3L
split単語分割に使用する正規表現パターンを指定(NULLで分割なし)“[[:space:][:punct:][:digit:]]+”
tolower単語分割後に小文字へ変換するか指定TRUE
marker単語境界を示す文字列を指定“_”
words文書の先頭から使用する単語数を指定(NULLで全単語)NULL
lower結果に含めるカウントの下限値を指定0L
method計算するカウントのタイプを指定c(“ngram”, “string”, “prefix”, “suffix”)
recursive文書ごとに個別にカウントするか指定(初期値は全文書まとめて)FALSE
persistent文書をインクリメンタル(逐次的)にカウントするか指定FALSE
useBytes文字単位ではなくバイト単位で処理するか指定FALSE
perl単語分割にPCREを使用するか指定TRUE
verboseタイミング統計を取得するか指定FALSE
decreasingカウントを降順で返すか指定FALSE
# 分かち書き済みの日本語テキスト例を定義
txt <- "データ 分析 は データ 収集 と データ 前処理 と データ 可視化 が 重要 です"

# ngramメソッドで単語内の文字n-gram(1〜3文字)をカウント
textcnt(txt, method = "ngram")

# prefixメソッドで長さ2までの接頭辞をカウント
textcnt(txt, method = "prefix", n = 2L)
_    _が   _が_    _で  _です    _と   _と_    _は   _は_    _デ  _デー    _分  _分析    _前  _前処    _収  _収集    _可  _可視    _重 
28      1      1      1      1      2      2      1      1      4      4      1      1      1      1      1      1      1      1      1 
_重要     が    が_     す    す_     で   です  です_     と    と_     は    は_     タ    タ_     デ   デー データ     ー   ータ  ータ_ 
1      1      1      1      1      1      1      1      2      2      1      1      4      4      4      4      4      4      4      4 
処   処理  処理_     分   分析  分析_     前   前処 前処理     化    化_     収   収集  収集_     可   可視 可視化     析    析_     理 
1      1      1      1      1      1      1      1      1      1      1      1      1      1      1      1      1      1      1      1 
理_     要    要_     視   視化  視化_     重   重要  重要_     集    集_ 
1      1      1      1      1      1      1      1      1      1      1 
attr(,"useBytes")
[1] FALSE
attr(,"class")
[1] "textcnt"


# suffixメソッドで接尾辞をカウントし(下限1)、結果をrに代入
r <- textcnt(txt, method = "suffix", lower = 1L)

# カウント値と文字数をデータフレームに変換
data.frame(counts = unclass(r), size = nchar(names(r)))
       counts size
と          2    1
タ          4    1
ータ        4    2
データ      4    3

# 順位やエンコーディング情報付きで整形表示
format(r)
       frq rank bytes Encoding
と       2    1     3    UTF-8
タ       4    3     3    UTF-8
ータ     4    3     6    UTF-8
データ   4    3     9    UTF-8

# stringメソッドで長さ3の単語シーケンスをカウント
textcnt(txt, method = "string")

# 空文字列で分割し、バイト単位でカウントする非効率な例
textcnt(txt, split = "", method = "string", n = 1L, useBytes = TRUE)

# persistent = TRUEでインクリメンタルにカウント(結果はまだ返らない)
textcnt(txt, method = "string", persistent = TRUE, n = 1L)

# persistent = FALSE(初期値)の呼び出しで蓄積された結果を取得
textcnt(txt, method = "string", n = 1L)
が   です     と     は データ   分析 前処理   収集 可視化   重要 
2      2      4      2      8      2      2      2      2      2 
attr(,"useBytes")
[1] FALSE
attr(,"class")
[1] "textcnt"

# 文書の先頭から3単語のみを対象にカウント
textcnt(txt, method = "string", words = 3L, n = 1L)

# CP932でエンコードされたテキストを想定(宣言はunknownのまま)
txt2 <- rawToChar(iconv("こんにちは", from = "UTF-8", to = "CP932", toRaw = TRUE)[[1]])
Encoding(txt2) <- "unknown"

# 宣言がunknownのままだと不正なUTF-8として扱われ、結果が空になる
textcnt(txt2, method = "suffix")

# iconvでCP932からUTF-8へ変換してからカウントすると正しく集計される
txt2 <- iconv(txt2, from = "CP932", to = "UTF-8")
r <- textcnt(txt2, method = "suffix")
# カウント結果の名前(パターン)のエンコーディング分布を確認
table(Encoding(names(r)))
# サフィックスカウントの結果を表示
r

# marker = "\1"で効率的なn-gramセットを構築
textcnt("いい", n = 3L, marker = "\1")
_い い_ 
1   1 
attr(,"useBytes")
[1] FALSE
attr(,"class")
[1] "textcnt"

textcnt("いい", n = 4L, marker = "\1")
textcnt("散歩", n = 5L, marker = "\1")

# marker = "\2"でCavnar-Trenkle方式のn-gramセットを構築
textcnt("散歩", n = 5L, marker = "\2")
_     _散   _散歩  _散歩_ _散歩__      散    散歩   散歩_  散歩__ 散歩___      歩     歩_    歩__   歩___  歩____ 
1       1       1       1       1       1       1       1       1       1       1       1       1       1       1 
attr(,"useBytes")
[1] FALSE
attr(,"class")
[1] "textcnt"

宣言エンコーディングが正しくないテキストをそのまま渡すと不正なUTF-8として扱われ、カウント結果が空になります。前段のencoding系コマンドで宣言や変換を正しく行ってから渡すのが安全です。

トークン分割とストップワード除去:tokenize・remove_stopwordsコマンド

これらのコマンドは、文書の前処理としてトークン分割や不要なストップワードの除去など、データクリーニング作業をおこないます。

【重要な注意】tokenize・remove_stopwordsは英語のような単語間を半角スペースで区切る言語を前提としており、日本語のようなマルチバイト文字を含む言語には対応していません。実際に日本語テキストを渡すとどうなるか、以下で確認します。

# テキストを定義
txt <- "「今日は晴れです」と彼女は言った。"
# トークン分割を試みる
x <- tokenize(txt)
# 分割結果を表示(マルチバイト文字が1バイトずつ分解されてしまう)
x
[1] "「" "今" "日" "は" "晴" "れ" "で" "す" "」" "と" "彼" "女" "は" "言" "っ" "た" "。"

# トークンを連結して元の文字列を再構築
t <- paste(x, collapse = "")
# 再構築結果を表示(連結すれば元に戻るが、分割自体に意味がない)
t
[1] "「今日は晴れです」と彼女は言った。"

# ストップワードの除去を試みる
# lines = TRUEでも日本語の単語境界を認識できず、何も除去されない
remove_stopwords(txt, words = c("は", "と"), lines = TRUE)
[1] "「今日は晴れです」と彼女は言った。"

tokenizeは日本語の文字を1バイトずつバラバラに分解してしまい、remove_stopwordsは「は」や「と」を指定しても何も除去されません。日本語で単語単位の分かち書きやストップワード除去を行いたい場合は、MeCabやRMeCab、あるいはSudachiのような形態素解析エンジン・パッケージを利用するか、次の応用例のようにあらかじめ分かち書き済みのテキストを用意したうえでtau以外の手段(base Rのstrsplit等)と組み合わせる必要があります。

応用例:分かち書き済み日本語テキストのストップワード除去から単語頻度カウントまで

先の通りremove_stopwordsは日本語では機能しないため、あらかじめ分かち書き(単語間を半角スペースで区切ったテキスト)を用意し、base Rのstrsplitでストップワードを除去してからtextcntへ渡すのが実用的な方法です。

# 分かち書き済みの日本語文書ベクトルを定義
docs <- c("R は データ 分析 に 便利 な 言語 です",
          "R は 統計 解析 や グラフ 作成 が 得意 です",
          "データ 分析 に は 前処理 が 重要 です")

# 除去したいストップワードを定義
sw <- c("は", "な", "や", "が", "に", "です")

# strsplitで単語単位に分割し、ストップワードを除いて再結合(base R)
docs2 <- sapply(strsplit(docs, " "), function(words) {
  paste(words[!words %in% sw], collapse = " ")
})
# 除去結果を表示
docs2
[1] "R データ 分析 便利 言語"      "R 統計 解析 グラフ 作成 得意" "データ 分析 前処理 重要"  

# 単語の出現回数を降順でカウント
textcnt(docs2, method = "string", n = 1L, decreasing = TRUE)
r データ   分析 グラフ   作成   便利 前処理   得意   統計   解析   言語   重要 
2      2      2      1      1      1      1      1      1      1      1      1 
attr(,"useBytes")
[1] FALSE
attr(,"class")
[1] "textcnt"


この記事が誰かの役に立ちますように。

スポンサーリンク
Prices and shipping availability may change. Please refer to the product page at time of purchase.
Content displayed on this site is provided by Amazon and may be updated or removed.
Amazon Associate, karada-good earns income through qualifying sales.