Python / NumPy первое вхождение подмассива - PullRequest
22 голосов
/ 18 августа 2011

В Python или NumPy, как лучше всего узнать первое вхождение подмассива?

Например, у меня есть

a = [1, 2, 3, 4, 5, 6]
b = [2, 3, 4]

Какой самый быстрый способ (запуститьпо времени), чтобы узнать, где b встречается в?Я понимаю, что для строк это очень легко, но как насчет списка или numpy ndarray?

Спасибо большое!

[РЕДАКТИРОВАНИЕ] Я предпочитаю NumPy решение, так как из моего опыта NumPy векторизация намного быстрее, чем понимание списка Python.Между тем, большой массив огромен, поэтому я не хочу превращать его в строку;это будет (слишком) долго.

Ответы [ 9 ]

17 голосов
/ 19 декабря 2013

Подход, основанный на свертке, который должен быть более эффективным с точки зрения памяти, чем подход, основанный на stride_tricks:

def find_subsequence(seq, subseq):
    target = np.dot(subseq, subseq)
    candidates = np.where(np.correlate(seq,
                                       subseq, mode='valid') == target)[0]
    # some of the candidates entries may be false positives, double check
    check = candidates[:, np.newaxis] + np.arange(len(subseq))
    mask = np.all((np.take(seq, check) == subseq), axis=-1)
    return candidates[mask]

При действительно больших массивах может быть невозможно использовать подход stride_tricks, ноодин все еще работает:

haystack = np.random.randint(1000, size=(1e6))
needle = np.random.randint(1000, size=(100,))
# Hide 10 needles in the haystack
place = np.random.randint(1e6 - 100 + 1, size=10)
for idx in place:
    haystack[idx:idx+100] = needle

In [3]: find_subsequence(haystack, needle)
Out[3]: 
array([253824, 321497, 414169, 456777, 635055, 879149, 884282, 954848,
       961100, 973481], dtype=int64)

In [4]: np.all(np.sort(place) == find_subsequence(haystack, needle))
Out[4]: True

In [5]: %timeit find_subsequence(haystack, needle)
10 loops, best of 3: 79.2 ms per loop
17 голосов
/ 18 августа 2011

Я предполагаю, что вы ищете решение для numpy, а не простое понимание списка или цикл.Одним из подходов может быть использование метода скользящее окно для поиска окон соответствующего размера.Вот функция roll_window:

>>> def rolling_window(a, size):
...     shape = a.shape[:-1] + (a.shape[-1] - size + 1, size)
...     strides = a.strides + (a. strides[-1],)
...     return numpy.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)
... 

Тогда вы можете сделать что-то вроде

>>> a = numpy.arange(10)
>>> numpy.random.shuffle(a)
>>> a
array([7, 3, 6, 8, 4, 0, 9, 2, 1, 5])
>>> rolling_window(a, 3) == [8, 4, 0]
array([[False, False, False],
       [False, False, False],
       [False, False, False],
       [ True,  True,  True],
       [False, False, False],
       [False, False, False],
       [False, False, False],
       [False, False, False]], dtype=bool)

Чтобы сделать это действительно полезным, вам нужно уменьшить его вдоль оси 1, используя all:

>>> numpy.all(rolling_window(a, 3) == [8, 4, 0], axis=1)
array([False, False, False,  True, False, False, False, False], dtype=bool)

Тогда вы можете использовать это, как если бы вы использовали логический массив.Простой способ получить индекс:

>>> bool_indices = numpy.all(rolling_window(a, 3) == [8, 4, 0], axis=1)
>>> numpy.mgrid[0:len(bool_indices)][bool_indices]
array([3])

Для списков вы можете адаптировать один из этих скользящих окон итераторов для использования аналогичного подхода.

Для очень большие массивы и подмассивы, вы можете сэкономить память следующим образом:

>>> windows = rolling_window(a, 3)
>>> sub = [8, 4, 0]
>>> hits = numpy.ones((len(a) - len(sub) + 1,), dtype=bool)
>>> for i, x in enumerate(sub):
...     hits &= numpy.in1d(windows[:,i], [x])
... 
>>> hits
array([False, False, False,  True, False, False, False, False], dtype=bool)
>>> hits.nonzero()
(array([3]),)

С другой стороны, это, вероятно, будет медленнее.Насколько медленнее не ясно без тестирования;см. Jamie о другой опции сохранения памяти, которая должна проверять ложные срабатывания.Я полагаю, что разница в скорости между этими двумя решениями будет сильно зависеть от характера ввода.

