言語処理100本ノック第2章 解答と解説 Q17~Q19まで

こんにちは。福田直起です。

前回に引き続き、言語処理100本ノック2020を解いているアウトプットとして、「第2章 : UNIXコマンド」の10問のうち、Q17~Q19の3問を解説していきます。
他の問題の回答は、まとめページにリンクを貼ってあります。

解答・解説の前に

この記事の

  • 主な対象者と目的
  • 実行環境
  • 解答・解説を読む際の注意

は、以前の記事で説明していますので、解答・解説を読む前にこれらを知っておきたい、という方はそちらを参照していただくようお願いします。

また、長い解説は折りたたんであります。折りたたんである解説は「解説を読む」をクリックすると見ることができます。

解答・解説

引き続き、アメリカで生まれた赤ちゃんのデータをタブ区切り形式で格納したファイルであるpopular-names.txtに対して、問題文で指示されたテキスト処理を行っていきます。
popular-names.txtのダウンロードの方法は以前の記事で解説しています。

Q17. 1列目の文字列の異なり

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

Python

解答コード

X = set()
   
with open("popular-names.txt") as file:
    lines = file.readlines()
    
for line in lines:
    X.add(line.split()[0])
   
length = len(X)
print("1列目の文字列の種類: " + str(length))
   
# 確認用にファイル出力
with open("data/q17.txt", mode="w") as output:
    output.write(str(length) + "\n")

実行結果

1列目の文字列の種類: 136

解説

第1章でも使った集合(set)を使って解いています。

UNIXコマンド

解答コード

# Pythonの処理結果をUNIXコマンドと比較して確認
!echo "Pythonの出力ファイルとUNIXコマンドで同様の処理をした結果を比較:"
  
!echo "cut -f 1 ./popular-names.txt | sort | uniq | wc -l > data/q17-bash.txt"
!cut -f 1 ./popular-names.txt | sort | uniq | wc -l > data/q17-bash.txt
  
!echo
!echo "diff data/q17-bash.txt data/q17.txt"
!diff data/q17-bash.txt data/q17.txt

実行結果

Pythonの出力ファイルとUNIXコマンドで同様の処理をした結果を比較:
cut -f 1 ./popular-names.txt | sort | uniq | wc -l > data/q17-bash.txt

diff data/q17-bash.txt data/q17.txt

解説

解説を読む

  1. ファイルの一列目だけをcutコマンドで取り出す
  2. 1.の結果をsortコマンドでソートする
  3. 2.の結果からuniqコマンドで重複を取り除く
  4. wc -lコマンドで3.の結果残った行を数える
  5. ファイルとして書き出す
  6. diffコマンドでPythonの結果と比較

という流れになっています。

UNIXsortコマンドについて

テキストファイルを、指定した基準に沿って行単位で並べ替えるコマンドです。
sort オプション ファイル名
のように使います。 今回は後述のuniqコマンドのために、単純なデータを重複行が連続して並んでいる形に整理しておきたいだけなので、特にオプションは指定していません。

使用例:

sort popular-names.txt

参考: 【 sort 】コマンド――テキストファイルを行単位で並べ替える:Linux基本コマンドTips(63) - @IT

UNIXuniqコマンドについて

重複する行を1つだけ残して削除するコマンドです。
uniq ファイル名
のように使います。 ただし、元のテキストが並べ替え済であることを前提にしているので、基本的には解答や使用例のようにsortと併用することになります。

使用例:

sort popular-names.txt | uniq

参考: 【 uniq 】コマンド――重複している行を削除する:Linux基本コマンドTips(64) - @IT

sortコマンドの-uオプションについて

解答では使いませんでしたが、sortコマンドに-uオプションをつけることで、uniqコマンドを使わなくても重複する行を1つだけ残して削除してくれます。

sort -uの使用例とsort | uniqとの結果の比較:

$ cat names.txt
Mary
Anna
Emma
Elizabeth
Anna
  
$ sort -u names.txt
Anna
Elizabeth
Emma
Mary
  
$ sort names.txt | uniq
Anna
Elizabeth
Emma
Mary

