Поиск большого массива по двум столбцам

1

У меня большой массив, который выглядит как-то внизу:

np.random.seed(42)

arr = np.random.permutation(np.array([
    (1,1,2,2,2,2,3,3,4,4,4),
    (8,9,3,4,7,9,1,9,3,4,50000)
]).T)

Он не сортируется, строки этого массива уникальны, я также знаю границы для значений в обоих столбцах, они равны [0, n] и [0, k]. Таким образом, максимально возможный размер массива равен (n+1)*(k+1), но фактический размер ближе к журналу.

Мне нужно найти массив по обоим столбцам, чтобы найти такую row которая arr[row,:] = (i,j) и вернуть -1 когда (i,j) отсутствует в массиве. Наивная реализация для такой функции:

def get(arr, i, j):
    cond = (arr[:,0] == i) & (arr[:,1] == j)
    if np.any(cond):
        return np.where(cond)[0][0]
    else:
        return -1

К сожалению, поскольку в моем случае arr очень большой (> 90M строк), это очень неэффективно, тем более, что мне нужно будет вызвать get() несколько раз.

В качестве альтернативы я попытался перевести это на dict с (i,j) ключами, чтобы

index[(i,j)] = row

к которым можно получить доступ:

def get(index, i, j):
   try:
      retuen index[(i,j)]
   except KeyError:
      return -1

Это работает (и намного быстрее, когда тестируется на меньших данных, чем у меня), но опять же, создавая dict on-fly by

index = {}
for row in range(arr.shape[0]):
    i,j = arr[row, :]
    index[(i,j)] = row

занимает огромное количество времени и ест много оперативной памяти в моем случае. Я также думал о первой сортировке arr а затем использовал что-то вроде np.searchsorted, но это никуда не привело.

Так что мне нужна быстрая функция get(arr, i, j) которая возвращает

>>> get(arr, 2, 3)
4
>>> get(arr, 4, 100)
-1 
Теги:
numpy

4 ответа

0
Лучший ответ

Кажется, я передумал эту проблему, есть простое решение. Я рассматривал возможность фильтрации и подмножества массива или использования index[(i,j)] = row dict index[(i,j)] = row. Фильтрация и подмножество были медленными (O (n) при поиске), в то время как использование dict было быстрым (время доступа O (1)), но создание dict было медленным и интенсивным.

Простым решением этой проблемы является использование вложенных dicts.

index = {}

for row in range(arr.shape[0]):
    i,j = arr[row, :]
    try:
        index[i][j] = row
    except KeyError:
        index[i] = {}
        index[i][j] = row

def get(index, i, j):
    try:
        return index[i][j]
    except KeyError:
        return -1

В качестве альтернативы вместо dict на более высоком уровне я мог бы использовать index = defaultdict(dict), что позволило бы присвоить index[i][j] = row напрямую, без try... except условий, но затем defaultdict(dict) создаст пустой {} при запросе на несуществующий i функцией get(index, i, j), поэтому он будет без необходимости увеличивать index.

Время доступа O (1) для первого dict и O (1) для вложенных dicts, поэтому в основном это O (1). Верхний уровень dict имеет управляемый размер (ограниченный n <n * k), а вложенные dicts малы (порядок вложенности выбран на основании того, что в моем случае k << n). Построение вложенного dict также очень быстро, даже для> 90M строк в массиве. Более того, его можно легко распространить на более сложные случаи.

1

Решение

Python предлагает тип set для хранения уникальных значений, но, к сожалению, не упорядоченная версия набора. Но вы можете использовать пакет с упорядоченным набором.

Создайте OrderedSet из данных. К счастью, это нужно сделать только один раз:

import ordered_set

o = ordered_set.OrderedSet(map(tuple, arr))

def ordered_get(o, i, j):
    try:
        return o.index((i,j))
    except KeyError:
        return -1

время выполнения

Поиск индекса значения должен быть O (1), согласно документации:

In [46]: %timeit get(arr, 2, 3)
10.6 µs ± 39 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [47]: %timeit ordered_get(o, 2, 3)
1.16 µs ± 14.6 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [48]: %timeit ordered_get(o, 2, 300)
1.05 µs ± 2.67 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Тестирование этого для гораздо большего массива:

a2 = random.randint(10000, size=1000000).reshape(-1,2)
o2 = ordered_set.OrderedSet()
for t in map(tuple, a2):
    o2.add(t)

In [65]: %timeit get(a2, 2, 3)
1.05 ms ± 2.14 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

In [66]: %timeit ordered_get(o2, 2, 3)
1.03 µs ± 2.12 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [67]: %timeit ordered_get(o2, 2, 30000)
1.06 µs ± 28.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Похоже, что это O (1) время исполнения.

  • 0
    Я только что попробовал это с 63M элементами, и экземпляр ipython использует 11.7G памяти. Возможно, стоит разделить набор данных или просто использовать компьютер с достаточным объемом памяти, если это нужно сделать за один раз.
  • 0
    Благодарю. К сожалению, в качестве точечного решения, имея около 100 миллионов элементов, я также сталкиваюсь с проблемами ОЗУ (я тестировал разные варианты своих идей, вашего решения и других подходов). Если вы посмотрите на исходный код, OrdredSet использует python dict под капотом ( github.com/LuminosoInsight/ordered-set/blob/master/… ), так что это не удивительно.
