numpyのarrayを並び替えた結果をインデックスで取得する

データを大きい順や小さい順に並び替えることはよくある操作ですが、
その結果として、”n番目の値は何だったのか?”よりも、”何番目の要素がn番目だったのか?”を知りたいケースは地味にあります。
[241,631,362,222,2,44] のようなデータがあって、 最大値は631だってことよりも、
最大値のindexは1だ(0から始まることに注意)ってことを得たい場合ですね。

そのような時、ソートした結果をインデックスのリストで得られると便利です。
それは numpyのargsortで得られます。
numpy.argsort

通常の並べ替え結果を返してくれる sort とそれぞれ実行してみましょう。

それぞれドキュメントを見るといくつか引数が用意されていて、多次元の場合の挙動や、ソートアルゴリズムなどを指定できます。
しかし、自分にとってはその中に昇順/降順の指定が”ない”ことの方が重要です。
デフォルトで昇順にソートするので、降順がいい時はどは別途指定します。

それでは、sortとargsortを昇順降順で試します。


import numpy as np
data = [241, 631, 362, 222, 2, 44]

print(list(np.sort(data)))
# [2, 44, 222, 241, 362, 631]
print(list(np.argsort(data)))
# [4, 5, 3, 0, 2, 1]

# 降順にしたい時は[::-1]を使う
print(list(np.sort(data))[::-1])
# [631, 362, 241, 222, 44, 2]
print(list(np.argsort(data))[::-1])
# [1, 2, 0, 3, 5, 4]

うまく動いていますね。

このブログでもすでに次の記事のコード中で利用しています。
参考:pythonでトピックモデル(LDA)

magic function 自体の説明を見る

jupyter notebook の便利な機能に magic functionがあります。
% や %% で始まるあれですね。
このblog記事中でも、処理時間を測ったり、トレジャーデータに接続したりと使ってます。

ただ、個々の関数の使い方の説明はわかっても、そもそも magic functionとは何者か、
という点の理解をこれまでおろそかにしてました。

それで、何を読めばいいのかちょっと調べていたのですが、
jupyter notebookで、%magicを実行すると、magic functionの説明が出てくるようです。

結構な長文が表示されたので、この記事中に貼り付けるようなことはしないのですが、
ザーッと眺めた限りでも自分が知らなかった情報が多く含まれているようです。

よく使うものについてはこのブログでも今後取り上げていきたいですが、
取り急ぎ、%magicを実行して出てきた文章を読んでいただくと色々参考になるのではないでしょうか。

matplotlibのhist()の戻り値

matplotlibでヒストグラムを書く時、次のように、hist()を使います。(リンク先は公式ドキュメント)


import matplotlib.pyplot as plt
import numpy as np
data = np.random.randn(100) * 10
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.hist(data)
plt.show()

ついこの間まで知らなかったのですが、ドキュメントによると、ax.hist(data) は戻り値を返しています。
戻されるのは、各区間の度数と、区間の区切り位置、描写に使われているmatplotlibのオブジェクトの3つです。
これらのうち、度数と区切り位置を取れるのは可視化とその他の集計を整合性を保ちながら行うのに便利そうです。

とりあえず、戻り値を受け取れるようにしてもう一回やってみましょう。
(図は今回省略します。)


import matplotlib.pyplot as plt
import numpy as np
data = np.random.randn(100) * 10
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
n, bins, patches = ax.hist(data)
plt.show()
print(n)
print(bins)
print(patches)

# 以下出力 (図は略)
[ 1.  5.  6. 18. 14. 16. 21. 13.  4.  2.]
[-29.53118806 -24.17059351 -18.80999896 -13.44940441  -8.08880985
  -2.7282153    2.63237925   7.9929738   13.35356835  18.7141629
  24.07475745]

ビンの数は今回何も指定していないので、デフォルトの10個です。
そのため、度数の配列nには10個の要素が含まれ、
区切り位置binsは右端左端が両方入っているので、11この要素を持つ配列になっています。

pythonの関数中でグローバル変数に代入する

前の記事のフィボナッチ数列の実装の中で、関数が呼び出された回数を数えるために少し仕掛けをいれました。
参考:pythonの関数をメモ化する

で、その中で globalと言うのを使っているのでその解説です。

ドキュメントはこちら。
global文

