Ответ 1
Инициализируйте список кортежей как Серию, затем вызовите factorize
:
pd.Series(tups).factorize()[0]
[0 1 2 3 4 1 2]
определение
factorize: сопоставить каждый уникальный объект в уникальное целое число. Как правило, диапазон целых чисел, сопоставленных с, равен нулю от 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
Вторая проблема
Хеширование не гарантируется уникальным!
Вопрос
Что такое супер быстрый способ факторизации списка кортежей?
Timing
обе оси находятся в лог-пространстве
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)
Инициализируйте список кортежей как Серию, затем вызовите factorize
:
pd.Series(tups).factorize()[0]
[0 1 2 3 4 1 2]
Простой способ сделать это - использовать 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]
@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})
Подход № 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])
Я не знаю о таймингах, но простой подход будет использовать 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))
Массив автоматически сортируется, но это не должно быть проблемой.
Я собирался дать этот ответ
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
ИЗМЕНИТЬ
Для больших данных со 100 тыс. записей мы имеем:
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