If your goal is speed and your data is easily categorizable, I would recommend a numpy solution.
Let's say you have
a = np.array(['A', 'B', 'A', 'B', 'A', 'C', 'D', 'E', 'D', 'E', 'D', 'F', 'G', 'A', 'B'])
tolerance = 1
To check if any elements are equal at exactly tolerance, you can do a diff-like operation, but with equality:
tolerance += 1
mask = a[:-tolerance] == a[tolerance:]
If you smear this boolean mask tolerance elements to the right, each contiguous run will be the elements you are interested in. One short way to do this is using np.lib.stride_tricks.as_strided:
def smear(mask, n):
view = np.lib.stride_tricks.as_strided(mask, shape=(n + 1, mask.size - n),
strides=mask.strides * 2)
view[1:, view[0]] = True
You could even make this into a one-liner, since it operates in-place:
np.lib.stride_tricks.as_strided(mask, shape=(n + 1, mask.size - n),
strides=mask.strides * 2)[1:, mask[:-n]] = True
Then you apply it:
smear(mask, tolerance)
Contiguous runs are easy to find and extract using a combination of np.diff, np.flatnonzero and np.split (ref):
result = np.split(a, np.flatnonzero(np.diff(m)) + 1)[1 - m[0]::2]
The only thing that is lacking in this solution is that it will not pick up matching elements that occur less than tolerance away of each-other. To do this, we can use np.lib.stride_tricks.as_strided to make our mask in a way that takes the tolerance into account (using np.any):
b = np.lib.stride_tricks.as_strided(np.r_[a, np.zeros(tolerance, dtype=a.dtype)],
shape=(tolerance + 1, a.size),
strides=a.strides * 2)
b is now a 3x15 array (where a has length 15), with the second dimension just being the characters following the start. Remember that this is just a view into the original data. For a large array, this operation is basically free.
Now you can apply np.any to the first dimension to figure out which characters are repeated within tolerance of each other:
mask = np.any(b[0] == b[1:], axis=0)
From here, we continue as before. This makes for a fairly small function:
TL;DR
def find_patterns(a, tol):
a = np.asanyarray(a)
tol += 1
b = np.lib.stride_tricks.as_strided(np.r_[a, np.zeros(tol, dtype=a.dtype)],
shape=(tol + 1, a.size),
strides=a.strides * 2)
mask = np.any(b[0] == b[1:], axis=0)
np.lib.stride_tricks.as_strided(mask, shape=(tol + 1, mask.size - tol),
strides=mask.strides * 2)[1:, mask[:-tol]] = True
return np.split(a, np.flatnonzero(np.diff(mask)) + 1)[1 - mask[0]::2]
>>> find_patterns(['A', 'B', 'A', 'B', 'A', 'C', 'D', 'E', 'D', 'E', 'D', 'F', 'G', 'A', 'B'], 1)
[array(['A', 'B', 'A', 'B', 'A'], dtype='<U1'),
array(['D', 'E', 'D', 'E', 'D'], dtype='<U1')]
>>> find_patterns(['A', 'B', 'C', 'A', 'D', 'E', 'A'], 1)
[]
>>> find_patterns(['A', 'B', 'C', 'A', 'D', 'E', 'A'], 2)
[array(['A', 'B', 'C', 'A', 'D', 'E', 'A'], dtype='<U1')]
Addendum
If you look through the references below, you will find that the methods for smearing a mask and finding the masked portions of the array shown above here are were chosen for conciseness, not speed. A faster way to smear a mask, taken from here, is:
def smear(mask, n):
n += 1
mask1 = mask.copy()
len0, len1 = 1, 1
while len0 + len1 < n:
mask[len0:] |= mask1[:-len0]
mask, mask1 = mask1, mask
len0, len1 = len1, len0 + len1
mask1[n - len0:] |= mask[:-n + len0]
return mask1
Similarly, a faster way to extract the contiguous masked regions from the array (taken from here) is:
def extract_masked(a, mask):
mask = np.concatenate(([False], mask, [False]))
idx = np.flatnonzero(mask[1:] != mask[:-1])
return [a[idx[i]:idx[i + 1]] for i in range(0, len(idx), 2)]
References