【问题标题】:In python, is there an efficient way of seperating an array with elements mapped to another array?在 python 中,是否有一种有效的方法可以将一个数组与映射到另一个数组的元素分开?
【发布时间】:2021-01-15 10:18:57
【问题描述】:

假设我有一个任意数组np.array([1,2,3,4,5,6]) 和另一个数组,该数组将数组中的特定元素映射到一个组np.array(['a','b', 'a','c','c', 'b']),现在我想根据给出的标签/组将它们分成三个不同的数组第二个数组,所以它们是a,b,c = narray([1,3]), narray([2,6]), narray([4,5])。是一个简单的forloop方法还是我在这里缺少一些有效的方法?

【问题讨论】:

  • 您可以简单地使用预定义的numpy拆分函数: import numpy as np arr = np.array([1, 2, 3, 4, 5, 6]) newarr = np.array_split(arr, 3 ) print(newarr[0]) print(newarr[1]) print(newarr[2]) 输出:[1 2] [3 4] [5 6]
  • @AliHassan 这不是 OP 想要的输出。
  • 拥有一个至少初始化数组的脚本会有所帮助 - 并且可能包括您对解决方案的最佳猜测 - 这样答案就可以从同一块代码开始。

标签: python arrays pandas numpy sorting


【解决方案1】:

当你写得高效时,我假设你在这里想要的实际上是

我将尝试简要讨论渐近效率。 在这种情况下,我们将N 称为输入大小,将K 称为唯一值的数量。

我的方法解决方案是结合使用 np.argsort() 和专门针对 NumPy 输入优化的定制 groupby_np()

import numpy as np


def groupby_np(arr, both=True):
    n = len(arr)
    extrema = np.nonzero(arr[:-1] != arr[1:])[0] + 1
    if both:
        last_i = 0
        for i in extrema:
            yield last_i, i
            last_i = i
        yield last_i, n
    else:
        yield 0
        yield from extrema
        yield n


def labeling_groupby_np(values, labels):
    slicing = labels.argsort()
    sorted_labels = labels[slicing]
    sorted_values = values[slicing]
    del slicing
    result = {}
    for i, j in groupby_np(sorted_labels, True):
        result[sorted_labels[i]] = sorted_values[i:j]
    return result

这具有复杂性O(N log N + K)N log N 来自排序步骤,K 来自最后一个循环。 有趣的是,N-dependent 和 K-dependent 步骤都很快,因为 N-dependent 部分是在低级别执行的,K-dependent 部分是 O(1) 和也很快。


类似以下的解决方案(非常类似于@theEpsilon 的答案):

import numpy as np


def labeling_loop(values, labels):
    labeled = {}
    for x, l in zip(values, labels):
        if l not in labeled:
            labeled[l] = [x]
        else:
            labeled[l].append(x)
    return {k: np.array(v) for k, v in labeled.items()}

使用两个循环并具有O(N + K)。我认为您不能轻易避免第二个循环(没有明显的速度损失)。至于第一个循环,这是在 Python 中执行的,它本身会带来很大的速度损失。


另一种可能性是使用np.unique()主循环 带到较低的级别。然而,这带来了其他挑战,因为一旦提取了唯一值,没有一些NumPy advanced indexing,即O(N),就没有有效的方法来提取信息来构造你想要的数组。这些解决方案的总体复杂度为O(K * N),但由于 NumPy 高级索引是在较低级别完成的,因此可以实现相对较快的解决方案,尽管其渐近复杂度比替代方案更差。

可能的实现包括(类似于@AjayVerma's@AKX's 的答案):

import numpy as np


def labeling_unique_bool(values, labels):
    return {l: values[l == labels] for l in np.unique(labels)}
import numpy as np


def labeling_unique_nonzero(values, labels):
    return {l: values[np.nonzero(l == labels)] for l in np.unique(labels)}

此外,可以考虑预先排序步骤,然后通过避免 NumPy 高级索引来加速切片部分。 然而,排序步骤可能比高级索引更昂贵,而且一般而言,对于我测试的输入,所提出的方法往往更快。

import numpy as np


def labeling_unique_argsort(values, labels):
    uniques, counts = np.unique(labels, return_counts=True)
    sorted_values = values[labels.argsort()]
    bound = 0
    result = {}
    for x, c in zip(uniques, counts):
        result[x] = sorted_values[bound:bound + c]
        bound += c
    return result

另一种方法,原则上很简洁(与我提出的方法相同),但在实践中很慢是使用排序和itertools.groupby()

import itertools
from operator import itemgetter


def labeling_groupby(values, labels):
    slicing = labels.argsort()
    sorted_labels = labels[slicing]
    sorted_values = values[slicing]
    del slicing
    result = {}
    for x, g in itertools.groupby(zip(sorted_labels, sorted_values), itemgetter(0)):
        result[x] = np.fromiter(map(itemgetter(1), g), dtype=sorted_values.dtype)
    return result

