在numpy中查找浮点数数组的唯一元素(使用delta值进行比较)

30

我有一个numpy的ndarray浮点值数组,想要查找这个数组中的唯一值。由于浮点精度问题,这可能会出现问题...因此,在确定哪些元素是唯一时,我想设置一个delta值用于比较。

有没有办法可以做到这一点?目前,我只是在执行以下操作:

unique(array)

这给了我类似于:

array([       -Inf,  0.62962963,  0.62962963,  0.62962963,  0.62962963,
    0.62962963])

当值看起来相同时(根据显示的小数位数),显然它们略有不同。

6个回答

32

另一个可能性是将数字四舍五入到最接近所需精度的值:

np.unique(a.round(decimals=4))

其中a是您的原始数组。

编辑:需要注意的是,根据我的测试,我的解决方案和@unutbu的解决方案速度几乎相同(我的可能快5%),因此两种方案都是好的解决方法。

编辑#2:这是为了解决Paul的问题。它肯定更慢,并且可能有一些优化可以进行,但我按原样发布它以演示该策略:

def eclose(a,b,rtol=1.0000000000000001e-05, atol=1e-08):
    return np.abs(a - b) <= (atol + rtol * np.abs(b))

x = np.array([6.4,6.500000001, 6.5,6.51])
y = x.flat.copy()
y.sort()
ci = 0

U = np.empty((0,),dtype=y.dtype)

while ci < y.size:
    ii = eclose(y[ci],y)
    mi = np.max(ii.nonzero())
    U = np.concatenate((U,[y[mi]])) 
    ci = mi + 1

print U

如果在精度范围内有很多重复的值,那么这应该是相当快的,但如果许多值是唯一的,那么这将变得很慢。另外,最好将U设置为一个列表并通过while循环进行附加,但这属于“进一步优化”。


1
+1:四舍五入将正确统一一些地板取整可能失败的边缘情况,并且速度更快。 - unutbu
这个答案对我非常有用。作为额外的奖励,我能够在一个二维NumPy数组中找到独特的,你可以在np.unique调用中添加axis=0。谢谢! - rayryeng

14

floorround在某些情况下都不能满足OP的要求,是这样吗?

np.floor([5.99999999, 6.0]) # array([ 5.,  6.])
np.round([6.50000001, 6.5], 0) #array([ 7.,  6.])

我会这样做(这可能不是最优解,而且肯定比其他答案慢):

import numpy as np
TOL = 1.0e-3
a = np.random.random((10,10))
i = np.argsort(a.flat)
d = np.append(True, np.diff(a.flat[i]))
result = a.flat[i[d>TOL]]

当然,这种方法会排除所有接近容差值的一组数中的除最大值外的任何其他值,这意味着如果所有值都非常接近,即使最大值减最小值大于容差,您也可能无法在数组中找到任何独特的值。

这里本质上是相同的算法,但更易于理解,应该更快,因为它避免了索引步骤:

a = np.random.random((10,))
b = a.copy()
b.sort()
d = np.append(True, np.diff(b))
result = b[d>TOL]

楼主还可以看一下scipy.cluster(一个更高级的方法)或者numpy.digitize(提供了另外两种方法的更高级版本)。


我原则上喜欢这个想法,但最后的警告似乎更大程度上偏离了OP的要求。 - JoshAdel
1
@JoshAdel:我必须假设OP的数据自然聚集(根据示例,它们似乎非常紧密地聚集在某些值周围),否则请求就没有太多意义。在这种情况下,将OP的数据数字化为任意阈值(可能会分裂聚类)似乎会带来更多的伤害而不是好处。 - Paul
有用的是能够进行定点精度截断。我心中有一种解决方法可以正确解决这个问题(虽然速度较慢),但我要到稍后才能发布它。 - JoshAdel

6

我刚刚注意到被接受的答案不起作用。例如,这种情况:

a = 1-np.random.random(20)*0.05
<20 uniformly chosen values between 0.95 and 1.0>
np.sort(a)
>>>> array([ 0.9514548 ,  0.95172218,  0.95454535,  0.95482343,  0.95599525,
             0.95997008,  0.96385762,  0.96679186,  0.96873524,  0.97016127,
             0.97377579,  0.98407259,  0.98490461,  0.98964753,  0.9896733 ,
             0.99199411,  0.99261766,  0.99317258,  0.99420183,  0.99730928])
TOL = 0.01

结果为:

a.flat[i[d>TOL]]
>>>> array([], dtype=float64)

由于排序后的输入数组中没有任何值足够间隔至少“TOL”,因此正确的结果应该是:

>>>> array([ 0.9514548,  0.96385762,  0.97016127,  0.98407259,
             0.99199411])

虽然这取决于你如何在“TOL”中选择数值,但你需要使用整数不受机器精度影响的事实:

np.unique(np.floor(a/TOL).astype(int))*TOL
>>>> array([ 0.95,  0.96,  0.97,  0.98,  0.99])

根据 %timeit 的测试结果,该解决方案的性能比建议的解决方案快了 5 倍。

请注意,“.astype(int)” 是可选的,但是如果删除它,则性能会恶化 1.5 倍,因为从 int 数组中提取唯一值要快得多。

您可能希望将“TOL”的一半添加到唯一值的结果中,以补偿向下取整效应:

(np.unique(np.floor(a/TOL).astype(int))+0.5)*TOL
>>>> array([ 0.955,  0.965,  0.975,  0.985,  0.995])

3
在当前版本的NumPy(1.23)中,numpy.unique有一个可选参数return_index,用于返回每个唯一值的第一次出现的索引。因此,您可以简单地在舍入数组上使用numpy.uniquereturn_index=True,并对原始数组进行索引以获得原始的非舍入值。像这样:
decimals = 3
X_unique_with_tolerance = X[np.unique(X.round(decimals), return_index=True)[1]].shape

2
像下面这样怎么样?
np.unique1d(np.floor(1e7*x)/1e7)

其中x是您的原始数组。


2
请注意,np.unique1d在版本1.4中已被弃用,并将在1.5中删除。 - JoshAdel
我可能有那些版本略有错误,但它绝对不在最新的文档中。建议使用np.unique代替。 - JoshAdel

1

我刚刚为我的一个小型numpy扩展包npx添加了对此的支持。

import npx

a = [0.1, 0.15, 0.7]
a_unique = npx.unique(a, tol=2.0e-1)

assert all(a_unique == [0.1, 0.7])

有趣,谢谢!值得注意的是,这仍然会遇到与二进制边缘相关的浮点问题,导致出现意外结果。例如,npx.unique([1.1+2.2, 3.3], tol=0.1) 给我两个结果,但如果我使用更严格的公差:npx.unique([1.1+2.2, 3.3], tol=0.01),我只得到一个结果。(是的,这通常是一个不明确的问题,因此没有好的通用解决方案。) - Mark Dickinson
也许如果tol的倍数形成了箱子的中心而不是它们的端点,那么这就不会那么令人惊讶了?仍然会有一些意外情况,但可能会更少或者不那么明显。 - Mark Dickinson
有趣的边缘情况!我刚刚在npx中修复了它,但是,可能还有其他情况。使用round()只会将问题移至其他地方。 - Nico Schlömer

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