pythonでは、関数の中でグローバル変数を”参照”することができます。
しかし、そのままではグローバル変数に”代入”はできません。

やってみましょう。
まずは、”参照”ができることの確認。


a = 1


def sample1():
    # グローバル変数aの値を参照できるためaの値1を戻す。
    return a


sample1()  # 1

次に、代入を試してみます。ちょっとわかりにくいですが、グローバル変数bを用意し、
関数中でbに代入していますがこのbが実はローカルの変数で、
関数の外でもともと宣言されていたbに影響していないことがわかります。


b = 2


def sample2():
    # グローバル変数bへの代入はできないので、このbはローカル変数
    b = 3
    print(b)


sample2()  # 3
# 変数bは変更されていない
print(b)  # 2

これが、global文を使うと、グローバル変数への代入が可能になります。


c = 10


def sample3():
    global c
    c = 20
    print(c)


sample3()  # 20
# 関数中の変更が反映されている
print(c)  # 20

pythonの関数をメモ化する

プログラムを書いていると再帰呼び出しする関数や整数値を引数にとる関数など、同じ値を渡して何度も実行される関数があります。
そのような場合、メモリーが十分あるのであれば毎回毎回処理を実行するより結果を保存しておく方が効率的です。

それを、メモ化(Memoization)と言うそうです。
メモ化 – Wikipedia

自分で実装するのも難しくないですが、pythonでメモ化を行うには、
@functools.lru_cache
と言う便利なデコレーターが用意されています。

ちなみに、lruは、Least Recently Usedの略です。
キャッシュアルゴリズム – Wikipedia

フィボナッチ数列で実際に使ってみましょう。
また、実際に効率化できている(関数中の処理の実行回数が減っている)ことを確認するために、
その関数が何回呼び出されたのかを記録し、表示します。
また、実行時間も測りましょう。

まず、メモ化をしない例。(jupyter notebookで動作させることを前提としたコードです。)


%%time
fib_count = 0  # 関数が呼び出された回数記録用


def fib(n):
    global fib_count
    fib_count = fib_count + 1
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)


print("F(100)=", fib(30))
print("実行回数:", fib_count)

# 以下出力
F(100)= 832040
実行回数: 2692537
CPU times: user 449 ms, sys: 3.35 ms, total: 453 ms
Wall time: 456 ms

fibが何度も繰り返し実行され、非常に無駄の多い実装になっていることがわかります。

続いて、lru_cacheでメモ化した例です。


%%time
from functools import lru_cache
fib_memo_count = 0  # 関数が呼び出された回数記録用


@lru_cache(maxsize=None)
def fib_memo(n):
    global fib_memo_count
    fib_memo_count = fib_memo_count + 1
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib_memo(n-1) + fib_memo(n-2)


print("F(100)=", fib_memo(30))
print("実行回数:", fib_memo_count)

# 以下出力
F(100)= 832040
実行回数: 31
CPU times: user 229 µs, sys: 120 µs, total: 349 µs
Wall time: 311 µs

こんどのfib_memoは31回しか実行されませんでした。
n=0,...,30 でそれぞれ1回ずつですね。
処理時間も桁違いに早くなっています。

pythonで集合(set)の包含関係を判定する

実は最近まで知らなかったのですが、pythonの集合(set)同士の包含関係を不等号で確認することができます。

ドキュメントはこちら
組み込み型 set(集合)型

念のため軽く用語の説明をしておくと、
集合Aと集合Bに対して、
Aの任意の要素xがBの要素でもある時、AはBの部分集合(subset)であるといいます。
それを記号で、 $A\subseteq B$ と書きます。
左右反転して、 $B\supseteq A$ と書くこともできます。
また、さらに集合Aと集合Bが等しくない時は、真部分集合(もしくは狭義の部分集合)といい、
$A\subset B$ もしくは $B\supset A$と書きます。

これをpythonではそれぞれ、 <=, >=, <, >, で計算できます。
戻り値はTrueかFalseです。
いくつかやってみましょう。


{1, 3, 5} <= {1, 2, 3, 4, 5}
# True

{1, 3, 5, 7} <= {1, 2, 3, 4, 5}
# False

{1, 3, 5} >= {1, 3, 5}
# True

{1, 3, 5} > {1, 3, 5}
# Flase

ハミング距離(Hamming distance)

