Pythonで配列のJaccard距離を計算する(scipy.spatial.distance.jaccard)

Jaccard距離とは2配列間の距離(類似性の逆)をその要素の正誤によって求める指標である。
しかし、配列の要素がNaNかNaNでないか(または0か0より大きいか)を区別したい場合と、完全に値が一致しているかしていないかを区別したい場合などがある。
scipyにはscipy.spatial.distance.jaccardとして提供されているが、挙動がよくわからなかったのでテストした。

以下のような配列を用意する。

>>> from scipy.spatial.distance import jaccard 
>>> import numpy as np
>>> array = np.array([[np.nan, np.nan],[np.nan, 0],[1, 2],[2, 2]])
>>> array
array([[ nan,  nan],
       [ nan,   0.],
       [  1.,   2.],
       [  2.,   2.]])

普通にJaccard距離を取ると

>>> jaccard(array[:, 0], array[:, 1])
0.75

即ち[ 2., 2.]のみが一致したとみなされ、距離は3/4となっている。

NaNかNaNでないかの違いだけを見たい場合、boolに変換する。

>>> np.array(array, dtype=bool)
array([[ True,  True],
       [ True, False],
       [ True,  True],
       [ True,  True]], dtype=bool)
>>> jaccard(np.array(array[:, 0], dtype=bool), np.array(array[:, 1], dtype=bool))
0.25

[ nan, 0.]のみが不一致とみなされた。
ここで注目すべきは、非常に気持ち悪いがnp.nanがTrueになるという点である。

Jaccardでは[ False, False]の比較は無いものとして扱われるので、例えば

>>> np.nan_to_num(array)
array([[ 0.,  0.],
       [ 0.,  0.],
       [ 1.,  2.],
       [ 2.,  2.]])
>>> jaccard(np.nan_to_num(array[:, 0]), np.nan_to_num(array[:, 1]))
0.5

とすると、[ 1., 2.], [ 2., 2.]の比較が行われ距離が1/2となる。
dtype=objectだとnp.nan_to_numが適用されないのでstrを含むデータを扱う場合は頑張って置換するしか無い。
NaNをFalse、0をTrueとして扱いたい場合もあるかも知れないが、これも簡単な方法はないので頑張って置換するしか無い。
むしろPandasのDataFrameにねじ込んで、isnull(), notnull(), all(), any(), sum()を駆使するほうが楽。
tryalnigro.hatenablog.com

同様の挙動をするものにscipy.spatial.distance.hammingscipy.spatial.distance.kulsinskiがある。
hammingは[ False, False]の比較も行うタイプのもので、kulsinskiは分子分母にoffsetが設けられたもの。