このように、sort -usort | uniqは同じ結果になります。

参考: 【 sort 】コマンド――テキストファイルを行単位で並べ替える:Linux基本コマンドTips(63) - @IT

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

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

Python

解答コード

%%time
  
with open("popular-names.txt") as input_file:
    input_file_lines = input_file.readlines()
      
splited_lines = [line.split() for line in input_file_lines]
# 以後の別解で比較に使用する
sorted_lines = sorted(splited_lines, reverse=True, key=lambda x: int(x[2]))
  
# Ubuntuのsortコマンドは指定された列以外も比較して並び替えるので、diffで結果を検証できるように、Pythonの処理も同様にする
sorted_lines_like_unix = sorted(splited_lines, reverse=True, key=lambda x: (int(x[2]), x[0], x[1], int(x[3])))
  
joined_lines = ["\t".join(line) for line in sorted_lines]
joined_lines_like_unix = ["\t".join(line) for line in sorted_lines_like_unix]
  
print("先頭10行を確認:")
for line in joined_lines[:10]:
    print(line)
     
with open("data/q18-1.txt", mode="w") as output:
    output.write("\n".join(joined_lines_like_unix))
    output.write("\n")
  
with open("data/q18-2.txt", mode="w") as output:
    output.write("\n".join(joined_lines))
    output.write("\n")
  
print()
print("実行時間:")

実行結果

先頭10行を確認:
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

実行時間:
CPU times: user 24.9 ms, sys: 1.43 ms, total: 26.4 ms
Wall time: 22.6 ms

解説

解説を読む コード中のコメントにあるようにこの問題は解き方が色々あるのですが、順番に解説していきます。

Jupyter Notebookのマジックコマンド%%timeについて

セルの初めに%%timeと記述することで、そのセルのプログラムの実行にかかった時間を計測してくれます。
ここでは、別解との実行速度の比較のために用いています。

結果は以下のように表示されます。

実行時間:
CPU times: user 24.9 ms, sys: 1.43 ms, total: 26.4 ms
Wall time: 22.6 ms

参考: Built-in magic commands — IPython 8.4.0 documentation

Jupyter Notebookのマジックコマンドの対象指定について

Jupyter Notebookのマジックコマンドは、%をつけるとその行を、%%をつけるとセルを対象にコマンドの処理を実行する仕組みになっています。

前述のtimeを例にすると
%time 時間を計測したい処理
と書くことで、その行の実行時間を計測してくれます。

%%%の違いについて、以下に実例を示しておきます。

%%time
  
print("print(\"Hello!\")の処理時間:")
%time print("Hello!")
  
print()
print("セル全体の処理時間:")

実行結果:

print("Hello!")の処理時間:
Hello!
CPU times: user 25 µs, sys: 0 ns, total: 25 µs
Wall time: 28.6 µs

セル全体の処理時間:
CPU times: user 1.22 ms, sys: 0 ns, total: 1.22 ms
Wall time: 1.14 ms

参考: Introducing IPython — IPython 8.4.0 documentation

Pythonsorted関数のオプションについて
reverseオプション

reverse=TrueのようにTrueを設定すると、リストを降順でソートすることができます。
省略した場合はFalse、すなわち昇順になります。

keyオプション

リストの各項目に指定された関数を適用し、その結果によってソートするためのコマンドです。

今回はリストのリストをソートしようとしているので、後述のラムダ式によってソート基準になる項目を呼び出すのに使っています。

参考: ソート HOW TO — Python 3.8.10 ドキュメント

Pythonラムダ式について

ラムダ式とは、関数的な処理をしたいが、関数を定義するほど長い処理でもないし、使いまわしたりもしないという場合に便利な記法です。

今回はlambda x: int(x[2])のようにすることで、各リストの2番目の項目(つまり3コラム目)をintに変換した値を返すという処理を書いています。   これが前述のkeyオプションに入力されることで、ソート条件が問題文の要件を満たすというわけですね。 また、lambda x: (int(x[2]), x[0], x[1], int(x[3])))のようにカッコでくくることで、複数のkeyを指定することもできます。

