自然言語100本ノック 2025 をやってみる(10-19)

の続きです。
こちらの「自然言語100本ノック 2025」の第二章です。
本章のミッションは以下の通り。
popular-names.txtは、アメリカで生まれた赤ちゃんの「名前」「性別」「人数」「年」をタブ区切り形式で格納したファイルである。以下の処理を行うプログラムを作成し、popular-names.txtを入力ファイルとして実行せよ。さらに、同様の処理をUNIXコマンドでも実行し、プログラムの実行結果を確認せよ。
popular-names.txtを読み込んで、以下をそれぞれ作成します
- Pythonで指定された処理を行うコード
- UNIXコマンドで同様の処理を行うコマンド

まずはファイル読み込みの方法を調べる
Pythonでどうやってファイル読み込みするのかわからなかったので調べました。
標準ライブラリで読み込む場合
file_path = 'popular-names.txt'
f = open(file_path,'r' , encoding='UTF-8')
data = f.read()
print(data)
f.close()
Pandasを使用する場合
お題のtxtファイルはタブ区切りになっています。
$ head -5 popular-names.txt
Mary F 7065 1880
Anna F 2604 1880
Emma F 2003 1880
Elizabeth F 1939 1880
Minnie F 1746 1880
セパレータの指定はr'\t'
で表現すればいいですね。
import pandas as pd
file_path = 'popular-names.txt'
df = pd.read_csv(file_path , sep=r'\t' , header=None)
print(df.head())
問題に合わせて扱いやすいほうを選択したいと思います。

コマンド実行環境
コマンド実行環境はWSL2、dashで代替します。
$ cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04.2 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04.2 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo
$ ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Mar 31 2024 /bin/sh -> dash

10. 行数のカウント
ファイルの行数をカウントせよ。確認にはwcコマンドを用いよ。
Python
DataFrameの行数もlen()で取得できるのですね。
以下のように記述してみました。
import pandas as pd
file_path = 'popular-names.txt'
df = pd.read_csv(file_path , sep=r"\t" , header=None)
len(df)
出力
2780
コマンド
設問分の指定通りwcコマンドを実行するだけです。
$ wc -l popular-names.txt
2780 popular-names.txt

11. 先頭からN行を出力
ファイルの先頭N行だけを表示せよ。例えば、N=10として先頭10行を表示せよ。確認にはheadコマンドを用いよ。
Python
単純にreadline10回してあげれば目的は達成できそうです。(が、もうちょっと宣言的に記載したいですね。)
N = 10
file_path = 'popular-names.txt'
f = open(file_path,'r' , encoding='UTF-8')
data = ""
for _ in range(N):
line = f.readline()
if not line:
break
data += line
f.close()
print(data)
改善:itertools.isliceを使って宣言的に
itertools.isliceを使えば、宣言的に書けるみたいです。
あとwithでファイルオープンしたほうが安全ですね。
改善してみました。
from itertools import islice
N = 10
file_path = 'popular-names.txt'
with open(file_path,'r' , encoding='UTF-8') as f:
print(''.join(islice(f , N)))
コマンド
コマンドで記述するならこうですね。
$ head -10 popular-names.txt
Mary F 7065 1880
Anna F 2604 1880
Emma F 2003 1880
Elizabeth F 1939 1880
Minnie F 1746 1880
Margaret F 1578 1880
Ida F 1472 1880
Alice F 1414 1880
Bertha F 1320 1880
Sarah F 1288 1880

12. 末尾のN行を出力
ファイルの末尾N行だけを表示せよ。例えば、N=10として末尾10行を表示せよ。確認にはtailコマンドを用いよ。
Python
全行探索して最後の10行を手続き的に取得する方法しか思いつかなかったので、使えるライブラリを調べることにしました。collections.dequeを使えばいいのですね。
from collections import deque
N = 10
file_path = 'popular-names.txt'
with open(file_path,'r' , encoding='UTF-8') as f:
last_lines = deque(f , maxlen=N)
for line in last_lines:
print(line , end = '')
コマンド
普通にtail使うだけですね。
$ tail -10 popular-names.txt
Liam M 19837 2018
Noah M 18267 2018
William M 14516 2018
James M 13525 2018
Oliver M 13389 2018
Benjamin M 13381 2018
Elijah M 12886 2018
Lucas M 12585 2018
Mason M 12435 2018
Logan M 12352 2018