二つの文字列がどのくらい異なるかを表す距離関数として、以前レーベンシュタイン距離と言うのを紹介しました。
参考:pythonで編集距離(レーベンシュタイン距離)を求める

これよりももっとシンプルな関数として、ハミング距離(Hamming distance)と言うのがあるのでその紹介です。
これは文字数が同じ二つの文字列に対して、単純に異なる文字を数えたものです。
ハミング距離 – Wikipedia

自分の場合、文字数が同じ時しか定義できないなどの理由により、レーベンシュタイン距離に比べて使う頻度は低いです。
ただ、文字数が同じでさえあれば高速に計算できます。

pythonでの実装ですが、レーベンシュタイン距離の時に使った、
python-Levenshtein に含まれています。

試しにやってみましょう。


import Levenshtein
print(Levenshtein.hamming("toned", "roses"))
# 3

この例の toned と roses では、ハミング距離もレーベンシュタイン距離もどちらも3ですが、
文字数が同じであってもこの2種類の距離の値が異なる例はあります。

例えば次のようなケースです。


text1 = 'abcdefg'
text2 = 'bcdefga'
print(Levenshtein.distance(text1, text2))
# 2
print(Levenshtein.hamming(text1, text2))
# 7

pythonのfilter関数の使い方

前回の記事がmap関数の話だったので、今回は使い方のよく似たfilter関数です。

ドキュメントはこちら。
組み込み関数

基本的な構文は以下の形で、iterable の各要素に functionを適用して、
結果が新なものだけを取り出せます。
filter(function, iterable)

map関数と同様に、戻り値はリストではなくイテレーターになるので最初は少し戸惑いました。
試しに、整数のうち偶数だけ抽出するフィルターを書いてみます。


def f(n):
    return n % 2 == 0


m = filter(f, range(10))
print(m)
# <filter object at 0x114d178d0>
print(list(m))
# [0, 2, 4, 6, 8]
print(list(m))
# []

細かい説明は mapの記事に書いた通りなのですが、
イテレーターを使うために、リストに変換するなり、nextで取り出すなりする必要があり、
一度取り出すともう一回listに変換しようとしても空のリストしか返ってこなくなります。

なお、内包表記でもほぼ同じ処理が実装でき、自分はこちらを使うことが多いです。


[x for x in range(10) if f(x)]
[0, 2, 4, 6, 8]

pythonのmap関数の使い方

前の記事でさらっと使っていたmap関数の使い方の紹介です。

ドキュメントはこちら。
組み込み関数

MathematicaのMapとよく似た関数(と聞いて、「ああ、あれね」と通じる人がどのくらいいらっしゃるのかわかりませんが)であり、
配列などの各要素に関数を順番に適用することができます。

使い方は map(適用したい関数, 配列1, ) です。
僕はpython初心者の頃、関数を適用した結果のリストがすぐ戻ってくると期待していたのに、イテレーターが戻ってきたので結構戸惑いました。

例えば、引数を2乗する関数で試してみましょう。


def f(x):
    return x**2


m = map(f, range(10))
print(m)
# <map object at 0x1156f19b0>

リストにしたければ、型変換してあげる必要があります。
ただし、通常のリストと違い、イテレーターなので、一度最後のデータかまで取り出すと、次の値が取れなくなります。


# 1回目は与えられたリストに関数を適用した結果が戻る
print(list(m))
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# 続けて全く同じように呼び出すと空のリストがかえる。
print(list(m))
# []

内包表記でほぼ同じ処理を実装できますが、違いは関数が実行されるタイミングです。
内包表記は、それが定義されたタイミングで計算され、
mapの場合は、値が必要になったタイミングで実行されます。

適用する関数にprint文を仕込んで、様子を見てみましょう。
最初に内包表記の場合、


def g(x):
    print(x)
    return x**2


l = [g(x) for x in range(10)]
# この時点で g が実行されているので、以下が出力される。
'''
0
1
2
3
4
5
6
7
8
9
'''
print(l)
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

次にmapの場合。


m1 =  map(g, range(10))
# まだgが実行されてないので、何も出力されない。

# nextを使って、最初の3個の値を取り出すと、その3この値に対してだけ関数gが実行される。
print(next(m1))
'''
0
0
'''
print(next(m1))
'''
1
1
'''
print(next(m1))
'''
2
4
'''