参考: 6. 式 (expression) — Python 3.8.10 ドキュメント

UNIXコマンド

解答コード

!echo "Pythonの出力ファイルとUNIXコマンドで同様の処理をした結果を比較:"
!echo "diff <(cat ./popular-names.txt | sort -rnk 3,3) data/q18-1.txt"
!diff <(cat ./popular-names.txt | sort -rnk 3,3) data/q18-1.txt

実行結果

Pythonの出力ファイルとUNIXコマンドで同様の処理をした結果を比較:
diff <(cat ./popular-names.txt | sort -rnk 3,3) data/q18-1.txt

解説

解説を読む

UNIXコマンドにおける複数オプションの指定の仕方

UNIXコマンドにおいて複数のオプションを指定する場合、以下の二通りの方法があります。 この解答のsort -rnk 3,3を例にして説明します。

1.sort -rnk 3,3
引数が必要なオプションが1個以下の場合は、このように指定したほうが短くなります。

2.sort -r -n -k 3,3
Q16のように複数のオプションにそれぞれ引数が必要な場合は、このような書き方が必要になります。 参考までにQ16のコマンドをもう一度提示しておきます。

split --lines=556 --numeric-suffixes=1  --additional-suffix=.txt ./popular-names.txt data/q16-unix-
sortコマンドのオプションについて

ここまででオプションの指定方法が2通りあることを理解していただけたと思うので、ここからは解答のsort -rnk 3,3のコマンドで使っているオプションについて解説していきます。

-rオプション

逆順、ここでは降順でソートするオプションです。

-nオプション

文字列を数値とみなしてソートするオプションです。
数値でソートしたい際はこのオプションを入れておかないと、意図したソートにならないので注意が必要です。

-kオプション

-k ソート基準にしたい最初の列,ソート基準にしたい最後の列
のように使ってソート基準にしたい列を指定するオプションです。
注意点ですが、ソート基準にしたい最後の列を指定しないと、ソート基準にしたい最初の列から各行の最後の列までを参照して並べ替えてしまうので、一列のみを指定したい場合は解答のように3,3などとする必要があります。

参考: 【 sort 】コマンド――テキストファイルを行単位で並べ替える:Linux基本コマンドTips(63) - @IT

注意:sortコマンドのUbuntuにおける挙動について

解答の以下のコメントが気になった方もいらっしゃるかもしれません。

Ubuntuのsortコマンドは指定された列以外も比較して並び替えるので、diffで結果を検証できるように、Pythonの処理も同様にする

この問題を解いていて気づいたことなのですが、Ubuntuのバージョンによっては、sortコマンドは厳格ではない挙動をします。
これについて詳しく知りたい方は、別記事で検証・解説していますので、そちらが参考になります。

別解

別解を読む

sortedを使うのではなく、バブルソートを書く別解

Pythonでは、sorted関数を使わなくても自分でソートを書くこともできます。
以下はバブルソートを書いた解です。

Python
解答コード
%%time
  
with open("popular-names.txt") as input_file:
    input_file_lines = input_file.readlines()
    
splited_lines = [line.split() for line in input_file_lines]
  
for i in range(len(splited_lines)):
    for j in range(len(splited_lines) - i -1):
        if int(splited_lines[j][2]) < int(splited_lines[j+1][2]): 
            splited_lines[j], splited_lines[j+1] = splited_lines[j+1], splited_lines[j] 
  
joined_lines = ["\t".join(line) for line in splited_lines]
  
print("先頭10行を確認:")
for line in joined_lines[:10]:
    print(line)
    
with open("data/q18-3.txt", mode="w") as output:
    output.write("\n".join(joined_lines))
    output.write("\n")
  
print()
print("実行時間:")
実行結果
先頭10行を確認:
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

実行時間:
CPU times: user 2.49 s, sys: 0 ns, total: 2.49 s
Wall time: 2.48 s
UNIXコマンド
解答コード
# 最初の解答の結果と比較して確認
!echo "ラムダ式を使う解の結果とバブルソートを書く解の出力ファイルを比較:"
!echo "diff data/q18-2.txt data/q18-3.txt"
!diff data/q18-2.txt data/q18-3.txt
実行結果
ラムダ式を使う解の結果とバブルソートを書く解の出力ファイルを比較:
diff data/q18-2.txt data/q18-3.txt

