【问题标题】:How to I factorize a list of tuples?如何分解元组列表?
【发布时间】:2017-05-26 18:20:24
【问题描述】:

定义
分解:将每个唯一对象映射成唯一整数。通常,映射到的整数范围是从零到 n - 1,其中 n 是唯一对象的数量。两种变体也是典型的。类型 1 是按照标识唯一对象的顺序进行编号的位置。类型 2 是首先对唯一对象进行排序,然后应用与类型 1 相同的过程。

设置
考虑元组列表tups

tups = [(1, 2), ('a', 'b'), (3, 4), ('c', 5), (6, 'd'), ('a', 'b'), (3, 4)]

我想把这个分解成

[0, 1, 2, 3, 4, 1, 2]

我知道有很多方法可以做到这一点。但是,我想尽可能高效地做到这一点。


我的尝试

pandas.factorize 并得到一个错误...

pd.factorize(tups)[0]

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-84-c84947ac948c> in <module>()
----> 1 pd.factorize(tups)[0]

//anaconda/envs/3.6/lib/python3.6/site-packages/pandas/core/algorithms.py in factorize(values, sort, order, na_sentinel, size_hint)
    553     uniques = vec_klass()
    554     check_nulls = not is_integer_dtype(original)
--> 555     labels = table.get_labels(values, uniques, 0, na_sentinel, check_nulls)
    556 
    557     labels = _ensure_platform_int(labels)

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.PyObjectHashTable.get_labels (pandas/_libs/hashtable.c:21804)()

ValueError: Buffer has wrong number of dimensions (expected 1, got 2)

numpy.unique 并得到不正确的结果...

np.unique(tups, return_inverse=1)[1]

array([0, 1, 6, 7, 2, 3, 8, 4, 5, 9, 6, 7, 2, 3])

​我可以在元组的哈希上使用其中任何一个

pd.factorize([hash(t) for t in tups])[0]

array([0, 1, 2, 3, 4, 1, 2])

耶!这就是我想要的……那有什么问题吗?

第一个问题
看看这种技术的性能下降

lst = [10, 7, 4, 33, 1005, 7, 4]

%timeit pd.factorize(lst * 1000)[0]
1000 loops, best of 3: 506 µs per loop

%timeit pd.factorize([hash(i) for i in lst * 1000])[0]
1000 loops, best of 3: 937 µs per loop

第二个问题
哈希不保证唯一!


问题
分解元组列表的超快速方法是什么?


时间
两个轴都在日志空间中

code

from itertools import count

def champ(tups):
    d = {}
    c = count()
    return np.array(
        [d[tup] if tup in d else d.setdefault(tup, next(c)) for tup in tups]
    )

def root(tups):
    return pd.Series(tups).factorize()[0]

def iobe(tups):
    return np.unique(tups, return_inverse=True, axis=0)[1]

def get_row_view(a):
    void_dt = np.dtype((np.void, a.dtype.itemsize * np.prod(a.shape[1:])))
    a = np.ascontiguousarray(a)
    return a.reshape(a.shape[0], -1).view(void_dt).ravel()

def diva(tups):
    return np.unique(get_row_view(np.array(tups)), return_inverse=1)[1]

def gdib(tups):
    return pd.factorize([str(t) for t in tups])[0]

from string import ascii_letters

def tups_creator_1(size, len_of_str=3, num_ints_to_choose_from=1000, seed=None):
    c = len_of_str
    n = num_ints_to_choose_from
    np.random.seed(seed)
    d = pd.DataFrame(np.random.choice(list(ascii_letters), (size, c))).sum(1).tolist()
    i = np.random.randint(n, size=size)
    return list(zip(d, i))

results = pd.DataFrame(
    index=pd.Index([100, 1000, 5000, 10000, 20000, 30000, 40000, 50000], name='Size'),
    columns=pd.Index('champ root iobe diva gdib'.split(), name='Method')
)

