使用scipy计算带有缺失值的成对距离

9
我对于scipy.spatial.distance.pdist如何处理缺失(nan)值感到有些困惑。
出于谨慎起见,让我们先排除矩阵维度的错误。根据文档:
“点通过矩阵X中的m个n维行向量来安排。”
因此,让我们在10维空间中生成三个带有缺失值的点:
numpy.random.seed(123456789)
data = numpy.random.rand(3, 10) * 5
data[data < 1.0] = numpy.nan

如果我计算这三个观测值的欧几里得距离:
pdist(data, "euclidean")

我得到的结果是:

array([ nan,  nan,  nan])

然而,如果我筛选掉所有含有缺失值的列,我确实可以获得正确的距离值:

valid = [i for (i, col) in enumerate(data.T) if ~numpy.isnan(col).any()]
pdist(data[:, valid], "euclidean")

我得到了如下结果:
array([ 3.35518662,  2.35481185,  3.10323893])
这种方法会浪费更多的数据,因为我不需要过滤整个矩阵,只需要处理一次比较的向量对。我能否使pdist或类似函数执行成对掩码处理呢?
编辑:
由于我的完整矩阵相当大,因此我对此处提供的小数据集进行了一些计时测试。
1.) Scipy函数。
%timeit pdist(data, "euclidean")
每个循环24.4微秒,3次中的最佳结果为10000个循环。

2.) 不幸的是,目前提供的解决方案大约慢了10倍。

%timeit numpy.array([pdist(data[s][:, ~numpy.isnan(data[s]).any(axis=0)], "euclidean") for s in map(list, itertools.combinations(range(data.shape[0]), 2))]).ravel()
1000次循环,3次中取最好的结果:每个循环花费231微秒。

3.) 然后我进行了一项“纯”Python的测试,并感到惊喜:

from scipy.linalg import norm

%%timeit
m = data.shape[0]
dm = numpy.zeros(m * (m - 1) // 2, dtype=float)
mask = numpy.isfinite(data)
k = 0
for i in range(m - 1):
    for j in range(i + 1, m):
        curr = numpy.logical_and(mask[i], mask[j])
        u = data[i][curr]
        v = data[j][curr]
        dm[k] = norm(u - v)
        k += 1
3次循环中的最佳10000次迭代时间为98.9微秒

因此,我认为前进的方法是将上述代码封装在一个函数中进行Cython化。


如果你使用Cython对其进行优化,可能会更好地直接在掩码数组周围构建它?此外,要小心将小样本的性能推广到大样本... - deinonychusaur
非常抱歉,我的评论写得很糟糕。在100维空间中运行1000个点的情况下,Python的解决方案仍然更快(2.65倍)。 - deinonychusaur
@deinonychusaur,“直接围绕掩码数组构建”是什么意思?如果您想隐藏数据,仍然需要使用提供给ma.array的掩码,对吧?也许您可以编辑您的答案或者我们聊一下。 - Midnighter
确实如此,但Python代码看起来更整洁。我在https://github.com/scipy/scipy/blob/v0.13.0/scipy/spatial/src/distance_wrap.c#L43上进行了挖掘,不知道那是否有所帮助。我们可以聊一下,但我不知道我还能提供多少帮助。 - deinonychusaur
@Midnighter,你的代码有进一步加速吗? - Gökhan Sever
我认为另一种加快这个比较的方式是使用多进程模块,让不同的核心处理不同的数据块。 - Gökhan Sever
2个回答

2

如果我理解正确,您想要计算两个向量在所有维度上具有有效值的距离。

不幸的是,pdist 在这方面不理解掩码数组,因此我修改了您的半解决方案以不减少信息。然而,它并不是最高效的解决方案,也不是最易读的:

np.array([pdist(data[s][:, ~numpy.isnan(data[s]).any(axis=0)], "euclidean") for s in map(list, itertools.combinations(range(data.shape[0]), 2))]).ravel()

将外部制成一个数组并使用ravel就是为了使其形状匹配您所期望的。

itertools.combinations生成data数组所有可能成对索引。

我只需要在这些索引上切片数据(必须是list而不是tuple才能正确地进行切片),并像您的代码一样进行成对过滤nan


谢谢您的回答。请查看我上面编辑过的问题。 - Midnighter

1

1
确实,那本来是一个很好的解决方案,但这个函数直到2019年12月才被添加,而这个问题已经几年了。但我会接受你的答案,因为这正是我今天要使用的函数。 - Midnighter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接