pandasライブラリを使った別解

Pythonでデータを扱うのによく使われるpandasライブラリを使った別解も載せておきます。

Python
解答コード
%%time
  
# Pandasを使う解
import pandas as pd
  
# 問題のファイルをデータとして読み込み、以降の問題でも使用する
data = pd.read_table('./popular-names.txt', header=None, sep='\t',
                     names=['name', 'sex', 'number', 'year'])
  
# Ubuntuのsortコマンドは指定された列以外も比較して並び替えるので、diffで結果を検証できるように、Pythonの処理も同様にする
# Pandasの単列ソートとsortedを使った単列ソートの結果が合わないので、sortコマンドと同様に全列でソートする
sorted_lines = data.sort_values(['number', 'name', 'sex', 'year'], ascending=False)
  
print("先頭10行を確認:")
print(sorted_lines[:10])
  
sorted_lines.to_csv('data/q18-4.txt', sep='\t', header=False, index=False)
  
print()
print("実行時間:")
実行結果
先頭10行を確認:
         name sex  number  year
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

実行時間:
CPU times: user 26.6 ms, sys: 0 ns, total: 26.6 ms
Wall time: 21.6 ms
解説

解説を読む

read_table()

データをpandasに読み込ませるためのメソッドです。
データはDataFrameという型になります(Pythonでは型を意識する必要はあまりありませんが、公式ドキュメントなどでよく出てくるので覚えておくと良いと思います)。 解答の
read_table('./popular-names.txt', header=None, sep='\t', names=['name', 'sex', 'number', 'year'])
を例に引数やオプションを解説します。

引数'./popular-names.txt'は、データとして読み込ませたいファイルへのパスを指定しています。
header=Noneオプションは、ヘッダーのないファイルを読み込みたい場合につけるオプションです。デフォルトだと一行目をヘッダーとして扱おうとするので注意が必要です。
sepオプションは、区切り文字を指定しています。
namesオプションは、リストやタプルで各列に名前をつけるオプションです。データの列に名前をつけて、コードの可読性を高められるのがpandasの大きなメリットのひとつなので、header=Noneオプションを設定した場合は指定したほうがいいでしょう。

参考: pandas.read_table — pandas 1.4.3 documentation

sort_values()

データをソートするためのメソッドです。   解答の
sort_values(['number', 'name', 'sex', 'year'], ascending=False)
を例に解説します。

引数['number', 'name', 'sex', 'year']の部分では、ソートの基準にする列と順序をリストで指定しています。
問題文的には本来numberだけを指定すればいいのですが、Pandasのソートも一列だけを指定しているのに、他の列を参照してソートしているような挙動をすることがあるので、今回はすべての列を指定しています。
これについては別記事で検証・解説しています。  
ascending=Falseオプションは、データを降順にソートするよう指定するオプションです。つけなかった場合は、昇順でのソートになります。

参考: pandas.DataFrame.sort_values — pandas 1.4.3 documentation

to_csv()

データをファイルに書き出すメソッドです。
名前にcsvと入っていますが、拡張子や区切り文字は自由に指定することができます。
解答の
to_csv('data/q18-4.txt', sep='\t', header=False, index=False)
を例に解説します。

引数'data/q18-4.txt'は、データの書き出し先のパスを指定しています。
sepオプションは、区切り文字を指定しています。
header=Falseオプションは、デフォルトでは一行目に追加されるヘッダー列を追加しないためのオプションです。
index=Falseオプションは、デフォルトでは左端列に追加されるインデックス列を追加しないためのオプションです。

参考: pandas.DataFrame.to_csv — pandas 1.4.3 documentation