最后,一种基于 Pandas 的方法,对于较大的输入非常简洁且相当快,但对于较小的输入则表现不佳(类似于 @Ehsan's answer):

def labeling_groupby_pd(values, labels):
    df = pd.DataFrame({'values': values, 'labels': labels})
    return df.groupby('labels').values.apply(lambda x: x.values).to_dict()

现在,说话很便宜,所以让我们将一些数字附加到 fastslow 并为不同的输入大小生成一些图。 K 的值上限为 52(英文字母的大小写字母)。当N远大于K时,达到封顶值的概率很高。

输入是通过以下方式以编程方式生成的:

def gen_input(n, p, labels=string.ascii_letters):
    k = len(labels)
    values = np.arange(n)
    labels = np.array([string.ascii_letters[i] for i in np.random.randint(0, int(k * p), n)])
    return values, labels

基准是针对p 的值从(1.0, 0.5, 0.1, 0.05) 生成的,这会改变K 的最大值。下面的图表按该顺序引用了p 值。


p=1.0(最多K = 52

...并以最快的速度放大


p=0.5(最多K = 26


p=0.1(最多K = 5


p=0.05(最多K = 2

...并以最快的速度放大


我们可以看到,除了非常小的输入外,所提出的方法如何优于迄今为止针对测试输入提出的其他方法。

(提供完整的基准测试here)。


也可以考虑将循环的某些部分移至 Numba / Cython,但我会将其留给感兴趣的读者。

【讨论】:

  • 为什么 Pandas 解决方案对于小输入的速度相对较慢?
  • 大 O 不是一切,但很高兴看到我赢了 ?
  • 虽然我喜欢所有详细的分析,但我认为这不一定是最好的比较。我认为很多时间上的差异来自创建正确数据结构的开销。我建议 OP 运行与您相同的分析,从一开始就消除可能的数据创建开销,并牢记输出所需的结构。话虽如此,很好的分析。
  • @IdeaO。我认为大部分持续开销都用于创建 DataFrame,但我不是 100% 确定
  • @Ehsan 跨解决方案创建统一结果需要一些开销,但这仅与非常小的输入相关。请参阅benchmarks with "native" containers
【解决方案2】:

您可以使用numpy.unique

x = np.array([1,2,3,4,5,6])
y = np.array(['a','b', 'a','c','c', 'b'])
print({value:x[y==value] for value in np.unique(y)})

输出

{'a': array([1, 3]), 'b': array([2, 6]), 'c': array([4, 5])}

【讨论】:

    【解决方案3】:

    这是pandas groupby的教科书用法:

    import pandas as pd
    df = pd.DataFrame({'A':[1,2,3,4,5,6],'B':['a','b','a','c','c','b']})
    a,b,c = df.groupby('B').A.apply(lambda x:x.values)
    #[1 3], [2 6], [4 5]
    

    【讨论】:

      【解决方案4】:

      确定有一些简单的调用可以一举完成,Numpy 大师很快就会启发我们,但是

      indices = np.array([1,2,3,4,5,6])
      values = np.array(['a', 'b', 'a', 'c', 'c', 'b'])
      indices_by_value = {}
      for value in np.unique(values):
        indices_by_value[value] = indices[values == value]
      

      会留给你

      {'a': array([1, 3]), 'b': array([2, 6]), 'c': array([4, 5])}
      

      【讨论】:

        【解决方案5】:

        你可以这样做:

        from collections import defaultdict
        d = defaultdict(list)
        letters = ['a', 'b', 'a', 'c', 'c', 'b']
        numbers = [1, 2, 3, 4, 5, 6]
        for l, n in zip(letters, numbers):
            d[l].append(n)
        

        d 会有你的答案。

        【讨论】:

        • OP 有 Numpy 数组,而不是裸 Python 列表。这可能有效,但不会像使用 Numpy 索引那样有效。 (也就是说,对于非 Numpy 可迭代对象,这是一个好方法。)
        • @AKX 在 O(n^2) 玻璃房子里的人也许不应该尝试向别人的 O(n) 房子扔石头:-P
        • 不是比赛。我认为任何解释清楚的答案都具有教育意义,并且可以对未来的读者有所帮助。我喜欢程序员解决问题的所有不同方式。
        【解决方案6】:

        使用 numpy 的掩码选择功能应该可以完成这项工作。 像这样:

        
        > import numpy as np
        > xx = np.array(range(5))
        > yy = np.array(['a','b','a','d','e'])
        > yy=='a'
         array([ True, False,  True, False, False])
        > xx[(yy=='a')]
        array([0, 2])
        

        考虑浏览标签数组的唯一值并逐步构建匹配字典。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-11-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-10-02
          相关资源
          最近更新 更多