Một kinh điển cartesian_product
(gần như)
Có nhiều cách tiếp cận vấn đề này với các tính chất khác nhau. Một số nhanh hơn những cái khác, và một số có mục đích chung hơn. Sau rất nhiều thử nghiệm và điều chỉnh, tôi đã thấy rằng hàm sau, tính toán một chiều n cartesian_product
, nhanh hơn hầu hết các hàm khác cho nhiều đầu vào. Đối với một cặp cách tiếp cận phức tạp hơn một chút, nhưng thậm chí nhanh hơn một chút trong nhiều trường hợp, xem câu trả lời của Paul Panzer .
Với câu trả lời đó, đây không còn là triển khai nhanh nhất của sản phẩm cartesian numpy
mà tôi biết. Tuy nhiên, tôi nghĩ rằng sự đơn giản của nó sẽ tiếp tục làm cho nó trở thành một chuẩn mực hữu ích để cải thiện trong tương lai:
def cartesian_product(*arrays):
la = len(arrays)
dtype = numpy.result_type(*arrays)
arr = numpy.empty([len(a) for a in arrays] + [la], dtype=dtype)
for i, a in enumerate(numpy.ix_(*arrays)):
arr[...,i] = a
return arr.reshape(-1, la)
Điều đáng nói là chức năng này sử dụng ix_
một cách bất thường; trong khi việc sử dụng tài liệu ix_
là để tạo các chỉ mục thành một mảng, điều đó xảy ra là các mảng có cùng hình dạng có thể được sử dụng để gán truyền phát. Rất cám ơn mgilson , người đã truyền cảm hứng cho tôi để thử sử dụng cách ix_
này và unutbu , người đã cung cấp một số phản hồi cực kỳ hữu ích cho câu trả lời này, bao gồm cả gợi ý sử dụng numpy.result_type
.
Các lựa chọn thay thế đáng chú ý
Đôi khi nhanh hơn để viết các khối bộ nhớ liền kề theo thứ tự Fortran. Đó là cơ sở của sự thay thế này, cartesian_product_transpose
đã được chứng minh nhanh hơn trên một số phần cứng so với cartesian_product
(xem bên dưới). Tuy nhiên, câu trả lời của Paul Panzer, sử dụng cùng một nguyên tắc, thậm chí còn nhanh hơn. Tuy nhiên, tôi bao gồm điều này ở đây cho độc giả quan tâm:
def cartesian_product_transpose(*arrays):
broadcastable = numpy.ix_(*arrays)
broadcasted = numpy.broadcast_arrays(*broadcastable)
rows, cols = numpy.prod(broadcasted[0].shape), len(broadcasted)
dtype = numpy.result_type(*arrays)
out = numpy.empty(rows * cols, dtype=dtype)
start, end = 0, rows
for a in broadcasted:
out[start:end] = a.reshape(-1)
start, end = end, end + rows
return out.reshape(cols, rows).T
Sau khi hiểu được cách tiếp cận của Panzer, tôi đã viết một phiên bản mới gần như nhanh như của anh ấy, và gần như đơn giản như cartesian_product
:
def cartesian_product_simple_transpose(arrays):
la = len(arrays)
dtype = numpy.result_type(*arrays)
arr = numpy.empty([la] + [len(a) for a in arrays], dtype=dtype)
for i, a in enumerate(numpy.ix_(*arrays)):
arr[i, ...] = a
return arr.reshape(la, -1).T
Điều này dường như có một số chi phí không đổi thời gian liên tục làm cho nó chạy chậm hơn so với Panzer cho các đầu vào nhỏ. Nhưng đối với các đầu vào lớn hơn, trong tất cả các thử nghiệm tôi đã chạy, nó thực hiện tốt như việc thực hiện nhanh nhất của anh ấy ( cartesian_product_transpose_pp
).
Trong các phần sau, tôi bao gồm một số thử nghiệm của các lựa chọn thay thế khác. Hiện tại chúng đã lỗi thời, nhưng thay vì nỗ lực trùng lặp, tôi đã quyết định để chúng ở đây vì lợi ích lịch sử. Đối với các bài kiểm tra cập nhật, xem câu trả lời của Panzer, cũng như của Nico Schlömer .
Thử nghiệm thay thế
Dưới đây là một loạt các bài kiểm tra cho thấy hiệu suất tăng mà một số chức năng này cung cấp liên quan đến một số lựa chọn thay thế. Tất cả các thử nghiệm hiển thị ở đây được thực hiện trên máy lõi tứ, chạy Mac OS 10.12.5, Python 3.6.1 và numpy
1.12.1. Biến thể trên phần cứng và phần mềm được biết là tạo ra kết quả khác nhau, vì vậy YMMV. Chạy thử nghiệm cho chính mình để chắc chắn!
Định nghĩa:
import numpy
import itertools
from functools import reduce
### Two-dimensional products ###
def repeat_product(x, y):
return numpy.transpose([numpy.tile(x, len(y)),
numpy.repeat(y, len(x))])
def dstack_product(x, y):
return numpy.dstack(numpy.meshgrid(x, y)).reshape(-1, 2)
### Generalized N-dimensional products ###
def cartesian_product(*arrays):
la = len(arrays)
dtype = numpy.result_type(*arrays)
arr = numpy.empty([len(a) for a in arrays] + [la], dtype=dtype)
for i, a in enumerate(numpy.ix_(*arrays)):
arr[...,i] = a
return arr.reshape(-1, la)
def cartesian_product_transpose(*arrays):
broadcastable = numpy.ix_(*arrays)
broadcasted = numpy.broadcast_arrays(*broadcastable)
rows, cols = numpy.prod(broadcasted[0].shape), len(broadcasted)
dtype = numpy.result_type(*arrays)
out = numpy.empty(rows * cols, dtype=dtype)
start, end = 0, rows
for a in broadcasted:
out[start:end] = a.reshape(-1)
start, end = end, end + rows
return out.reshape(cols, rows).T
# from https://stackoverflow.com/a/1235363/577088
def cartesian_product_recursive(*arrays, out=None):
arrays = [numpy.asarray(x) for x in arrays]
dtype = arrays[0].dtype
n = numpy.prod([x.size for x in arrays])
if out is None:
out = numpy.zeros([n, len(arrays)], dtype=dtype)
m = n // arrays[0].size
out[:,0] = numpy.repeat(arrays[0], m)
if arrays[1:]:
cartesian_product_recursive(arrays[1:], out=out[0:m,1:])
for j in range(1, arrays[0].size):
out[j*m:(j+1)*m,1:] = out[0:m,1:]
return out
def cartesian_product_itertools(*arrays):
return numpy.array(list(itertools.product(*arrays)))
### Test code ###
name_func = [('repeat_product',
repeat_product),
('dstack_product',
dstack_product),
('cartesian_product',
cartesian_product),
('cartesian_product_transpose',
cartesian_product_transpose),
('cartesian_product_recursive',
cartesian_product_recursive),
('cartesian_product_itertools',
cartesian_product_itertools)]
def test(in_arrays, test_funcs):
global func
global arrays
arrays = in_arrays
for name, func in test_funcs:
print('{}:'.format(name))
%timeit func(*arrays)
def test_all(*in_arrays):
test(in_arrays, name_func)
# `cartesian_product_recursive` throws an
# unexpected error when used on more than
# two input arrays, so for now I've removed
# it from these tests.
def test_cartesian(*in_arrays):
test(in_arrays, name_func[2:4] + name_func[-1:])
x10 = [numpy.arange(10)]
x50 = [numpy.arange(50)]
x100 = [numpy.arange(100)]
x500 = [numpy.arange(500)]
x1000 = [numpy.arange(1000)]
Kết quả kiểm tra:
In [2]: test_all(*(x100 * 2))
repeat_product:
67.5 µs ± 633 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
dstack_product:
67.7 µs ± 1.09 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
cartesian_product:
33.4 µs ± 558 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
cartesian_product_transpose:
67.7 µs ± 932 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
cartesian_product_recursive:
215 µs ± 6.01 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
cartesian_product_itertools:
3.65 ms ± 38.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [3]: test_all(*(x500 * 2))
repeat_product:
1.31 ms ± 9.28 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
dstack_product:
1.27 ms ± 7.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
cartesian_product:
375 µs ± 4.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
cartesian_product_transpose:
488 µs ± 8.88 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
cartesian_product_recursive:
2.21 ms ± 38.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product_itertools:
105 ms ± 1.17 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [4]: test_all(*(x1000 * 2))
repeat_product:
10.2 ms ± 132 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
dstack_product:
12 ms ± 120 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product:
4.75 ms ± 57.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product_transpose:
7.76 ms ± 52.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product_recursive:
13 ms ± 209 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product_itertools:
422 ms ± 7.77 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Trong mọi trường hợp, cartesian_product
như được định nghĩa ở đầu câu trả lời này là nhanh nhất.
Đối với những hàm chấp nhận số lượng mảng đầu vào tùy ý, thì cũng đáng để kiểm tra hiệu năng len(arrays) > 2
. (Cho đến khi tôi có thể xác định lý do tại sao cartesian_product_recursive
ném lỗi trong trường hợp này, tôi đã xóa nó khỏi các thử nghiệm này.)
In [5]: test_cartesian(*(x100 * 3))
cartesian_product:
8.8 ms ± 138 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product_transpose:
7.87 ms ± 91.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product_itertools:
518 ms ± 5.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [6]: test_cartesian(*(x50 * 4))
cartesian_product:
169 ms ± 5.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
cartesian_product_transpose:
184 ms ± 4.32 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
cartesian_product_itertools:
3.69 s ± 73.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [7]: test_cartesian(*(x10 * 6))
cartesian_product:
26.5 ms ± 449 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
cartesian_product_transpose:
16 ms ± 133 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
cartesian_product_itertools:
728 ms ± 16 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [8]: test_cartesian(*(x10 * 7))
cartesian_product:
650 ms ± 8.14 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
cartesian_product_transpose:
518 ms ± 7.09 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
cartesian_product_itertools:
8.13 s ± 122 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Như các thử nghiệm này cho thấy, cartesian_product
vẫn cạnh tranh cho đến khi số lượng mảng đầu vào tăng lên trên (khoảng) bốn. Sau đó, cartesian_product_transpose
có một cạnh nhẹ.
Cần nhắc lại rằng người dùng có phần cứng và hệ điều hành khác có thể thấy các kết quả khác nhau. Ví dụ: các báo cáo unutbu nhìn thấy các kết quả sau cho các thử nghiệm này bằng Ubuntu 14.04, Python 3.4.3 và numpy
1.14.0.dev0 + b7050a9:
>>> %timeit cartesian_product_transpose(x500, y500)
1000 loops, best of 3: 682 µs per loop
>>> %timeit cartesian_product(x500, y500)
1000 loops, best of 3: 1.55 ms per loop
Dưới đây, tôi đi vào một vài chi tiết về các thử nghiệm trước đây tôi đã chạy dọc theo các dòng này. Hiệu suất tương đối của các phương pháp này đã thay đổi theo thời gian, đối với phần cứng khác nhau và các phiên bản khác nhau của Python và numpy
. Mặc dù nó không hữu ích ngay lập tức cho những người sử dụng các phiên bản cập nhật của numpy
nó, nhưng nó minh họa cách mọi thứ đã thay đổi kể từ phiên bản đầu tiên của câu trả lời này.
Một thay thế đơn giản: meshgrid
+dstack
Câu trả lời hiện được chấp nhận sử dụng tile
và repeat
để phát hai mảng với nhau. Nhưng meshgrid
chức năng thực tế là điều tương tự. Đây là đầu ra của tile
và repeat
trước khi được chuyển sang transpose:
In [1]: import numpy
In [2]: x = numpy.array([1,2,3])
...: y = numpy.array([4,5])
...:
In [3]: [numpy.tile(x, len(y)), numpy.repeat(y, len(x))]
Out[3]: [array([1, 2, 3, 1, 2, 3]), array([4, 4, 4, 5, 5, 5])]
Và đây là đầu ra của meshgrid
:
In [4]: numpy.meshgrid(x, y)
Out[4]:
[array([[1, 2, 3],
[1, 2, 3]]), array([[4, 4, 4],
[5, 5, 5]])]
Như bạn có thể thấy, nó gần như giống hệt nhau. Chúng tôi chỉ cần định hình lại kết quả để có được kết quả chính xác như vậy.
In [5]: xt, xr = numpy.meshgrid(x, y)
...: [xt.ravel(), xr.ravel()]
Out[5]: [array([1, 2, 3, 1, 2, 3]), array([4, 4, 4, 5, 5, 5])]
Thay vì tái định hình vào thời điểm này, tuy nhiên, chúng ta có thể vượt qua sản lượng meshgrid
để dstack
và Reshape sau đó, giúp tiết kiệm một số công việc:
In [6]: numpy.dstack(numpy.meshgrid(x, y)).reshape(-1, 2)
Out[6]:
array([[1, 4],
[2, 4],
[3, 4],
[1, 5],
[2, 5],
[3, 5]])
Trái ngược với tuyên bố trong nhận xét này , tôi không thấy bằng chứng nào cho thấy các đầu vào khác nhau sẽ tạo ra các đầu ra có hình dạng khác nhau, và như đã chứng minh ở trên, chúng làm những việc rất giống nhau, vì vậy sẽ rất lạ nếu chúng làm như vậy. Xin vui lòng cho tôi biết nếu bạn tìm thấy một ví dụ mẫu.
Kiểm tra meshgrid
+ dstack
so với repeat
+transpose
Hiệu suất tương đối của hai phương pháp này đã thay đổi theo thời gian. Trong phiên bản trước của Python (2.7), kết quả sử dụng meshgrid
+ dstack
nhanh hơn đáng kể cho các đầu vào nhỏ. (Lưu ý rằng các bài kiểm tra này là từ một phiên bản cũ của câu trả lời này.) Định nghĩa:
>>> def repeat_product(x, y):
... return numpy.transpose([numpy.tile(x, len(y)),
numpy.repeat(y, len(x))])
...
>>> def dstack_product(x, y):
... return numpy.dstack(numpy.meshgrid(x, y)).reshape(-1, 2)
...
Đối với đầu vào có kích thước vừa phải, tôi đã thấy một sự tăng tốc đáng kể. Nhưng tôi đã thử lại các thử nghiệm này với các phiên bản Python mới hơn (3.6.1) và numpy
(1.12.1), trên một máy mới hơn. Hai cách tiếp cận gần như giống hệt nhau bây giờ.
Kiểm tra cũ
>>> x, y = numpy.arange(500), numpy.arange(500)
>>> %timeit repeat_product(x, y)
10 loops, best of 3: 62 ms per loop
>>> %timeit dstack_product(x, y)
100 loops, best of 3: 12.2 ms per loop
Thử nghiệm mới
In [7]: x, y = numpy.arange(500), numpy.arange(500)
In [8]: %timeit repeat_product(x, y)
1.32 ms ± 24.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [9]: %timeit dstack_product(x, y)
1.26 ms ± 8.47 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Như mọi khi, YMMV, nhưng điều này cho thấy rằng trong các phiên bản gần đây của Python và numpy, chúng có thể hoán đổi cho nhau.
Chức năng sản phẩm tổng quát
Nói chung, chúng tôi có thể hy vọng rằng việc sử dụng các hàm tích hợp sẽ nhanh hơn cho các đầu vào nhỏ, trong khi đối với các đầu vào lớn, một hàm xây dựng có mục đích có thể nhanh hơn. Hơn nữa, đối với một sản phẩm n chiều tổng quát, tile
và repeat
sẽ không giúp ích gì, vì chúng không có các chất tương tự chiều cao rõ ràng hơn. Vì vậy, nó cũng đáng để điều tra hành vi của các chức năng được xây dựng có mục đích là tốt.
Hầu hết các thử nghiệm có liên quan xuất hiện ở đầu câu trả lời này, nhưng đây là một vài thử nghiệm được thực hiện trên các phiên bản trước của Python và numpy
để so sánh.
Các cartesian
chức năng được định nghĩa trong câu trả lời khác sử dụng để thực hiện khá tốt cho đầu vào lớn hơn. (Nó giống như hàm được gọi cartesian_product_recursive
ở trên.) Để so sánh cartesian
với dstack_prodct
, chúng tôi chỉ sử dụng hai chiều.
Ở đây một lần nữa, thử nghiệm cũ cho thấy một sự khác biệt đáng kể, trong khi thử nghiệm mới cho thấy hầu như không có.
Kiểm tra cũ
>>> x, y = numpy.arange(1000), numpy.arange(1000)
>>> %timeit cartesian([x, y])
10 loops, best of 3: 25.4 ms per loop
>>> %timeit dstack_product(x, y)
10 loops, best of 3: 66.6 ms per loop
Thử nghiệm mới
In [10]: x, y = numpy.arange(1000), numpy.arange(1000)
In [11]: %timeit cartesian([x, y])
12.1 ms ± 199 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [12]: %timeit dstack_product(x, y)
12.7 ms ± 334 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Như trước, dstack_product
vẫn đập cartesian
ở quy mô nhỏ hơn.
Thử nghiệm mới ( thử nghiệm cũ dư thừa không được hiển thị )
In [13]: x, y = numpy.arange(100), numpy.arange(100)
In [14]: %timeit cartesian([x, y])
215 µs ± 4.75 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [15]: %timeit dstack_product(x, y)
65.7 µs ± 1.15 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
Những sự phân biệt này, tôi nghĩ, thú vị và đáng ghi lại; nhưng cuối cùng họ học tập Như các bài kiểm tra ở đầu câu trả lời này cho thấy, tất cả các phiên bản này hầu như luôn chậm hơn cartesian_product
, được xác định ngay từ đầu câu trả lời này - bản thân nó chậm hơn một chút so với các câu trả lời nhanh nhất trong số các câu trả lời cho câu hỏi này.