UNIXコマンド
解答コード
# UNIXコマンドと比較して確認
!echo "UNIXコマンドで同様の処理をした結果とPythonの出力ファイルを比較:"
!echo "diff <(cat ./popular-names.txt | sort -rnk 3,3) data/q18-4.txt"
!diff <(cat ./popular-names.txt | sort -rnk 3,3) data/q18-4.txt
実行結果
UNIXコマンドで同様の処理をした結果とPythonの出力ファイルを比較:
diff <(cat ./popular-names.txt | sort -rnk 3,3) data/q18-4.txt

各解答の実行速度の比較

まず、3つの解答の実行速度を比較します。

sorted関数

実行時間:
CPU times: user 24.9 ms, sys: 1.43 ms, total: 26.4 ms
Wall time: 22.6 ms

バブルソートを書く

実行時間:
CPU times: user 2.49 s, sys: 0 ns, total: 2.49 s
Wall time: 2.48 s

pandasを使う

実行時間:
CPU times: user 26.6 ms, sys: 0 ns, total: 26.6 ms
Wall time: 21.6 ms

僅差ですが、今回はpandasを使うのが一番早いですね。
一方、バブルソートを書くのは速度的には不利なようです。

ソートアルゴリズムとその計算量の比較

それでは、なぜそれぞれの方法で速度に違いが出たのでしょうか。
それは、それぞれの方法で使われているソートアルゴリズムの違いにあります。

sorted関数のアルゴリズムには、公式ドキュメントによると、Timsortというアルゴリズムが使われています。

バブルソートを書く解のアルゴリズムは、当然バブルソートですね。

pandassort_values()は、標準ではクイックソートが使われます。
ちなみに、ソートアルゴリズムkindオプションで指定することもできます。
どう指定すればどんなソートアルゴリズムが選べるかは、以下の公式ドキュメントが参考になります。
pandas.DataFrame.sort_values — pandas 1.4.3 documentation

それでは、それぞれの計算量を見てみましょう。

方法 ソートアルゴリズム 平均計算時間 最悪計算時間
sorted関数 Timsort O(NlogN) O(NlogN)
バブルソートを書く バブルソート O(n2) O(n2)
pandasを使う クイックソート O(NlogN) O(n2)

計算時間はO(NlogN)よりO(n^2)の方が長いため、平均計算時間と最悪計算時間がO(n^2)であるバブルソートが速度的に不利であることがわかります。
また、最悪計算時間同士の比較では、Timsortがもっとも速度的に有利ですね。
従って、今回扱った方法の中では、Timsortを採用しているsorted関数が最も効率よくソートが行えると言えるでしょう。

それぞれの方式のメリット・デメリット

ここまでの内容から、それぞれの方式のメリット・デメリットを挙げておきます。

  • sorted
    • 実行速度:比較的速い
    • メリット:外部ライブラリをインポートする必要がない
    • デメリット:pandasと違って、列に名前をつけられない
  • ソートを自分で書く
    • 実行速度:比較的遅い
    • メリット:あまりない
    • デメリット:アルゴリズムが適切でないと処理速度が遅くなる
  • pandasを使う
    • 実行速度 : 比較的速い
    • メリット:データの列に名前をつけて扱うことができ、コードの可読性が高くなる
    • デメリット:pandasでのデータの扱いについて基本的な知識が必要

ソートを自分で書くような用途ではPythonよりC++のような高速な言語の方が向いているため、Pythonでソートを書くメリットはあまりないのですが、今回は問題を解くにおいて、既成の関数やライブラリをなるべく使わずに解くことにも意味があると考えたため、ソートを自分で書く方法でも解答しました。

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

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

Python

解答コード

%%time
  
def count_names(lines: list) -> dict:
    name_count_dict = {}
    
    for item in lines:
        if item in name_count_dict:
            name_count_dict[item] = name_count_dict[item] + 1
            
        else:
            name_count_dict[item] = 1
    
    return name_count_dict
    
def sort_names(name_count_dict: dict) -> list:
    # タプルのリストにし、降順にソート
    return sorted(name_count_dict.items(), reverse=True, key=lambda x: (int(x[1]), x[0])) 
    
def create_name_count_ranking(lines: list) -> list:
    name_count_dict = count_names(lines)
    name_count_ranking = sort_names(name_count_dict)
    
    return name_count_ranking

  
