Канонічний cartesian_product
(майже)
Існує багато підходів до цієї проблеми з різними властивостями. Деякі швидші за інших, а деякі - більш загального призначення. Після багатьох тестувань та налаштувань я виявив, що наступна функція, яка обчислює n-мірність cartesian_product
, швидше, ніж більшість інших, для багатьох входів. Про пару підходів, які є трохи складнішими, але навіть трохи швидшими у багатьох випадках, дивіться відповідь Пола Панзера .
З огляду на цю відповідь, це вже не найшвидше реалізація декартового продукту в numpy
тому, що мені відомо. Однак я думаю, що його простота продовжить робити корисним орієнтиром для подальшого вдосконалення:
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)
Варто згадати, що цю функцію використовують ix_
незвично; тоді як задокументоване використання - ix_
це генерування індексів у масиві, просто так трапляється, що масиви з однаковою формою можуть використовуватися для широкомовного призначення. Величезне спасибі mgilson , який надихнув мене спробувати використовувати ix_
цей спосіб, та unutbu , який надав надзвичайно корисні відгуки щодо цієї відповіді, включаючи пропозицію використовувати numpy.result_type
.
Помітні альтернативи
Іноді швидше писати суміжні блоки пам'яті у порядку Fortran. Це основа цієї альтернативи, cartesian_product_transpose
яка виявилася швидшою для деяких апаратних засобів, ніж cartesian_product
(див. Нижче). Однак відповідь Пола Пензера, який використовує той самий принцип, ще швидша. Все-таки я включаю це тут для зацікавлених читачів:
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
Після того, як зрозуміти підхід Panzer, я написав нову версію, яка майже така ж швидка, як і його, і майже така ж проста 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
Це, мабуть, має деякий накладний накладний час, що змушує його працювати повільніше, ніж Panzer для невеликих входів. Але що стосується більших показників, то в усіх тестах, які я проводив, він виконує так само добре, як і його найшвидше виконання ( cartesian_product_transpose_pp
).
У наступних розділах я включаю деякі тести інших альтернатив. Зараз вони дещо застаріли, але, замість того, щоб повторювати зусилля, я вирішив залишити їх тут, не маючи історичних інтересів. Щоб дізнатися про сучасні тести, див відповідь Panzer, а також Ніко Шльомер .
Тести на альтернативи
Ось безліч тестів, які показують підвищення продуктивності, що деякі з цих функцій надають відносно ряду альтернативних варіантів. Усі показані тут тести проводилися на чотирьохядерній машині, що працює під управлінням Mac OS 10.12.5, Python 3.6.1 та numpy
1.12.1. Відомо, що варіанти апаратного та програмного забезпечення дають різні результати, тому YMMV. Виконайте ці тести для себе, щоб бути впевненим!
Визначення:
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)]
Результати тесту:
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)
У всіх випадках, cartesian_product
як визначено на початку, ця відповідь є найшвидшою.
Для тих функцій, які приймають довільну кількість вхідних масивів, варто також перевірити продуктивність len(arrays) > 2
. (Поки я не можу визначити, чому cartesian_product_recursive
в цьому випадку видається помилка, я її видалив із цих тестів.)
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)
Як показують ці тести, він cartesian_product
залишається конкурентоспроможним, поки кількість вхідних масивів не зросте вище (приблизно) чотирьох. Після цього cartesian_product_transpose
має невеликий край.
Варто ще раз зазначити, що користувачі з іншими апаратними та операційними системами можуть бачити різні результати. Наприклад, звіти unutbu, які бачать такі результати для цих тестів, використовуючи Ubuntu 14.04, Python 3.4.3 та 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
Нижче я розглядаю декілька деталей про попередні тести, які я проходив у цих напрямках. Відносна ефективність цих підходів з часом змінювалася для різних апаратних засобів та різних версій Python та numpy
. Хоча це не одразу корисно людям, які використовують сучасні версії numpy
, воно ілюструє, як змінилися речі з моменту першої версії цієї відповіді.
Проста альтернатива: meshgrid
+dstack
В даний час прийнята відповідь використовує tile
та repeat
транслює два масиви разом. Але meshgrid
функція робить практично те саме. Ось результат tile
та repeat
перед тим, як перейти до транспозиції:
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])]
І ось результат meshgrid
:
In [4]: numpy.meshgrid(x, y)
Out[4]:
[array([[1, 2, 3],
[1, 2, 3]]), array([[4, 4, 4],
[5, 5, 5]])]
Як бачите, він майже однаковий. Нам потрібно лише переробити результат, щоб отримати точно такий же результат.
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])]
Замість того, щоб переробляти на цьому етапі, однак ми можемо передати висновок meshgrid
до dstack
та змінити його згодом, що економить певну роботу:
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]])
Всупереч твердженню в цьому коментарі , я не бачив жодних доказів того, що різні входи даватимуть різні форми виходів, і як показано вище, вони роблять дуже подібні речі, тож було б досить дивно, якби це було. Будь ласка, повідомте мене, якщо ви знайдете контрприклад.
Тестування meshgrid
+ dstack
проти repeat
+transpose
Відносна ефективність цих двох підходів з часом змінювалася. У більш ранній версії Python (2.7) результат з використанням meshgrid
+ dstack
був помітно швидшим для невеликих входів. (Зауважте, що ці тести виходять із старої версії цієї відповіді.) Визначення:
>>> 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)
...
Для введення середнього розміру я побачив значну швидкість. Але я повторно повторив ці тести з більш новими версіями Python (3.6.1) та numpy
(1.12.1) на більш новій машині. Два підходи зараз майже однакові.
Старий тест
>>> 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
Новий тест
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)
Як завжди, YMMV, але це говорить про те, що в останніх версіях Python і numpy вони взаємозамінні.
Узагальнені функції продукту
Загалом, ми можемо очікувати, що використання вбудованих функцій буде швидше для малих входів, тоді як для великих входів функція, побудована за призначенням, може бути швидшою. Крім того , для узагальненого п-мірного продукту, tile
а repeat
не допоможе, тому що вони не мають чітких Багатовимірні аналоги. Тож варто вивчити і поведінку цілеспрямованих функцій.
Більшість відповідних тестів з'являються на початку цієї відповіді, але ось кілька тестів, виконаних на більш ранніх версіях Python та numpy
для порівняння.
cartesian
Функція , певна в іншому відповіді використовується для виконання досить добре для великих входів. (Це те ж саме, що і функція називається cartesian_product_recursive
вище.) Для того , щоб порівняти cartesian
з dstack_prodct
, ми використовуємо тільки два виміри.
Тут знову старий тест показав істотну різницю, тоді як новий тест не показує майже жодного.
Старий тест
>>> 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
Новий тест
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)
Як і раніше, dstack_product
все ще б'ється cartesian
при менших масштабах.
Новий тест ( надлишковий старий тест не показаний )
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)
Ці відмінності, я думаю, цікаві та варті того, щоб їх записати; але вони зрештою є академічними. Як показали тести на початку цієї відповіді, всі ці версії майже завжди повільніші cartesian_product
, визначені на самому початку цієї відповіді - що саме по собі трохи повільніше, ніж найшвидші реалізації серед відповідей на це питання.