Open14

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

onaka_hettaonaka_hetta

https://zenn.dev/onakahetta/scraps/41bc9333045b97
の続きです。

https://nlp100.github.io/2025/ja/ch02.html
こちらの「自然言語100本ノック 2025」の第二章です。

本章のミッションは以下の通り。

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

popular-names.txtを読み込んで、以下をそれぞれ作成します

  • Pythonで指定された処理を行うコード
  • UNIXコマンドで同様の処理を行うコマンド
onaka_hettaonaka_hetta

まずはファイル読み込みの方法を調べる

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())

問題に合わせて扱いやすいほうを選択したいと思います。

onaka_hettaonaka_hetta

コマンド実行環境

コマンド実行環境は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
onaka_hettaonaka_hetta

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
onaka_hettaonaka_hetta

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
onaka_hettaonaka_hetta

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
onaka_hettaonaka_hetta

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
onaka_hettaonaka_hetta

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
onaka_hettaonaka_hetta

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))

onaka_hettaonaka_hetta

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
$
onaka_hettaonaka_hetta

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
onaka_hettaonaka_hetta

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
onaka_hettaonaka_hetta

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
onaka_hettaonaka_hetta

感想・メモ

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

Pandasはまだ手になじんでいない感じがします。
もう少し演習を増やしてしていきたいなと思います。

第三章

https://zenn.dev/onakahetta/scraps/b97d67c7f67e39