Эффективная реализация Python сравнений массивов numpy
фон
у меня есть два массива numpy, которые я хотел бы использовать для выполнения некоторых операций сравнения наиболее эффективным/быстрым способом. Оба содержат только неподписанные ints.
pairs
- это n x 2 x 3
массив, который содержит длинный список парных 3D-координат (для некоторой номенклатуры pairs
массив содержит набор пар...) - т. е.
# full pairs array
In [145]: pairs
Out[145]:
array([[[1, 2, 4],
[3, 4, 4]],
.....
[[1, 2, 5],
[5, 6, 5]]])
# each entry contains a pair of 3D coordinates
In [149]: pairs[0]
Out[149]:
array([[1, 2, 4],
[3, 4, 4]])
positions
это n x 3
массив, который содержит набор 3D координаты
In [162]: positions
Out[162]:
array([[ 1, 2, 4],
[ 3, 4, 5],
[ 5, 6, 3],
[ 3, 5, 6],
[ 6, 7, 5],
[12, 2, 5]])
цель
Я хочу создать массив, который является подмножеством pairs
массив, но содержит только записи, где не более одной из пар находится в массиве позиций-т. е. не должно быть пар, где обе пары лежат в массиве позиций. Для некоторой информации о домене каждая пара будет иметь по крайней мере одну из парных позиций в списке позиций.
подходы пытались до сих пор
Мой первоначальный наивный подход к петля над каждой парой в pairs
массив и вычесть каждую из двух парных позиций из positions
вектор, определяющий, если в обоих случаях мы нашли совпадение, указанное наличием 0 в обоих векторах, которые исходят из операций вычитания:
if (~(positions-pair[0]).any(axis=1)).any() and
(~(positions-pair[1]).any(axis=1)).any():
# both members of the pair were in the positions array -
# these weren't the droids we were looking for
pass
else:
# append this set of pairs to a new matrix
это отлично работает и используеткакой-то векторизация, но, вероятно, есть лучший способ сделать это?
для некоторых других чувствительных к производительности частей этой программы у меня есть переписанные вещи с Cython, которые привели к массовому ускорению, хотя в этом случае (по крайней мере, на основе наивной вложенной реализации для цикла) это было немного медленнее, чем подход, описанный выше.
если у людей есть предложения, я рад профилировать и отчитываться (у меня есть вся инфраструктура профилирования).
2 ответов
подход #1
как уже упоминалось в вопросе, оба массива содержат только unsigned ints
, который может быть использован для слияния XYZ
в эквивалентную версию линейных индексов, которая была бы уникальной для каждого уникального XYZ
триплет. Реализация будет выглядеть примерно так -
maxlen = np.max(pairs,axis=(0,1))
dims = np.append(maxlen[::-1][:-1].cumprod()[::-1],1)
pairs1D = np.dot(pairs.reshape(-1,3),dims)
positions1D = np.dot(positions,dims)
mask_idx = ~(np.in1d(pairs1D,positions1D).reshape(-1,2).all(1))
out = pairs[mask_idx]
поскольку вы имеете дело с 3D-координатами, вы также можете использоватьcdist
для проверки идентичны XYZ
тройни между входной массив. Далее перечислены две реализации с учетом этой идеи.
подход #2
from scipy.spatial.distance import cdist
p0 = cdist(pairs[:,0,:],positions)
p1 = cdist(pairs[:,1,:],positions)
out = pairs[((p0==0) | (p1==0)).sum(1)!=2]
подход #3
mask_idx = ~((cdist(pairs.reshape(-1,3),positions)==0).any(1).reshape(-1,2).all(1))
out = pairs[mask_idx]
во время выполнения тестов -
In [80]: n = 5000
...: pairs = np.random.randint(0,100,(n,2,3))
...: positions= np.random.randint(0,100,(n,3))
...:
In [81]: def cdist_split(pairs,positions):
...: p0 = cdist(pairs[:,0,:],positions)
...: p1 = cdist(pairs[:,1,:],positions)
...: return pairs[((p0==0) | (p1==0)).sum(1)!=2]
...:
...: def cdist_merged(pairs,positions):
...: mask_idx = ~((cdist(pairs.reshape(-1,3),positions)==0).any(1).reshape(-1,2).all(1))
...: return pairs[mask_idx]
...:
...: def XYZ_merged(pairs,positions):
...: maxlen = np.max(pairs,axis=(0,1))
...: dims = np.append(maxlen[::-1][:-1].cumprod()[::-1],1)
...: pairs1D = np.dot(pairs.reshape(-1,3),dims)
...: positions1D = np.dot(positions,dims)
...: mask_idx1 = ~(np.in1d(pairs1D,positions1D).reshape(-1,2).all(1))
...: return pairs[mask_idx1]
...:
In [82]: %timeit cdist_split(pairs,positions)
1 loops, best of 3: 662 ms per loop
In [83]: %timeit cdist_merged(pairs,positions)
1 loops, best of 3: 615 ms per loop
In [84]: %timeit XYZ_merged(pairs,positions)
100 loops, best of 3: 4.02 ms per loop
проверить результаты -
In [85]: np.allclose(cdist_split(pairs,positions),cdist_merged(pairs,positions))
Out[85]: True
In [86]: np.allclose(cdist_split(pairs,positions),XYZ_merged(pairs,positions))
Out[86]: True
уточняя мой комментарий:
расширения pairs
чтобы быть более интересным. Не стесняйтесь тестировать с большим, более реалистичным массивом:
In [260]: pairs = np.array([[[1,2,4],[3,4,4]],[[1,2,5],[5,6,5]],[[3,4,5],[3,5,6]],[[6,7,5],[1,2,3]]])
In [261]: positions = np.array([[ 1, 2, 4],
[ 3, 4, 5],
[ 5, 6, 3],
[ 3, 5, 6],
[ 6, 7, 5],
[12, 2, 5]])
разверните оба массива в широковещательные формы:
In [262]: I = pairs[None,...]==positions[:,None,None,:]
In [263]: I.shape
Out[263]: (6, 4, 2, 3)
большой логический массив, показывающий соответствие элементов по всем измерениям. Fell свободно заменять другие сравнения (difference ==0
, np.isclose
для поплавков и т. д.).
In [264]: J = I.all(axis=-1).any(axis=0).sum(axis=-1)
In [265]: J
Out[265]: array([1, 0, 2, 1])
консолидировать результаты по различным измерениям. Матч все числа по координатам, совпадают с любыми позициями, подсчет совпадений по парам.
In [266]: pairs[J==1,...]
Out[266]:
array([[[1, 2, 4],
[3, 4, 4]],
[[6, 7, 5],
[1, 2, 3]]])
J==1
представляют элементы, где совпадает только одно значение пары. (см. примечание в конце)
сочетание any
, and
и sum
работа это для теста, но может потребоваться регулировка с большим тестом(ы). Но идея вообще применима.
для размера массивов, которые https://stackoverflow.com/a/31901675/901925 тесты, мое решение довольно медленно. Особенно это делает ==
тест, который приводит к I
С (5000, 5000, 2, 3)
.
сжатие последнего измерения очень помогает
dims = np.array([10000,100,1]) # simpler version of dims from XYZmerged
pairs1D = np.dot(pairs.reshape(-1,3),dims)
positions1D = np.dot(positions,dims)
I1d = pairs1D[:,None]==positions1D[None,:]
J1d = I1d.any(axis=-1).reshape(pairs.shape[:2]).sum(axis=-1)
я изменил J1d
expression to match mine-подсчитать количество совпадений на пару.
на in1d1
это Divakar
использует еще быстрее:
mask = np.in1d(pairs1D, positions1D).reshape(-1,2)
Jmask = mask.sum(axis=-1)
я только что понял, что ОП просит at most one of the pairs is in the positions array
. Где, как я проверяю для exactly one match per pair
. поэтому все мои тесты должны быть изменены на pairs[J<2,...]
.
(в моей конкретной случайной выборке для n=5000 это оказывается всем. Нет pairs
где оба находятся в positions
. 54 из 5000 у J==1
, остальные 0-Нет).