with open("popular-names.txt") as input_file:
    input_file_lines = input_file.readlines()
    
lines = [line.split()[0] for line in input_file_lines]
name_count_ranking = create_name_count_ranking(lines)
  
print("先頭10行を確認:")
print(name_count_ranking[:10])
  
with open("data/q19-1.txt", mode="w") as output:
    for line in name_count_ranking:
        line_str = str(line[1]) + " " + line[0] + "\n"
        output.write(line_str)
  
print()
print("実行時間:")

実行結果

先頭10行を確認:
[('James', 118), ('William', 111), ('Robert', 108), ('John', 108), ('Mary', 92), ('Charles', 75), ('Michael', 74), ('Elizabeth', 73), ('Joseph', 70), ('Margaret', 60)]

実行時間:
CPU times: user 7.6 ms, sys: 2.34 ms, total: 9.94 ms
Wall time: 6.62 ms

解説

解説を読む 「各行の1列目の文字列」とは名前のことなので、「データ内で名前の人気ランキングを作る」という風に解釈できる問題です。
従って、以下の3つの関数を実装して解きました。

  1. 各行の1列目の文字列のみを切り出した内容を、名前がキーで出現数が値の辞書型として集計するcount_names
  2. 名前がキーで出現数が値の辞書型をタプルのリストにし、降順にソートするsort_names
  3. 上記2つの関数を適切な順番で呼び出すcreate_name_count_ranking

解答コードにはcount_namessort_namesを直接呼び出す処理はないので、create_name_count_rankingに内包してしまってもエラーになったりはしません。 しかし、関数の役割は最小にするほうが可読性が高いコードになるので、このように3つの関数に分けています。

UNIXコマンド

解答コード

!echo "UNIXコマンドで同様の処理をした結果とPythonの出力ファイルを比較:"
!echo "diff <(cut -f 1 ./popular-names.txt | sort | uniq -c | sort -rn | sed 's/^ *\| *$//g') data/q19-1.txt"
!diff <(cut -f 1 ./popular-names.txt | sort | uniq -c | sort -rn | sed 's/^ *\| *$//g') data/q19-1.txt

実行結果

UNIXコマンドで同様の処理をした結果とPythonの出力ファイルを比較:
diff <(cut -f 1 ./popular-names.txt | sort | uniq -c | sort -rn | sed 's/^ *\| *$//g') data/q19-1.txt

解説

解説を読む UNIXコマンドは、

  1. ファイルの一列目だけをcutコマンドで取り出す
  2. 1.の結果をsortコマンドでソートする
  3. 2.の結果からuniq -cコマンドで重複行を取り除きつつ、重複行になっている項目それぞれの数をカウントする(オプションについては以下で解説します)
  4. 3.の結果をsort -rnコマンドでソートする
  5. sed 's/^ *\| *$//g'コマンドで余計な空白を取り除く
  6. diffコマンドでPythonの結果と比較

という流れになっています。

uniqコマンドの-cオプションについて

重複行を取り除きつつ、重複行になっている項目それぞれの数をカウントするオプションです。 結果のフォーマットは118 Jamesのように数→項目の順になります。今回はPythonも同じように出力するようにしています。

参考: 【 uniq 】コマンド――重複している行を削除する:Linux基本コマンドTips(64) - @IT

別解

別解を読む

collectionsライブラリを使った別解

解答では辞書型を用いた集計関数を実装していますが、Pythoncollectionsライブラリには、 リスト内に同じ項目が何回出てくるかの辞書を作ってくれる関数があります。

Python
解答コード
%%time
  
import collections
  
with open("popular-names.txt") as input_file:
    input_file_lines = input_file.readlines()
    
lines = [line.split()[0] for line in input_file_lines]
count_tuples = sorted(collections.Counter(lines).items(), key=lambda i: (i[1], i[0]), reverse=True)
  
print("先頭10行を確認:")
print(count_tuples[:10])
  
with open("data/q19-2.txt", mode="w") as output:
    for line in count_tuples:
        line_str = str(line[1]) + " " + line[0] + "\n"
        output.write(line_str)
  
