言語処理100本ノック【NLP 10 ～ 19】

# はじめに
自然言語処理を専門として研究に励んでいる大学院生なのですが，[言語処理100本ノック](https://nlp100.github.io/ja/)をやったことがなかったので，アドベントカレンダに合わせて取り組んでみることにしました．
簡単な説明を付した記事を投稿予定ですが，どこまで続くかは忙しさによるのであまり期待しないでください……

さて，今回は10〜19です．
普段使用している言語がPythonなのでその実装を示そうかなとも思いましたが，既に多くの人が解説している & それほど難しくないということで，UNIXコマンドを示したいと思います．

前回の記事もあるので，良ければ見てみて下さい！

https://qiita.com/Yorozuya59/items/f02f0c514d6f89554dc5


# 10. 行数のカウント
行数をカウントせよ．確認にはwcコマンドを用いよ．

```bash: 10.sh
wc -l $1

# Usage:
# $ bash 10.sh popular-names.txt 
```

個人的にはあまり使わないコマンドですが，研究においても度々登場（フィルタを掛けた後のファイルのサイズ確認など）するので，存在を知っておく程度で良いかなと思っています．

さて，コマンド自体の説明です．
先ず，`wc -l popular-names.txt`の`-l`オプションは行数を表示するように指定するものです．また，bashファイルとして扱うとき，引数は`$1`で受け取ることができます．（2つ目以降も`$2`や`$3`として受け取ることができます．）

普段，コマンドをbashファイルに書き留めておくのは，非常によく使うコマンド or 長いコマンド or 複雑なコマンドを実行する場合が多いです．
今回の場合であれば使うメリットが少ないですが，特に複数人のプロジェクトなどでは役に立つこともあります．

長くなりましたが，（Python）プログラムで実装するなら，`\n`区切りで文字列を取得して，その文字列の個数をカウントするのが良いでしょうか．

:::note warn
以下のコードは検証していません．
:::

```python
with open('popular-names.txt', 'r') as fp:
    lines = fp.readlines()

print(len(lines))
```


# 11. タブをスペースに置換
タブ1文字につきスペース1文字に置換せよ．確認にはsedコマンド，trコマンド，もしくはexpandコマンドを用いよ．

```bash: 11.sh
sed -e "s/\t/\ /g" $1 

# Usage:
# $ bash 11.sh popular-names.txt | less
```

こちらのコマンドも知っておくと役に立つことがあるという程度でしょうか．
`sed`コマンドは`"s/変更前/変更後/g"`として与えることで置換してくれます．

こちらも，Pythonで実装するなら，ということを考えてみます．
str型には`replace`という組み込み関数が用意されているので，そちらを利用するのが良いのではないでしょうか．

:::note warn
以下のコードは検証していません．
:::

```python
with open('popular-names.txt', 'r') as fp:
    lines = fp.readlines()

print(*[line.replace('\t', ' ') for line in lines])
```


# 12. 1列目をcol1.txtに，2列目をcol2.txtに保存
各行の1列目だけを抜き出したものをcol1.txtに，2列目だけを抜き出したものをcol2.txtとしてファイルに保存せよ．確認にはcutコマンドを用いよ．

```bash: 12.sh
cut -f 1 $1 > col1.txt
cut -f 2 $1 > col2.txt

# Usage:
# $ bash 12.sh popular-names.txt
```

こちらのコマンドも知っておくと役に立つことがあるという程度でしょうか．
`cut`コマンドは`-f`オプションで（デフォルトでは）タブ文字を区切りとして，指定の列を抜き出す操作をします．

では，Pythonで実装するなら，ということを考えてみます．
標準ライブラリのみで実装しても問題ないのですが，そればかりだと面白くないのでPolarsを用いた実装も検討してみます．

:::note warn
以下のコードは検証していません．
:::

```python
import polars as pl 


df = pl.read_csv('popular-names.txt', separator='\t', has_header=False)

column1 = df.get_column('column_1').to_list()
column2 = df.get_column('column_2').to_list()

with open('col1.txt', 'w'), open('col2.txt', 'w') as fp1, fp2:
    fp1.writelines(column1)
    fp2.writelines(column2)
```


# 13. col1.txtとcol2.txtをマージ
12で作ったcol1.txtとcol2.txtを結合し，元のファイルの1列目と2列目をタブ区切りで並べたテキストファイルを作成せよ．確認にはpasteコマンドを用いよ．

```bash: 13.sh
paste $1 $2 > $3 

# Usage:
# $ bash 12.sh popular-names.txt && bash 13.sh col1.txt col2.txt col1_col2.txt 
```

こちらのコマンドも知っておくと役に立つことがあるという程度でしょうか．
オプションとかも用いる必要がないため，特筆するべきことも無さそうです．

こちらについても，Pythonで実装するなら，ということを考えてみます．

:::note warn
以下のコードは検証していません．
:::

```python
with open('col1.txt', 'r'), open('col2.txt', 'r') as fp1, fp2:
    column1 = fp1.readlines()
    column2 = fp2.readlines()

lines = [f'{elem1}\t{elem2}' for elem1, elem2 in zip(column1, column2)]

with open('merged.txt', 'w') as fp:
    fp.writelines(lines)
```


# 14. 先頭からN行を出力
自然数Nをコマンドライン引数などの手段で受け取り，入力のうち先頭のN行だけを表示せよ．確認にはheadコマンドを用いよ．

```bash: 14.sh
head -n $1 $2

# Usage:
# $ bash 14.sh 5 popular-names.txt
```

こちらのコマンドは確実に知っておくべきコマンドの1つと言っても過言ではないのではないでしょうか？次の問題で使用する`tail`コマンドも合わせて知っておくと良いかなと思います．

さて，Pythonで実装するならこんな感じでしょうか．

:::note warn
以下のコードは検証していません．
:::

```python
n = int(input())

with open('popular-names.txt', 'r') as fp:
    lines = fp.readlines()

print(*lines[: n])
```


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

```bash:15.sh
tail -n $1 $2

# Usage:
# $ bash 15.sh 5 popular-names.txt
```

`tail`コマンドについての補足になりますが，個人的には`tail`コマンドは`-f`オプションと同時に使うことが多い気がします．`-f`オプションを付けるとファイルに追記が発生したときに自動的に追記内容を表示してくれます．ログファイルを確認するときなどに非常に便利です．

Pythonでの実装を示すまでもないですが，一応．

:::note warn
以下のコードは検証していません．
:::

```python
n = int(input())

with open('popular-names.txt', 'r') as fp:
    lines = fp.readlines()

print(*lines[: -n])
```


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

```bash: 16.sh
total_lines=$(wc -l < $1)
lines_per_file=$(( ($total_lines + $2 - 1) / $2 ))

split -d -l $lines_per_file -a 4 --additional-suffix=.txt $1 ./output_dir/16_

# Usage:
# $ mkdir output_dir && rm -f output_dir/* && bash 16.sh popular-names.txt 10
```

少しスクリプトが複雑になりましたが，処理としては単純です．1つのファイルあたりの行数を求めて，その通りに分割するだけです．
オプションは，`-d`が分割後のファイルを数字で管理，`-l`が分割後のファイルの行数を指定，`-a`が分割後のファイル名の接尾を何桁で表現するかを指定，`--additional-suffix`が分割後のファイルの拡張子を指定，といった感じです．

ほとんど同じ操作をPythonで実装してみると次のような感じでしょうか．

:::note warn
以下のコードは検証していません．
:::

```python
n = int(input())

with open('popular-names.txt', 'r') as fp:
    lines = fp.readlines()

for idx, split_lines in enumerate(lines):
    with open(f'./output_dir/16_{idx}', 'w') as fp:
        fp.writelines(split_lines)
```


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

```bash: 17.sh
cut -f 1 $1 | sort | uniq -c | sort -nr

# Usage:
# $ bash 17.sh popular-names.txt
```

本問題も既に登場したコマンドを如何に組み合わせるかという問題ですね．
手順としては，1列目を取り出し，ソートして，ユニークな個数をカウントし，カウントの逆順にソートすることで得られます．

Polarsを使った実装を検討してみましょう．

:::note warn
以下のコードは検証していません．
:::

```python
import polars as pl 


df = pl.read_csv('popular-names.txt', separator='\t', has_header=False)
counts = df.get_column('column_1').value_counts(sort=True)

print(counts)
```


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

```bash: 18.sh
sort -nr -k 3 $1

# Usage:
# $ bash 18.sh popular-names.txt | less 
```

説明することが無くなってきたので，早速Pythonの実装を確認してみます．

:::note warn
以下のコードは検証していません．
:::

```python
import polars as pl 


df = pl.read_csv('popular-names.txt', separator='\t', has_header=False)
df = df.sort('column_3', descending=True)

print(df)
```


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

```bash: 19.sh
cut -f 1 $1 | sort | uniq -c | sort -nr 

# Usage:
# $ bash 19.sh popular-names.txt
```

本問題もこれまでの組み合わせですね．
早速Pythonの実装を確認してみます．

:::note warn
以下のコードは検証していません．
:::

```python
import polars as pl 


df = pl.read_csv('popular-names.txt', separator='\t', has_header=False)
rank = (
    df.get_column('column_1')
    .value_counts(sort=True)
    .get_column('column_1')
)

print(rank)
```


# 最後に
いかがでしたでしょうか？

個人的には，第2章の問題はソースコードの実装よりもUNIXコマンドの方が難しかったように思いました．