将数组排序到索引数组指定的bin中的最有效方法?

时间:2019-03-18 17:11:26

标签: python pandas numpy scipy pythran

示例任务:

data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
idx  = np.array([2, 0, 1, 1, 2, 0, 1, 1, 2])

预期结果:

binned = np.array([2, 6, 3, 4, 7, 8, 1, 5, 9])

约束:

  • 应该很快。

  • 应为O(n+k),其中n是数据长度,k是容器数。

  • 应该稳定,即保留箱内的顺序。

明显的解决方案

data[np.argsort(idx, kind='stable')]

O(n log n)

O(n+k)解决方案

def sort_to_bins(idx, data, mx=-1):
    if mx==-1:
        mx = idx.max() + 1
    cnts = np.zeros(mx + 1, int)
    for i in range(idx.size):
        cnts[idx[i] + 1] += 1
    for i in range(1, cnts.size):
        cnts[i] += cnts[i-1]
    res = np.empty_like(data)
    for i in range(data.size):
        res[cnts[idx[i]]] = data[i]
        cnts[idx[i]] += 1
    return res

循环且缓慢。

在纯numpy <scipy <pandas <numba / pythran中是否有更好的方法?

1 个答案:

答案 0 :(得分:1)

以下是一些解决方案:

  1. 无论如何都使用np.argsort,毕竟它是快速编译的代码。

  2. 使用np.bincount来获取垃圾箱大小,并使用np.argpartition来获取固定数目的垃圾箱,O(n)。缺点:目前尚无稳定的算法,因此我们必须对每个bin进行排序。

  3. 使用scipy.ndimage.measurements.labeled_comprehension。这大致完成了所需的操作,但不知道如何实现。

  4. 使用pandas。我是一个完整的pandas菜鸟,所以我在这里用groupby拼凑的东西可能不是最佳选择。

  5. 在压缩的稀疏行格式和压缩的稀疏列格式之间使用scipy.sparse切换恰好实现了我们正在寻找的确切操作。

  6. 在问题中的循环代码上使用pythran(我相信numba也可以)。只需在numpy导入后插入顶部

#pythran export sort_to_bins(int[:], float[:], int)

然后编译

# pythran stb_pthr.py

基准测试100 bins,可变项目数:

enter image description here

带回家:

如果您对numba / pythran感到满意,那么就可以了,如果没有,scipy.sparse可以很好地扩展。

代码:

import numpy as np
from scipy import sparse
from scipy.ndimage.measurements import labeled_comprehension
from stb_pthr import sort_to_bins as sort_to_bins_pythran
import pandas as pd

def sort_to_bins_pandas(idx, data, mx=-1):
    df = pd.DataFrame.from_dict(data=data)
    out = np.empty_like(data)
    j = 0
    for grp in df.groupby(idx).groups.values():
        out[j:j+len(grp)] = data[np.sort(grp)]
        j += len(grp)
    return out

def sort_to_bins_ndimage(idx, data, mx=-1):
    if mx==-1:
        mx = idx.max() + 1
    out = np.empty_like(data)
    j = 0
    def collect(bin):
        nonlocal j
        out[j:j+len(bin)] = np.sort(bin)
        j += len(bin)
        return 0
    labeled_comprehension(data, idx, np.arange(mx), collect, data.dtype, None)
    return out

def sort_to_bins_partition(idx, data, mx=-1):
    if mx==-1:
        mx = idx.max() + 1
    return data[np.argpartition(idx, np.bincount(idx, None, mx)[:-1].cumsum())]

def sort_to_bins_partition_stable(idx, data, mx=-1):
    if mx==-1:
        mx = idx.max() + 1
    split = np.bincount(idx, None, mx)[:-1].cumsum()
    srt = np.argpartition(idx, split)
    for bin in np.split(srt, split):
        bin.sort()
    return data[srt]

def sort_to_bins_sparse(idx, data, mx=-1):
    if mx==-1:
        mx = idx.max() + 1    
    return sparse.csr_matrix((data, idx, np.arange(len(idx)+1)), (len(idx), mx)).tocsc().data

def sort_to_bins_argsort(idx, data, mx=-1):
    return data[idx.argsort(kind='stable')]

from timeit import timeit
exmpls = [np.random.randint(0, K, (N,)) for K, N in np.c_[np.full(16, 100), 1<<np.arange(5, 21)]]

timings = {}
for idx in exmpls:
    data = np.arange(len(idx), dtype=float)
    ref = None
    for x, f in (*globals().items(),):
        if x.startswith('sort_to_bins_'):
            timings.setdefault(x.replace('sort_to_bins_', '').replace('_', ' '), []).append(timeit('f(idx, data, -1)', globals={'f':f, 'idx':idx, 'data':data}, number=10)*100)
            if x=='sort_to_bins_partition':
                continue
            if ref is None:
                ref = f(idx, data, -1)
            else:
                assert np.all(f(idx, data, -1)==ref)

import pylab
for k, v in timings.items():
    pylab.loglog(1<<np.arange(5, 21), v, label=k)
pylab.xlabel('#items')
pylab.ylabel('time [ms]')
pylab.legend()
pylab.show()