これらの性質により、上手く使えば実行時間やメモリの節約が可能になるそうです。
(それを実感するほど上手く使えたことはほとんどないのですが)
ただ、pythonを使っていく上で、イテレーターの扱いに慣れておくのは有益なので、学んでおいて損はなかったと思ってます。

pythonのthisモジュールに定義されている変数について

以前紹介した The Zen of Python の記事の中で、
thisというモジュールに仕掛けられたEaster Eggを紹介しました。

その記事中ではインポートした瞬間に表示される文字列にしか焦点を当ててませんでしたが、
このモジュールに含まれている関数や定数についてもちょっと面白いので紹介します。

このthisの中で、どんな変数やメソッドが定義されているのか、dir関数で見てみましょう。


import this
# 略

dir(this)
'''
['__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'c',
 'd',
 'i',
 's']
'''

“__”で始まる特別な値以外に、c, d, i, s の4つの変数が含まれています。
これらのうち、iとcはそれぞれ整数ですがあまり意味はありません。
ちょっと面白いのは、dとsです。

まず、 dの方は次の辞書です。


print(this.d)
# 以下出力
{'A': 'N', 'B': 'O', 'C': 'P', 'D': 'Q', 'E': 'R', 'F': 'S', 'G': 'T', 'H': 'U', 'I': 'V', 'J': 'W', 'K': 'X', 'L': 'Y', 'M': 'Z', 'N': 'A', 'O': 'B', 'P': 'C', 'Q': 'D', 'R': 'E', 'S': 'F', 'T': 'G', 'U': 'H', 'V': 'I', 'W': 'J', 'X': 'K', 'Y': 'L', 'Z': 'M', 'a': 'n', 'b': 'o', 'c': 'p', 'd': 'q', 'e': 'r', 'f': 's', 'g': 't', 'h': 'u', 'i': 'v', 'j': 'w', 'k': 'x', 'l': 'y', 'm': 'z', 'n': 'a', 'o': 'b', 'p': 'c', 'q': 'd', 'r': 'e', 's': 'f', 't': 'g', 'u': 'h', 'v': 'i', 'w': 'j', 'x': 'k', 'y': 'l', 'z': 'm'}

そして、 sは次の意味不明な文字列が入ってます。


print(this.s)
Gur Mra bs Clguba, ol Gvz Crgref

Ornhgvshy vf orggre guna htyl.
Rkcyvpvg vf orggre guna vzcyvpvg.
Fvzcyr vf orggre guna pbzcyrk.
Pbzcyrk vf orggre guna pbzcyvpngrq.
Syng vf orggre guna arfgrq.
Fcnefr vf orggre guna qrafr.
Ernqnovyvgl pbhagf.
Fcrpvny pnfrf nera'g fcrpvny rabhtu gb oernx gur ehyrf.
Nygubhtu cenpgvpnyvgl orngf chevgl.
Reebef fubhyq arire cnff fvyragyl.
Hayrff rkcyvpvgyl fvyraprq.
Va gur snpr bs nzovthvgl, ershfr gur grzcgngvba gb thrff.
Gurer fubhyq or bar-- naq cersrenoyl bayl bar --boivbhf jnl gb qb vg.
Nygubhtu gung jnl znl abg or boivbhf ng svefg hayrff lbh'er Qhgpu.
Abj vf orggre guna arire.
Nygubhtu arire vf bsgra orggre guna *evtug* abj.
Vs gur vzcyrzragngvba vf uneq gb rkcynva, vg'f n onq vqrn.
Vs gur vzcyrzragngvba vf rnfl gb rkcynva, vg znl or n tbbq vqrn.
Anzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!

ただこの文字列、薄目にみてみると、各単語の文字数が The Zen of Python が似ています。

実はこれはちょっとした暗号になっていて、
this.s の各文字を、this.dの辞書で変換すると、 The Zen of Python が現れます。

書き方はいろいろあると思いますが次のような形でやってみましょう。


print("".join(map(lambda x: this.d.get(x, x), this.s)))
# 以下出力
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

モジュール this の実装自体が
The Zen of Python に反してややこしい値になっているというちょっとした遊び心のようです。

ちなみに、 this モジュールのソースコードも読んでみたのですが、 mapではなく、内包表記でやっているようですね。
sやdを定義したあとに、次のように書いてありました。
よく考えなくてもこれで十分ですね。


print("".join([d.get(c, c) for c in s]))