17 голосов
/ 18 августа 2011

Следующий код должен работать:

[x for x in xrange(len(a)) if a[x:x+len(b)] == b]

Возвращает индекс, с которого начинается паттерн.

8 голосов
/ 18 августа 2011

вы можете вызвать метод tostring () для преобразования массива в строку, а затем вы можете использовать быстрый поиск строки. этот метод может быть быстрее, если у вас есть много подмассивов для проверки.

import numpy as np

a = np.array([1,2,3,4,5,6])
b = np.array([2,3,4])
print a.tostring().index(b.tostring())//a.itemsize
2 голосов
/ 18 августа 2011

Еще одна попытка, но я уверен, что есть более питонский и эффективный способ сделать это ...

def array_match(a, b):
    for i in xrange(0, len(a)-len(b)+1):
        if a[i:i+len(b)] == b:
            return i
    return None
a = [1, 2, 3, 4, 5, 6]
b = [2, 3, 4]

print array_match(a,b)
1

(Этот первый ответ не был в рамках вопроса, как упомянул cdhowie)

set(a) & set(b) == set(b)
1 голос
/ 02 апреля 2017

Вот довольно простой вариант:

def first_subarray(full_array, sub_array):
    n = len(full_array)
    k = len(sub_array)
    matches = np.argwhere([np.all(full_array[start_ix:start_ix+k] == sub_array) 
                   for start_ix in range(0, n-k+1)])
    return matches[0]

Тогда, используя оригинальные векторы a, b, получим:

a = [1, 2, 3, 4, 5, 6]
b = [2, 3, 4]
first_subarray(a, b)
Out[44]: 
array([1], dtype=int64)
1 голос
/ 11 января 2017

Я знаю, что это довольно старый вопрос, но недавно мне пришлось решить его быстрым и эффективным способом, и самый быстрый метод (особенно для длинных массивов), который я нашел, был, я решил оставить его здесь для справки:

data = np.array([1, 2, 3, 4, 5, 6])
sequence = np.array([3, 4, 5])
data.tostring().index(sequence.tostring())//data.itemize

Вы должны быть осторожны, чтобы и массив, и последовательность имели одинаковый тип d.

0 голосов
/ 05 июля 2019

Быстрое сравнение трех предложенных решений (среднее время 100 итераций для случайно созданных векторов.):

import time
import collections
import numpy as np


def function_1(seq, sub):
    # direct comparison
    seq = list(seq)
    sub = list(sub)
    return [i for i in range(len(seq) - len(sub)) if seq[i:i+len(sub)] == sub]

def function_2(seq, sub):
    # Jamie's solution
    target = np.dot(sub, sub)
    candidates = np.where(np.correlate(seq, sub, mode='valid') == target)[0]
    check = candidates[:, np.newaxis] + np.arange(len(sub))
    mask = np.all((np.take(seq, check) == sub), axis=-1)
    return candidates[mask]

def function_3(seq, sub):
    # HYRY solution
    return seq.tostring().index(sub.tostring())//seq.itemsize


# --- assessment time performance
N = 100

seq = np.random.choice([0, 1, 2, 3, 4, 5, 6], 3000)
sub = np.array([1, 2, 3])

tim = collections.OrderedDict()
tim.update({function_1: 0.})
tim.update({function_2: 0.})
tim.update({function_3: 0.})

for function in tim.keys():
    for _ in range(N):
        seq = np.random.choice([0, 1, 2, 3, 4], 3000)
        sub = np.array([1, 2, 3])
        start = time.time()
        function(seq, sub)
        end = time.time()
        tim[function] += end - start

timer_dict = collections.OrderedDict()
for key, val in tim.items():
    timer_dict.update({key.__name__: val / N})

print(timer_dict)

Что приведет (на моей старой машине) к:

OrderedDict([
('function_1', 0.0008518099784851074), 
('function_2', 8.157730102539063e-05), 
('function_3', 6.124973297119141e-06)
])
0 голосов
/ 14 сентября 2017

Создать массив (или преобразовать), как это

>>> ar = numpy.array([1,2,3,4,5,1,2,8,9,1,2,3,4,6], dtype=str)
>>> ar.tostring()
'12345128912346'
>>> ss.count('123')
2
>>> ss.index('123')
0
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...