【问题标题】:Extend numpy mask by n cells to the right for each bad value, efficiently有效地将每个坏值的 numpy 掩码向右扩展 n 个单元格
【发布时间】:2015-12-18 19:08:16
【问题描述】:

假设我有一个长度为 30 的数组,其中包含 4 个错误值。我想为那些坏值创建一个掩码,但由于我将使用滚动窗口函数,我还希望在每个坏值之后将固定数量的后续索引标记为坏值。在下面,n = 3:

我希望尽可能高效地执行此操作,因为此例程将在包含数十亿数据点的大型数据系列上运行多次。因此,我需要尽可能接近 numpy 矢量化解决方案,因为我想避免 python 循环。

为避免重新输入,这里是数组:

import numpy as np
a = np.array([4, 0, 8, 5, 10, 9, np.nan, 1, 4, 9, 9, np.nan, np.nan, 9,\
              9, 8, 0, 3, 7, 9, 2, 6, 7, 2, 9, 4, 1, 1, np.nan, 10])

【问题讨论】:

  • 有什么你自己尝试过的吗?
  • 你有没有想过 Python 和 numpy 允许的所有花哨的索引形式?
  • 自从他绘制图形并有一些带有小示例数组的代码时,他显然已经考虑了一些......
  • @ Evert:是的,很明显我可以遍历每个错误的索引值并将 [1, 2, 3] 添加到每个索引值,获取所有这些(即唯一值)的集合并将其用作屏蔽不良价值指数。但这需要一个循环。如果我有 1m 数据点和 100k 错误值,您可以看到我的循环将如何花费大量时间,这是在机器学习环境中,因此该例程会有很多重复。很高兴听到更多关于花哨的索引可能性的信息。猜猜我可以使用 pandas rolling apply 但这仍然是一个循环(尽管因为用 C 实现可能很快)。任何纯粹的矢量?
  • 对于不规则的间隔,你真的不能让它成为“纯矢量”(即矩形)。最好的办法是使用编译循环,例如@askewchan 的reduceat。

标签: python numpy bigdata


【解决方案1】:

晚了几年,但我提出了一个完全矢量化的解决方案,不需要循环或复制(除了掩码本身)。这个解决方案有点(可能)危险,因为它使用numpy.lib.stride_tricks.as_strided。它也没有@swentzel's solution那么快。

我们的想法是获取蒙版并创建它的 2D 视图,其中第二维只是当前元素之后的元素。如果头部是True,那么您可以将一整列设置为True。由于你是在处理一个视图,所以设置一个列实际上会在掩码中设置以下元素。

从数据开始:

import numpy as np
a = np.array([4, 0, 8, 5, 10, 9, np.nan, 1, 4, 9, 9, np.nan, np.nan, 9,\
              9, 8, 0, 3, 7, 9, 2, 6, 7, 2, 9, 4, 1, 1, np.nan, 10])
n = 3

现在,我们将屏蔽a.size + n 元素加长,这样您就不必手动处理最后的n 元素:

mask = np.empty(a.size + n, dtype=np.bool)
np.isnan(a, out=mask[:a.size])
mask[a.size:] = False

现在是最酷的部分:

view = np.lib.stride_tricks.as_strided(mask, shape=(n + 1, a.size),
                                       strides=mask.strides * 2)