Показать ещё 5 комментариев
1

Частичным решением будет:

In [36]: arr
Out[36]: 
array([[    2,     9],
       [    1,     8],
       [    4,     4],
       [    4, 50000],
       [    2,     3],
       [    1,     9],
       [    4,     3],
       [    2,     7],
       [    3,     9],
       [    2,     4],
       [    3,     1]])

In [37]: (i,j) = (2, 3)

# we can use 'assume_unique=True' which can speed up the calculation    
In [38]: np.all(np.isin(arr, [i,j], assume_unique=True), axis=1, keepdims=True)
Out[38]: 
array([[False],
       [False],
       [False],
       [False],
       [ True],
       [False],
       [False],
       [False],
       [False],
       [False],
       [False]])

# we can use 'assume_unique=True' which can speed up the calculation
In [39]: mask = np.all(np.isin(arr, [i,j], assume_unique=True), axis=1, keepdims=True)

In [40]: np.argwhere(mask)
Out[40]: array([[4, 0]])

Если вам нужен конечный результат в виде скаляра, то не используйте аргумент keepdims и отбрасывайте массив на скаляр, например:

    # we can use 'assume_unique=True' which can speed up the calculation
In [41]: mask = np.all(np.isin(arr, [i,j], assume_unique=True), axis=1)

In [42]: np.argwhere(mask)
Out[42]: array([[4]])

In [43]: np.asscalar(np.argwhere(mask))
Out[43]: 4
  • 0
    Это примерно в 3-4 раза медленнее , чем get в этом вопросе, по словам моего тестирования. Это быстрее для больших массивов?
  • 0
    Проблема заключается в том, что при каждом поиске (каждый раз медленно) выполняется поиск по всем строкам, в то время как мне нужно что-то более быстрое, например, таблица хеширования, которая не перебирает строки
Показать ещё 3 комментария
0
def get_agn(arr, i, j):
    idx = np.flatnonzero((arr[:,0] == j) & (arr[:,1] == j))
    return -1 if idx.size == 0 else idx[0]

Кроме того, на всякий случай, когда вы думаете о решении ordered_set, здесь лучше (однако в обоих случаях см. Тесты времени ниже):

d = { (i, j): k for k, (i, j) in enumerate(arr)}
def unordered_get(d, i, j):
    return d.get((i, j), -1)

и это "полный" эквивалент (который строит словарь внутри функции):

def unordered_get_full(arr, i, j):
    d = { (i, j): k for k, (i, j) in enumerate(arr)}
    return d.get((i, j), -1)

Сроки испытаний:

Сначала определите функцию @kmario23:

def get_kmario23(arr, i, j):
    # fundamentally, kmario23 code re-aranged to return scalars
    # and -1 when (i, j) not found:
    mask = np.all(np.isin(arr, [i,j], assume_unique=True), axis=1)
    idx = np.argwhere(mask)[0]
    return -1 if idx.size == 0 else np.asscalar(idx[0])

Во-вторых, определите функцию @ChristophTerasa (оригинал и полную версию):

import ordered_set
o = ordered_set.OrderedSet(map(tuple, arr))
def ordered_get(o, i, j):
    try:
        return o.index((i,j))
    except KeyError:
        return -1

def ordered_get_full(arr, i, j):
    # "Full" version that builds ordered set inside the function
    o = ordered_set.OrderedSet(map(tuple, arr))
    try:
        return o.index((i,j))
    except KeyError:
        return -1

Создайте несколько больших данных:

arr = np.random.randint(1, 2000, 200000).reshape((-1, 2))

Сроки:

In [55]: %timeit get_agn(arr, *arr[-1])
149 µs ± 3.17 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

In [56]: %timeit get_kmario23(arr, *arr[-1])
1.42 ms ± 17.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

In [57]: %timeit get_kmario23(arr, *arr[0])
1.2 ms ± 14.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Упорядоченные заданные тесты:

In [80]: o = ordered_set.OrderedSet(map(tuple, arr))

In [81]: %timeit ordered_get(o, *arr[-1])
1.74 µs ± 32.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [82]: %timeit ordered_get_full(arr, *arr[-1]) # include ordered set creation time
166 ms ± 2.16 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Неупорядоченные словарные тесты:

In [83]: d = { (i, j): k for k, (i, j) in enumerate(arr)}

In [84]: %timeit unordered_get(d, *arr[-1])
1.18 µs ± 21.1 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [85]: %timeit unordered_get_full(arr, *arr[-1])
102 ms ± 1.45 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Таким образом, принимая во внимание время, необходимое для создания либо упорядоченного или неупорядоченного словаря, эти методы довольно медленные. Вы должны планировать запуск нескольких сотен поисков по тем же данным для этих методов. Даже тогда нет необходимости использовать пакет ordered_set - регулярные словари бывают быстрее.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню