Pythonで2次元配列(リストのリスト)をソート

Modified: | Tags: Python, リスト

Pythonで2次元配列(リストのリスト)をソートする方法について説明する。

NumPyやpandasを使うほうが楽なので、外部ライブラリを使用できる環境であればそちらを使うのがオススメ。

本記事のサンプルコードでは、出力を見やすくするためにpprintを使っている。

import pprint

コード中でpprint.pprint()の引数としてwidth=20を指定しているが、出力を整えるためなので特に気にしなくてよい。

2次元配列に対するsorted()やsort()のデフォルトの振る舞い

以下の2次元配列(リストのリスト)を例とする。

l_2d = [[20, 3, 100], [1, 200, 30], [300, 10, 2]]
pprint.pprint(l_2d, width=20)
# [[20, 3, 100],
#  [1, 200, 30],
#  [300, 10, 2]]

sorted()関数やリストのsort()メソッドでソートする場合、デフォルトでは各リストが比較されソートされる。

リスト同士の比較は最初の等しくない要素に対して行われるので、この例では各リストの最初(先頭)の要素が比較されソートされる。

pprint.pprint(sorted(l_2d), width=20)
# [[1, 200, 30],
#  [20, 3, 100],
#  [300, 10, 2]]

sort()sorted()についての詳細は以下の記事を参照。

2次元配列を行ごと・列ごとにソート

2次元配列を行ごと・列ごとに独立してソートする方法について述べる。

sorted()とリスト内包表記を利用

行ごとにソートすることは、各リストをそれぞれソートすることに等しい。リスト内包表記で各リストにsorted()を適用すればよい。

l_2d = [[20, 3, 100], [1, 200, 30], [300, 10, 2]]
pprint.pprint(l_2d, width=20)
# [[20, 3, 100],
#  [1, 200, 30],
#  [300, 10, 2]]

pprint.pprint([sorted(l) for l in l_2d], width=20)
# [[3, 20, 100],
#  [1, 30, 200],
#  [2, 10, 300]]

列ごとにソートしたい場合、元のオブジェクト(リストのリスト)を転置してから各リストをソートし、再度転置する。転置にはzip()*を利用する。

pprint.pprint([list(x) for x in zip(*[sorted(l) for l in zip(*l_2d)])], width=20)
# [[1, 3, 2],
#  [20, 10, 30],
#  [300, 200, 100]]

NumPyを利用: np.sort()

NumPyを利用するともっと楽。以下の2次元配列(リストのリスト)を例とする。

l_2d = [[20, 3, 100], [1, 200, 30], [300, 10, 2]]
pprint.pprint(l_2d, width=20)
# [[20, 3, 100],
#  [1, 200, 30],
#  [300, 10, 2]]

np.sort()を使う。デフォルトでは行ごと、axis=0とすると列ごとにソートされる。返り値はnumpy.ndarray

import numpy as np

print(np.sort(l_2d))
# [[  3  20 100]
#  [  1  30 200]
#  [  2  10 300]]

print(np.sort(l_2d, axis=0))
# [[  1   3   2]
#  [ 20  10  30]
#  [300 200 100]]

print(type(np.sort(l_2d)))
# <class 'numpy.ndarray'>

numpy.ndarrayをリストに変換したい場合はtolist()を使う。

print(np.sort(l_2d).tolist())
# [[3, 20, 100], [1, 30, 200], [2, 10, 300]]

print(type(np.sort(l_2d).tolist()))
# <class 'list'>

なお、NumPyにおいて多次元配列として扱われるのは、各次元の要素数が等しい場合のみ。要素数が異なるリストのリストはエラーになるので注意。

l_2d_error = [[1, 2], [3, 4, 5]]

# print(np.sort(l_2d_error))
# ValueError: setting an array element with a sequence. The requested array has an inhomogeneous shape after 1 dimensions. The detected shape was (2,) + inhomogeneous part.

numpy.ndarrayのメソッドとしてもsort()が提供されている。詳細は以下の記事を参照。

2次元配列を特定の行・列を基準にソート

行ごと・列ごとに独立してソートするのではなく、特定の行・列を基準にソートする方法について述べる。

sorted()やsort()の引数keyを利用

基本的な考え方

上述のように、デフォルトでは1列目(各リストの先頭の要素)を基準にソートされる。

l_2d = [[20, 3, 100], [1, 200, 30], [300, 10, 2]]
pprint.pprint(l_2d, width=20)
# [[20, 3, 100],
#  [1, 200, 30],
#  [300, 10, 2]]

pprint.pprint(sorted(l_2d), width=20)
# [[1, 200, 30],
#  [20, 3, 100],
#  [300, 10, 2]]

2列目や3列目を基準に並べ替えたい場合、sorted()関数やsort()メソッドの引数keyを利用する。

keyに関数などの呼び出し可能オブジェクトを指定すると、各要素にその関数を適用した結果に従ってソートされる。

今回の例では、リストの任意の位置(インデックス)の要素を取得する関数を指定すればよい。ラムダ式(無名関数)またはoperatorモジュールのitemgetter()を使う。

ラムダ式を使う例。2列目と3列目を基準にしている。0始まりなので指定するのは12

