【问题标题】:Sorting array of objects by row using custom dtype使用自定义 dtype 按行对对象数组进行排序
【发布时间】:2021-01-20 17:04:09
【问题描述】:

我正在尝试按行按字典顺序对一些数组进行排序。整数情况完美:

>>> arr = np.random.choice(10, size=(5, 3))
>>> arr
array([[1, 0, 2],
       [8, 0, 8],
       [1, 8, 4],
       [1, 3, 9],
       [6, 1, 8]])
>>> np.ndarray(arr.shape[0], dtype=[('', arr.dtype, arr.shape[1])], buffer=arr).sort()
>>> arr
array([[1, 0, 2],
       [1, 3, 9],
       [1, 8, 4],
       [6, 1, 8],
       [8, 0, 8]])

我也可以进行排序

np.ndarray(arr.shape[0], dtype=[('', arr.dtype)] * arr.shape[1], buffer=arr).sort()

在这两种情况下,结果是相同的。但是,对象数组并非如此:

>>> selection = np.array(list(string.ascii_lowercase), dtype=object)
>>> arr = np.random.choice(selection, size=(5, 3))
>>> arr
array([['t', 'p', 'g'],
       ['n', 's', 'd'],
       ['g', 'g', 'n'],
       ['g', 'h', 'o'],
       ['f', 'j', 'x']], dtype=object)
>>> np.ndarray(arr.shape[0], dtype=[('', arr.dtype, arr.shape[1])], buffer=arr).sort()
>>> arr
array([['t', 'p', 'g'],
       ['n', 's', 'd'],
       ['g', 'h', 'o'],
       ['g', 'g', 'n'],
       ['f', 'j', 'x']], dtype=object)
>>> np.ndarray(arr.shape[0], dtype=[('', arr.dtype)] * arr.shape[1], buffer=arr).sort()
>>> arr
array([['f', 'j', 'x'],
       ['g', 'g', 'n'],
       ['g', 'h', 'o'],
       ['n', 's', 'd'],
       ['t', 'p', 'g']], dtype=object)

显然只有dtype=[('', arr.dtype)] * arr.shape[1] 的情况才能正常工作。这是为什么? dtype=[('', arr.dtype, arr.shape[1])] 有什么不同?排序显然在做某事,但乍一看,排序似乎是荒谬的。是否使用指针作为排序键?

就其价值而言,np.searchsorted 似乎在进行与np.sort 相同的比较,正如预期的那样。

【问题讨论】:

  • 我认为第一种情况将结构化数组的元素包装在对象数组周围(例如[(['f', 'r', 'h'],)]),而第二种情况直接从元素创建结构化数组(例如[('f', 'r', 'h')])。我猜第一种情况按数组排序,第二种按元素排序。
  • @Ehsan。这基本上就是代码显示的内容。我有点好奇为什么。 sort 特别提到结构字段按字典顺序排序,但在这种情况下如何应用还不清楚。如果我们按指针值对数组进行排序,那么为什么不以同样的方式对标量进行排序。如果我们通过对象比较对标量进行排序,那么为什么不以同样的方式对数组进行排序呢?
  • @hpaulj 有什么想法吗?

标签: python numpy sorting


【解决方案1】:

这实际上很好用

In [16]: selection = np.array(list(string.ascii_lowercase))

In [17]: arr = np.random.choice(selection, size=(5, 3))

In [18]: arr
Out[18]:
array([['x', 'l', 'i'],
       ['k', 'h', 'b'],
       ['y', 'h', 'w'],
       ['i', 'u', 't'],
       ['v', 'u', 'k']], dtype='<U1')

In [19]: np.ndarray(arr.shape[0], dtype=[('', arr.dtype, arr.shape[1])], buffer=arr).sort()

In [20]: arr
Out[20]:
array([['i', 'u', 't'],
       ['k', 'h', 'b'],
       ['v', 'u', 'k'],
       ['x', 'l', 'i'],
       ['y', 'h', 'w']], dtype='<U1')

问题在于使用 dtype object 进行选择。

