numpy.median.reduceat的快速替代方案


12

关于此答案,是否存在一种快速方法来计算具有不等数量元素的组的数组的中值?

例如:

data =  [1.00, 1.05, 1.30, 1.20, 1.06, 1.54, 1.33, 1.87, 1.67, ... ]
index = [0,    0,    1,    1,    1,    1,    2,    3,    3,    ... ]

然后,我想计算数量与每组中位数之间的差(例如,组的中位数01.025,则第一个结果为1.00 - 1.025 = -0.025)。因此,对于上面的数组,结果将显示为:

result = [-0.025, 0.025, 0.05, -0.05, -0.19, 0.29, 0.00, 0.10, -0.10, ...]

既然np.median.reduceat还不存在,还有另一种快速的方法来实现这一目标吗?我的数组将包含数百万行,因此速度至关重要!

可以假定索引是连续且有序的(如果不是,则很容易对其进行转换)。


性能比较的示例数据:

import numpy as np

np.random.seed(0)
rows = 10000
cols = 500
ngroup = 100

# Create random data and groups (unique per column)
data = np.random.rand(rows,cols)
groups = np.random.randint(ngroup, size=(rows,cols)) + 10*np.tile(np.arange(cols),(rows,1))

# Flatten
data = data.ravel()
groups = groups.ravel()

# Sort by group
idx_sort = groups.argsort()
data = data[idx_sort]
groups = groups[idx_sort]

您是否scipy.ndimage.median在链接答案中添加了建议时间?在我看来,每个标签不需要相等数量的元素。还是我错过了什么?
安德拉斯·迪克

因此,当您说几百万行时,您的实际数据集是2D数组,并且您正在对每行进行此操作吗?
Divakar

@Divakar见编辑以问题为测试数据
让-保罗

您已经在初始数据中给出了基准,我对其进行了夸大以保持格式不变。一切都以我膨胀的数据集为基准。现在更改它是不合理的
roganjosh

Answers:


7

有时,如果您真的想加快计算速度,而原生numpy无法做到,则需要编写非惯用的numpy代码。

numba将您的python代码编译为低级C。由于许多numpy本身通常与C一样快,所以如果您的问题不适合使用numpy进行本机矢量化,那么这最终将非常有用。这是一个示例(我假设索引是连续的并且已排序,这也反映在示例数据中):

import numpy as np
import numba

# use the inflated example of roganjosh https://stackoverflow.com/a/58788534
data =  [1.00, 1.05, 1.30, 1.20, 1.06, 1.54, 1.33, 1.87, 1.67]
index = [0,    0,    1,    1,    1,    1,    2,    3,    3] 

data = np.array(data * 500) # using arrays is important for numba!
index = np.sort(np.random.randint(0, 30, 4500))               

# jit-decorate; original is available as .py_func attribute
@numba.njit('f8[:](f8[:], i8[:])') # explicit signature implies ahead-of-time compile
def diffmedian_jit(data, index): 
    res = np.empty_like(data) 
    i_start = 0 
    for i in range(1, index.size): 
        if index[i] == index[i_start]: 
            continue 

        # here: i is the first _next_ index 
        inds = slice(i_start, i)  # i_start:i slice 
        res[inds] = data[inds] - np.median(data[inds]) 

        i_start = i 

    # also fix last label 
    res[i_start:] = data[i_start:] - np.median(data[i_start:])

    return res

以下是一些使用IPython的%timeit魔术的时机:

>>> %timeit diffmedian_jit.py_func(data, index)  # non-jitted function
... %timeit diffmedian_jit(data, index)  # jitted function
...
4.27 ms ± 109 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
65.2 µs ± 1.01 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

使用问题中的更新示例数据,这些数字(即python函数的运行时与JIT加速功能的运行时)是

>>> %timeit diffmedian_jit.py_func(data, groups) 
... %timeit diffmedian_jit(data, groups)
2.45 s ± 34.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
93.6 ms ± 518 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