print()
print("実行時間:")
実行結果
先頭10行を確認:
[('James', 118), ('William', 111), ('Robert', 108), ('John', 108), ('Mary', 92), ('Charles', 75), ('Michael', 74), ('Elizabeth', 73), ('Joseph', 70), ('Margaret', 60)]

実行時間:
CPU times: user 5.29 ms, sys: 1.62 ms, total: 6.92 ms
Wall time: 4.43 ms
解説

collections.Counter(集計対象のリスト)のように記述することで、指定したリストに同じ値が何回出てくるかを辞書形式(keyが列の値、valueが出現回数)で集計してくれます。
使用例

import collections
  
sample_list = [1, 2, 3, 4, 5, 1, 3, 1]
sample_dict = collections.Counter(sample_list)
  
print(sample_dict[1])

参考: collections --- コンテナデータ型 — Python 3.8.10 ドキュメント

UNIXコマンド
解答コード
!echo "UNIXコマンドで同様の処理をした結果とPythonの出力ファイルを比較:"
!echo "diff <(cut -f 1 ./popular-names.txt | sort | uniq -c | sort -rn | sed 's/^ *\| *$//g') data/q19-2.txt"
!diff <(cut -f 1 ./popular-names.txt | sort | uniq -c | sort -rn | sed 's/^ *\| *$//g') data/q19-2.txt
実行結果
UNIXコマンドで同様の処理をした結果とPythonの出力ファイルを比較:
diff <(cut -f 1 ./popular-names.txt | sort | uniq -c | sort -rn | sed 's/^ *\| *$//g') data/q19-2.txt

pandasライブラリを使った別解

Q18で使用したpandasライブラリにも、この問題を解くのに便利な関数が存在するので、それを使った別解を紹介します。

Python
解答コード
%%time
  
import pandas as pd
  
# Q18で読み込んだデータを使用
value_counts = data['name'].value_counts()
count_tuples = sorted(value_counts.items(), key=lambda i: (i[1], i[0]), reverse=True)
  
print("先頭10行を確認:")
print(count_tuples[:10])
  
with open("data/q19-3.txt", mode="w") as output:
    for line in count_tuples:
        line_str = str(line[1]) + " " + line[0] + "\n"
        output.write(line_str)
  
print()
print("実行時間:")
実行結果
先頭10行を確認:
[('James', 118), ('William', 111), ('Robert', 108), ('John', 108), ('Mary', 92), ('Charles', 75), ('Michael', 74), ('Elizabeth', 73), ('Joseph', 70), ('Margaret', 60)]

実行時間:
CPU times: user 5.1 ms, sys: 1.57 ms, total: 6.67 ms
Wall time: 6.47 ms
解説

pandasではdata['列名'].value_counts()のように記述することで、指定列に同じ値が何回出てくるかを辞書形式(keyが列の値、valueが出現回数)で集計してくれます。

参考: pandas.DataFrame.value_counts — pandas 1.4.3 documentation

UNIXコマンド
解答コード
!echo "UNIXコマンドで同様の処理をした結果とPythonの出力ファイルを比較:"
!echo "diff <(cut -f 1 ./popular-names.txt | sort | uniq -c | sort -rn |sed 's/^ *\| *$//g') data/q19-3.txt"
!diff <(cut -f 1 ./popular-names.txt | sort | uniq -c | sort -rn | sed 's/^ *\| *$//g') data/q19-3.txt
実行結果
UNIXコマンドで同様の処理をした結果とPythonの出力ファイルを比較:
diff <(cut -f 1 ./popular-names.txt | sort | uniq -c | sort -rn |sed 's/^ *\| *$//g') data/q19-3.txt

おわりに

以上、言語処理100本ノックの第2章のQ17~Q19について解説しました。
今回はPythonの結果とUNIXコマンドの結果を合わせるのにかなり苦労しましたが、いい勉強になったと感じています。
他の問題の回答は、まとめページにリンクを貼ってあります。

また、ブレインズコンサルティングでは一緒に働いてくれる仲間を募集しています。 ご興味のある方は、ぜひ一度採用サイトをご覧ください。