In [21]: selection = np.array(list(string.ascii_lowercase), dtype = object)

In [22]: arr = np.random.choice(selection, size=(5, 3))

In [23]: arr
Out[23]:
array([['b', 'h', 'e'],
       ['o', 'z', 'c'],
       ['g', 'v', 'z'],
       ['r', 'n', 'k'],
       ['a', 'h', 't']], dtype=object)

In [24]: np.ndarray(arr.shape[0], dtype=[('', arr.dtype, arr.shape[1])], buffer=arr).sort()

In [25]: arr
Out[25]:
array([['o', 'z', 'c'],
       ['b', 'h', 'e'],
       ['r', 'n', 'k'],
       ['a', 'h', 't'],
       ['g', 'v', 'z']], dtype=object)

注意dtype = 'O' 表示python 的numpy 类型objectsee here for more,我认为它不提供比较运算符。

通常情况下,您提供的两种类型应该仍然有效。

【讨论】:

  • 在我的例子中,str 是支持丰富比较的任意对象的代表。我注意到这种排序非常适合整数,而 numpy 支持的其他类型也不足为奇。问题在于,根据 dtype 的排列,objects 被排序器进行不同的比较。我想至少了解发生了什么,如果可能的话,了解如何解决这个问题。这个答案只是证实了我已经发现和讨论的内容。
  • 这与你发现的相反。案例dtype=[('', arr.dtype, arr.shape[1])] 对您不起作用,但在第一种情况下有效。原因是这是np.dtype('f_0','U1', (3,)) 类型,它转换为“维数为3 的固定大小的字符数组”,可以按字典顺序排序。在我的第二种情况下,dtype 是 python object(与上面的代码相同)或dtype('f_0','O', (3,))。这转换为“对象大小为 3 的固定大小的数组”。但是对象可以有任意的内存布局。
  • 我对使用非对象 dtype 不感兴趣。我已经确定它在问题中可以正常工作,尽管不是专门针对固定宽度的字符数组。将 dtype 设置为具有 shape[1] 独立字段可以对对象数组中指向的对象进行丰富的比较。将 dtype 设置为具有 shape[1] 元素数组的单个字段似乎可以比较指针,尽管我什至不确定。问题仅限于此。想象一下没有 numpy 模拟的任意自定义类型,而不是字符串。
  • 我认为你没有抓住重点。字符串确实定义了完整的丰富比较,这正是我选择它们作为 MCVE 而不是制作自定义类型的原因。不要让 numpy Unicode 类型让你分心。
  • 是的,我明白了。但是您的示例,在将 dtype 设置为具有 shape[1] 独立字段时能够比较指向的对象的情况,您可以进行丰富的比较。问题(据我了解)是当您将 dtype 设置为具有 shape[1] 元素数组的单个字段时,您不再可以,并且它会中断。不清楚它比较指针。我给出的示例表明,如果类型设置正确(而不是将其声明为对象的通用关键字),您仍然可以进行这些丰富的比较。
【解决方案2】:

对整数进行排序的事实恰好是巧合,这可以通过查看浮点运算的结果来验证:

>>> arr = np.array([[0.5, 1.0, 10.2],
                    [0.4, 2.0, 11.0],
                    [1.0, 2.0, 4.0]])
>>> np.sort(np.ndarray(arr.shape[0], dtype=[('', arr.dtype, arr.shape[1])], buffer=arr))
array([([ 0.5,  1. , 10.2],),
       ([ 1. ,  2. ,  4. ],),
       ([ 0.4,  2. , 11. ],)], dtype=[('f0', '<f8', (3,))])
>>> np.sort(np.ndarray(arr.shape[0], dtype=[('', arr.dtype)] * arr.shape[1], buffer=arr))
array([(0.4, 2., 11. ),
       (0.5, 1., 10.2),
       (1. , 2.,  4. )],
      dtype=[('f0', '<f8'), ('f1', '<f8'), ('f2', '<f8')])

另一个提示来自查看数字0.50.41.0 的位:

0.5 = 0x3FE0000000000000
0.4 = 0x3FD999999999999A
1.0 = 0x3FF6666666666666

