将3D列表转换为3D NumPy数组

5
目前,我有一个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数组,或者在这种情况下循环遍历数组并添加所需的内容是更好的解决方案?

1
当预期的操作是计算密集型且需要按照某种规律进行时,将其转换为NumPy数组是有意义的。因此,仅仅添加一个标量4可能不值得麻烦。 - Divakar
这种不规则的特性是某个过程的结果吗?如果您的列表元素[0]可以扩展以包括无数据值,例如[0,-1,-1],则会产生更统一的列表结构。然后,这可以轻松转换为numpy掩码数组。掩码数组的无数据值将设置为-1,然后所有后续计算都将排除这些单元格位置。如果更容易“修复”列表而不是玩弄补丁数据结构以满足数组要求,则可能会发现此功能很有用。 - NaN
5个回答

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 的目的,这个方案非常有效。我的解决方案是对你的升级,我通过一系列相关的答案进行了迭代式的改进。 - Mad Physicist
@MadPhysicist:没有看到你的编辑;你的解决方案确实比我的更通用(现在给它点赞了),而我的解决方案可能更容易理解,如果一个人没有太多的Python/编程背景的话。 - Cleb

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,并仅使用您关心的元素。在您的情况下,这似乎不是非常方便。
另一种方法是直接对嵌套数组应用函数。幸运的是,我在回答this question时写了一个snippet/recipe,它完全满足您的需求,包括支持任意层次的嵌套和您选择的运算符。我在此处升级它,以接受列表中任何位置的非可迭代嵌套元素(包括原始输入),并进行基本形式的广播。
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 的答案。 - Ziyad Edher
@Wintro。我认为你做得很对。这更像是一段我一直在不断改进的片段,用于类似问题的解决。 - Mad Physicist

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

1
如果我们将您的列表转换为数组,我们将得到一个包含对象的2D数组。
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)

在对象数组上执行数学运算有时成功,有时失败。一些操作确实会传播到元素,但这取决于元素的行为。

我认为这并不适用于原帖的实际问题。 - Mad Physicist
1
我正在将“来自Python嵌套列表的数据转换为NumPy数组...同时保留其结构(因为稍后需要将其转换回去)”。由于数组开销较大,因此仍然使用纯列表可能会更快,但如果您确实想要使用数组,请按照这种方式进行操作。 - hpaulj

1
很不幸,输入结构是一个不规则列表。如果可以通过指定无数据值来调整生成列表的方法,则可以做更多的事情。我在最初的帖子中发表了这个评论,但我将演示如何改变原始设计以便在返回列表的同时获取更多的数据。
我将其作为函数完成,以便能够对输入和输出进行注释以供参考。
def num_46():
    """(num_46)... Masked array from ill-formed list
    :  https://dev59.com/r5zga4cB1Zd3GeqP-QGu
    :  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”检查将严重影响性能。 - Ziyad Edher
@Wintro 我更好奇单例列表是如何在第一次出现的。如果它们是在构建过程中添加的,那么掩码数组不会检查它们。然而,处理列表时则是另一回事。如果你打算使用数组而不是列表进行分析,这将更有用。无论如何,将其转换为数组会增加开销,必须权衡是否仅以列表形式进行处理。 - NaN

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