numpyの[::-1].sort()で配列を降順にソートできる理由を考える

きっかけ

東京大学 松尾研究室の「第3期 Data Science Online Course」が、先週から始まった。
online course3 | 東京大学グローバル消費インテリジェンス寄付講座
「グローバル消費インテリジェンス寄付講座」、Global Consumer Intelligenceを略してGCI講座と書かれることもある。……名称が結構色々ある。
13週間にわたってデータサイエンスの講座をオンラインで受講する。また、受講生はWeb上の環境を利用することができ、毎週課題を提出する。

なお講座のテキストはこちらで一般公開されている。
GCIデータサイエンティスト育成講座・演習コンテンツ公開 | 東京大学松尾研究室 - Matsuo Lab

先週は初回であり、基本的な内容だったのであまり難しくはなかったのだが、1箇所だけ引っかかった。それが今回の話である。

問題のコード:numpyの[::-1].sort()で配列を降順ソート

以下のようにするとnumpy配列が降順にソートできる、とテキストには書いてあった。

import numpy as np  
  
sample_array = np.array([1,4,2,5,3])  
  
sample_array[::-1].sort()  
print("ソート後:", sample_array)  
  
#→ ソート後: [5 4 3 2 1]  

……あれ?

  • sort関数は標準では配列を昇順にソートする
  • [::-1]は配列を逆順にする
    ということも講座テキストには書いてある。

俺の予想は

  • sample_array[::-1]は配列を逆順にするから、[1,4,2,5,3]を[3,5,2,4,1]にする
  • その後、sort関数で配列を昇順にソートするから、表示されるのは[1 2 3 4 5]である
    だった。しかし実際には、予想と違って降順にソートされている。

なぜなんだろう?

以下、

  • 1次元のnumpy配列(ndarray)のみを対象にする。2次元以上については触れない。
  • メモリの効率性については触れない。
  • 計算時間の効率性については触れない。

numpy.sortと numpy.ndarray.sortがある

少し紛らわしいが、ndarrayをソートするにはnumpy.sortと numpy.ndarray.sortがある。
numpy.sortはndarrayを引数に取って、その配列をソートした配列を返す。
https://docs.scipy.org/doc/numpy-1.15.1/reference/generated/numpy.sort.html
一方、numpy.ndarray.sortは呼び出し元のインスタンスであるndarrayをソートする。返り値はNoneである。
https://docs.scipy.org/doc/numpy-1.15.1/reference/generated/numpy.ndarray.sort.html

type(sample_array)  
#→ numpy.ndarray  

というわけで、sample_array.sort()は、numpy.ndarray型のオブジェクトであるsample_arrayのメソッドを呼び出している。今回使っているのはnumpy.ndarray.sortだわ。
以下、ndarray.sortと書く。

なお、numpy.sortを用いて配列を降順ソートする方法は後述。

スライスの中身を変えて試す→「その部分列をソートし、他の部分は不変」になる

どういうわけで降順ソートになるのか、納得できなかったので、スライスの中身を変えて色々試してみた。

sample_array = np.array([9,7,6,5,1,2,3])  
  
sample_array[0:3:1].sort()  
print("ソート後:", sample_array)  
#→ソート後: [6 7 9 5 1 2 3]  

sample_array[0:3:1] は配列の最初の3要素を指す。
結果を見ると、最初の3要素だけが昇順にソートされていて、他の値は変わらない。

sample_array = np.array([1,100,4,400,3,300,2,200])  
  
sample_array[::2].sort()  
print("ソート後:", sample_array)  
#→ソート後: [  1 100   2 400   3 300   4 200]  

sample_array[::2] は配列の最初から1つずつ飛ばしていった要素(配列を1番めから数えたときの奇数番目)を指す。
結果を見ると、該当する要素だけが昇順にソートされていて、他の値は変わらない。

sample_array = np.array([1,100,4,400,3,300,2,200])  
  
print("ソート前の部分列:", sample_array[::-2])  
sample_array[::2].sort()  
print("ソート後:", sample_array)  
#→ソート前の部分列: [200 300 400 100]  
#→ソート後: [  1 400   4 300   3 200   2 100]  

sample_array[::-2] は配列の最後から最初に向かって1つずつ飛ばしていった要素を指す。

結果を見ると、該当する要素だけが降順にソートされていて、他の値は変わらない。
この結果から考えると、
sort関数に[200 300 400 100]を渡して[100 200 300 400]を得て、それを元の場所に入れ直した。
つまり、もともと200があった場所に100を入れて……ということをやっているみたいだ……?
sort関数がやってることって、配列[200 300 400 100]から[100 200 300 400]への写像作用素と思っておけば良いのかな?

その部分列を取り出してソートするには?

部分列を取り出して昇順にソートしたものを得たいときにはどうするか。
まず、いったん他の変数に代入するという方法がある。

sample_array = np.array([9,7,6,5,1,2,3])  
  
a = sample_array[0:3:1]  
a.sort()  
print("ソート後:", a)  
  
#→ソート後: [6 7 9]  

または、ndarray.sortではなくnp.sortを使い、引数に部分列を指定する。

sample_array = np.array([9,7,6,5,1,2,3])  
  
b = np.sort(sample_array[0:3:1])  
print("ソート後:", b)  
  
#→ソート後: [6 7 9]  

最初の例に戻る

最初の例も、sample_array[::-2]と同様に考えれば一応納得できる。下記に再掲。

sample_array = np.array([1,4,2,5,3])  
  
sample_array[::-1].sort()  
print("ソート後:", sample_array)  
  
#→ ソート後: [5 4 3 2 1]  

sort関数に最初の配列の逆である[3 5 2 4 1]を渡して[1 2 3 4 5]を得て、それを元の場所に入れ直した。
結果的に、配列は降順ソートされた[5 4 3 2 1]になった。
(sort関数に何かオプションを指定したわけではないので、sort関数のほうは「降順にソートした」とは思っていない)

色々と実際に試してみて挙動を確認してみたけど、この辺が限界っぽい。
試してみるだけだと、なんでこういう風になっているのか、が分からない。
「sample_array[::-1].sort()は配列を逆順にしたあとにソートしていると思うが、なぜそうならないのか?」と聞かれたら答えられないしなー。
ndarray.sort関数には(C++言語でいうところの)ポインタとか参照が渡っているのか……?
(ここで唐突にC++が出てきたのは、俺が普段業務で触っているのがC++だからである)
公式ドキュメントを「reference」とかで探してみてもうまくヒットしない。

おまけ 配列を降順にソートする他の方法

個人的にはこのやり方で降順ソートをするのは直感的でないと思ったので、
別の書き方のほうが読んで分かりやすいのなら、そっちを使おうと思った。

sample_array = np.array([1,4,2,5,3])  
sample_array.sort()  
print(sample_array[::-1])  
#→[5 4 3 2 1]  
  
sample_array = np.array([1,4,2,5,3])  
print(np.sort(sample_array)[::-1])  
#→[5 4 3 2 1]  

ただし、降順ソートした結果を実際に使おうとすると変数に代入しないといけないから配列のコピーが発生する。

……と、記事を最後まで書いて気づいたけど、ほとんど同じ質問がStack Overflowにあったわ。
python - Efficiently sorting a numpy array in descending order? - Stack Overflow

それでは。