13. タブをスペースに置換
ファイルの先頭10行に対して、タブ1文字につきスペース1文字に置換して出力せよ。確認にはsedコマンド、trコマンド、もしくはexpandコマンドなどを用いよ。
python
replaceでタブをスペースに変換して出力してみました。
N = 10
file_path = 'popular-names.txt'
with open(file_path,'r' , encoding='UTF-8') as f:
for line in last_lines:
print(line.replace('\t' , ' ') , end = '')
コマンド
sedで変換してみます。
$ head -10 popular-names.txt | sed 's/\t/ /g'
Mary F 7065 1880
Anna F 2604 1880
Emma F 2003 1880
Elizabeth F 1939 1880
Minnie F 1746 1880
Margaret F 1578 1880
Ida F 1472 1880
Alice F 1414 1880
Bertha F 1320 1880
Sarah F 1288 1880
trだとこうですね。
head -10 popular-names.txt | tr '\t' ' '
expandは知らなかったので調べてみました。
タブをスペースに変換するためのコマンドだそうです。
デフォルトだとタブを8つのスペースに変換してしまうので、オプションでスペース1つに変換するように指定しました。
head -10 popular-names.txt | expand -t 1

14. 1列目を出力
ファイルの先頭10行に対して、各行の1列目だけを抜き出して表示せよ。確認にはcutコマンドなどを用いよ。
Python
Pandasで書いてみます。
import pandas as pd
df = pd.read_csv(file_path , sep=r'\t' , header=None)
df[0][0:10]
Python改善:df.locでアクセスする
df[0][0:10]
といったようなアクセスはよろしくないそう。df.locでアクセスします
import pandas as pd
df = pd.read_csv(file_path , sep=r'\t' , header=None)
df.loc[0:9 , 0]
コマンド
コマンドだとこんな感じですね。
$ head -10 popular-names.txt | cut -f 1
Mary
Anna
Emma
Elizabeth
Minnie
Margaret
Ida
Alice
Bertha
Sarah

15. ファイルをN分割する
ファイルを行単位でN分割し、別のファイルに格納せよ。例えば、N=10としてファイルを10分割せよ。同様の処理をsplitコマンドで実現せよ。
python
ひとまず自力で書いてみます。あまり行をもうちょっと上手いこと書きたかったですが。。。
import pandas as pd
file_path = 'popular-names.txt'
df = pd.read_csv(file_path , sep=r'\t' , header=None)
N = 13
def df_to_csv(start_cursor , end_cursor):
df[start_cursor : end_cursor].to_csv(
f"popular-names-python_{start_cursor}-{end_cursor}.txt",
sep="\t",
header=None,
index=None
)
start_cursor = 0
end_cursor = 0
number_of_line = len(df) // N
for _ in range(N):
end_cursor = start_cursor + number_of_line
df_to_csv(start_cursor , end_cursor)
start_cursor = end_cursor
# あまり行対策
if end_cursor < len(df):
df_to_csv(start_cursor , len(df))

16. ランダムに各行を並び替える
ファイルを行単位でランダムに並び替えよ(注意: 各行の内容は変更せずに並び替えよ)。同様の処理をshufコマンドで実現せよ。
python
randomライブラリになんかあるんだろうなと思ってたらshuffleメソッドを見つけました。以下のように記述して動かしてみましたが、問題なさそうです。
import random
file_path = 'popular-names.txt'
with open(file_path,'r' , encoding='UTF-8') as f:
lines = f.readlines()
random.shuffle(lines)
with open("shuffled-popular-names.txt" , "w" , encoding="UTF-8") as f:
f.writelines(lines)
コマンド
$ shuf shuffled-popular-names.txt > shufコマンド結果!.txt
$
$ head shufコマンド結果!.txt
Linda F 52710 1946
Ronald M 19835 1939
Jason M 52680 1976
John M 7550 1897
Joshua M 27537 2000
John M 75991 1958
Emily F 12647 2014
Edward M 20597 1923
Sophia F 13928 2018
Sandra F 24701 1945
$
$
$ head popular-names.txt
Mary F 7065 1880
Anna F 2604 1880
Emma F 2003 1880
Elizabeth F 1939 1880
Minnie F 1746 1880
Margaret F 1578 1880
Ida F 1472 1880
Alice F 1414 1880
Bertha F 1320 1880
Sarah F 1288 1880
$

