言語処理100本ノック-30(pandas使用):形態素解析結果の読み込み

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)[「第4章: 形態素解析」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch4)の[30本目「形態素解析結果の読み込み」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec30)記録です。
[「形態素解析」](https://ja.wikipedia.org/wiki/%E5%BD%A2%E6%85%8B%E7%B4%A0%E8%A7%A3%E6%9E%90)の章に入り、本格的な言語処理っぽくなってきました。形態素解析は「お待ちしております」のような文を「お待ち」「し」「て」「おり」「ます」に分割し、それぞれに品詞などの情報を加えるやつです。詳しくは[「Wikipedia」](https://ja.wikipedia.org/wiki/%E5%BD%A2%E6%85%8B%E7%B4%A0%E8%A7%A3%E6%9E%90)などを参照ください。

# 参考リンク

|リンク|備考|
|:--|:--|
|[030.形態素解析結果の読み込み.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/04.%E5%BD%A2%E6%85%8B%E7%B4%A0%E8%A7%A3%E6%9E%90/030.%E5%BD%A2%E6%85%8B%E7%B4%A0%E8%A7%A3%E6%9E%90%E7%B5%90%E6%9E%9C%E3%81%AE%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF.ipynb)|回答プログラムのGitHubリンク|
|[素人の言語処理100本ノック:30](http://qiita.com/segavvy/items/1f517e06aa3bc5fc2316)|多くのソース部分のコピペ元|
|[MeCab公式](https://taku910.github.io/mecab/)|最初に見ておくMeCabのページ|

# 環境
今回からPython3.8.1を使っています(前回までは3.6.9)。第3章「正規表現」で`collections.OrderdDict`を使って順序付き辞書型に対応していましたが、[Python3.7.1以降では標準の辞書型でも順序保証](https://docs.python.org/ja/3/whatsnew/3.7.html)をしてくれています。特に3.6.9に固執する理由もなかったので、環境を新しくしました。
MeCabのインストール方法は忘れました。１年前にインストールしたのですが、特に躓いた記憶がないです。

|種類|バージョン|内容|
|:--|:--|:--|
|OS|Ubuntu18.04.01 LTS|仮想で動かしています|
|pyenv|1.2.16|複数Python環境を使うことがあるのでpyenv使っています|
|Python|3.8.1|pyenv上でpython3.8.1を使っています<br>パッケージはvenvを使って管理しています|
|Mecab|0.996-5|apt-getでインストール|

上記環境で、以下のPython追加パッケージを使っています。通常のpipでインストールするだけです。

|種類|バージョン|
|:--|:--|
|pandas|1.0.1|

# [第4章: 形態素解析](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch4)
## 学習内容
> 夏目漱石の小説『吾輩は猫である』に形態素解析器MeCabを適用し，小説中の単語の統計を求めます．
>> 形態素解析, MeCab, 品詞, 出現頻度, Zipfの法則, [matplotlib](http://matplotlib.org/), [Gnuplot](http://www.gnuplot.info/)

## ノック内容
> 夏目漱石の小説『吾輩は猫である』の文章（[neko.txt](http://www.cl.ecei.tohoku.ac.jp/nlp100/data/neko.txt)）をMeCabを使って形態素解析し，その結果をneko.txt.mecabというファイルに保存せよ．このファイルを用いて，以下の問に対応するプログラムを実装せよ．

なお，問題37, 38, 39は[matplotlib](http://matplotlib.org/)もしくは[Gnuplot](http://www.gnuplot.info/)を用いるとよい．

### [30. 形態素解析結果の読み込み](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec30)
> 形態素解析結果（neko.txt.mecab）を読み込むプログラムを実装せよ．ただし，各形態素は表層形（surface），基本形（base），品詞（pos），品詞細分類1（pos1）をキーとするマッピング型に格納し，1文を形態素（マッピング型）のリストとして表現せよ．第4章の残りの問題では，ここで作ったプログラムを活用せよ．

### 課題補足(「MeCab」について)
形態素解析の定番である「MeCab」です。その他形態素解析器との比較として、記事[「2019年末版 形態素解析器の比較」](https://qiita.com/hi-asano/items/aaf406db875f1c81530e)を参考にしました(比較した結果「MeCab」でやろうと考えました)。
MeCabを使うと分割した字句に対して以下のフォーマットで情報を判断してくれます。区切り文字がタブ(`\t`)とカンマがあるのが注意点です(なぜなのでしょう？)。

> 表層形\t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音

例えば「すもももももももものうち」の場合は以下の出力結果となります。

|No|表層形|品詞|品詞細分類1|品詞細分類2|品詞細分類3|活用型|活用形|原形|読み|発音|
|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|:--|
|1|すもも|名詞|一般|*|*|*|*|すもも|スモモ|スモモ|
|2|も|助詞|係助詞|*|*|*|*|も|モ|モ|
|3|もも|名詞|一般|*|*|*|*|もも|モモ|モモ|
|4|も|助詞|係助詞|*|*|*|*|も|モ|モ|
|5|もも|名詞|一般|*|*|*|*|もも|モモ|モモ|
|6|の|助詞|連体化|*|*|*|*|の|ノ|ノ|
|7|うち|名詞|非自立|副詞可能|*|*|*|うち|ウチ|ウチ|
|8|EOS||||||||||
  
# 回答
## 回答プログラム(MeCab実行) [第4章_ 形態素解析.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/04.%E5%BD%A2%E6%85%8B%E7%B4%A0%E8%A7%A3%E6%9E%90/%E7%AC%AC4%E7%AB%A0_%20%E5%BD%A2%E6%85%8B%E7%B4%A0%E8%A7%A3%E6%9E%90.ipynb)
第4章の前提となる以下のMeCab実行部分です。

> 夏目漱石の小説『吾輩は猫である』の文章（[neko.txt](http://www.cl.ecei.tohoku.ac.jp/nlp100/data/neko.txt)）をMeCabを使って形態素解析し，その結果をneko.txt.mecabというファイルに保存せよ

コマンド1つで終わる簡単な処理です。

```bash
mecab neko.txt -o neko.txt.mecab
```

一応、Pythonでも以下のように`mecab-python3`(ver0.996.3)を使ってプログラムを作成したのですが、コマンド実行時と結果が微妙に異なります。**EOS(End Of Statement)で文が区切れていなかった**のが後続のノックに対して致命的でした。オプションの指定方法などが悪いのかもしれませんが、あまり突き詰めたい内容でもないのでPythonプログラムの実行結果は後続で利用していていません。

```python
import MeCab
mecab = MeCab.Tagger()

with open('./neko.txt') as in_file, \
    open('./neko.txt.mecab', mode='w') as out_file:   
    out_file.write(mecab.parse(in_file.read()))
```

## 回答プログラム(リスト作成) [030.形態素解析結果の読み込み.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/04.%E5%BD%A2%E6%85%8B%E7%B4%A0%E8%A7%A3%E6%9E%90/030.%E5%BD%A2%E6%85%8B%E7%B4%A0%E8%A7%A3%E6%9E%90%E7%B5%90%E6%9E%9C%E3%81%AE%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF.ipynb)

```python
from pprint import pprint

import pandas as pd

def read_text():
    # 0:表層形(surface)
    # 1:品詞(pos)
    # 2:品詞細分類1(pos1)
    # 7:基本形(base)
    df = pd.read_table('./neko.txt.mecab', sep='\t|,', header=None, 
                       usecols=[0, 1, 2, 7], names=['surface', 'pos', 'pos1', 'base'], 
                       skiprows=4, skipfooter=1 ,engine='python')
    # 本当は空白はpos1だが、ずれてしまっている
    return df[df['pos'] != '空白']

df = read_text()
print(df.info())

target = []
morphemes = []

for i, row in df.iterrows():
    if row['surface'] == 'EOS' \
     and len(target) != 0:
        morphemes.append(df.loc[target].to_dict(orient='records'))
        target = []
    else:
        target.append(i)

print(len(morphemes))
pprint(morphemes[:5])
```

## 回答解説
### ファイル読込部
MeCabが作成したファイルを`read_table`で読み込んでいます。区切り文字がタブ(`\t`)とカンマ(`,`)なのが少し面倒です。パラメータ`sep`で正規表現(`|`でOR)を使い、`engine`を'python'にすることで実現しています。
`skiprows`と`skipfooter`はファイルの中身を見て邪魔だったので設定しました。

```python:
def read_text():
    # 0:表層形(surface)
    # 1:品詞(pos)
    # 2:品詞細分類1(pos1)
    # 7:基本形(base)
    df = pd.read_table('./neko.txt.mecab', sep='\t|,', header=None, 
                       usecols=[0, 1, 2, 7], names=['surface', 'pos', 'pos1', 'base'], 
                       skiprows=4, skipfooter=1 ,engine='python')    
    return df
```

データフレームは以下の情報です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/989f96fb-5a94-667b-f508-cf65d5cc517f.png)

### 空白に関する`read_table`問題
わかりにくいですが、以下のような` `(半角スペース)が行頭に来る場合、`read_table`関数で読込時に列がずれます。` `と`\t`(タブ)を無視し、１列目が「記号」と認識されます。パラメータ`skipinitialspace`を設定したりと、いくつか試行錯誤をしましたが解決できませんでした。おそらく、pandasのバグではないかと考えています。
今回は特にこだわる必要がなかったので、「空白」行を対象外としています。

```
　	記号,空白,*,*,*,*,　,　,　
```

### DataFrame情報
読み込んだファイルのDataFrameとしての情報を`df.info()`で出力すると以下のとおりです。

```bash
<class 'pandas.core.frame.DataFrame'>
Int64Index: 212143 entries, 0 to 212552
Data columns (total 4 columns):
 #   Column   Non-Null Count   Dtype 
---  ------   --------------   ----- 
 0   surface  212143 non-null  object
 1   pos      202182 non-null  object
 2   pos1     202182 non-null  object
 3   base     202182 non-null  object
dtypes: object(4)
memory usage: 8.1+ MB
None
```

### 辞書型リスト出力
> 各形態素は表層形（surface），基本形（base），品詞（pos），品詞細分類1（pos1）をキーとするマッピング型に格納し，1文を形態素（マッピング型）のリストとして表現せよ

マッピング型(辞書型)のリストにします。ですが、私は**以降のノックでは使っておらず**完全にPython練習用です(以降のノックではpandas使うとこんな面倒な処理いらないです)。
EOS(End Of Statement)が出た場合に１文の終わりなので、それまでの形態素を`to_dict`関数で出力します。

```python:
target = []
morphemes = []

for i, row in df.iterrows():
    if row['surface'] == 'EOS' \
     and len(target) != 0:
        morphemes.append(df.loc[target].to_dict(orient='records'))
        target = []
    else:
        target.append(i)
```

### 出力結果(実行結果)
プログラム実行すると以下の結果が出力されます(先頭5文のみ)。ちなみに１行目の「吾輩は猫である」が名詞なのは、書籍タイトルの固有名詞となっているからです。本の中の文としては分解されるのが正しいですが、そこまではできていないです。

```bash:出力結果
[[{'base': '吾輩は猫である', 'pos': '名詞', 'pos1': '固有名詞', 'surface': '吾輩は猫である'},
  {'base': '。', 'pos': '記号', 'pos1': '句点', 'surface': '。'}],
 [{'base': '名前', 'pos': '名詞', 'pos1': '一般', 'surface': '名前'},
  {'base': 'は', 'pos': '助詞', 'pos1': '係助詞', 'surface': 'は'},
  {'base': 'まだ', 'pos': '副詞', 'pos1': '助詞類接続', 'surface': 'まだ'},
  {'base': '無い', 'pos': '形容詞', 'pos1': '自立', 'surface': '無い'},
  {'base': '。', 'pos': '記号', 'pos1': '句点', 'surface': '。'}],
 [{'base': None, 'pos': None, 'pos1': None, 'surface': 'EOS'},
  {'base': 'どこ', 'pos': '名詞', 'pos1': '代名詞', 'surface': 'どこ'},
  {'base': 'で', 'pos': '助詞', 'pos1': '格助詞', 'surface': 'で'},
  {'base': '生れる', 'pos': '動詞', 'pos1': '自立', 'surface': '生れ'},
  {'base': 'た', 'pos': '助動詞', 'pos1': '*', 'surface': 'た'},
  {'base': '火遁', 'pos': '名詞', 'pos1': '一般', 'surface': 'かとん'},
  {'base': 'と', 'pos': '助詞', 'pos1': '格助詞', 'surface': 'と'},
  {'base': '見当', 'pos': '名詞', 'pos1': 'サ変接続', 'surface': '見当'},
  {'base': 'が', 'pos': '助詞', 'pos1': '格助詞', 'surface': 'が'},
  {'base': 'つく', 'pos': '動詞', 'pos1': '自立', 'surface': 'つか'},
  {'base': 'ぬ', 'pos': '助動詞', 'pos1': '*', 'surface': 'ぬ'},
  {'base': '。', 'pos': '記号', 'pos1': '句点', 'surface': '。'}],
 [{'base': '何', 'pos': '名詞', 'pos1': '代名詞', 'surface': '何'},
  {'base': 'でも', 'pos': '助詞', 'pos1': '副助詞', 'surface': 'でも'},
  {'base': '薄暗い', 'pos': '形容詞', 'pos1': '自立', 'surface': '薄暗い'},
  {'base': 'じめじめ', 'pos': '副詞', 'pos1': '一般', 'surface': 'じめじめ'},
  {'base': 'した', 'pos': '名詞', 'pos1': '一般', 'surface': 'した'},
  {'base': '所', 'pos': '名詞', 'pos1': '接尾', 'surface': '所'},
  {'base': 'で', 'pos': '助詞', 'pos1': '格助詞', 'surface': 'で'},
  {'base': 'ニャーニャー', 'pos': '副詞', 'pos1': '一般', 'surface': 'ニャーニャー'},
  {'base': '泣く', 'pos': '動詞', 'pos1': '自立', 'surface': '泣い'},
  {'base': 'て', 'pos': '助詞', 'pos1': '接続助詞', 'surface': 'て'},
  {'base': 'いた事', 'pos': '名詞', 'pos1': '一般', 'surface': 'いた事'},
  {'base': 'だけ', 'pos': '助詞', 'pos1': '副助詞', 'surface': 'だけ'},
  {'base': 'は', 'pos': '助詞', 'pos1': '係助詞', 'surface': 'は'},
  {'base': '記憶', 'pos': '名詞', 'pos1': 'サ変接続', 'surface': '記憶'},
  {'base': 'する', 'pos': '動詞', 'pos1': '自立', 'surface': 'し'},
  {'base': 'て', 'pos': '助詞', 'pos1': '接続助詞', 'surface': 'て'},
  {'base': 'いる', 'pos': '動詞', 'pos1': '非自立', 'surface': 'いる'},
  {'base': '。', 'pos': '記号', 'pos1': '句点', 'surface': '。'}],
 [{'base': '吾輩', 'pos': '名詞', 'pos1': '代名詞', 'surface': '吾輩'},
  {'base': 'は', 'pos': '助詞', 'pos1': '係助詞', 'surface': 'は'},
  {'base': 'ここ', 'pos': '名詞', 'pos1': '代名詞', 'surface': 'ここ'},
  {'base': 'で', 'pos': '助詞', 'pos1': '格助詞', 'surface': 'で'},
  {'base': '始める', 'pos': '動詞', 'pos1': '自立', 'surface': '始め'},
  {'base': 'て', 'pos': '助詞', 'pos1': '接続助詞', 'surface': 'て'},
  {'base': '人間', 'pos': '名詞', 'pos1': '一般', 'surface': '人間'},
  {'base': 'という', 'pos': '助詞', 'pos1': '格助詞', 'surface': 'という'},
  {'base': 'もの', 'pos': '名詞', 'pos1': '非自立', 'surface': 'もの'},
  {'base': 'を', 'pos': '助詞', 'pos1': '格助詞', 'surface': 'を'},
  {'base': '見る', 'pos': '動詞', 'pos1': '自立', 'surface': '見'},
  {'base': 'た', 'pos': '助動詞', 'pos1': '*', 'surface': 'た'},
  {'base': '。', 'pos': '記号', 'pos1': '句点', 'surface': '。'}]]
```