テキスト分析の大通り#05: 形態素解析結果の縦持ち(Mecab編)
前回まででMecab、Sudachiを利用した形態素解析を実施しました。続いてこれを単語ごとに縦持ちすることを考えたいと思います。文書のデータを単語に分解し、これをカテゴリカルなデータとして扱うことで集計をしたり、他のデータと結合することが容易になります。MecabとSudachiで形態素解析の区切り方が異なるため、それぞれに対して縦持ちを検討しますが、今回はMecabの方を行ってみます。
Mecabの形態素解析結果
前々回、Mecabで形態素解析した結果のデータをTeradataのテーブル上に保存しました。docid | cat | docdescという列の並びで、文書番号と、若林春日のカテゴリー種別、そして形態素解析結果が1文書1列で格納されています。以下はdocdescの1行に入っているデータです。おさえなおしますが複数行ではなく1行です。
みるとこの1行の中に、改行コードが埋め込まれていて、1単語ごとに改行、そして元々の単語と名詞などの別の間にはタブ区切りがなされていることが分かります。そしてさらに名詞と固有名詞の間にはカンマで区切られていることが分かります。そのため、改行コードで縦持ちし、その中は横に(別の列に)分解することにしましょう。また、最後のEOSは不要なので取り除きます。
縦分解、横分解
実行するSQLは以下の通りです。各単語に関する属性では不要なものも多く、もともとの単語、品詞、そして正規表現の部分だけを抜き出します。
まずsrc内の処理ですが、regexp_split_to_tableという縦分解させるテーブル関数を利用して、docdesc内を区切り文字までで縦持ちさせます。今回の区切り文字は改行コード(\n)なので区切り文字を指定し、処理を行います。そのまま維持させる列としてdocidを合わせて入力します。
出力はoutkey、token_nbr、result_stringを指定してます。この時にデータ型も指定していますが、その際にvarcharのデータ長は、入力データの長さがそのまま入る長さである必要があります。そしてa1ではその結果を入力として、列名の調整を行っています。outkeyはもともとdocidなので元に戻します。seqnoは各docid内におけるresult_stringの発生順序を意味する数値データです。result_string内には区切り文字で分解された文字列が入っています。
次のクエリーでは、strtok関数を用い、横に分解しています。docidとseqnoはそのままで、単語はタブ区切りになっているのでそれを区切り文字に指定し、その1番目を取得しています(word)。そしてその次の品詞(pos)はstrtokの処理を入れ子にしていて、まずタブ区切りの2番目を取得し、その次にカンマ区切りの1番目を取得しています。以降もカンマ区切りで取得可能ですが不要なものも多いのでコメントアウトし、正規表現を意味する部分のみnorm列として残しています。最後にresult_stringがEOSである列は除外して処理は完了です。
分解されたデータは以下のようになります(docid=17のみ)。
文章が長い場合
今回、もともとの文章はそんなに長くないため、テーブルに入れる際のデータを可変長の文字型(varchar, character varying)で設定しましたが、unicodeの場合varcharの長さは32000文字までという制約があります。形態素解析をして膨らませて、結果32000文字以上になる場合はCLOB (Character Large OBject)型でいったん定義した列に格納するか、文章自体をあらかじめ改行コードや「。」で分解しておく必要があります。改行コードや「。」で分解してから形態素解析にかける方法は上述のregexp_split_to_tableを使うやり方で可能なため割愛しますが、CLOBで定義した列を分解していく方法を以下に記載します。SQLは以下。
まず最初のテーブルですが、Mecabの形態素解析をした結果を格納したテーブルと基本的には同じです。唯一の違いとして、docdesc部分のみCLOBで定義しています。
そしてこれをいったん、3万文字(限界は32000文字ですが余裕をもって)区切りで分けたテーブルにします(jumbo.aud04_mecab_temp)。ここに入れるデータですが、まずsubdocidは、3万文字ずつで分割した1番目、2番目という風に番号が振られます。docidが10番とすると、かける10で100となります(もしunionの数が10以上の場合はdocidに10ではなく、100をかけてください)。最後の桁は常にゼロです。これにsubdocidを足します。これによって101と102というsuperdocidが出来上がります。ここから、docidが10の文章は101番と102番の文章で構成されることになります。最後にwhere句で、docdescが空の行を落としています。文章によっては3万文字に達していない場合があり、その際Unionの2番目の行はいなくなります。
次の処理では、一旦縦に分解した後、3万文字区切りで分かれてしまったデータをつなぎ直し、最後に横に分解します。最初のクエリー(src)内ではsuperdocidのまま、先ほど同様改行コードを区切り文字に縦に分解し、別行にします。
次にjoint内の処理ですが、まずtrunc関数とmod関数を用いて、docidとsubdocidを再現します。この時点でsuperdocidはお役御免です。また、rndescではdocidとsubdocidの単位、つまりsuperdocid単位で、seqnoの降順で番号を振ります。最後の行に1がセットされるようにしたいです。続いて、rnthruoutではdocid単位、subdocidとseqnoの昇順で番号を振ります。これは仮の、最終的なseqnoの代替になります。現在のseqnoはsuperdocid単位のため、docid単位では重複してしまっています。そしてdocidごとにsubdocidの最大値を取得します。Unionしているのが2つで、上述のwhere句で3万文字に達していない場合は落としているので、値は1もしくは2となるはずです。最後にresult_stringですが、コメントアウトに入れたSQLを使って最大文字長を確認し、その値をデータ長として、架空で150と入れている部分にセットします。そして次のクエリー(a2)では、lag関数を用いて1行前のデータを横付けします。docid単位で、rnthruout順です。これによって、superdocidで泣き別れていたresultstringが横隣りに来て、つなげることが可能となります。ただ、つなげたいのはsubdocid=1の最後の行と、subdocid=2の最初の行だけです。そのため、次のクエリー(a3)でsubdocidが2以上かつseqno=1(これはsuperdocid単位でユニーク、最初の行)の場合にはlag関数で取得した前行のresult_string(aaa)を頭につなげ、それ以外の場合は、そのままのresult_stringを採用します。一方、泣き別れしたもう一つの行はつなぎ終わったのでお役御免です。where句でrndsecが1、つまりsuperdocid単位で最後の行、なおかつsubdocidの最後のブロック以外に属している行を除外します。分かりやすいよう、データサンプルを以下に示します。4行目でさしすせそをつなげたので、3行目は用なしとなり、削除されます。
最後に、docidに対して10かけたのを忘れていたので元に戻し、削除した分を詰める形でdocid単位での行番号を振り直し、最終的なseqnoとして列を用意します。これでdocid, seqno, result_stringが完全に再現できたため、最後のクエリー(jointの次)にこのデータを回します。その後の処理は上述の3万文字以内に収まるパターンと同じため割愛します。以上です。
(TeradataやPython、およびPythonライブラリのインストールや環境構築、辞書登録、参考にしたページ等は以下にまとめています)
///