在 little-endian 机器上,我们有 0x00 &lt; 0x66 &lt; 0x9A(上面显示的最后一个字节在前)。

可以通过查看the source code中的排序函数来验证确切的答案。例如,在quicksort.c.src 中,我们看到所有非明确数字的类型(包括不是标量的结构字段)都由npy_quicksort 泛型函数处理。它使用函数cmp 作为比较器,使用宏GENERIC_SWAPGENERIC_COPY 分别进行交换和复制。

函数cmp 定义为PyArray_DESCR(arr)-&gt;f-&gt;compare。宏在npysort_common.h 中定义为逐元素操作。

所以最终结果是,对于任何非标量类型,包括打包数组结构字段,比较都是逐字节进行的。对于对象,这当然是指针的数值。对于浮点数,这将是 IEEE-754 表示。正整数似乎可以正常工作的事实是由于我的平台使用 little-endian 编码这一事实造成的。以二进制补码形式存储的负整数可能不会产生正确的结果。

【讨论】:

    【解决方案3】:

    这可能不是一个完美的答案,但我希望我能帮助你:

    1.) 为什么它不能正常工作:因为dtype=[('', arr.dtype)] * arr.shape[1] != dtype=[('', arr.dtype, arr.shape[1])]

    2.) 这两者有什么区别?好吧,虽然第一个将长度添加到列表中,但第二个将列表相乘。 这意味着第一个的输出类似于:[('', dtype('O'), 3)] 而第二个是[('', dtype('O')), ('', dtype('O')), ('', dtype('O'))]

    3.) 排序显然做错了 - 没有输入只是格式错误

    4.) 是否使用指针作为排序键?你的意思是它是否通过数据键格式化数据?然后不,它根据数据本身对它们进行排序。

    编辑: 好吧,说得更清楚:

    首先我认为你误解了@Mike MacNeil's anwer。为了使它更具可塑性,这里有一些例子:

    让我们考虑一个类 Foo:

    class Foo:
        def __init__(self, id):
            self._id = id
        
        def get_id(self):
            return self._id
    
        def __le__(self, ob):
            return self < ob or self == ob
    
        def __lt__(self, ob):
            return self.get_id() < ob.get_id()
    
        def __ge__(self, ob):
            return not self < ob
    
        def __gt__(self, ob):
            return not self <= ob
    
        def __eq__(self, ob):
            return self.get_id() == ob.get_id()
    
        def __str__(self):
            return f'Foo({self.get_id()})'
    
        def __repr__(self):
            rep = super().__repr__()
            return f'{str(self)} {rep[rep.index("at"):rep.index(">")]}'
    

    我们看到比较已经像string 一样实现了。我还实现了__repr__()__str__() 方法,请稍等一下,您就会明白为什么:

    让我们在第一步中创建一个 numpy 数组:

    >>> arr4 = np.array([[Foo(1), Foo(2), Foo(3)],
            [Foo(4), Foo(5), Foo(6)],
            [Foo(7), Foo(8), Foo(9)],
            [Foo(10), Foo(11), Foo(12)]])
    

    如果我们打印它,它将看起来像这样:

    >>> arr4
    array([[Foo(1) at 0x000002411F753F08, Foo(2) at 0x000002411F73FF48, Foo(3) at 0x000002411F74EE48],
           [Foo(4) at 0x000002411F74EE88, Foo(5) at 0x000002411F74EE08, Foo(6) at 0x000002411F756148],
           [Foo(7) at 0x000002411F7561C8, Foo(8) at 0x000002411F756208, Foo(9) at 0x000002411F756248],
           [Foo(10) at 0x000002411F756288, Foo(11) at 0x000002411F7562C8,
            Foo(12) at 0x000002411F756308]], dtype=object)
    

    如果我们现在打印 ndarray...

    >>> np.ndarray(arr4.shape[0], dtype=[('', arr4.dtype, arr4.shape[1])], buffer=arr4)
    array([([Foo(1) at 0x000002411F753F08, Foo(2) at 0x000002411F73FF48, Foo(3) at 0x000002411F74EE48],),
           ([Foo(4) at 0x000002411F74EE88, Foo(5) at 0x000002411F74EE08, Foo(6) at 0x000002411F756148],),
           ([Foo(7) at 0x000002411F7561C8, Foo(8) at 0x000002411F756208, Foo(9) at 0x000002411F756248],),
           ([Foo(10) at 0x000002411F756288, Foo(11) at 0x000002411F7562C8, Foo(12) at 0x000002411F756308],)], dtype=[('f0', 'O', (3,))])
    

    ...我们看到它的形状基本上与

    >>> np.ndarray(arr.shape[0], dtype=[('', arr.dtype, arr.shape[1])], buffer=arr)
    array([['t', 'p', 'g'],
           ['n', 's', 'd'],
           ['g', 'h', 'o'],
           ['g', 'g', 'n'],
           ['f', 'j', 'x']], dtype=[('f0', 'O', (3,))])
    

    使用np.ndarray(arr4.shape[0], dtype=[('', arr4.dtype, arr4.shape[1])], buffer=arr4).sort() 对 Foo-Array 进行排序后,我们看到 arr4 的输出类似于:

    >>> arr4
    array([[Foo(1) at 0x000002411F753F08, Foo(2) at 0x000002411F73FF48, Foo(3) at 0x000002411F74EE48],
           [Foo(10) at 0x000002411F756288, Foo(11) at 0x000002411F7562C8, Foo(12) at 0x000002411F756308],
           [Foo(4) at 0x000002411F74EE88, Foo(5) at 0x000002411F74EE08, Foo(6) at 0x000002411F756148],
           [Foo(7) at 0x000002411F7561C8, Foo(8) at 0x000002411F756208, Foo(9) at 0x000002411F756248]], dtype=object)
    

    虽然

    >>> Foo(10) > Foo(4)
    True
    

    (仍然np.ndarray(arr4.shape[0], dtype=[('', arr4.dtype)] * arr4.shape[1], buffer=arr4).sort() 可以使用定义的比较函数打印出使用 id-key 排序的预期结果。)

    dtype=object 的比较规则并不像您期望的那样仅使用标准比较函数,而是比较对象表示(→ 在这种情况下,这意味着例如 repr(Foo(10)) &lt; repr(Foo(2)) 将是 True,尽管我们实际上会期望 Foo(10) 大于 Foo(2))。

    但是通过告诉 numpy 确切的尺寸/形状,numpy 使用标准比较,这将导致预期的结果,因为它现在知道一行的所有元素都来自完全相同的类型,而不仅仅是一些紧握在一起的随机对象成一个数组。这就是为什么您的示例也不适用于string,但可以用于str,因为numpy (&lt;U1) 将原生支持str

    【讨论】:

    • 这是对问题的重述,不是答案
    • 希望你明白我们想让你告诉什么... ;)
    【解决方案4】:

    您的第一个方法dtype=[('', arr.dtype, arr.shape[1])], buffer=arr).sort() 似乎正在尝试对 dtype=object 进行排序,但它没有足够的信息对其进行排序。当您使用第二种方法dtype=[('', arr.dtype)] * arr.shape[1], buffer=arr).sort() 时,它会解压缩对象,从而允许排序方法“查看”它应该排序的内容。当您在标量上使用这些方法时,sort 方法可以看到它们是标量而不是对象。

    这都是我的猜测,但对我来说是有道理的。如果有人可以纠正我,请做!

    【讨论】:

    • 我不介意投反对票...如果答案没有用,那就没有用。请让我知道为什么它没有用,以便我可以增加自己的理解。谢谢! :)
    • 您的回答是您自己承认的纯粹猜想,我已经在问题中讨论过。这是猜测,无助于我对这个主题的理解。我也想学习,但发布任何想到的内容都无济于事。
    • 太棒了,谢谢@MadPhysicist,很抱歉没有帮助我回答。
    猜你喜欢
    • 2018-05-12
    • 1970-01-01
    • 1970-01-01
    • 2019-05-26
    • 1970-01-01
    • 2014-06-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多