【问题标题】:Converting a 3D List to a 3D NumPy array将 3D 列表转换为 3D NumPy 数组
【发布时间】:2017-03-10 10:51:57
【问题描述】:

目前,我有一个锯齿状数组格式的 3D Python 列表。
A = [[[0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0], [0], [0]]]

有什么方法可以将此列表转换为 NumPy 数组,以便使用某些 NumPy 数组运算符,例如为每个元素添加一个数字。
A + 4 将给出[[[4, 4, 4], [4, 4, 4], [4, 4, 4]], [[4], [4], [4]]]

分配B = numpy.array(A) 然后尝试B + 4 会引发类型错误。
TypeError: can only concatenate list (not "float") to list

是否可以在保留结构的同时将锯齿状 Python 列表转换为 NumPy 数组(我稍后需要将其转换回来),或者在这种情况下循环遍历数组并添加所需的更好解决方案?

【问题讨论】:

  • 当预期的操作计算量很大并且要以某种常规模式执行时,转换为 NumPy 数组是有意义的。所以,仅仅添加一个标量 4 可能不值得麻烦。
  • 锯齿状的本质是某种过程的结果吗?如果您的 [0] 列表元素可以扩展为包含 nodata 值,例如 [0, -1, -1] 导致更统一的列表结构,我将很有用。然后可以轻松地将其转换为 numpy 掩码数组。掩码数组的 nodata 值将设置为 -1,然后所有后续计算将排除这些单元格位置。如果“修复”列表比修补该数据结构以满足数组要求更容易,您可能会发现这很有用。

标签: python list python-3.x numpy


【解决方案1】:

循环和添加可能会更好,因为您希望保留原始结构。另外,您提到的错误表明您需要展平 numpy 数组,然后添加到每个元素。尽管 numpy 操作往往比列表操作快,但转换、展平和还原很麻烦,并且可能会抵消任何收益。