使用加速代码,这在较小的情况下相当于65倍的加速,在较大的情况下相当于26倍的加速(当然,与慢速循环代码相比)。另一个好处是(与使用本地numpy进行典型矢量化处理不同),我们不需要额外的内存来达到这种速度,而这全都在于优化和编译的低级代码最终得以运行。


上面的函数假定int64默认情况下为numpy int数组,而在Windows上实际上并非如此。因此,另一种方法是从的调用中删除签名,以numba.njit触发适当的即时编译。但这意味着函数将在第一次执行时进行编译,这可能会与计时结果混为一谈(我们可以使用代表性的数据类型手动执行一次该函数,或者只接受第一次计时执行会慢得多,这应该被忽略)。这正是我试图通过指定签名来阻止提前触发编译的尝试。

无论如何,在适当的JIT情况下,我们需要的装饰器只是

@numba.njit
def diffmedian_jit(...):

请注意,我为jit编译的函数显示的上述时序仅在编译函数后才适用。这要么在定义时发生(使用急切编译,将显式签名传递给numba.njit),要么在第一个函数调用期间发生(使用惰性编译,当没有签名传递给numba.njit)。如果该函数仅执行一次,则该方法的速度也应考虑编译时间。通常,只有在编译+执行的总时间少于未编译的运行时的情况下,才值得编译函数(在上述情况下,这是正确的,因为本机python函数非常慢)。这通常发生在您多次调用编译函数时。

正如max9111在评论中指出的,的一个重要功能numbacache关键字 to jit。传递cache=Truenumba.jit会将已编译的函数存储到磁盘,以便在给定python模块的下一次执行期间,该函数将从那里加载而不是重新编译,这从长远来看又可以节省您的运行时间。


@Divakar实际上,它假设索引是连续的和已排序的,这似乎是OP数据中的假设,并且也自动包含在roganjosh的index数据中。我会为此留个字条,谢谢:)
Andras Deak

好的,连续性不会自动包含在内...但是我很确定它还是必须连续的。嗯...
Andras Deak

1
@AndrasDeak可以肯定地假设标签是连续的并且已排序(无论如何都不容易固定标签)
Jean-Paul

1
@AndrasDeak查看测试数据的问题编辑(以使问题之间的性能比较保持一致)
Jean-Paul

1
您可以提及关键字,cache=True以避免在每次重新启动解释器时重新编译。
max9111

5

一种方法是仅在Pandas此处使用groupby。我稍微扩大了输入大小,以更好地了解时序(因为创建DF会产生开销)。

import numpy as np
import pandas as pd

data =  [1.00, 1.05, 1.30, 1.20, 1.06, 1.54, 1.33, 1.87, 1.67]
index = [0,    0,    1,    1,    1,    1,    2,    3,    3]

data = data * 500
index = np.sort(np.random.randint(0, 30, 4500))

def df_approach(data, index):
    df = pd.DataFrame({'data': data, 'label': index})
    df['median'] = df.groupby('label')['data'].transform('median')
    df['result'] = df['data'] - df['median']

给出以下内容timeit

%timeit df_approach(data, index)
5.38 ms ± 50.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

对于相同的样本量,我将Aryerezdict方法设为:

%timeit dict_approach(data, index)
8.12 ms ± 3.47 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

但是,如果我们将输入增加10倍,则时间变为:

%timeit df_approach(data, index)
7.72 ms ± 85 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit dict_approach(data, index)
30.2 ms ± 10.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

但是,以牺牲一些可靠性为代价,Divakar使用纯numpy给出了答案:

%timeit bin_median_subtract(data, index)
573 µs ± 7.48 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

根据新的数据集(实际上应该在开始时设置):

%timeit df_approach(data, groups)
472 ms ± 2.52 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit bin_median_subtract(data, groups) #https://stackoverflow.com/a/58788623/4799172
3.02 s ± 31.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit dict_approach(data, groups) #https://stackoverflow.com/a/58788199/4799172
<I gave up after 1 minute>

