言語処理100本ノック(2020)-50: データの入手・整形


[言語処理100本ノック 2020 (Rev2)](https://nlp100.github.io/ja/)の[「第6章: 機械学習」](https://nlp100.github.io/ja/ch06.html)の[50本目「データの入手・整形」](https://nlp100.github.io/ja/ch06.html#50-%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%85%A5%E6%89%8B%E6%95%B4%E5%BD%A2)記録です。
多分[「第6章: 機械学習」](https://nlp100.github.io/ja/ch06.html)は52以降をやりませんが、[「第8章: ニューラルネット」](https://nlp100.github.io/ja/ch08.html)をやりたかったため、前提となるこのノックに取り組みました。カンニングもしているので、コーディング自体は30分程度で終わっていますが、久々に100本ノック関連の記事書いたり、Google Colaboratory やGitHubを久々に使ったりとしていたら3時間くらいかかってしましました。**ノック自体は非常に簡単**で、ただの準備運動です。
[記事「まとめ: 言語処理100本ノックで学べることと成果」](https://qiita.com/FukuharaYohei/items/6f0af3c4e270d8af2d99)に[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)についてはまとめていますが、追加で差分の[言語処理100本ノック 2020 (Rev2)](https://nlp100.github.io/ja/)についても更新します。

# 参考リンク

|リンク|備考|
|:--|:--|
|[050.データの入手・整形.ipynb](https://github.com/YoheiFukuhara/nlp100-2020/blob/main/06.%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92/50_%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%85%A5%E6%89%8B%E3%83%BB%E6%95%B4%E5%BD%A2.ipynb)|回答プログラムのGitHubリンク|
|[Qiita記事](https://qiita.com/yamaru/items/374e6e2c045387f4d3b4#50-%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%85%A5%E6%89%8B%E6%95%B4%E5%BD%A2)|多くのソース部分のコピペ元|
|[まとめ: 言語処理100本ノックで学べることと成果](https://qiita.com/FukuharaYohei/items/6f0af3c4e270d8af2d99)|言語処理100本ノックまとめ記事|

# 環境
後々GPUを使わないと厳しいので、Goolge Colaboratory使いました。Pythonやそのパッケージでより新しいバージョンありますが、新機能使っていないので、プリインストールされているものをそのまま使っています。

|種類|バージョン|内容|
|:--|:--|:--|
|Python|3.7.11|Google Colaboratoryのバージョン|
|google|2.0.3|Google Colaboratoryで提供されていたもの|
|pandas|1.1.5|Google Colaboratoryで提供されていたもの|
|scikit-learn|0.22.2|Google Colaboratoryで提供されていたもの|

# [第6章: 機械学習](https://nlp100.github.io/ja/ch06.html)
## 学習内容
> 『文書分類器を機械学習で構築します．さらに，機械学習手法の評価方法を学びます．

## ノック内容
> 本章では，Fabio Gasparetti氏が公開している[News Aggregator Data Set](https://archive.ics.uci.edu/ml/datasets/News+Aggregator)を用い，ニュース記事の見出しを「ビジネス」「科学技術」「エンターテイメント」「健康」のカテゴリに分類するタスク（カテゴリ分類）に取り組む．

### [50. データの入手・整形](https://nlp100.github.io/ja/ch06.html#50-%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%85%A5%E6%89%8B%E6%95%B4%E5%BD%A2)
>[News Aggregator Data Set](https://archive.ics.uci.edu/ml/datasets/News+Aggregator)をダウンロードし、以下の要領で学習データ（train.txt），検証データ（valid.txt），評価データ（test.txt）を作成せよ．

> 1. ダウンロードしたzipファイルを解凍し，readme.txtの説明を読む．
1. 情報源（publisher）が”Reuters”, “Huffington Post”, “Businessweek”, “Contactmusic.com”, “Daily Mail”の事例（記事）のみを抽出する．
1. 抽出された事例をランダムに並び替える．
1. 抽出された事例の80%を学習データ，残りの10%ずつを検証データと評価データに分割し，それぞれtrain.txt，valid.txt，test.txtというファイル名で保存する．ファイルには，１行に１事例を書き出すこととし，カテゴリ名と記事見出しのタブ区切り形式とせよ（このファイルは後に問題70で再利用する）．

> 学習データと評価データを作成したら，各カテゴリの事例数を確認せよ．

# 回答
## 回答結果
ノックの本題である「各カテゴリの事例数」です。一応、データセットごとの数値も出力しています。
略字の説明です。
> b = business, t = science and technology, e = entertainment, m = health

```:各カテゴリの事例数
--total--
e    152828
b    115967
t    108503
m     45639
Name: category, dtype: int64
--train--
b    4501
e    4235
t    1220
m     728
Name: category, dtype: int64
--valid--
b    563
e    529
t    153
m     91
Name: category, dtype: int64
--test--
b    563
e    530
t    152
m     91
Name: category, dtype: int64
```

## 回答プログラム [050.データの入手・整形.ipynb](https://github.com/YoheiFukuhara/nlp100-2020/blob/main/06.%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92/50_%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%85%A5%E6%89%8B%E3%83%BB%E6%95%B4%E5%BD%A2.ipynb)
GitHubには確認用コードも含めていますが、ここには必要なものだけ載せています。

```python
from google.colab import drive
import pandas as pd
from sklearn.model_selection import train_test_split

drive.mount('/content/drive')

BASE_PATH = '/content/drive/MyDrive/ColabNotebooks/ML/NLP100_2020/06.MachineLearning'

# quotingをデフォルトの0にすると、ダブルコーテーションがTITLEの文字列先頭にあったときに、変な分割をしてしまう
df = pd.read_csv(BASE_PATH + '/input/newsCorpora.csv', 
                 header=None, sep='\t', usecols=[0, 1, 3, 4], index_col=0, 
                 names=['id', 'title', 'publisher', 'category'], quoting=3)

df = df.loc[df['publisher'].isin(['Reuters', 'Huffington Post', 'Businessweek', 'Contactmusic.com', 'Daily Mail']), ['title', 'category']]

train, valid_test = train_test_split(df, test_size=0.2, random_state=123, stratify=df['category'])
valid, test = train_test_split(valid_test, test_size=0.5, random_state=123, stratify=valid_test['category'])


# データの保存
train.to_csv(BASE_PATH + '/train.txt', sep='\t', index=False)
valid.to_csv(BASE_PATH + '/valid.txt', sep='\t', index=False)
test.to_csv(BASE_PATH + '/test.txt', sep='\t', index=False)

# 事例数の確認
print('--total--')
print(df['category'].value_counts())
print('--train--')
print(train['category'].value_counts())
print('--valida--')
print(valid['category'].value_counts())
print('--test--')
print(test['category'].value_counts())
```

## 回答解説
いくつか回答の補足説明です。

### データ確認
[News Aggregator Data Set](https://archive.ics.uci.edu/ml/datasets/News+Aggregator)を確認します。
対象のファイルは`"newsCorpora.csv"`で以下の列があるようです。

|No.|列名|使用|内容|
|:-:|:-:|:-:|:--|
|0|ID|Indexとして使用|Numeric ID||
|1|TITLE|使用(説明変数)|News title||
|2|URL|不使用|Url|
|3|PUBLISHER|使用(フィルタ用)|Publisher name|
|4|CATEGORY|使用(目的変数)|News category (b = business, t = science and technology, e = entertainment, m = health)||
|5|STORY|不使用|Alphanumeric ID of the cluster that includes news about the same story|
|6|HOSTNAME|不使用|Url hostname|
|7|TIMESTAMP|不使用|Approximate time the news was published, as the number of milliseconds since the epoch 00:00:00 GMT, January 1, 1970|

少しわかりにくかったですが、以下のリンクからデータをダウンロードし、zip内の`"newsCorpora.csv"`を使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/dae92ce2-ed63-94da-ad6e-3053384a3cde.png)

### データ読込
pandasでデータ読込します。
不要な列は読み込みません。`TITLE`と`PUBLISHER`と`CATEGORY`以外は使わないようなので読込しません。`ID`はインデックス列として使いました。
`TITLE`にはダブルコーテーションで始まる文字列があるようで、その場合、意図しないエスケープ処理が行われるため`quoting=3`として、ダブルコーテーションによるエスケープ処理を無効化しました。

```python:
# quotingをデフォルトの0にすると、ダブルコーテーションがTITLEの文字列先頭にあったときに、変な分割をしてしまう
df = pd.read_csv(BASE_PATH + '/input/newsCorpora.csv', 
                 header=None, sep='\t', usecols=[0, 1, 3, 4], index_col=0, 
                 names=['id', 'title', 'publisher', 'category'], quoting=3)
```

### データ数確認
`info`でデータ数を確認します。[News Aggregator Data Set](https://archive.ics.uci.edu/ml/datasets/News+Aggregator)に書かれている以下の数値と一致します。

> 422937 news pages and divided up into:

```python
df.info()
```

```:info結果
<class 'pandas.core.frame.DataFrame'>
Int64Index: 422937 entries, 1 to 422937
Data columns (total 3 columns):
 #   Column     Non-Null Count   Dtype 
---  ------     --------------   ----- 
 0   title      422937 non-null  object
 1   publisher  422935 non-null  object
 2   category   422937 non-null  object
dtypes: object(3)
memory usage: 12.9+ MB
```

### データ内容確認
そのままDataFrameの内容を確認。

```python
print(df)
```

```:print結果
                                                    title  ... category
id                                                         ...         
1       Fed official says weak data caused by weather,...  ...        b
2       Fed's Charles Plosser sees high bar for change...  ...        b
3       US open: Stocks fall after Fed official hints ...  ...        b
4       Fed risks falling 'behind the curve', Charles ...  ...        b
5       Fed's Plosser: Nasty Weather Has Curbed Job Gr...  ...        b
...                                                   ...  ...      ...
422933  Surgeons to remove 4-year-old's rib to rebuild...  ...        m
422934  Boy to have surgery on esophagus after battery...  ...        m
422935  Child who swallowed battery to have reconstruc...  ...        m
422936  Phoenix boy undergoes surgery to repair throat...  ...        m
422937  Phoenix boy undergoes surgery to repair throat...  ...        m

[422937 rows x 3 columns]
```

### フィルタ
`publisher`でフィルタして、列`publisher`をドロップします。

```python
df = df.loc[df['publisher'].isin(['Reuters', 'Huffington Post', 'Businessweek', 'Contactmusic.com', 'Daily Mail']), ['title', 'category']]
df.info()
```

データが1/30くらいまで少なくなりました。

```:info結果
<class 'pandas.core.frame.DataFrame'>
Int64Index: 13356 entries, 13 to 422838
Data columns (total 2 columns):
 #   Column    Non-Null Count  Dtype 
---  ------    --------------  ----- 
 0   title     13356 non-null  object
 1   category  13356 non-null  object
dtypes: object(2)
memory usage: 313.0+ KB
```

### データ分割
おなじみ`train_test_split`関数を使って分割します。
再現性があるように適当な数値でrandom_state固定しておきます。`stratify`を指定すると、指定列の件数を分割時に考慮してくれるようです(よく確認していないけど合っているはず)。
3分割を一気にできないか関数を探しましたが、[`fast-ml`パッケージ](https://pypi.org/project/fast-ml/)でGitHubにStar数が5しかないものしか見つからなかったので諦めました。

```python
train, valid_test = train_test_split(df, test_size=0.2, random_state=123, stratify=df['category'])
valid, test = train_test_split(valid_test, test_size=0.5, random_state=123, stratify=valid_test['category'])
```