Python:排序函数在存在 nan 时中断
Posted
技术标签:
【中文标题】Python:排序函数在存在 nan 时中断【英文标题】:Python: sort function breaks in the presence of nan 【发布时间】:2011-05-13 12:11:03 【问题描述】:sorted([2, float('nan'), 1])
返回[2, nan, 1]
(至少在 Activestate Python 3.1 实现上。)
我知道nan
是一个奇怪的对象,所以如果它出现在排序结果中的随机位置,我不会感到惊讶。但它也弄乱了容器中非nan数字的排序,这真是出乎意料。
我向related question 询问了关于max
的问题,基于此,我理解了为什么sort
会这样工作。但这应该被认为是一个错误吗?
文档只是说“返回一个新的排序列表 [...]”,而没有指定任何细节。
编辑: 我现在同意这不违反 IEEE 标准。但是,我认为,从任何常识的角度来看,这都是一个错误。即使是不经常承认错误的微软,也已经认识到这是一个错误,并在最新版本中修复了它:http://connect.microsoft.com/VisualStudio/feedback/details/363379/bug-in-list-double-sort-in-list-which-contains-double-nan。
无论如何,我最终遵循了@khachik 的回答:
sorted(list_, key = lambda x : float('-inf') if math.isnan(x) else x)
与默认情况下执行此操作的语言相比,我怀疑它会导致性能下降,但至少它可以工作(除非我引入了任何错误)。
【问题讨论】:
Not a Number(NAN) 是数字排序或任何期望数字的无效输入;所以我不会认为这是一个错误。 @Frayser:这不太正确。它在Python中无效吗?不,因为 Python 不会引发异常。它在 IEEE754 中无效吗?不,因为它提供了非常具体的行为(至少对于安静的nan
)。它在其他一些标准中无效吗?
虽然“nan”最终会随机出现在结果列表中的某个位置是可以理解的,但更难理解的是,错误地排序仍然在最后的数值显然是正确的行为:sorted([ 1.0, 2.0, 3.0, float('nan'), 4.0, 3.0, 2.0, 1.0]) => [1.0, 2.0, 3.0, nan, 1.0, 2.0, 3.0, 4.0]。见bugs.python.org/issue12286。
"但是它也弄乱了容器中非nan数字的排序,真是出乎意料。" - 完全正确 - 但是,我认为问题出在 .sort()
上,我只有在弄清楚后才进行此问答:\ 感谢您的录制!
@Noah 截至 2019 年,该问题线程已关闭 :(
【参考方案1】:
前面的答案很有用,但可能不清楚问题的根源。
在任何语言中,排序都会在输入值的域上应用由比较函数或其他方式定义的给定排序。例如,小于,又名operator <,
,当且仅当小于定义了对输入值的合适排序时,才能始终使用。
但这不适用于浮点值和小于:
“NaN 是无序的:它不等于、大于或小于任何东西,包括它自己。” (GNU C 手册中的清晰散文,但适用于所有基于现代IEEE754
的浮点)
所以可能的解决方案是:
首先删除 NaN,使输入域通过 定义一个自定义比较函数(a.k.a.谓词) 定义 NaN 的排序,例如小于任何数,或大于 比任何数字。
任何一种方法都可以使用任何语言。
实际上,考虑到 python,如果您不太关心最快的性能,或者删除 NaN 是上下文中所需的行为,我更愿意删除 NaN。
否则,您可以在较旧的 python 版本中通过“cmp”或通过 this 和 functools.cmp_to_key()
使用合适的谓词函数。自然,后者比先删除 NaN 更尴尬。在定义此谓词函数时,需要注意避免更差的性能。
【讨论】:
IEEE 754 要求 max(NaN, 1) 返回 1。如果 Python 遵循标准会很好,但事实并非如此。如果它遵循自己的规则,它至少可以有一些合理的规则,而不是随机的不稳定行为。 澄清一下,我同意你的观点,float('nan') < 1 or float('nan') >= 1
应该返回 False。似乎在最新的 IEEE 标准 (IEEE 754 = IEEE 754-2008) 中为函数 minimum
和 maximum
(必须返回数字)设置了一个例外,但对于 sort
或常规比较却没有。跨度>
cmp_to_key 是一个非常迂回的解决方案。您真正需要的只是一个用其他东西替换 NaN 的关键函数(例如,一个无穷大或一个比任何东西都少的自定义对象)。
“定义一个自定义比较函数(也称为谓词),它确实定义了 NaN 的排序,例如小于任何数字,或大于任何数字。”是不够的,因为它没有定义比较 2 个 NAN 的情况。由于 NAN 有多种编码,因此需要对 2 个 NAN 进行一些一致的 比较。在这种情况下,可能会比较内存位。【参考方案2】:
我不确定该错误,但解决方法可能如下:
sorted(
(2, 1, float('nan')),
lambda x,y: x is float('nan') and -1
or (y is float('nan') and 1
or cmp(x,y)))
导致:
('nan', 1, 2)
或在排序或其他任何操作之前删除nan
s。
【讨论】:
我将为 Python 3 重写此代码,并处理nan
为 numpy.nan
的情况。
我怀疑当列表有 2 个 NAN 时这会失败。当 cmp(n1, n2) 为 -1 且 cmp(n2, n1) 也为 -1 时,许多排序例程都会失败。【参考方案3】:
问题是如果list
包含NAN
则没有正确的顺序,因为如果a1 <= a2 <= a3 <= ... <= an
对序列a1, a2, a3, ..., an
进行排序。如果这些 a 值中的任何一个是 NAN
,则排序属性会中断,因为所有 a, a <= NAN and NAN <= a
都是 false
。
【讨论】:
【参考方案4】:假设您想保留 NaN 并将它们作为最低“值”排序,这里有一个解决方法,可以同时使用 non-unique nan、unique numpy nan, 数字和非数字对象:
def is_nan(x):
return (x is np.nan or x != x)
list_ = [2, float('nan'), 'z', 1, 'a', np.nan, 4, float('nan')]
sorted(list_, key = lambda x : float('-inf') if is_nan(x) else x)
# [nan, nan, nan, 1, 2, 4, 'a', 'z']
【讨论】:
我喜欢这个答案。我不明白为什么 nan 不能定义为 -inf 或 inf。例如,我从数学上理解不能比较 0 和 1/0,但这不应该妨碍合理的语言结构来处理这个问题。 @demongolem 如果 nan 与 -inf 类似,那么 [nan, -inf, nan, -inf] 的列表将被视为已排序。【参考方案5】:IEEE754 是在这种情况下定义浮点运算的标准。该标准将操作数的比较操作定义为错误,其中至少一个是 NaN。因此,这不是错误。在对数组进行操作之前,您需要处理 NaN。
【讨论】:
-1 Python 不遵循 IEEE754,它要求有两个 NaN:信令和非信令,以及两个比较运算符:信令和非信令。此外,IEEE754-2008 特别要求max
与nan
比较时返回数字。
如果是信号 NaN (sNaN) 那么硬件会引发异常。对于安静的 NaN (qNaN),硬件不会引发异常,如果期望每个处理浮点值的库例程都检查 qNaN,那就太麻烦了。
如果您在 FP 硬件基于 IEEE754 的机器上运行 CPython,那么您将得到这样的结果。另外,IEEE754在什么意义上定义了max?
Python 文档对 IEEE754 有这样的说法:“今天(2010 年 7 月)几乎所有机器都使用 IEEE-754 浮点算法,几乎所有平台都将 Python 浮点数映射到 IEEE-754 “双精度” 。”另外,非常感谢反对票。仅仅因为你不喜欢这个答案并不意味着你应该射杀信使!! ;-)
@max 我们可以随心所欲地争论,但它就是这样,你只需要预处理数组并检查 NaN - 如果你不喜欢它的完成方式那你就得和 Guido 一起解决了!!!【参考方案6】:
总结问题:
NaN
每次比较都会返回False
,因此它会保持在列表中的位置:
>>> sorted([float('nan'), 0])
[nan, 0]
>>> sorted([0, float('nan')])
[0, nan]
-0.0
这是 == 到 0.0
,但具有不同的 repr、不同的 json 表示和略有不同的数值属性。相同的问题是正零和负零将保持与原始列表中相同的顺序:
>>> sorted([0.0, -0.0])
[0.0, -0.0]
>>> sorted([-0.0, 0.0])
[-0.0, 0.0]
其他解决方案?
@khachik 的解决方案对NaN
和-inf
的排序行为不一致
>>> key=lambda x: float('-inf') if math.isnan(x) else x
>>> sorted([float('nan'), float('-inf')], key=key)
[nan, -inf]
>>> sorted([float('-inf'), float('nan')], key=key)
[-inf, nan]
解决方案:更复杂的按键功能。
因此,符号和 nans 存在问题。我们可以将它们包含在一个关键函数中:
def stable_float_sort_key(x: float):
return math.copysign(1, x), math.isnan(x), x
这适用于上述所有示例:
>>> sorted([float('nan'), 0.0], key=stable_float_sort_key)
[0.0, nan]
>>> sorted([0.0, float('nan')], key=stable_float_sort_key)
[0.0, nan]
>>> sorted([float('nan'), float('-inf')], key=stable_float_sort_key)
[-inf, nan]
>>> sorted([float('-inf'), float('nan')], key=stable_float_sort_key)
[-inf, nan]
>>> sorted([0.0, -0.0], key=stable_float_sort_key)
[-0.0, 0.0]
>>> sorted([-0.0, 0.0], key=stable_float_sort_key)
[-0.0, 0.0]
确实,您可以编写一个假设检验,表明它在所有浮点数上都是一致的:
import json
from hypothesis import given, settings
from hypothesis import strategies as st
@given(nums=st.lists(st.floats()), random=st.randoms())
@settings(max_examples=10000)
def test_stable_json_sorting(nums, random):
shuffled = list(nums)
random.shuffle(shuffled)
l1 = sorted(nums, key=stable_float_sort_key)
l2 = sorted(shuffled, key=stable_float_sort_key)
assert json.dumps(l1) == json.dumps(l2)
然而,它确实有一些奇怪之处,因为一些 NaN 是负数!例如:
>>> sorted([float('nan'), -0.0, 0.0, float('-nan')], key=stable_float_sort_key)
[-0.0, nan, 0.0, nan]
如果这让您感到困扰,您可以通过切换顺序来解决此问题:
def stable_float_sort_key(x: float):
return math.isnan(x), math.copysign(1, x), x
首先对负数进行排序,然后是正数,然后是 NaN。
这些有意义吗?
当然,其他回答者是正确的,从某种意义上说,这没有任何意义。 NaN 的比较是某种概念上的错误。但是,即使在问题没有“意义”的情况下,您也可能需要不变量,例如将由相同代码生成的浮点数集序列化为完全相同的 JSON 表示,尽管哈希随机化(我的用例)。这更像是 python 代码的正式属性,而不是根据 IEEE 标准有“正确答案”的东西。
【讨论】:
【参考方案7】:无论标准如何,在许多情况下,用户定义的 float 和 NA
值的排序都是有用的。例如,我正在对股票收益进行排序,并希望 NA
最后(因为这些无关紧要)从高到低。有4种可能的组合
-
升序浮点值,
NA
值最后
升序浮点值,NA
值在前
浮点值降序,NA
值最后
浮点值降序,NA
值在前
这是一个通过将NA
值有条件地替换为+/- inf
来覆盖所有场景的函数
import math
def sort_with_na(x, reverse=False, na_last=True):
"""Intelligently sort iterable with NA values
For reliable behavior with NA values, we should change the NAs to +/- inf
to guarantee their order rather than relying on the built-in
``sorted(reverse=True)`` which will have no effect. To use the ``reverse``
parameter or other kwargs, use functools.partial in your lambda i.e.
sorted(iterable, key=partial(sort_with_na, reverse=True, na_last=False))
:param x: Element to be sorted
:param bool na_last: Whether NA values should come last or first
:param bool reverse: Return ascending if ``False`` else descending
:return bool:
"""
if not math.isnan(x):
return -x if reverse else x
else:
return float('inf') if na_last else float('-inf')
测试 4 种组合中的每一种
from functools import partial
a = [2, float('nan'), 1]
sorted(a, key=sort_with_na) # Default
sorted(a, key=partial(sort_with_na, reverse=False, na_last=True)) # Ascend, NA last
sorted(a, key=partial(sort_with_na, reverse=False, na_last=False)) # Ascend, NA first
sorted(a, key=partial(sort_with_na, reverse=True, na_last=True)) # Descend, NA last
sorted(a, key=partial(sort_with_na, reverse=True, na_last=False)) # Descend, NA first
【讨论】:
【参考方案8】:弹性排序涉及比较 2 个项目并返回:小于、等于、大于。
如果cmp(a,b)
是“更大”,那么cmp(b,a)
必须是“更少”。
如果cmp(a,b)
为“零”,则cmp(b,a)
必须为“零”。
迄今为止的答案中缺少的是比较两个float
s 的情况,它们都是NANs 并保留上述属性。 2 NAN 应该进行比较,或者可能基于对其有效负载的某种一致解释。
替代比较算法把所有的NAN > +inf
if isnan(a)
if isnan(b)
return 0 (or maybe compare payloads/bit patterns)
return 1
if isnan(b) return 1
if a > b return 1
if a < b return -1
return 0
【讨论】:
以上是关于Python:排序函数在存在 nan 时中断的主要内容,如果未能解决你的问题,请参考以下文章