言語処理100本ノック2020年版を解いてみた【第2章：UNIXコマンド 15〜19】

東北大乾・岡崎研（現乾・鈴木研）で作成された、新人研修の一つであるプログラミング基礎勉強会の教材『[言語処理100本ノック 2020年版](https://nlp100.github.io/ja/)』をPython（3.7）で解いた記事の第4弾です。

独学でPythonを勉強してきたので、間違いやもっと効率の良い方法があるかもしれません。
改善点を見つけた際はご指摘いただけると幸いです。

今回取り組んだ部分に関しては一部pandasというデータ分析ライブラリーを使わなかった場合と使った場合の2パターンご紹介しています。
これらの言語処理は将来的に機械学習に用いることが多いと思いますので、機械学習でも用いられるpandasを使った方が、より活用の幅が広がるかもしれません。
後半はpandasでやらないと面倒なことが多くなってきたので、pandasのみで行っています。

UNIXコマンドの実行環境はMacOSですので、WindowsやLinuxをお使いの方は適宜自分の環境に合わせたやり方を調べていただければと思います。

ソースコードは[GitHub](https://github.com/Soh1121/NLP100-2020)にも公開しています。

## 第2章: UNIXコマンド

> [popular-names.txt](https://nlp100.github.io/data/popular-names.txt)は，アメリカで生まれた赤ちゃんの「名前」「性別」「人数」「年」をタブ区切り形式で格納したファイルである．以下の処理を行うプログラムを作成し，[popular-names.txt](https://nlp100.github.io/data/popular-names.txt)を入力ファイルとして実行せよ．さらに，同様の処理をUNIXコマンドでも実行し，プログラムの実行結果を確認せよ．

### 15. 末尾のN行を出力

> 自然数Nをコマンドライン引数などの手段で受け取り，入力のうち末尾のN行だけを表示せよ．確認にはtailコマンドを用いよ．

```python:15.py
import sys


if len(sys.argv) != 2:
    print("Set an argument N, for exapmle '$ python 15.py 3'.")
    sys.exit()

n = int(sys.argv[1])
file_name = "popular-names.txt"

with open(file_name) as rf:
    lines = rf.readlines()

for i in lines[len(lines) - n:len(lines)]:
    print("".join(i.rstrip()))
```

ファイルオブジェクトに対して`readlines()`メソッドでファイル全体を1行ずつリストとして読み込みます。
入力のうち末尾N行出力するだけであれば、リストの末尾から`n`回出力するだけで良かったのですが、今回はUNIXコマンドと出力の形を統一するために上記のように`len(lines) - 1`から`len(lines)`までのリストから1行ずつ出力する形としました。

```sh:15.sh
#!/bin/bash

if [ $# -ne 1 ]; then
  echo "指定された引数は$#個です。" 1>&2
  echo "実行するには数字を1個引数に指定してください。" 1>&2
  exit 1
fi

diff <(tail -n $1 popular-names.txt) <(python 15.py $1)
```

末尾`n`行を出力するUNIXコマンドは`tail`コマンドを用います。
オプションに`-n`を指定して行数を指定すれば意図したとおりに出力できます。

pandasでは

```python:15pd.py
import sys
import pandas as pd


if len(sys.argv) != 2:
    print("Set a argument N, for example '$ python 15pd.py 3'.")
    sys.exit()

n = int(sys.argv[1])
data_frame = pd.read_table("popular-names.txt", header=None)
print(data_frame.tail(n))
```

DataFrameオブジェクトとしてファイルから読み込んで、`tail()`メソッドで引数に行数を指定するだけで求める形で出力できます。

### 16. ファイルをN分割する

> 自然数Nをコマンドライン引数などの手段で受け取り，入力のファイルを行単位でN分割せよ．同様の処理をsplitコマンドで実現せよ．

**※問題文を読み間違え、ファイルをN行ずつに分割しています**
（記事を書いている際に気づいたので一通りやり終えたら修正予定です。）

これは、`diff`で差分を確認するのに一番苦労しました…

```python:16.py
import sys


if len(sys.argv) != 2:
    print("Set an argument N, for exapmle '$ python 15.py 3'.")
    sys.exit()

n = int(sys.argv[1])
file_name = "popular-names.txt"

with open(file_name) as rf:
    lines = rf.readlines()

file_count = 0
i = 1
output = ""
for line in lines:
    output += line
    if i <= n - 1:
        i += 1
        continue
    q, mod = divmod(file_count, 26)
    prefix = "./output/16/py_split_file_"
    suffix_1 = chr(ord('a') + q)
    suffix_2 = chr(ord('a') + mod)
    write_file = "{}{}{}".format(prefix, suffix_1, suffix_2)
    with open(write_file, mode='w') as wf:
        wf.write(output)
    file_count += 1
    output = ""
    i = 1
```

こちらもファイル全体を1行ずつリストとして読み込みます。
そして、アウトプット用に作成した変数に1行ずつ追加していき、N行追加し終えたらファイル出力しました。
ファイル出力は`split`コマンドによる出力のファイル名にするための処理が長々と続いています…

`split`の結果と比較するためのシェルスクリプトは

```sh:16.sh
#!/bin/bash

SH="sh_"
PY="py_"
HEAD="split_file_"

if [ $# -ne 1 ]; then
  echo "指定された引数は$#個です。" 1>&2
  echo "実行するには数字を1個引数に指定してください。" 1>&2
  exit 1
fi

split -l $1 popular-names.txt ./output/16/$SH$HEAD

for i in a b c d e f g h i j k l m n o p q r s t u v w x y z
do
  for j in a b c d e f g h i j k l m n o p q r s t u v w x y z
  do
    ADDRESS="./output/16/"
    SHFILE=$ADDRESS$SH$HEAD$i$j
    PYFILE=$ADDRESS$PY$HEAD$i$j
    if [ -e $SHFILE -a -e $PYFILE ]; then
      diff $SHFILE $PYFILE
    fi
  done
done
```

ファイル名の指定に苦戦していますが、肝心のところは`split`コマンドです。
`-l`オプションで行数を指定しています。

`split`以下の部分は`diff`で比較するための処理なので説明は割愛します。

pandasによる処理は

```python:16pd.py
import sys
import pandas as pd


if len(sys.argv) != 2:
    print("Set a argument N, for example '$ python 15pd.py 3'.")
    sys.exit()

n = int(sys.argv[1])
data_frame = pd.read_table("popular-names.txt", header=None)

file_count = 0
i = 0
while i < len(data_frame):
    q, mod = divmod(file_count, 26)
    prefix = "./output/16/py_split_file_"
    suffix_1 = chr(ord('a') + q)
    suffix_2 = chr(ord('a') + mod)
    write_file = "{}{}{}".format(prefix, suffix_1, suffix_2)
    data_frame[i:i+n].to_csv(write_file, sep='\t', index=False, header=None)
    i += n
    file_count += 1
```

pandasではDataFrameオブジェクトの`i`番目の要素から`i+n`番目の要素までスライスで指定してファイル出力することで課題を解決しました。
pandasを使わずとも同じことはできるので、どちらがよいか検討するのもいいかもしれません。

### 17. １列目の文字列の異なり

> 1列目の文字列の種類（異なる文字列の集合）を求めよ．確認にはcut, sort, uniqコマンドを用いよ．

```python:17.py
file_name = "popular-names.txt"
with open(file_name) as f:
    lines = f.readlines()
    item1 = list(map(lambda x: x.split()[0], lines))

item1 = list(set(item1))
item1.sort()
print("\n".join(item1))
```

この課題はファイル全体を1行ずつのリストとして取得し、`map`関数を用いて1列目を得た後、リストを指定して1文字ずつに分解しています。
`sort`コマンドを用いるという支持があったので、`set`で集合にしたものをリスト化してソートしました。
振り返ってみると、`map`関数の結果を`list`にするよりも`set`にした方が適切でしたね…

シェルスクリプトは下記のようにしました。

```sh:17.sh
#!/bin/bash

diff <(cut -f 1 -d $'\t' popular-names.txt | sort | uniq) <(python 17.py)
```

`cut`コマンドは`-f`オプションで何列目かを指定、`-d`オプションで区切り文字をタブ文字に指定しています。
切り出した1列目は`sort`コマンドに渡して、アルファベット順にした後、`uniq`コマンドで重複を取り除きました。

pandasに加えてnumpyも使うと楽にできて、さすがライブラリというところです。

```python:17pd.py
import pandas as pd
import numpy as np

data_frame = pd.read_table("popular-names.txt", header=None)
print("\n".join(np.sort(data_frame[0].unique())))
```

pandasでデータを読み込んで、`unique()`メソッドで重複を除去。
numpyでソートして改行で`join`することで一気に課題をクリアできます。

### 18. 各行を3コラム目の数値の降順にソート

> 各行を3コラム目の数値の逆順で整列せよ（注意: 各行の内容は変更せずに並び替えよ）．確認にはsortコマンドを用いよ（この問題はコマンドで実行した時の結果と合わなくてもよい）．

ここからはすべてpandasを利用しています。

```python:18.py
import pandas as pd


data_frame = pd.read_table("popular-names.txt", header=None)
print(data_frame.sort_values(2, ascending=False))
```

`sort_values()`メソッドを使うと簡単にできます。　
第一引数はソートする列の指定。
第二引数の`ascending`は昇順・降順の指定で、デフォルトは昇順の`True`ですが、ここでは降順との指示なので`False`としています。

`sort`コマンドは次の使い方。

```sh:18.sh
#!/bin/bash

sort -t $'\t' -k 3 -n -r popular-names.txt
```

`-t`オプションで区切り文字を、`-k`オプションで列数を、`-r`オプションで降順を指定して、`-n`オプションで数値としてソートしています。

### 19. 各行の1コラム目の文字列の出現頻度を求め，出現頻度の高い順に並べる

> 各行の1列目の文字列の出現頻度を求め，その高い順に並べて表示せよ．確認にはcut, uniq, sortコマンドを用いよ．

```python:19.py
import pandas as pd

data_frame = pd.read_table("popular-names.txt", header=None)
data_frame_sort = data_frame[0].value_counts()
print(pd.Series(data_frame_sort.index.values, data_frame_sort.values))
```

DataFrameオブジェクトに対して、`value_counts()`メソッドを使うことで課題をクリアしています。
`value_counts()`メソッドはユニークな要素の値が`index`、その出現頻度が`data`となるSeriesオブジェクトをデフォルトでは降順に返します。
出力する際はは見やすいように、要素と出現頻度を入れ替えて出力しました。

シェルスクリプトでは

```sh:19.sh
#!/bin/bash

cut -f 1 -d $'\t' popular-names.txt | sort | uniq -c | sort -k 1 -n -r
```

としました。
`sort`までは`17.sh`と一緒ですが、`uniq`コマンドに`-c`オプションを指定して重複した行数も算出しています。
その後、`sort`コマンドを再度用いて`-k`で1列目（重複した行数）を指定、数値として降順でソートして出力しました。

## まとめ

この記事では言語処理100本ノック2020年版 第2章：UNIXコマンドの問題番号15〜19までを解いてみました。

UNIXコマンドの使い方に苦戦したものの、pandasに使い慣れてくるというメリットも。
ライブラリを用いるだけでやりたいことが簡単にできることも多いと身を持って知れたので、今後はライブラリをどんどん活用していきたいと思います。

まだまだ未熟なので、よりよい解答がありましたらぜひご指摘ください！！
よろしくお願いいたします。

## つづき

- [言語処理100本ノック2020年版を解いてみた【第3章: 正規表現 20〜24】](https://qiita.com/Soh1121/items/19405032949c4657fc8c)
- [言語処理100本ノック2020年版を解いてみた【第3章：正規表現 25〜29】](https://qiita.com/Soh1121/items/6449cd67a7b77d8bd244)
- [言語処理100本ノック2020年版を解いてみた【第4章：形態素解析 30〜34】](https://qiita.com/Soh1121/items/dd769127d6e494459967)

## 前回まで

- [言語処理100本ノック2020年版を解いてみた【第1章：準備運動 00〜04】](https://qiita.com/Soh1121/items/bd970195f93a23540d4c)
- [言語処理100本ノック2020年版を解いてみた【第1章：準備運動 05〜09】](https://qiita.com/Soh1121/items/edd976bc58909dd79062)
- [言語処理100本ノック2020年版を解いてみた【第2章：UNIXコマンド 10〜14】](https://qiita.com/Soh1121/items/5d35e6718d3f17a9976e)