У меня есть умеренно большой массив np
(который, однако, может стать больше в будущем):
import numpy as np
x = np.arange(100_000).reshape((10_000,10))
Мне нужно итеративно выбрать случайную выборку (строку), следя за тем, чтобы я никогда не выбирал одну и ту же выборку дважды. В настоящее время я делаю
rng = np.random.default_rng(seed=42)
indices = list(range(len(x)))
for _ in range(1000):
i = rng.choice(indices)
## do something with x[i]
indices.remove(i)
Однако я читал, что remove
работает довольно медленно. Есть ли лучший способ отслеживать индексы, которые я уже использовал?
Одним из вариантов может быть сохранение списка указателей (индексов) в массиве, первоначально в таком порядке, чтобы вам не требовалось располагать их в каком-либо определенном порядке для выборки и чтобы вы могли эффективно менять местами и извлекать:
rng = np.random.default_rng(seed=42)
indices = np.arange(len(x))
indices_left = len(x)
for _ in range(1000):
i = rng.integers(indices_left)
## do something with x[indices[i]]
indices[i] = indices[indices_left - 1] # swap
indices_left -= 1 # pop
Просто используйте rng.choice
и заранее сгенерируйте индексы (выборка из диапазона 0, N-1 без замены), а затем выполните итерацию по ним.
rng = np.random.default_rng(seed=42)
idxs = rng.choice(range(len(x)), size=1000, replace=False)
for i in idxs:
# do something with x[i]
Гибкий способ сделать это — взять ваш явный список индексов и использовать функцию shuffle()
вашей библиотеки, чтобы зашифровать его (random.shuffle(indices)
на простом Python, но я не помню точно, как это пишется с использованием numpy
random
) .
Тогда ваш цикл станет:
for _ in range(1000):
i = indices.pop()
## do something with x[i]
Я говорю «гибкий», потому что он не ограничивается одним циклом. indices
всегда содержит коллекцию еще не посещенных индексов. Таким образом, при желании его можно повторно использовать в других циклах или даже передавать в функции.
Или, например, если в какой-то момент вам понадобится кусок k
свежих индексов,
ichunk = indices[-k:]
del indices[-k:]
Использование pop()
важно для скорости: удаление из конца списка обходится дешево. По сути, Python просто уменьшает внутренний указатель на то место, где заканчивается список. Аналогично для del[-k:]
. В общем, remove()
должен физически переместить каждый элемент вправо от удаленного элемента влево на позицию, чтобы «заполнить дыру». В среднем это занимает время, пропорциональное длине списка. Но при снятии с конца дырки для заполнения не остается.
Чтобы получить эффект исходного кода ОП, я ожидаю, что вместо этого вы захотите передать
size=1000
.