有效地将不均匀的列表列表转换为用 nan 填充的最小包含数组

Posted

技术标签:

【中文标题】有效地将不均匀的列表列表转换为用 nan 填充的最小包含数组【英文标题】:efficiently convert uneven list of lists to minimal containing array padded with nan 【发布时间】:2017-03-26 22:30:30 【问题描述】:

考虑列表l

l = [[1, 2, 3], [1, 2]]

如果我将其转换为np.array,我将得到一个一维对象数组,[1, 2, 3] 在第一个位置,[1, 2] 在第二个位置。

print(np.array(l))

[[1, 2, 3] [1, 2]]

我想要这个

print(np.array([[1, 2, 3], [1, 2, np.nan]]))

[[  1.   2.   3.]
 [  1.   2.  nan]]

我可以用循环来做到这一点,但我们都知道循环有多不受欢迎

def box_pir(l):
    lengths = [i for i in map(len, l)]
    shape = (len(l), max(lengths))
    a = np.full(shape, np.nan)
    for i, r in enumerate(l):
        a[i, :lengths[i]] = r
    return a

print(box_pir(l))

[[  1.   2.   3.]
 [  1.   2.  nan]]

我如何以快速、矢量化的方式做到这一点?


时机

设置函数

%%cython
import numpy as np

def box_pir_cython(l):
    lengths = [len(item) for item in l]
    shape = (len(l), max(lengths))
    a = np.full(shape, np.nan)
    for i, r in enumerate(l):
        a[i, :lengths[i]] = r
    return a

def box_divikar(v):
    lens = np.array([len(item) for item in v])
    mask = lens[:,None] > np.arange(lens.max())
    out = np.full(mask.shape, np.nan)
    out[mask] = np.concatenate(v)
    return out

def box_hpaulj(LoL):
    return np.array(list(zip_longest(*LoL, fillvalue=np.nan))).T

def box_simon(LoL):
    max_len = len(max(LoL, key=len))
    return np.array([x + [np.nan]*(max_len-len(x)) for x in LoL])

def box_dawg(LoL):
    cols=len(max(LoL, key=len))
    rows=len(LoL)
    AoA=np.empty((rows,cols, ))
    AoA.fill(np.nan)
    for idx in range(rows):
        AoA[idx,0:len(LoL[idx])]=LoL[idx]
    return AoA

def box_pir(l):
    lengths = [len(item) for item in l]
    shape = (len(l), max(lengths))
    a = np.full(shape, np.nan)
    for i, r in enumerate(l):
        a[i, :lengths[i]] = r
    return a

def box_pandas(l):
    return pd.DataFrame(l).values

【问题讨论】:

【参考方案1】:

如果这仅适用于 2D 列表,这可能是您的答案:

from numpy import nan

def even(data):
    maxlen = max(len(l) for l in data)
    for l in data:
        l.extend([nan] * (maxlen - len(l)))

如果您不想修改实际列表:

from numpy import nan

def even(data):
    res = data.copy()
    maxlen = max(len(l) for l in res)
    for l in res:
        l.extend([nan] * (maxlen - len(l)))
    return res

【讨论】:

【参考方案2】:

这似乎与this question 很接近,其中填充是zeros 而不是NaNs。那里发布了有趣的方法,以及基于broadcastingboolean-indexingmine。所以,我只需修改我的帖子中的一行来解决这种情况 -

def boolean_indexing(v, fillval=np.nan):
    lens = np.array([len(item) for item in v])
    mask = lens[:,None] > np.arange(lens.max())
    out = np.full(mask.shape,fillval)
    out[mask] = np.concatenate(v)
    return out

示例运行 -

In [32]: l
Out[32]: [[1, 2, 3], [1, 2], [3, 8, 9, 7, 3]]

In [33]: boolean_indexing(l)
Out[33]: 
array([[  1.,   2.,   3.,  nan,  nan],
       [  1.,   2.,  nan,  nan,  nan],
       [  3.,   8.,   9.,   7.,   3.]])

In [34]: boolean_indexing(l,-1)
Out[34]: 
array([[ 1,  2,  3, -1, -1],
       [ 1,  2, -1, -1, -1],
       [ 3,  8,  9,  7,  3]])

我在该问答中发布了所有已发布方法的一些运行时结果,这可能很有用。

【讨论】:

@piRSquared 列表理解在性能上会比map 更好吗? @piRSquared 啊不,没关系。另外,我不知道map 可能存在兼容性问题。所以,我认为这是一个公平的编辑。肯定会看到一些运行时测试会很有趣! @piRSquared 可爱,非常全面的基准测试!正如我所见,所有数据大小都没有明确的赢家。不过比赛很好。 有趣的是,我制作的子列表越广,你的表现就越好。【参考方案3】:

也许是这样的?不了解您的硬件,但意味着 l2 = [list(range(20)), list(range(30))] * 10000 的 100 个循环需要 16 毫秒。

from numpy import nan


def box(l):
    max_lenght = len(max(l, key=len))
    return [x + [nan]*(max_lenght-len(x)) for x in l]

【讨论】:

我真的很喜欢max(l, key=len) 嗯,我真的很喜欢地图(len, l) ^^. 问题是numpy没有扩展生成器,所以不是真正的比较。 @piRSquared 我同意。它看起来比我做的要短得多:max(len(v) for v in l)【参考方案4】:

可能最快的列表版本使用itertools.zip_longest(在Py2中可能是izip_longest):

In [747]: np.array(list(itertools.zip_longest(*ll,fillvalue=np.nan))).T
Out[747]: 
array([[  1.,   2.,   3.],
       [  1.,   2.,  nan]])

普通的zip 产生:

In [748]: list(itertools.zip_longest(*ll))
Out[748]: [(1, 1), (2, 2), (3, None)]

另一个 zip '转置':

In [751]: list(zip(*itertools.zip_longest(*ll)))
Out[751]: [(1, 2, 3), (1, 2, None)]

通常从列表(甚至是列表的对象数组)开始时,坚持使用列表方法会更快。创建数组或数据框需要大量开销。

这不是第一次被问到这个问题。

How can I pad and/or truncate a vector to a specified length using numpy?

我的回答包括这个zip_longest 和你的box_pir

我认为还有一个使用扁平数组的快速 numpy 版本,但我不记得细节了。它可能是 Warren 或 Divakar 给的。

我认为“扁平化”版本的工作原理如下:

In [809]: ll
Out[809]: [[1, 2, 3], [1, 2]]
In [810]: sll=np.hstack(ll)     # all values in a 1d array
In [816]: res=np.empty((2,3)); res.fill(np.nan)  # empty target

获取值所在的扁平索引。这是关键的一步。这里r_的使用是迭代的;快速版本可能使用cumsum

In [817]: idx=np.r_[0:3, 3:3+2]
In [818]: idx
Out[818]: array([0, 1, 2, 3, 4])
In [819]: res.flat[idx]=sll
In [820]: res
Out[820]: 
array([[  1.,   2.,   3.],
       [  1.,   2.,  nan]])

=================

所以缺少的链接是>np.arange()广播

In [897]: lens=np.array([len(i) for i in ll])
In [898]: mask=lens[:,None]>np.arange(lens.max())
In [899]: mask
Out[899]: 
array([[ True,  True,  True],
       [ True,  True, False]], dtype=bool)
In [900]: idx=np.where(mask.ravel())
In [901]: idx
Out[901]: (array([0, 1, 2, 3, 4], dtype=int32),)

【讨论】:

【参考方案5】:

我可以将其写成对每个已填充默认值的子数组进行切片分配的形式:

def to_numpy(LoL, default=np.nan):
    cols=len(max(LoL, key=len))
    rows=len(LoL)
    AoA=np.empty((rows,cols, ))
    AoA.fill(default)
    for idx in range(rows):
        AoA[idx,0:len(LoL[idx])]=LoL[idx]
    return AoA

我在 Divakar 的 Boolean Indexing 中添加了 f4 并添加到计时测试中。至少在我的测试中,(Python 2.7 和 Python 3.5;Numpy 1.11)它不是最快的。

时间表明izip_longestf2 对于大多数列表来说稍微快一些,但对于较大的列表来说,切片分配(即f1)更快:

from __future__ import print_function
import numpy as np
try:
    from itertools import izip_longest as zip_longest 
except ImportError:
    from itertools import zip_longest   

def f1(LoL):
    cols=len(max(LoL, key=len))
    rows=len(LoL)
    AoA=np.empty((rows,cols, ))
    AoA.fill(np.nan)
    for idx in range(rows):
        AoA[idx,0:len(LoL[idx])]=LoL[idx]
    return AoA

def f2(LoL):
    return np.array(list(zip_longest(*LoL,fillvalue=np.nan))).T

def f3(LoL):
    max_len = len(max(LoL, key=len))
    return np.array([x + [np.nan]*(max_len-len(x)) for x in LoL])

def f4(LoL):
    lens = np.array([len(item) for item in LoL])
    mask = lens[:,None] > np.arange(lens.max())
    out = np.full(mask.shape,np.nan)
    out[mask] = np.concatenate(LoL)
    return out  

if __name__=='__main__':
    import timeit   
    for case, LoL in (('small', [list(range(20)), list(range(30))] * 1000),
                      ('medium', [list(range(20)), list(range(30))] * 10000),
                      ('big', [list(range(20)), list(range(30))] * 100000),
                      ('huge', [list(range(20)), list(range(30))] * 1000000)):
        print(case)
        for f in (f1, f2, f3, f4):
            print("   ",f.__name__, timeit.timeit("f(LoL)", setup="from __main__ import f, LoL", number=100) )      

打印:

small
    f1 0.245459079742
    f2 0.209980010986
    f3 0.350691080093
    f4 0.332141160965
medium
    f1 2.45869493484
    f2 2.32307982445
    f3 3.65722203255
    f4 3.55545687675
big
    f1 25.8796288967
    f2 26.6177148819
    f3 41.6916451454
    f4 41.3140149117
huge
    f1 262.429639101
    f2 295.129109859
    f3 427.606887817
    f4 441.810388088

【讨论】:

以上是关于有效地将不均匀的列表列表转换为用 nan 填充的最小包含数组的主要内容,如果未能解决你的问题,请参考以下文章

填充 UITableView 的最有效方法

如何在熊猫中用空列表[]填充数据框Nan值?

前向填充位数组的最有效方法

data.frame 方法的最有效列表?

无法在pandas中通过lambda填充多列中的NaN值

如何有效地将具有一定周期性的列表拆分为多个列表?