言語処理100本ノック-83(pandas使用):単語／文脈の頻度の計測

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)の83本目「単語／文脈の頻度の計測」の記録です。
約800MBのファイルに対する処理なので時間がかかります(7分程度)。一括で読むとメモリエラーが出るかと思いpandasのchunksizeオプションを使って乗り切ろうとしていたら全然できずに苦労しました。結局一括で読み込めて特に問題なかったです。

# 参考リンク

|リンク|備考|
|:--|:--|
|[083.単語／文脈の頻度の計測.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/09.%E3%83%99%E3%82%AF%E3%83%88%E3%83%AB%E7%A9%BA%E9%96%93%E6%B3%95%20(I)/083.%20%E5%8D%98%E8%AA%9E%EF%BC%8F%E6%96%87%E8%84%88%E3%81%AE%E9%A0%BB%E5%BA%A6%E3%81%AE%E8%A8%88%E6%B8%AC.ipynb)|回答プログラムのGitHubリンク|
|[素人の言語処理100本ノック:83](http://qiita.com/segavvy/items/614689e7c4e2ba387929)|言語処理100本ノックで常にお世話になっています|
|[言語処理100本ノック 2015年版 (83,84)](http://kenichia.hatenablog.com/entry/2016/03/03/173146)|第9章では参考にしました|
|[Pandas の groupby の使い方](https://qiita.com/propella/items/a9a32b878c77222630ae)|pandasのgroupby使い方がわかりやすい|
|[to_pickle関数](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.to_pickle.html)|to_pickle関数の公式ヘルプ|

# 環境

|種類|バージョン|内容|
|:--|:--|:--|
|OS|Ubuntu18.04.01 LTS|仮想で動かしています|
|pyenv|1.2.15|複数Python環境を使うことがあるのでpyenv使っています|
|Python|3.6.9|pyenv上でpython3.6.9を使っています<br>3.7や3.8系を使っていないことに深い理由はありません<br>パッケージはvenvを使って管理しています|

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

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

# 課題
## [第9章: ベクトル空間法 (I)](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch9)
>[enwiki-20150112-400-r10-105752.txt.bz2](http://www.cl.ecei.tohoku.ac.jp/nlp100/data/enwiki-20150112-400-r10-105752.txt.bz2)は，2015年1月12日時点の英語のWikipedia記事のうち，約400語以上で構成される記事の中から，ランダムに1/10サンプリングした105,752記事のテキストをbzip2形式で圧縮したものである．このテキストをコーパスとして，単語の意味を表すベクトル（分散表現）を学習したい．第9章の前半では，コーパスから作成した単語文脈共起行列に主成分分析を適用し，単語ベクトルを学習する過程を，いくつかの処理に分けて実装する．第9章の後半では，学習で得られた単語ベクトル（300次元）を用い，単語の類似度計算やアナロジー（類推）を行う．
>
>なお，問題83を素直に実装すると，大量（約7GB）の主記憶が必要になる． メモリが不足する場合は，処理を工夫するか，1/100サンプリングのコーパス[enwiki-20150112-400-r100-10576.txt.bz2](http://www.cl.ecei.tohoku.ac.jp/nlp100/data/enwiki-20150112-400-r100-10576.txt.bz2)を用いよ．

今回は*「1/100サンプリングのコーパス[enwiki-20150112-400-r100-10576.txt.bz2](http://www.cl.ecei.tohoku.ac.jp/nlp100/data/enwiki-20150112-400-r100-10576.txt.bz2)」*を使っています。

### 83. 単語／文脈の頻度の計測
>82の出力を利用し，以下の出現分布，および定数を求めよ．
>
>- $ f(t,c) $: 単語$ t $と文脈語$ c $の共起回数
>- $ f(t,∗) $: 単語$ t $の出現回数
>- $ f(∗,c) $: 文脈語$ c $の出現回数
>- $ N $:単語と文脈語のペアの総出現回数

### 課題補足
「単語$ t $」は[前回の記事](https://qiita.com/FukuharaYohei/items/64e20ced30cba76383dc#%E6%96%87%E8%84%88%E8%AA%9E%E3%81%A8%E3%81%AF)に書いた**「対象語(Target Word)」**のことです。
仮に82の出力が以下のようになっていたとします。

|t(対象語)|c(文脈語)|
|:-:|:-:|
|t1|c1|
|t1|c2|
|t2|c1|
|t1|c1|

そうすると以下を出力します。いわゆるSQLのGroup By ですね。

**$f(t,c)$: 単語$ t $と文脈語$ c $の共起回数**

|t(対象語)|c(文脈語)|共起回数|
|:-:|:-:|:-:|
|t1|c1|2|
|t1|c2|1|
|t2|c1|1|

**$ f(t,∗) $: 単語$ t $の出現回数**

|t(対象語)|出現回数|
|:-:|:-:|
|t1|3|
|t2|1|

**$ f(∗,c) $: 文脈語$ c $の出現回数**

|c(文脈語)|出現回数|
|:-:|:-:|
|c1|3|
|c2|1|

**$ N $:単語と文脈語のペアの総出現回数**
4(全体が4行だから)

# 回答

## 回答プログラム [083.単語／文脈の頻度の計測.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/09.%E3%83%99%E3%82%AF%E3%83%88%E3%83%AB%E7%A9%BA%E9%96%93%E6%B3%95%20(I)/083.%20%E5%8D%98%E8%AA%9E%EF%BC%8F%E6%96%87%E8%84%88%E3%81%AE%E9%A0%BB%E5%BA%A6%E3%81%AE%E8%A8%88%E6%B8%AC.ipynb)

```python
import sys

import pandas as pd

df = pd.read_table('./082.context.txt', header=None, names=['t', 'c'])
print(df.info())

def to_pickle_file(grouped, path):
    print('length:', grouped.size)
    grouped.to_pickle(path)

to_pickle_file(df.groupby(['t','c'])['c'].agg('count'), './083_group_tc.zip')
to_pickle_file(df.groupby('t')['c'].agg('count'), './083_group_t.zip')
to_pickle_file(df.groupby('c')['c'].agg('count'), './083_group_c.zip')
```

## 回答解説
pandas使って列名を`t`として`c`ファイルを読み込んでいます。

```python
df = pd.read_table('./082.context.txt', header=None, names=['t', 'c'])
print(df.info())
```

`df.info()`の結果として以下が出力され、*「$ N $:単語と文脈語のペアの総出現回数」*の結果が68000317だとわかります。メモリを1GBほど使っているのもわかります。ちなみに読込部分に限ると1.5分ほどかかりました。

```bash
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 68000317 entries, 0 to 68000316
Data columns (total 2 columns):
t    object
c    object
dtypes: object(2)
memory usage: 1.0+ GB
None
```

ここで、pandasの`groupby`結果のカウントを`pickle`で保存しています。ファイル拡張子をzipにすると自動で圧縮してくれるので便利です。ここで保存したファイルを読み込むと、保存時と同じpandasのSeriesオブジェクトとして復元できます。

```python
def to_pickle_file(grouped, path):
    print('length:', grouped.size)
    grouped.to_pickle(path)
```

今回のメイン部分です。pandasの`groupby`を使ってグルーピングをして、その結果をカウントしています。

```python
to_pickle_file(df.groupby(['t','c'])['c'].agg('count'), './083_group_tc.zip')
to_pickle_file(df.groupby('t')['c'].agg('count'), './083_group_t.zip')
to_pickle_file(df.groupby('c')['c'].agg('count'), './083_group_c.zip')
```

ちなみに以下が各処理の情報です。

||行数|処理時間|ファイルサイズ|
|:-:|:-:|:-:|:-:|
|$ f(t,c) $|21,327,945行|4min 38s|103.7MB|
|$ f(t,*) $|388,836行|34.7s|2.8MB|
|$ f(*,c) $|388,836行|24.2s|2.8MB|


# Tips/トラブルシュート系

## Tips: ファイルの縮小
対象とするファイルサイズが大きい(約800MB)ので、試行錯誤が非常にしにくかったです。そのため、最初は最初の10万行のみを対象したファイルを作成してコーディングをしていました。

```bash
cat 082.context.txt | head -n 100000 >> 082.context_mini.txt
```

## 大きいサイズのファイルで特定行の出力
`DataFrame`でXX行目にエラー、とあったときに`head`と`tail`を組み合わせてファイルの中身を見ました。通常は、単純にファイルを開くだけなのですが、大きいサイズのファイルの場合は開くだけで時間がかかるので、こんなことをしていました。
下記コマンドではファイルの12198行目から3行を表示しています。ちなみにこのエラーは、[前回記事の「こぼれ話」]()に書いた文のトークン化に関する失敗です。

```bash
$ cat 082.context.txt | head -n 124150 | tail -n 3

"b")("s"	"c
−	"b")("s"
−	"c
```