# jitted (using @numba.njit('f8[:](f8[:], i4[:]') on Windows) from  https://stackoverflow.com/a/58788635/4799172
%timeit diffmedian_jit(data, groups)
132 ms ± 3.12 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

感谢您的回答!为了与其他答案保持一致,您是否可以根据对我的问题的编辑中提供的示例数据来测试您的解决方案?
Jean-Paul

@ Jean-Paul时间已经一致了,不是吗?他们使用了我最初的基准数据,在没有使用它们的情况下,我为他们提供了相同的基准时间
roganjosh

我忽略了您,并且已经添加了对Divakar答案的引用,因此您的答案确实已经在不同方法之间进行了很好的比较,谢谢!
Jean-Paul

1
@ Jean-Paul我在底部添加了最新的时间,因为它实际上极大地改变了一切
roganjosh

1
很抱歉在发布问题时不添加测试集,非常感谢您现在仍然添加了测试结果!谢谢!!!
Jean-Paul

4

也许您已经做到了,但是如果没有,那么看看是否足够快:

median_dict = {i: np.median(data[index == i]) for i in np.unique(index)}
def myFunc(my_dict, a): 
    return my_dict[a]
vect_func = np.vectorize(myFunc)
median_diff = data - vect_func(median_dict, index)
median_diff

输出:

array([-0.025,  0.025,  0.05 , -0.05 , -0.19 ,  0.29 ,  0.   ,  0.1  ,
   -0.1  ])

在说明明显的风险,np.vectorize非常为一个循环瘦包装,所以我不希望这种方法特别快。
安德拉斯·迪克

1
@AndrasDeak我不同意:)我将继续关注,如果有人可以发布更好的解决方案,则将其删除。
Aryerez,

1
我认为即使弹出更快的方法,您也不必删除它:)
Andras Deak

@roganjosh这可能是因为您未定义dataindexnp.array如问题中所述。
Aryerez

1
@ Jean-Paul roganjosh在我和他的方法之间进行了时间比较,这里的其他人也比较了他们的方法。它取决于计算机硬件,因此每个人都没有必要检查自己的方法,但是看来我想出了最慢的解决方案。
Aryerez '19

4

这是一种基于NumPy的方法,用于获取正bin / index值的binned中值-

def bin_median(a, i):
    sidx = np.lexsort((a,i))

    a = a[sidx]
    i = i[sidx]

    c = np.bincount(i)
    c = c[c!=0]

    s1 = c//2

    e = c.cumsum()
    s1[1:] += e[:-1]

    firstval = a[s1-1]
    secondval = a[s1]
    out = np.where(c%2,secondval,(firstval+secondval)/2.0)
    return out

为了解决我们的减法具体情况-

def bin_median_subtract(a, i):
    sidx = np.lexsort((a,i))

    c = np.bincount(i)

    valid_mask = c!=0
    c = c[valid_mask]    

    e = c.cumsum()
    s1 = c//2
    s1[1:] += e[:-1]
    ssidx = sidx.argsort()
    starts = c%2+s1-1
    ends = s1

    starts_orgindx = sidx[np.searchsorted(sidx,starts,sorter=ssidx)]
    ends_orgindx  = sidx[np.searchsorted(sidx,ends,sorter=ssidx)]
    val = (a[starts_orgindx] + a[ends_orgindx])/2.
    out = a-np.repeat(val,c)
    return out

很好的答案!您是否有任何提高速度的迹象,例如df.groupby('index').transform('median')
Jean-Paul

@ Jean-Paul可以对数百万的实际数据集进行测试吗?
Divakar

查看测试数据的编辑问题
Jean-Paul

@ Jean-Paul编辑了一个更简单的解决方案。如果需要,请确保将其用于测试。
Divakar,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.