17. 1列目の文字列の異なり
1列目の文字列の異なり(文字列の種類)を求めよ。確認にはcut, sort, uniqコマンドを用いよ。
Python
Pandasで書いてみます。名前の列を切り出してunique関数を適用すればいいですね。
import pandas as pd
file_path = 'popular-names.txt'
df = pd.read_csv(file_path , sep=r"\s+" , header=None)
len(df.iloc[:,0].unique())
出力結果:136
コマンド
全部パイプでつないであげるだけでよさそうです。
$ cut -f 1 popular-names.txt | sort | uniq | wc -l
136

18. 各行の1列目の文字列の出現頻度を求め、出現頻度の高い順に並べる
1列目の文字列の出現頻度を求め、出現頻度と名前を出現頻度の多い順に並べて表示せよ。確認にはcut, uniq, sortコマンドを用いよ。
Python
Pandasで書いてみます。列を切り出してvalue_counts()関数を適用します。
import pandas as pd
file_path = 'popular-names.txt'
df = pd.read_csv(file_path , sep=r"\s+" , header=None)
df.iloc[: , 0].value_counts()
出力
0
James 118
William 111
Robert 108
John 108
Mary 92
...
Julie 1
Laura 1
Scott 1
Rachel 1
Lucas 1
Name: count, Length: 136, dtype: int64
コマンド
$ cut -f 1 popular-names.txt | sort | uniq -c | sort -r
118 James
111 William
108 Robert
108 John
92 Mary
75 Charles
74 Michael
73 Elizabeth
70 Joseph

19. 3列目の数値の降順に各行を並び替える
3列目の数値の逆順でファイルの各行を整列せよ(注意: 各行の内容は変更せずに並び替えよ)。同様の処理をsortコマンドで実現せよ。
「数値の逆順」は普通に降順という解釈でいいのでしょうか…?
数「字」の逆順なら「3列目の値を文字列として逆順にした結果を基に並び替え」を意図していそうですが。数「値」の逆順なので普通に降順にするだけと解釈しました。
Python
import pandas as pd
file_path = 'popular-names.txt'
df = pd.read_csv(file_path , sep=r"\s+" , header=None)
result = df.sort_values(by=2, ascending=False)
# 上から10行だけ確認
result.iloc[0:10,:]
結果
0 1 2 3
1340 Linda F 99689 1947
1360 Linda F 96211 1948
1350 James M 94757 1947
1550 Michael M 92704 1957
1351 Robert M 91640 1947
1380 Linda F 91016 1949
1530 Michael M 90656 1956
1570 Michael M 90517 1958
1370 James M 88584 1948
1490 Michael M 88528 1954
コマンド
$ sort -k3,3nr popular-names.txt | head
Linda F 99689 1947
Linda F 96211 1948
James M 94757 1947
Michael M 92704 1957
Robert M 91640 1947
Linda F 91016 1949
Michael M 90656 1956
Michael M 90517 1958
James M 88584 1948
Michael M 88528 1954

感想・メモ
コマンドはさして苦労せず組み立てられました。shコマンドは便利で扱いやすいです。
また、だいぶ前に「1日1問、半年以内に習得 シェル・ワンライナー160本ノック」という書籍でワンライナーを練習していた経験が生きました。
Pandasはまだ手になじんでいない感じがします。
もう少し演習を増やしてしていきたいなと思います。
第三章