如何在 Python 3 中使用“isclose”比较而不是绝对相等 (==) 对带有浮点数的元组进行排序?

How to sort tuples with floats using "isclose" comparisons rather than absolute equality (==) in Python 3?

提问人:OnDraganov 提问时间:8/18/2023 更新时间:8/19/2023 访问量:137

问:

我想主要按浮点数对对列表排序,其次按整数排序。问题在于比较浮点数是否相等——在许多情况下,我们得到的是任意比较,而不是求助于次阶。

my_list = [
  (0, 1.2),
  (2, 0.07076863960397783),
  (1, 0.07076863960397785),
  (4, 0.02)
]

我想主要按浮点值排序,其次按整数排序。 运行返回以下结果:sorted(my_list, key=lambda x: (x[1], x[0]))

[(4, 0.02), (2, 0.07076863960397783), (1, 0.07076863960397785), (0, 1.2)]

但是,由于两个浮点数非常接近,因此它们实际上应该被认为是相等的,因此所需的顺序是这样的:

[(4, 0.02), (1, 0.07076863960397785), (2, 0.07076863960397783), (0, 1.2)]

如何合并或进入排序?math.isclosenp.isclose

我的尝试

我目前的方法是将浮点数包装到一个类中,该类使用 numpy 的 .isclose

class FloatComparisonWrapper:
    def __init__(self, num):
        self.value = num

    def __lt__(self, other):
        return self.__ne__(other) and self.value < other.value

    def __gt__(self, other):
        return self.__ne__(other) and self.value > other.value

    def __eq__(self, other):
        return np.isclose(self.value, other.value)

    def __ne__(self, other):
        return not self.__eq__(other)

    def __le__(self, other):
        return self.__eq__(other) or self.value <= other.value

    def __ge__(self, other):
        return self.__eq__(other) or self.value >= other.value

然后给出正确的顺序。sorted(my_list, key=lambda x: (FloatComparisonWrapper(x[1]), x[0]))

这是一个好的解决方案吗?是pythonic吗?这是最好的解决方案吗?有没有更快的方法可以做到这一点?

python-3.x 排序 浮点

评论

0赞 dawg 8/18/2023
四舍五入到所需的精度:sorted(my_list, key=lambda t: (round(t[1],6),t[0]))
0赞 OnDraganov 8/18/2023
对浮点数进行四舍五入并不能解决浮点相等比较问题,它可以让你得到类似 to be 的东西,对吧?round(0.123456789123, 6)0.123457000001
0赞 dawg 8/18/2023
如果你不满意,你可以截断:对浮点数的任何解释都涉及妥协。如果对几乎相等的浮点数进行舍入或截断,则确实满足了总顺序的需要。如果使用,可能会遇到不对称的问题。roundsorted(my_list, key=lambda t: (int(t[1]*1e6),t[0]))isclose
0赞 OnDraganov 8/19/2023
我不确定它是如何实现的,以及它是否可以像我上面写的那样产生不同的浮点数——如果可以的话,它什么也解决不了。截断是非常不可扩展的。我的浮子的数量级可能会有很大差异。所以总的来说,我认为仍然会赢得这两种解决方案。但也许我需要更详细地写下我的情况,因为对于所表达的问题抽象,可能没有好的通用解决方案。roundisclose

答:

-1赞 Woodford 8/18/2023 #1

稍微多一点的 Pythonic 是利用functools.total_ordering来减少以下内容:FloatComparisonWrapper

@functools.total_ordering
class FloatComparisonWrapper:
    def __init__(self, num):
        self.value = num

    def __le__(self, other):
        return self.__eq__(other) or self.value < other.value

    def __eq__(self, other):
        return np.isclose(self.value, other.value)

这个解决方案是好的还是最好的是一个见仁见智的问题,它是否足够快并不重要。

评论

0赞 OnDraganov 8/18/2023
默认情况下,该装饰器是否首先检查?__lt__not __eq__(...)
0赞 Woodford 8/19/2023
无论哪种方式,我都看不出它在哪里指定。您可以改为实现,这应该涵盖该问题。已编辑以显示。__le__
3赞 chux - Reinstate Monica 8/18/2023 #2

这是一个好的解决方案吗?

不。
OP的方法在排序方面存在缺陷。

由于两个浮标非常接近,因此它们确实应该被认为是相等的

考虑是否所有值都几乎接近同一浮点值。有些货币对是“相等的”,有些则不是。 OP 的目标失败了,因为比较没有形成总订单


从C的角度来看,这可能适用于Python,因为我认为这个问题与语言无关。

这不起作用,因为 、 必须形成一致的顺序。compare(a, b)compare(b,c)compare(a,c)

当相同的对象(由大小字节组成,无论它们在数组中的当前位置如何)多次传递给比较函数时,结果应彼此一致。也就是说,对于 qsort,它们将定义数组的总排序,...C1723 § 7.24.5 4.

考虑

(2, float_before(x)), // Lowest value that "is close" to x.
(1, x),
(0, float_after(x)), // Greatest value that "is close" to x.