【讨论】:

    【解决方案2】:

    由于 numpy 只能处理规则形状的数组,它会检查嵌套迭代的所有元素对于给定维度的长度是否相同。如果不是,它仍然会创建一个数组,但类型为 np.object 而不是您所期望的 np.int

    >>> B = np.array(A)
    >>> B
    array([[[0, 0, 0], [0, 0, 0], [0, 0, 0]],
           [[0], [0], [0]]], dtype=object)
    

    在这种情况下,“对象”是列表。加法是为列表定义的,但仅限于扩展原始列表的其他列表,因此您的错误。 [0, 0] + 4 是一个错误,而 [0, 0] + [4][0, 0, 4]。两者都不是你想要的。

    有趣的是,numpy 会使数组嵌套的对象部分尽可能低。您创建的数组实际上是一个包含列表的 2D numpy 数组,而不是包含嵌套列表的 1D 数组:

    >>> B[0, 0]
    [0, 0, 0]
    >>> B[0, 0, 0]
    Traceback (most recent call last):
    
      File "<ipython-input-438-464a9bfa40bf>", line 1, in <module>
        B[0, 0, 0]
    
    IndexError: too many indices for array
    

    正如您所指出的,当涉及到参差不齐的数组时,您有两种选择。第一种是填充数组,使其不参差不齐,将其转换为 numpy,并仅使用您关心的元素。在您的情况下,这似乎不太方便。

    另一种方法是将函数直接应用于嵌套数组。幸运的是,我写了一个snippet/recipe 来响应this question,它完全满足您的需求,能够支持任意级别的嵌套和您选择的运算符。我在这里升级它以接受列表中任何地方的不可迭代嵌套元素,包括原始输入并执行原始形式的广播:

    from itertools import repeat
    
    def elementwiseApply(op, *iters):
        def isIterable(x):
            """
            This function is also defined in numpy as `numpy.iterable`.
            """
            try:
                iter(x)
            except TypeError:
                return False
            return True
    
        def apply(op, *items):
            """
            Applies the operator to the given arguments. If any of the
            arguments are iterable, the non-iterables are broadcast by
            `itertools.repeat` and the function is applied recursively
            on each element of the zipped result.
            """
            elements = []
            count = 0
            for iter in items:
                if isIterable(iter):
                    elements.append(iter)
                    count += 1
                else:
                    elements.append(itertools.repeat(iter))
            if count == 0:
                return op(*items)
            return [apply(op, *items) for items in zip(*elements)]
    
        return apply(op, *iters)
    

    这是一个非常通用的解决方案,几乎适用于任何类型的输入。以下是几个示例运行,显示了它与您的问题的相关性:

    >>> from operator import add
    >>> elementwiseApply(add, 4, 4)
    8
    >>> elementwiseApply(add, [4, 0], 4)
    [8, 4]
    >>> elementwiseApply(add, [(4,), [0, (1, 3, [1, 1, 1])]], 4)
    [[8], [4, [5, 7, [5, 5, 5]]]]
    >>> elementwiseApply(add, [[0, 0, 0], [0, 0], 0], [[4, 4, 4], [4, 4], 4])
    [[4, 4, 4], [4, 4], 4]
    >>> elementwiseApply(add, [(4,), [0, (1, 3, [1, 1, 1])]], [1, 1, 1])
    [[5], [1, [2, 4, [2, 2, 2]]]]
    

    结果始终是一个新列表或标量,具体取决于输入的类型。输入的数量必须是操作员接受的数量。例如,operator.add 总是需要两个输入。

    【讨论】:

    • 我真的很佩服这个解决方案的通用性,它在我所要求的一般领域提供了各种各样的功能;但是,对于我的担忧来说,它有点宽泛了,我相信寻找与我类似的问题的答案的人会对直接解决方案比更广泛的解决方案更感兴趣。出于这些原因,我选择了@Cleb 的答案。
    • @Wintro。我认为你做对了。这更像是一个 sn-p,我一直在为许多类似的问题迭代改进。
    【解决方案3】:

    如果我们把你的列表变成一个数组,我们会得到一个二维的对象数组

    In [1941]: A = [[[0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0], [0], [0]]]
    In [1942]: A = np.array(A)
    In [1943]: A.shape
    Out[1943]: (2, 3)
    In [1944]: A
    Out[1944]: 
    array([[[0, 0, 0], [0, 0, 0], [0, 0, 0]],
           [[0], [0], [0]]], dtype=object)
    

    当我尝试A+1 时,它会遍历A 的元素并尝试为每个元素执行+1。在数值数组的情况下,它可以在快速编译的代码中做到这一点。对于对象数组,它必须为每个元素调用+ 操作。

    In [1945]: A+1
    ...
    TypeError: can only concatenate list (not "int") to list
    

    让我们在A 上进行平面迭代再试一次:

    In [1946]: for a in A.flat:
          ...:     print(a+1)
    ....
    TypeError: can only concatenate list (not "int") to list
    

    A 的元素是列表; + 表示列表是一个连接:

    In [1947]: for a in A.flat:
          ...:     print(a+[1])
          ...:     
    [0, 0, 0, 1]
    [0, 0, 0, 1]
    [0, 0, 0, 1]
    [0, 1]
    [0, 1]
    [0, 1]
    

    如果A 的元素本身是数组,我认为+1 可以工作。

    In [1956]: for i, a in np.ndenumerate(A):
          ...:     A[i]=np.array(a)
          ...:     
    In [1957]: A
    Out[1957]: 
    array([[array([0, 0, 0]), array([0, 0, 0]), array([0, 0, 0])],
           [array([0]), array([0]), array([0])]], dtype=object)
    In [1958]: A+1
    Out[1958]: 
    array([[array([1, 1, 1]), array([1, 1, 1]), array([1, 1, 1])],
           [array([1]), array([1]), array([1])]], dtype=object)
    

    为了回到纯列表形式,我们将tolist 应用于对象数组的元素和数组本身:

    In [1960]: A1=A+1
    In [1961]: for i, a in np.ndenumerate(A1):
          ...:     A1[i]=a.tolist()
    
    In [1962]: A1
    Out[1962]: 
    array([[[1, 1, 1], [1, 1, 1], [1, 1, 1]],
           [[1], [1], [1]]], dtype=object)
    In [1963]: A1.tolist()
    Out[1963]: [[[1, 1, 1], [1, 1, 1], [1, 1, 1]], [[1], [1], [1]]]
    

    这是向嵌套列表的所有元素添加值的一种相当圆的方法。我可以通过一次迭代来做到这一点:

    In [1964]: for i,a in np.ndenumerate(A):
          ...:     A[i]=[x+1 for x in a]
          ...:     
    In [1965]: A
    Out[1965]: 
    array([[[1, 1, 1], [1, 1, 1], [1, 1, 1]],
           [[1], [1], [1]]], dtype=object)
    

    所以对对象数组进行数学运算是偶然的。某些操作确实会传播到元素,但即使是这些操作也取决于元素的行为方式。

    【讨论】:

    • 我不认为这真的适用于 OP 的实际问题。
    • 我正在转换from a jagged Python list to a NumPy array ... while retaining the structure (I will need to convert it back later) 。由于数组开销,使用纯列表可能会更快,但如果您确实想要使用数组路线,这是一种/方法。
    【解决方案4】:

    @SonderingNarcissit 和 @MadPhysicist 的回答已经很不错了。

    这是为列表中的每个元素添加一个数字并保持结构的快速方法。你可以用任何你喜欢的函数替换return_number,如果你不仅想添加一个数字,还想用它做其他事情:

    def return_number(my_number):
        return my_number + 4    
    
    def add_number(my_list):
    
        if isinstance(my_list, (int, float)):
            return return_number(my_list)
        else:
            return [add_number(xi) for xi in my_list]
    
    A = [[[0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0], [0], [0]]]
    

    然后

    print(add_number(A))
    

    为您提供所需的输出:

    [[[4, 4, 4], [4, 4, 4], [4, 4, 4]], [[4], [4], [4]]]
    

    所以它的作用是递归地查看您的列表列表,每次找到一个数字时,它都会添加值 4;这应该适用于任意深度的嵌套列表。目前仅适用于数字和列表;如果你也有例如如果列表中还有字典,则必须添加另一个 if 子句。

    【讨论】:

    • 对于 OP 的目的,这非常有效。我的解决方案是对您的解决方案的相当全面的升级,我一直在通过一系列相关答案进行迭代。
    • @MadPhysicist:没有看到您的编辑;您的解决方案确实比我的解决方案更通用(现在赞成),而如果一个人没有太多 Python/编程背景,我的解决方案可能更容易理解。
    • 同意。对于这个问题,我会赞成我的,但肯定会选择你的。
    【解决方案5】:

    不幸的是,输入结构是一个锯齿状列表。如果可以通过不分配数据值来调整用于生成列表的方法,那么可以做的事情就更多了。我在最初的帖子中发表了此评论,但我将演示如何更改原件的设计,以便在启用列表返回的同时获取更多数据。

    我已将其作为一个函数完成,因此我可以评论输入和输出以供进一步参考。

    def num_46():
        """(num_46)... Masked array from ill-formed list
        :  http://stackoverflow.com/questions/40289943/
        :  converting-a-3d-list-to-a-3d-numpy-array
        :  A =[[[0, 0, 0], [0, 0, 0], [0, 0, 0]], 
        :      [[0, 0, 0], [0, 0, 0], [0, 0, 0]], [[0], [0], [0]]]
        """
        frmt = """
        :Input list...
        {}\n
        :Masked array data
        {}\n
        :A sample calculations:
        :  a.count(axis=0) ... a.count(axis=1) ... a.count(axis=2)
        {}\n
        {}\n
        {}\n
        : and finally:  a * 2
        {}\n
        :Return it to a list...
        {}
        """
        a_list = [[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
                  [[9, 10, 11], [12, 13, 14], [15, 16, 17]],
                  [[18, -1, -1], [21, -1, -1], [24, -1, -1]]]
        mask_val = -1
        a = np.ma.masked_equal(a_list, mask_val)
        a.set_fill_value(mask_val)
        final = a.tolist(mask_val)
        args = [a_list, a,
                a.count(axis=0), a.count(axis=1), a.count(axis=2),
                a*2, final]
        print(dedent(frmt).format(*args))
        return a_list, a, final
    
    
    #----------------------
    if __name__ == "__main__":
        """Main section...   """
        A, a, c = num_46()
    

    一些结果表明使用掩码数组可能比锯齿状/格式错误的列表结构更可取。

    :Input list...
    [[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
     [[9, 10, 11], [12, 13, 14], [15, 16, 17]],
     [[18, -1, -1], [21, -1, -1], [24, -1, -1]]]
    
    :Masked array data
    [[[0 1 2]
      [3 4 5]
      [6 7 8]]
    
     [[9 10 11]
      [12 13 14]
      [15 16 17]]
    
     [[18 - -]
      [21 - -]
      [24 - -]]]
    
    :A sample calculations:
    :  a.count(axis=0) ... a.count(axis=1) ... a.count(axis=2)
    [[3 2 2]
     [3 2 2]
     [3 2 2]]
    
    [[3 3 3]
     [3 3 3]
     [3 0 0]]
    
    [[3 3 3]
     [3 3 3]
     [1 1 1]]
    
    : and finally:  a * 2
    [[[0 2 4]
      [6 8 10]
      [12 14 16]]
    
     [[18 20 22]
      [24 26 28]
      [30 32 34]]
    
     [[36 - -]
      [42 - -]
      [48 - -]]]
    
    :Return it to a list...
    [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], [12, 13, 14], [15, 16, 17]], [[18, -1, -1], [21, -1, -1], [24, -1, -1]]]
    

    希望这对某人有所帮助。

    【讨论】:

    • 用一些None 值掩盖我的数据可能是个好主意,但后来我在列表中多次递归运行,并且列表远大于此处显示的。所以我认为None-检查每个值和列表中的附加值会严重影响性能。
    • @Wintro 我很好奇列表单例是如何到达的。我在构建过程中添加了它们,然后掩码数组不检查它们。但是,处理列表就是这种情况。如果您打算使用数组而不是列表进行分析,这将更加有用。在任何情况下,转换为数组都有其开销,必须权衡仅以列表形式进行处理。
    猜你喜欢
    • 1970-01-01
    • 2011-05-19
    • 2021-12-29
    • 1970-01-01
    • 2019-10-31
    • 1970-01-01
    • 1970-01-01
    • 2017-07-26
    • 2015-12-26
    相关资源
    最近更新 更多