将3D numpy数组转换为2D numpy数组(其中内容是元组)

8
我有一个3D numpy数组np.random.rand(6602, 3176, 2),我想将它转换为一个2D数组(numpypandas.DataFrame),其中每个值都是一个元组,使得形状为(6602, 3176)
这个问题帮助我看到如何减少维数,但我仍然在元组方面有困难。

5
我认为我有一个更好的问题:为什么你想要那个?严格来说,你所询问的需要使用NumPy类型为“object”的数组,但这对于你似乎正在处理的问题并不是一个好的应用场景。也许你遇到了XY问题?然而,对于大多数实际应用,坚持使用3D数组,并想出一种巧妙地使用NumPy函数的axis参数的方法可能是更好的选择。 - norok2
@norok2 你说得很有道理。也许我应该重新考虑一个更优雅的解决方案。谢谢,这个链接很有趣。 - Newskooler
其中每个值都是一个元组,形状为(6602,3176)。您能否重新表述这个语句。 - moe asal
4个回答

9
这是一行代码,对于完整的(6602,3176,2)问题需要几秒钟时间。
a = np.random.rand(6602, 3176, 2)

b = a.view([(f'f{i}',a.dtype) for i in range(a.shape[-1])])[...,0].astype('O')

这里的技巧是将视图转换为跨越一行的复合数据类型。当这样的复合数据类型转换为对象时,每个复合元素都会被转换为元组。 更新(感谢@hpaulj):有一个库函数可以精确地执行我们手动执行的视图转换:numpy.lib.recfunctions.unstructured_to_structured 使用此函数,我们可以编写上述更易读的版本:
import numpy.lib.recfunctions as nlr

b = nlr.unstructured_to_structured(a).astype('O')

非常优雅!一个问题:为什么要用 f'f{i}' 而不是只用 str(i) - norok2
1
@norok2 习惯使然,我猜。 - Paul Panzer
3
numpy.lib.recfunctions.unstructured_to_structured 是将数组转换为结构化数据类型的新推荐工具。在这种情况下,它只是消除了需要使用 [...,0] 步骤的必要性。只需使用 unstructured_to_structured(a) 即可。 - hpaulj

2

如果你真的想做你想做的事情,你必须将数组的dtype设置为object。例如,如果你有上述数组:

a = np.random.rand(6602, 3176, 2)

你可以创建一个形状为(6602, 3176)的第二个空数组,并将dtype设置为object:
b = np.empty(a[:,:,0].shape, dtype=object)

并用元组填充您的数组。

但最终没有什么大的优势!我只是使用切片从您的初始数组a中获取元组。您可以访问索引n(第一维)和m(第二维)的元组,忽略第三维,并对您的3d数组进行切片:

a[n,m,:]

0

如果您希望使用 list 而不是 tuple,可以使用以下技巧:

  1. 使用 .tolist() 将数组转换为 listlist
  2. 确保更改其中一个最内层的list 的大小(misalign)
  3. listlist 再次转换为 NumPy 数组
  4. 修复第2点修改。

该函数在以下函数中实现:last_dim_as_list():

import numpy as np


def last_dim_as_list(arr):
    if arr.ndim > 1:
        # : convert to list of lists
        arr_list = arr.tolist()
        # : misalign size of the first innermost list
        temp = arr_list
        for _ in range(arr.ndim - 1):
            temp = temp[0]
        temp.append(None)
        # : convert to NumPy array
        # (uses `object` because of the misalignment)
        result = np.array(arr_list)
        # : revert the misalignment
        temp.pop()
    else:
        result = np.empty(1, dtype=object)
        result[0] = arr.tolist()
    return result

np.random.seed(0)
in_arr = np.random.randint(0, 9, (2, 3, 2))
out_arr = last_dim_as_list(in_arr)


print(in_arr)
# [[[5 0]
#   [3 3]
#   [7 3]]
#  [[5 2]
#   [4 7]
#   [6 8]]]
print(in_arr.shape)
# (2, 3, 2)
print(in_arr.dtype)
# int64

print(out_arr)
# [[list([5, 0]) list([3, 3]) list([7, 3])]
#  [list([5, 2]) list([4, 7]) list([6, 8])]]
print(out_arr.shape)
# (2, 3)
print(out_arr.dtype)
# object

然而,我不建议采用这种方法,除非你真的知道自己在做什么。 大多数情况下,最好将所有内容保留为更高维度的NumPy数组,并充分利用NumPy索引


请注意,这也可以使用明确的循环来完成,但是所提出的方法对于足够大的输入应该会更快:
def last_dim_as_list_loop(arr):
    shape = arr.shape
    result = np.empty(arr.shape[:-1], dtype=object).ravel()
    for k in range(arr.shape[-1]):
        for i in range(result.size):
            if k == 0:
                result[i] = []
            result[i].append(arr[..., k].ravel()[i])
    return result.reshape(shape[:-1])


out_arr2 = last_dim_as_list_loop(in_arr)

print(out_arr2)
# [[list([5, 0]) list([3, 3]) list([7, 3])]
#  [list([5, 2]) list([4, 7]) list([6, 8])]]
print(out_arr2.shape)
# (2, 3)
print(out_arr2.dtype)
# object

但是这个最后的时间并不是特别惊人:

%timeit last_dim_as_list(in_arr)
# 2.53 µs ± 37.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit last_dim_as_list_loop(in_arr)
# 12.2 µs ± 21.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

@PaulPanzer 提出的基于 view 的方法 proposed 非常优雅,比 last_dim_as_list() 中提出的技巧更高效,因为它只需要循环(内部)一次数组,而不是两次:

def last_dim_as_tuple(arr):
    dtype = [(str(i), arr.dtype) for i in range(arr.shape[-1])]
    return arr.view(dtype)[..., 0].astype(object)

因此,在足够大的输入上,时间更有利:

in_arr = np.random.random((6602, 3176, 2))


%timeit last_dim_as_list(in_arr)
# 4.9 s ± 73.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit last_dim_as_tuple(in_arr)
# 3.07 s ± 117 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

0
一种向量化的方法(有点棘手):
mat = np.random.rand(6602, 3176, 2)

f = np.vectorize(lambda x:tuple(*x.items()), otypes=[np.ndarray])
mat2 = np.apply_along_axis(lambda x:dict([tuple(x)]), 2, mat)
mat2 = np.vstack(f(mat2))

mat2.shape
Out: (6602, 3176)

type(mat2[0,0])
Out: tuple

这似乎相当低效。您能否解释一下执行步骤背后的思路? - norok2
@norok2,是的确如此。它在第三个轴中创建字典代理以减少其作为元组的大小。我这样做是因为仅使用np.apply_along_axis(lambda x: tuple(x), 2, mat)将其转换为元组会忽略元组类型并返回输入数组的副本。 - dtrckd

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