最后一部分至关重要。 mask.strides 是一个类似于 (1,) 的元组(因为 bools 通常大约有那么多字节。加倍意味着您需要 1 个字节的步骤来移动 any 维度中的一个元素。

现在您需要做的就是展开掩码:

view[1:, view[0]] = True

就是这样。现在mask 有你想要的。请记住,这只有效,因为分配索引在最后更改的值之前。 view[1:] |= view[0] 是你无法逃脱的。

出于测试目的,n 的定义似乎与问题有所不同,因此以下函数将其考虑在内:

def madphysicist0(a, n):
    m = np.empty(a.size + n - 1, dtype=np.bool)
    np.isnan(a, out=m[:a.size])
    m[a.size:] = False

    v = np.lib.stride_tricks.as_strided(m, shape=(n, a.size), strides=m.strides * 2)
    v[1:, v[0]] = True
    return v[0]

V2

从现有最快的答案中取出一片叶子,我们只需要复制log<sub>2</sub>(n) 行,而不是n 行:

def madphysicist1(a, n):
    m = np.empty(a.size + n - 1, dtype=np.bool)
    np.isnan(a, out=m[:a.size])
    m[a.size:] = False

    v = np.lib.stride_tricks.as_strided(m, shape=(n, a.size), strides=m.strides * 2)

    stop = int(np.log2(n))
    for k in range(1, stop + 1):
        v[k, v[0]] = True
    if (1<<k) < n:
        v[-1, v[(1<<k) - 1]] = True
    return v[0]

由于每次迭代都会使掩码的大小翻倍,因此它的工作速度比斐波那契要快一点:https://math.stackexchange.com/q/894743/295281

【讨论】:

  • 你的 madphysicist1 没有通过 allagree() 测试。我将只测试第一个例程 madphysicist0。您可能需要检查并编辑它。
  • 我对您进行了基准测试,您的速度很快,但不如 swenzel1 或 moarningstar 快。也许您的 madphysicist1 程序会更快?但正如我所说,它不能正确输出。一旦它发生,我会重新安排。以下是包含您在内的结果图表的链接:dropbox.com/s/rntk0wpieobwz9v/madphysicist0_included.png?dl=0
  • 您似乎在 FALSE 的运行中添加了太多的 FALSE,使用 madphysicist1(a, n)。
  • @ThomasBrowne。我去看看。
【解决方案2】:

这里是基准测试结果。我已经包含了我自己的(“op”),我开始使用它,它循环坏索引并向它们添加 1...n,然后使用唯一值来查找掩码索引。您可以在下面的代码中看到它以及所有其他响应。

无论如何,这是结果。刻面是沿 x(10 到 10e7)的数组大小和沿 y(5、50、500、5000)的窗口大小。然后是每个方面的编码人员,得分为 log-10,因为我们谈论的是微秒到几分钟。

@swenzel 似乎是他的第二个答案的赢家,取代了@moarningsun 的第一个答案(moarningsun 的第二个答案是通过大量内存使用使机器崩溃,但这可能是因为它不是为大 n 或非稀疏 a 设计的) .

由于(必要的)对数标度,该图表无法准确反映这些贡献中最快的一个。它们甚至比体面的循环解决方案快几十倍、几百倍。在最大的情况下,swenzel1 比 op 快 1000 倍,并且 op 已经在使用 numpy。

请注意,我使用了针对优化的英特尔 MKL 库编译的 numpy 版本,该库充分利用了自 2012 年以来出现的 AVX 指令。在某些矢量用例中,这会将 i7/Xeon 速度提高 5 倍。其中一些贡献可能比其他贡献受益更多。

这是到目前为止运行所有提交答案的完整代码,包括我自己的。函数 allagree() 确保结果正确,而 timeall() 将为您提供一个长格式的 pandas Dataframe,所有结果都在几秒钟内。

您可以使用新代码轻松地重新运行它,或者更改我的假设。请记住,我没有考虑其他因素,例如内存使用情况。另外,我使用 R ggplot2 来制作图形,因为我对 seaborn/matplotlib 的了解不够好,无法让它做我想做的事。

为完整起见,所有结果一致:

In [4]: allagree(n = 7, asize = 777)
Out[4]:
             AGML0 AGML1 askewchan0 askewchan1 askewchan2 moarningsun0  \
AGML0         True  True       True       True       True         True
AGML1         True  True       True       True       True         True
askewchan0    True  True       True       True       True         True
askewchan1    True  True       True       True       True         True
askewchan2    True  True       True       True       True         True
moarningsun0  True  True       True       True       True         True
swenzel0      True  True       True       True       True         True
swenzel1      True  True       True       True       True         True
op            True  True       True       True       True         True

             swenzel0 swenzel1    op
AGML0            True     True  True
AGML1            True     True  True
askewchan0       True     True  True
askewchan1       True     True  True
askewchan2       True     True  True
moarningsun0     True     True  True
swenzel0         True     True  True
swenzel1         True     True  True
op               True     True  True

感谢所有提交的人!

在 R 中使用 pd.to_csv 和 read.csv 导出 timeall() 的输出后的图形代码:

ww <- read.csv("ww.csv")    
ggplot(ww, aes(x=coder, y=value, col = coder)) + geom_point(size = 3) + scale_y_continuous(trans="log10")+ facet_grid(nsize ~ asize) + theme(axis.text.x = element_text(angle = 90, hjust = 1)) + ggtitle("Fastest by coder") + ylab("time (seconds)")

测试代码:

# test Stack Overflow 32706135 nan shift routines

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from timeit import Timer
from scipy import ndimage
from skimage import morphology
import itertools
import pdb
np.random.seed(8472)


def AGML0(a, n):                               # loop itertools
    maskleft = np.where(np.isnan(a))[0]
    maskright = maskleft + n
    mask = np.zeros(len(a),dtype=bool)
    for l,r in itertools.izip(maskleft,maskright): 
        mask[l:r] = True
    return mask


def AGML1(a, n):                               # loop n
    nn = n - 1
    maskleft = np.where(np.isnan(a))[0]
    ghost_mask = np.zeros(len(a)+nn,dtype=bool)
    for i in range(0, nn+1):
        thismask = maskleft + i
        ghost_mask[thismask] = True
    mask = ghost_mask[:len(ghost_mask)-nn]
    return mask


def askewchan0(a, n):
    m = np.isnan(a)
    i = np.arange(1, len(m)+1)
    ind = np.column_stack([i-n, i]) # may be a faster way to generate this
    ind.clip(0, len(m)-1, out=ind)
    return np.bitwise_or.reduceat(m, ind.ravel())[::2]


def askewchan1(a, n):
    m = np.isnan(a)
    s = np.full(n, True, bool)
    return ndimage.binary_dilation(m, structure=s, origin=-(n//2))


def askewchan2(a, n):
    m = np.isnan(a)
    s = np.zeros(2*n - n%2, bool)
    s[-n:] = True
    return morphology.binary_dilation(m, selem=s)


def moarningsun0(a, n):
    mask = np.isnan(a)
    cs = np.cumsum(mask)
    cs[n:] -= cs[:-n].copy()
    return cs > 0


def moarningsun1(a, n):
    mask = np.isnan(a)
    idx = np.flatnonzero(mask)
    expanded_idx = idx[:,None] + np.arange(1, n)
    np.put(mask, expanded_idx, True, 'clip')
    return mask


def swenzel0(a, n):
    m = np.isnan(a)
    k = m.copy()
    for i in range(1, n):
        k[i:] |= m[:-i]
    return k


def swenzel1(a, n=4):
    m = np.isnan(a)
    k = m.copy()

    # lenM and lenK say for each mask how many
    # subsequent Trues there are at least
    lenM, lenK = 1, 1

    # we run until a combination of both masks will give us n or more
    # subsequent Trues
    while lenM+lenK < n:
        # append what we have in k to the end of what we have in m
        m[lenM:] |= k[:-lenM]

        # swap so that m is again the small one
        m, k = k, m

        # update the lengths
        lenM, lenK = lenK, lenM+lenK

    # see how much m has to be shifted in order to append the missing Trues
    k[n-lenM:] |= m[:-n+lenM]
    return k


def op(a, n):
    m = np.isnan(a)
    for x in range(1, n):
        m = np.logical_or(m, np.r_[False, m][:-1])
    return m


# all the functions in a list. NB these are the actual functions, not their names
funcs = [AGML0, AGML1, askewchan0, askewchan1, askewchan2, moarningsun0, swenzel0, swenzel1, op]

def allagree(fns = funcs, n = 10, asize = 100):
    """ make sure result is the same from all functions """
    fnames = [f.__name__ for f in fns]
    a = np.random.rand(asize)
    a[np.random.randint(0, asize, int(asize / 10))] = np.nan
    results = dict([(f.__name__, f(a, n)) for f in fns])
    isgood = [[np.array_equal(results[f1], results[f2]) for f1 in fnames] for f2 in fnames]
    pdgood = pd.DataFrame(isgood, columns = fnames, index = fnames)
    if not all([all(x) for x in isgood]):
        print "not all results identical"
        pdb.set_trace()
    return pdgood


def timeone(f):
    """ time one of the functions across the full range of a nd n """
    print "Timing", f.__name__
    Ns = np.array([10**x for x in range(0, 4)]) * 5 # 5 to 5000 window size
    As = [np.random.rand(10 ** x) for x in range(1, 8)] # up to 10 million data data points
    for i in range(len(As)): # 10% of points are always bad
        As[i][np.random.randint(0, len(As[i]), len(As[i]) / 10)] = np.nan
    results = np.array([[Timer(lambda: f(a, n)).timeit(number = 1) if n < len(a) \
                        else np.nan for n in Ns] for a in As])
    pdresults = pd.DataFrame(results, index = [len(x) for x in As], columns = Ns)
    return pdresults


def timeall(fns = funcs):
    """ run timeone for all known funcs """
    testd = dict([(x.__name__, timeone(x)) for x in fns])
    testdf = pd.concat(testd.values(), axis = 0, keys = testd.keys())
    testdf.index.names = ["coder", "asize"]
    testdf.columns.names = ["nsize"]
    testdf.reset_index(inplace = True)
    testdf = pd.melt(testdf, id_vars = ["coder", "asize"])
    return testdf

【讨论】:

  • 有点跑题了,但你是如何制作这张图片的?
  • @AGML: R ggplot 2 库。代码包含在上面。只需将 timeall() 结果导出到 csv,作为变量(这里称为 ww)带入 R,它应该可以保持不变。尝试了半天在 Seaborn 中复制,结果回到 R 中,图形仍然是至高无上的。
  • 从问题中的图片中,我期待一个漂亮的基准概述......你交付了:)
【解决方案3】:

您可以使用与运行平均过滤器相同的 cumsum 技巧:

def cumsum_trick(a, n):
    mask = np.isnan(a)
    cs = np.cumsum(mask)
    cs[n:] -= cs[:-n].copy()
    return cs > 0

不幸的是,需要额外的.copy(),因为内部进行了一些缓冲操作顺序。可以说服 numpy 反向应用减法,但要使其工作,cs 数组必须具有负步幅:

def cumsum_trick_nocopy(a, n):
    mask = np.isnan(a)
    cs = np.cumsum(mask, out=np.empty_like(a, int)[::-1])
    cs[n:] -= cs[:-n]
    out = cs > 0
    return out

但这似乎很脆弱,而且实际上并没有那么快。

我想知道某处是否有编译过的信号处理函数可以执行此精确操作..


对于稀疏的初始掩码和小的n,这个也相当快:

def index_expansion(a, n):
    mask = np.isnan(a)
    idx = np.flatnonzero(mask)
    expanded_idx = idx[:,None] + np.arange(1, n)
    np.put(mask, expanded_idx, True, 'clip')
    return mask

【讨论】:

  • 之所以需要复制,是因为cs元素的读写顺序。我将就地 ufunc 应用程序与花哨的索引混淆了,后者确实使用了某种缓冲。
【解决方案4】:

又一个答案!
它只是采用您已有的掩码并将逻辑或应用于自身的转换版本。很好的矢量化和疯狂的快速! :D

def repeat_or(a, n=4):
    m = np.isnan(a)
    k = m.copy()

    # lenM and lenK say for each mask how many
    # subsequent Trues there are at least
    lenM, lenK = 1, 1

    # we run until a combination of both masks will give us n or more
    # subsequent Trues
    while lenM+lenK < n:
        # append what we have in k to the end of what we have in m
        m[lenM:] |= k[:-lenM]

        # swap so that m is again the small one
        m, k = k, m

        # update the lengths
        lenM, lenK = lenK, lenM+lenK

    # see how much m has to be shifted in order to append the missing Trues
    k[n-lenM:] |= m[:-n+lenM]

    return k

不幸的是,我无法让m[i:] |= m[:-i] 运行……修改和使用掩码来修改自身可能是个坏主意。它确实适用于m[:-i] |= m[i:],但这是错误的方向。
无论如何,我们现在不是二次增长,而是像斐波那契一样的增长,它仍然比线性更好。
(我从没想过我会写一个真正与斐波那契数列相关的算法,而不是一些奇怪的数学问题。)

在“真实”条件下使用大小为 1e6 和 1e5 NAN 的数组进行测试:

In [5]: a = np.random.random(size=1e6)

In [6]: a[np.random.choice(np.arange(len(a), dtype=int), 1e5, replace=False)] = np.nan

In [7]: %timeit reduceat(a)
10 loops, best of 3: 65.2 ms per loop

In [8]: %timeit index_expansion(a)
100 loops, best of 3: 12 ms per loop

In [9]: %timeit cumsum_trick(a)
10 loops, best of 3: 17 ms per loop

In [10]: %timeit repeat_or(a)
1000 loops, best of 3: 1.9 ms per loop

In [11]: %timeit agml_indexing(a)
100 loops, best of 3: 6.91 ms per loop

我会将进一步的基准留给 Thomas。

【讨论】:

  • 您已加入比赛。跟不上明天的基准测试结果!
  • 我想你想要 m = np.isnan(a) 在第 2 行。
  • 当然! :D 当你不使用复制和粘贴而只是从内存中写入时会发生这种情况......
  • 这在概念上与@AGML 的第二个答案相同,但避免了昂贵的布尔索引。
  • 好吧,如果您将掩码的移动视为相同的概念,那么是的。但是恕我直言,我们的实现完全不同。 :) 顺便提一句。 AGML 不使用布尔索引,而是直接索引。他们需要在每次迭代时增加索引,然后使用它们来索引掩码并将相应的值设置为True。我将在几分钟内添加更多优化,以便使用 n 更好地扩展,这在 AGML 当前的解决方案中是不可能的。
【解决方案5】:

这样的?

maskleft = np.where(np.isnan(a))[0]
maskright = maskleft + n
mask = np.zeros(len(a),dtype=bool)
for l,r in itertools.izip(maskleft,maskright): 
   mask[l:r] = True

或者,由于 n 很小,最好循环它:

maskleft = np.where(np.isnan(a))[0]
mask = np.zeros(len(a),dtype=bool)
for i in range(0,n):
  thismask = maskleft+i
  mask[thismask] = True

除了 n 上的循环外,以上都是完全向量化的。但是循环是完全可并行化的,因此您可以使用例如获得 n 倍的加速。多处理或 Cython,如果你愿意麻烦的话。

编辑:根据@askewchan 解决方案 2 可能会导致超出范围的错误。它在range(0,n) 中也存在索引问题。可能的更正:

maskleft = np.where(np.isnan(a))[0]
ghost_mask = np.zeros(len(a)+n,dtype=bool)
for i in range(0, n+1):
    thismask = maskleft + i
    ghost_mask[thismask] = True
mask = ghost_mask[:len(ghost_mask)-n]

【讨论】:

  • 很好,但我们正在循环播放。我希望 Numpy、Scipy 或 Pandas 的一些组合能够以矢量化的形式巧妙地做到这一点。我可能太挑剔了。不幸的是,我已经在同时为多个数据系列使用多处理队列中的所有处理器。我真正希望的是一种很好的代数方式,可以尽可能快地运行它,并可能利用 AVX 寄存器。我意识到我在这里要求很高......你的循环可能足够快,我会试一试。
  • 我认为没有办法完全避免循环,但如果 n 很小,您应该能够通过在 n 上并行循环来获得同等的性能。
  • 如果这仍然太慢,不幸的是,您可能正在进入 Cython 领域。如果有必要,这里有一个类似的问题可以开始:reddit.com/r/learnpython/comments/2xqlwj/…
  • 您的第一个解决方案比我能想出的任何小尺寸解决方案都快。您的第二个答案没有正确处理末尾n 内的True 值(使索引maskleft + i 超出范围)。
  • 查看 AGML 下面的结果...随意更改代码。
【解决方案6】:

这也可以认为是morphological dilation 问题,这里使用scipy.ndimage.binary_dilation:

def dilation(a, n):
    m = np.isnan(a)
    s = np.full(n, True, bool)
    return ndimage.binary_dilation(m, structure=s, origin=-(n//2))

注意origin:此参数确保structure(我称之为内核)从input(你的掩码m)的左侧开始.通常out[i] 处的值是structure(即structure[n//2])中心在in[i] 处的膨胀,但您希望structure[0] 位于in[i]。

您也可以使用在左侧填充Falses 的内核来执行此操作,如果您使用来自scikit-image 的binary_dilation,则需要这样做:

def dilation_skimage(a, n):
    m = np.isnan(a)
    s = np.zeros(2*n - n%2, bool)
    s[-n:] = True
    return skimage.morphology.binary_dilation(m, selem=s)

两者的时间似乎没有太大变化:

dilation_scipy
small:    10 loops, best of 3: 47.9 ms per loop
large: 10000 loops, best of 3: 88.9 µs per loop

dilation_skimage
small:    10 loops, best of 3: 47.0 ms per loop
large: 10000 loops, best of 3: 91.1 µs per loop

【讨论】:

  • 谁说 Python 科学生态系统不是绝对出色,也没有吸引到最优秀的人才。 Askewchan,你是一个大师。我已经发现您的原始答案对我的用例非常有效。我正在准备一些基准测试与循环版本,我将在今晚发布。
  • 哈哈,谢谢@Thomas 的客气话。很高兴能提供帮助。时间安排以及您的最佳选择很大程度上取决于len(a)、np.count_nonzero(m) 和n 本身的相对大小。不要忽视@AGML 的回答,这在某些情况下会更快。
  • 非常聪明!我很想知道时间安排,因为“围绕一些索引对窗口做一些事情”似乎是一个相当普遍的问题。
  • 你用来计时的数组看起来怎么样?
【解决方案7】:

您可以将np.ufunc.reduceat 与np.bitwise_or 一起使用:

import numpy as np
a = np.array([4, 0, 8, 5, 10, 9, np.nan, 1, 4, 9, 9, np.nan, np.nan, 9,
              9, 8, 0, 3, 7, 9, 2, 6, 7, 2, 9, 4, 1, 1, np.nan, 10])
m = np.isnan(a)
n = 4
i = np.arange(1, len(m)+1)
ind = np.column_stack([i-n, i]) # may be a faster way to generate this
ind.clip(0, len(m)-1, out=ind)

np.bitwise_or.reduceat(m, ind.ravel())[::2]

关于您的数据:

print np.column_stack([m, reduced])
[[False False]
 [False False]
 [False False]
 [False False]
 [False False]
 [False False]
 [ True  True]
 [False  True]
 [False  True]
 [False  True]
 [False False]
 [ True  True]
 [ True  True]
 [False  True]
 [False  True]
 [False  True]
 [False False]
 [False False]
 [False False]
 [False False]
 [False False]
 [False False]
 [False False]
 [False False]
 [False False]
 [False False]
 [False False]
 [False False]
 [ True  True]
 [False  True]]

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-03-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多