即使 和 是连续的“相等”值,如 OP 所建议的,(以及 ,),排序将得出不同的(并且可能是不确定的:例如无限循环)结果,具体取决于比较的应用顺序,因为不相等。float_before(x)xxfloat_after(x)compare(float_before(x), float_after(x))


除非一阶值相等,否则不应应用二次比较。几乎相等是不够的,也可能是真的,但事实并非如此。a ≈ bb ≈ ca ≈ c

当 OP 几乎相等时,并且由于它们的主比较是相等的,但次要的比较使它们大于有序的,并暗示 .然而,由于不接近,并且主要排序是 。这与.a > bb > ca > caca < ca > c


有没有更快的方法可以做到这一点?

我建议放弃“两个浮点数非常接近,以至于它们真的应该被认为是相等的”方法进行主要比较。只需进行值比较即可。在尝试使其更快之前,请正确使用功能。isclose()

// Pseudo code
if (primary_compare_as_equal(a,b))
  return secondary compare(a,b)
return primary_compare(a,b)

评论

0赞 OnDraganov 8/18/2023
这是一个很好的观点,也是要牢记的重要事情。但是你提出的解决方案是错误的——这是我示例中的第一个排序调用所做的,它给出了错误的排序。两个花车是一样的,这不仅是一厢情愿的想法,而且它们真的应该是相同的。它们不同的唯一原因是浮点运算就是这样。
0赞 chux - Reinstate Monica 8/18/2023
@OnDraganov 一般来说,你所寻求的目标是无法实现的。对于选择 4 示例示例,它可能没问题,但不能扩展到常规值集。考虑一个集合:a:(2, 0.07076863960397783), b:(1, 0.07076863960397785), c:(0, 0.07076863960397787),其中 0.07076863960397787 接近 0.07076863960397785,但不接近 0.07076863960397783。根据你的比较,你有 a > b、b > c,但 c > a。
0赞 OnDraganov 8/18/2023
我知道,我理解你的观点,并同意这在总体上是不明确的(这可能是为什么没有开箱即用的解决方案的正当理由)。但是在我的应用程序中,根据实际的浮点比较保持顺序会导致一个完全错误的结果(这就是我首先得到这个问题的方式)。我想选项是 1) 改变浮点数的生成方式,2) 验证一些额外的假设,以便这没问题,3) 使用一些额外的信息有不同的方法(我的二级顺序不是整数,而是包含部分关系)
0赞 chux - Reinstate Monica 8/18/2023
@OnDraganov 也许(另一篇)帖子详细介绍了您遇到的问题,因为这里用于解决问题的方法有弱点?
1赞 chux - Reinstate Monica 8/19/2023
@OnDraganov 建议从这个问答中了解你能做什么,然后发布另一个问答。参考这篇文章,有一个明显不同的问题。
0赞 Alain T. 8/18/2023 #3

您可以使用 functools 来实现与 math.isclose 的比较:cmp_to_key

from functools import cmp_to_key
from math import isclose

my_list.sort(key=cmp_to_key(lambda a,b: not isclose(a[1],b[1]) and a[1]-b[1]
                                        or a[0]-b[0]))

print(my_list)
[(4, 0.02), (1, 0.07076863960397785), (2, 0.07076863960397783), (0, 1.2)]

如果你要进行比较类,我建议基于它并尽量减少间接(函数调用级别)的数量:float

from math import isclose

class FloatCompare(float):
     def __lt__(self,other): 
          return float.__lt__(self,other) and not isclose(self,other)    
    
my_list.sort(key=lambda x:(FloatCompare(x[1]),x[0]))

print(my_list)

[(4, 0.02), (1, 0.07076863960397785), (2, 0.07076863960397783), (0, 1.2)]

请注意,您可以定义所有比较方法,但 sort() 实际上只使用 __lt__list.sort

评论

0赞 OnDraganov 8/18/2023
我偶然发现了 ,但现在不鼓励使用它,因为它只是为了向后兼容 Python 2 解决方案吗?cmp_to_key
0赞 Alain T. 8/18/2023
在可以使用简单键函数的情况下,这显然不是一个好主意。但是对于拓扑排序和无法使用简单键函数的情况,它与创建比较包装器相同(无需创建自己的类的额外麻烦)。我找不到文档说它“不鼓励”或严格限制为 Python 2 转换。
0赞 OnDraganov 8/18/2023
是的,我最初从 stackoverflow 上的一个不同问题中得到了“气馁”的想法,我再也找不到了。官方文档说:“这个函数主要用作从 Python 2 转换的程序的过渡工具,它支持使用比较函数”——我想这取决于解释和品味来决定这是否不鼓励在其他情况下使用它。不过,你写它的方式非常简洁。我没有意识到任何负数或任何正数分别与 -1 或 1 一样好,而 False 在这里与 0 一样好。
1赞 Alain T. 8/18/2023
False 实际上从未返回(尽管它确实等同于零)。顺便说一句,如果你的第二个比较不是数字,获得 -1,0,1 值的一个好方法是这样表示:(a[0]>b[0]) - (a[0]<b[0])