pprint.pprint(sorted(l_2d, key=lambda x: x[1]), width=20)
# [[20, 3, 100],
#  [300, 10, 2],
#  [1, 200, 30]]

pprint.pprint(sorted(l_2d, key=lambda x: x[2]), width=20)
# [[300, 10, 2],
#  [1, 200, 30],
#  [20, 3, 100]]

operator.itemgetter()を使う例。

import operator

pprint.pprint(sorted(l_2d, key=operator.itemgetter(1)), width=20)
# [[20, 3, 100],
#  [300, 10, 2],
#  [1, 200, 30]]

pprint.pprint(sorted(l_2d, key=operator.itemgetter(2)), width=20)
# [[300, 10, 2],
#  [1, 200, 30],
#  [20, 3, 100]]

引数keyやoperatorモジュールについての詳細は以下の記事を参照。

特定の行を基準にソートしたい場合は、転置してから上の処理を行い、再度転置すればよい。

複数の行・列を基準にソート

重複した値を持つ場合を例とする。

l_2d_dup = [[1, 3, 100], [1, 200, 30], [1, 3, 2]]
pprint.pprint(l_2d_dup, width=20)
# [[1, 3, 100],
#  [1, 200, 30],
#  [1, 3, 2]]

デフォルトでは各リストの最初の等しくない要素が比較されソートされるので、1列目が等しい場合は2列目、2列目も等しい場合は3列目の値でソートされる。

pprint.pprint(sorted(l_2d_dup), width=20)
# [[1, 3, 2],
#  [1, 3, 100],
#  [1, 200, 30]]

先頭から順番にではなく、任意の複数の列を基準にソートしたい場合は、引数keyを使う。

operator.itemgetter()の引数に複数の値(インデックス)を指定すると、最初の値が等しい場合は次の値で比較されソートされる。以下の例では1列目と3列目を基準にしている。

pprint.pprint(sorted(l_2d_dup, key=operator.itemgetter(0, 2)), width=20)
# [[1, 3, 2],
#  [1, 200, 30],
#  [1, 3, 100]]

ラムダ式でも同様の処理が可能。

pprint.pprint(sorted(l_2d_dup, key=lambda x: (x[0], x[2])), width=20)
# [[1, 3, 2],
#  [1, 200, 30],
#  [1, 3, 100]]

特定の行を基準にソートしたい場合は、転置してから上の処理を行い、再度転置すればよい。

pandasを利用: sort_values()

pandasを利用するともっと楽。以下の2次元配列(リストのリスト)を例とする。

l_2d_dup = [[1, 3, 100], [1, 200, 30], [1, 3, 2]]
pprint.pprint(l_2d_dup, width=20)
# [[1, 3, 100],
#  [1, 200, 30],
#  [1, 3, 2]]

リストのリストからpandas.DataFrameを生成する。行名・列名は便宜上つけているだけなので何でもよい。後述のように省略も可能。

import pandas as pd

df = pd.DataFrame(l_2d_dup, columns=['A', 'B', 'C'], index=['X', 'Y', 'Z'])
print(df)
#    A    B    C
# X  1    3  100
# Y  1  200   30
# Z  1    3    2

sort_values()で特定の列や行を基準にソートできる。デフォルトでは列、axis=1とすると行を指定できる。

print(df.sort_values('C'))
#    A    B    C
# Z  1    3    2
# Y  1  200   30
# X  1    3  100

print(df.sort_values('Z', axis=1))
#    A    C    B
# X  1  100    3
# Y  1   30  200
# Z  1    2    3

複数指定も可能。

print(df.sort_values(['A', 'C']))
#    A    B    C
# Z  1    3    2
# Y  1  200   30
# X  1    3  100

pandas.DataFrame生成時にcolumns, index引数を省略すると行名・列名は連番になる。行名・列名がどちらも数値になって分かりにくいが、これでも問題なく処理できる。

df = pd.DataFrame(l_2d_dup)
print(df)
#    0    1    2
# 0  1    3  100
# 1  1  200   30
# 2  1    3    2

print(df.sort_values(2))
#    0    1    2
# 2  1    3    2
# 1  1  200   30
# 0  1    3  100

print(df.sort_values(2, axis=1))
#    0    2    1
# 0  1  100    3
# 1  1   30  200
# 2  1    2    3

print(df.sort_values([0, 2]))
#    0    1    2
# 2  1    3    2
# 1  1  200   30
# 0  1    3  100

昇順・降順を指定する引数ascendingなど、sort_values()の詳細は以下の記事を参照。

pandas.DataFrameをリストやnumpy.ndarrayと相互に変換することも可能。

例えば、以下のようにpandas.DataFrameをリストに変換できる。

print(df.sort_values([0, 2]).values.tolist())
# [[1, 3, 2], [1, 200, 30], [1, 3, 100]]

print(type(df.sort_values([0, 2]).values.tolist()))
# <class 'list'>

NumPyを利用: np.argsort()

NumPyで配列を特定の行・列を基準にソートするにはnp.argsort()を使う方法がある。

詳細は以下の記事を参照されたいが、結構ややこしいので特にNumPyにこだわる必要がなければ上述のpandasを使うほうが簡単。

関連カテゴリー

関連記事