自身のQiita記事(こちら)の転載です。
東北大乾・岡崎研(現乾・鈴木研)で作成された、新人研修の一つであるプログラミング基礎勉強会の教材『言語処理100本ノック 2020年版』をPython(3.7)で解いた記事の第4弾です。
独学でPythonを勉強してきたので、間違いやもっと効率の良い方法があるかもしれません。
改善点を見つけた際はご指摘いただけると幸いです。
今回取り組んだ部分に関しては一部pandasというデータ分析ライブラリーを使わなかった場合と使った場合の2パターンご紹介しています。
これらの言語処理は将来的に機械学習に用いることが多いと思いますので、機械学習でも用いられるpandasを使った方が、より活用の幅が広がるかもしれません。
後半はpandasでやらないと面倒なことが多くなってきたので、pandasのみで行っています。
UNIXコマンドの実行環境はMacOSですので、WindowsやLinuxをお使いの方は適宜自分の環境に合わせたやり方を調べていただければと思います。
ソースコードはGitHubにも公開しています。
第2章: UNIXコマンド
popular-names.txtは,アメリカで生まれた赤ちゃんの「名前」「性別」「人数」「年」をタブ区切り形式で格納したファイルである.以下の処理を行うプログラムを作成し,popular-names.txtを入力ファイルとして実行せよ.さらに,同様の処理をUNIXコマンドでも実行し,プログラムの実行結果を確認せよ.
15. 末尾のN行を出力
自然数Nをコマンドライン引数などの手段で受け取り,入力のうち末尾のN行だけを表示せよ.確認にはtailコマンドを用いよ.
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行ずつ出力する形としました。
#!/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では
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
で差分を確認するのに一番苦労しました…
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
の結果と比較するためのシェルスクリプトは
#!/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による処理は
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列目の文字列の異なり
1列目の文字列の種類(異なる文字列の集合)を求めよ.確認にはcut, sort, uniqコマンドを用いよ.
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
にした方が適切でしたね…
シェルスクリプトは下記のようにしました。
#!/bin/bash
diff <(cut -f 1 -d $'\t' popular-names.txt | sort | uniq) <(python 17.py)
cut
コマンドは-f
オプションで何列目かを指定、-d
オプションで区切り文字をタブ文字に指定しています。
切り出した1列目はsort
コマンドに渡して、アルファベット順にした後、uniq
コマンドで重複を取り除きました。
pandasに加えてnumpyも使うと楽にできて、さすがライブラリというところです。
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を利用しています。
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
コマンドは次の使い方。
#!/bin/bash
sort -t $'\t' -k 3 -n -r popular-names.txt
-t
オプションで区切り文字を、-k
オプションで列数を、-r
オプションで降順を指定して、-n
オプションで数値としてソートしています。
19. 各行の1コラム目の文字列の出現頻度を求め,出現頻度の高い順に並べる
各行の1列目の文字列の出現頻度を求め,その高い順に並べて表示せよ.確認にはcut, uniq, sortコマンドを用いよ.
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オブジェクトをデフォルトでは降順に返します。
出力する際はは見やすいように、要素と出現頻度を入れ替えて出力しました。
シェルスクリプトでは
#!/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に使い慣れてくるというメリットも。
ライブラリを用いるだけでやりたいことが簡単にできることも多いと身を持って知れたので、今後はライブラリをどんどん活用していきたいと思います。
まだまだ未熟なので、よりよい解答がありましたらぜひご指摘ください!!
よろしくお願いいたします。