for i in results.index:
    tups = tups_creator_1(i, max(1, int(np.log10(i))), max(10, i // 10))
    for j in results.columns:
        stmt = '{}(tups)'.format(j)
        setup = 'from __main__ import {}, tups'.format(j)
        results.set_value(i, j, timeit(stmt, setup, number=100) / 100)

results.plot(title='Avg Seconds', logx=True, logy=True)

【问题讨论】:

  • 您是否必须保持这样的顺序或类似:[0, 3, 1, 4, 2, 3, 1] 也可以?
  • @Divakar 目前我不在乎。您可以选择哪个更方便。
  • 我认为我们需要一个更好的基准测试,它同时包含字符串和数字,当然应该足够大,并且重复的数量与示例中的一致。
  • 这是 0.20.1 上的一个错误,已在 0.20.2 中修复(尚未发布)
  • 对于我们这些非 Pandas 用户来说,如果您能描述一下 factorize 的含义,那就太好了,尤其是如果您想要基于 numpy 的答案。

标签: python pandas numpy


【解决方案1】:

一个简单的方法是使用dict 来保存以前的访问:

>>> d = {}
>>> [d.setdefault(tup, i) for i, tup in enumerate(tups)]
[0, 1, 2, 3, 4, 1, 2]

如果您需要保持数字顺序,则稍作更改:

>>> from itertools import count
>>> c = count()
>>> [d[tup] if tup in d else d.setdefault(tup, next(c)) for tup in tups]
[0, 1, 2, 3, 4, 1, 2, 5]

或者写成:

>>> [d.get(tup) or d.setdefault(tup, next(c)) for tup in tups]
[0, 1, 2, 3, 4, 1, 2, 5]

【讨论】:

  • 这是一个非常好的答案!再次感谢。请参阅我更新的时间问题。
【解决方案2】:

将您的元组列表初始化为一个系列,然后调用factorize

pd.Series(tups).factorize()[0]

[0 1 2 3 4 1 2]

【讨论】:

  • 这可能是完美的答案。我唯一的怀疑是 pd.Series 构造的开销。
【解决方案3】:

@AChampion's 使用setdefault 让我想知道defaultdict 是否可以用于这个问题。因此,从 AC 的答案中随意抄袭:

In [189]: tups = [(1, 2), ('a', 'b'), (3, 4), ('c', 5), (6, 'd'), ('a', 'b'), (3, 4)]

In [190]: import collections
In [191]: import itertools
In [192]: cnt = itertools.count()
In [193]: dd = collections.defaultdict(lambda : next(cnt))

In [194]: [dd[t] for t in tups]
Out[194]: [0, 1, 2, 3, 4, 1, 2]

其他 SO 问题中的计时显示 defaultdict 比直接使用 setdefault 稍慢。这种方法的简洁性仍然很有吸引力。

In [196]: dd
Out[196]: 
defaultdict(<function __main__.<lambda>>,
            {(1, 2): 0, (3, 4): 2, ('a', 'b'): 1, (6, 'd'): 4, ('c', 5): 3})

【讨论】:

    【解决方案4】:

    方法#1

    将每个元组转换为 2D 数组的一行,使用 NumPy ndarray 的 views 概念将每一行视为一个标量,最后使用 np.unique(... return_inverse=True) 进行因式分解 -

    np.unique(get_row_view(np.array(tups)), return_inverse=1)[1]
    

    get_row_view 取自 here

    示例运行 -

    In [23]: tups
    Out[23]: [(1, 2), ('a', 'b'), (3, 4), ('c', 5), (6, 'd'), ('a', 'b'), (3, 4)]
    
    In [24]: np.unique(get_row_view(np.array(tups)), return_inverse=1)[1]
    Out[24]: array([0, 3, 1, 4, 2, 3, 1])
    

    方法 #2

    def argsort_unique(idx):
        # Original idea : https://stackoverflow.com/a/41242285/3293881 
        n = idx.size
        sidx = np.empty(n,dtype=int)
        sidx[idx] = np.arange(n)
        return sidx
    
    def unique_return_inverse_tuples(tups):
        a = np.array(tups)
        sidx = np.lexsort(a.T)
        b = a[sidx]
        mask0 = ~((b[1:,0] == b[:-1,0]) & (b[1:,1] == b[:-1,1]))
        ids = np.concatenate(([0], mask0  ))
        np.cumsum(ids, out=ids)
        return ids[argsort_unique(sidx)]
    

    示例运行 -

    In [69]: tups
    Out[69]: [(1, 2), ('a', 'b'), (3, 4), ('c', 5), (6, 'd'), ('a', 'b'), (3, 4)]
    
    In [70]: unique_return_inverse_tuples(tups)
    Out[70]: array([0, 3, 1, 2, 4, 3, 1])
    

    【讨论】:

    • 我还没有测试任何东西。但是,我相信np.unique 会在您使用return_inverse=1 时进行排序。这使得这O(nlogn)。如果我错了,请纠正我。
    • @piRSquared 数组转换本身看起来就像是这个瓶颈。鉴于混合类型的数据,这里看起来 NumPy 不是最佳选择。
    【解决方案5】:

    我不知道时间安排,但一种简单的方法是在各个轴上使用 numpy.unique

    tups = [(1, 2), ('a', 'b'), (3, 4), ('c', 5), (6, 'd'), ('a', 'b'), (3, 4)]
    res = np.unique(tups, return_inverse=1, axis=0)
    print res
    

    产生

    (array([['1', '2'],
           ['3', '4'],
           ['6', 'd'],
           ['a', 'b'],
           ['c', '5']],
          dtype='|S11'), array([0, 3, 1, 4, 2, 3, 1], dtype=int64))
    

    数组是自动排序的,但这应该不是问题。

    【讨论】:

    • 我怎么错过了np.uniqueaxis参数!!谢谢!对@Divikar 的回答仍然有同样的批评。我相信这是 O(nlongn) 不会像pd.factorize 那样快。不过我会测试一下看看。
    • 这只适用于 numpy 1.13... 1.12 没有轴
    • @GergesDib 这就是我错过它的原因:-)
    【解决方案6】:

    我要给出这个答案

    pd.factorize([str(x) for x in tups])
    

    但是,在运行了一些测试之后,它并没有成为所有测试中最快的。由于我已经完成了这项工作,我将在此处显示以进行比较:

    @AChampion

    %timeit [d[tup] if tup in d else d.setdefault(tup, next(c)) for tup in tups]
    1000000 loops, best of 3: 1.66 µs per loop
    

    @Divakar

    %timeit np.unique(get_row_view(np.array(tups)), return_inverse=1)[1]
    # 10000 loops, best of 3: 58.1 µs per loop
    

    @self

    %timeit pd.factorize([str(x) for x in tups])
    # 10000 loops, best of 3: 65.6 µs per loop
    

    @root

    %timeit pd.Series(tups).factorize()[0] 
    # 1000 loops, best of 3: 199 µs per loop
    

    编辑

    对于具有 100K 条目的大数据,我们有:

    tups = [(np.random.randint(0, 10), np.random.randint(0, 10)) for i in range(100000)]
    

    @root

    %timeit pd.Series(tups).factorize()[0] 
    100 loops, best of 3: 10.9 ms per loop
    

    @AChampion

    %timeit [d[tup] if tup in d else d.setdefault(tup, next(c)) for tup in tups]
    
    # 10 loops, best of 3: 16.9 ms per loop
    

    @Divakar

    %timeit np.unique(get_row_view(np.array(tups)), return_inverse=1)[1]
    # 10 loops, best of 3: 81 ms per loop
    

    @self

    %timeit pd.factorize([str(x) for x in tups])
    10 loops, best of 3: 87.5 ms per loop
    

    【讨论】:

    • 这是针对小数据的,我敢肯定这对于大数据看起来会有所不同。
    【解决方案7】:

    您可以使用SKLearn的MultiLabelBinarizer,它将为您提供一系列二进制编码:

    from sklearn.preprocessing import MultiLabelBinarizer
    
    mlb = MultiLabelBinarizer()
    codes = mlb.fit_transform(np.array(tups)) # Must be passed as an array
    
    >>> codes
    array([[1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
           [0, 0, 0, 0, 0, 0, 1, 1, 0, 0],
           [0, 0, 1, 1, 0, 0, 0, 0, 0, 0],
           [0, 0, 0, 0, 1, 0, 0, 0, 1, 0],
           [0, 0, 0, 0, 0, 1, 0, 0, 0, 1],
           [0, 0, 0, 0, 0, 0, 1, 1, 0, 0],
           [0, 0, 1, 1, 0, 0, 0, 0, 0, 0]])
    

    可以使用np.packbits(codes) 将这些转换为小数(如果需要):

    array([192,   0, 195,   0,  34,   4,  64, 195,   0], dtype=uint8)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-08-06
      • 2019-10-20
      • 1970-01-01
      • 1970-01-01
      • 2012-05-01
      • 2020-04-05
      • 1970-01-01
      • 2014-04-27
      相关资源
      最近更新 更多