Python有my_sample = random.sample(range(100), 10)
随机抽样,无需替换[0, 100)
。
假设我已经对n
这样的数字进行了采样,现在我想再抽样一次而不进行替换(不包括任何先前采样的n
),如何超高效地进行这样的操作?
更新:从“合理有效”变为“超级有效”(但忽略了常数因素)
答案 0 :(得分:19)
如果你事先知道你想要多个样本而没有重叠,最简单的方法就是random.shuffle()
list(range(100))
(Python 3 - 可以跳过Python中的list()
2),然后根据需要剥离切片。
s = list(range(100))
random.shuffle(s)
first_sample = s[-10:]
del s[-10:]
second_sample = s[-10:]
del s[-10:]
# etc
Else @ Chronial的答案相当有效。
答案 1 :(得分:9)
如果抽样的数量远远少于人口,只需抽样,检查是否已选择并重复。这可能听起来很愚蠢,但是你选择相同的数字会有指数衰减的可能性,所以如果你有一小部分没有选择,它会比O(n)
快得多。< / p>
Python使用Mersenne Twister作为其PRNG, good adequate。我们可以完全使用其他东西来以可预测的方式生成非重叠数字。
当x² mod p
和2x < p
为素数时,二次残差p
是唯一的。
如果你&#34;翻转&#34;残余p - (x² % p)
,此时也是p = 3 mod 4
,结果将是剩余的空格。
这不是一个非常令人信服的数字传播,所以你可以增加力量,添加一些软糖常数,然后分布非常好。
首先我们需要生成素数:
from itertools import count
from math import ceil
from random import randrange
def modprime_at_least(number):
if number <= 2:
return 2
number = (number // 4 * 4) + 3
for number in count(number, 4):
if all(number % factor for factor in range(3, ceil(number ** 0.5)+1, 2)):
return number
您可能会担心生成素数的成本。对于10⁶元素,这需要十分之一毫秒。运行[None] * 10**6
需要的时间比此长,并且因为它只计算一次,所以这不是一个真正的问题。
此外,该算法不需要精确值;只需要一个最多是大于输入数的常数因子的东西。这可以通过保存值列表并搜索它们来实现。如果进行线性扫描,则为O(log number)
,如果进行二分查找,则为O(log number of cached primes)
。事实上,如果你使用galloping,你可以将其降低到O(log log number)
,这基本上是不变的(log log googol = 2
)。
然后我们实现了生成器
def sample_generator(up_to):
prime = modprime_at_least(up_to+1)
# Fudge to make it less predictable
fudge_power = 2**randrange(7, 11)
fudge_constant = randrange(prime//2, prime)
fudge_factor = randrange(prime//2, prime)
def permute(x):
permuted = pow(x, fudge_power, prime)
return permuted if 2*x <= prime else prime - permuted
for x in range(prime):
res = (permute(x) + fudge_constant) % prime
res = permute((res * fudge_factor) % prime)
if res < up_to:
yield res
并检查它是否有效:
set(sample_generator(10000)) ^ set(range(10000))
#>>> set()
现在,关于这一点的可爱之处在于,如果您忽略了首要性测试(大约O(√n)
,其中n
是元素的数量,则此算法具有时间复杂度O(k)
,其中k
是样本sizeit&{39}和O(1)
内存使用情况!从技术上讲,这是O(√n + k)
,但实际上它是O(k)
。
您不需要经过验证的PRNG。这个PRNG比线性同余生成器(它很受欢迎; Java uses it)要好得多,但它不像Mersenne Twister那样经过验证。
您不首先生成具有不同功能的任何项目。这可以通过数学避免重复,而不是检查。下一节我将介绍如何删除此限制。
简短方法必须不足(k
必须接近n
)。如果k
仅为n
的一半,请按照我原来的建议行事。
极节省内存。这需要不断的记忆......甚至不是O(k)
!
生成下一个项目的恒定时间。实际上,这在实际上也是相当快的:它不像内置的Mersenne Twister一样 ,但它在2倍的范围内。
< / LI>凉意。
要删除此要求:
您不首先生成具有不同功能的任何项目。这可以避免重复数学,而不是检查。
我及时制作了最好的算法和空间复杂度,这是我以前的生成器的简单扩展。
这里是纲要(n
是数字池的长度,k
是&#34;外国&#34;键的数量:
O(√n)
;所有合理输入的O(log log n)
由于O(√n)
费用,这是我的算法中唯一在算法复杂性方面不完善的因素。实际上,这不会有问题,因为预先计算会将其降低到O(log log n)
,这与不变的时间无法接近。
如果您将迭代耗尽任何固定百分比,则费用可以免费摊销。
这不是一个实际问题。
O(1)
密钥生成时间显然这无法改进。
O(k)
密钥生成时间如果你有从外部生成的密钥,只要求它不能是这个生成器已经生成的密钥,那么这些密钥将被称为&#34;外键&#34;。外键被假定为完全随机的。因此,任何能够从池中选择项目的功能都可以这样做。
因为可以有任意数量的外键并且它们可以是完全随机的,所以完美算法的最坏情况是O(k)
。
O(k)
如果假设外键完全独立,则每个外键代表不同的信息项。因此必须存储所有密钥。该算法恰好在看到密钥时丢弃密钥,因此内存成本将在生成器的生命周期内清除。
嗯,这是我的算法。它实际上很简单:
def sample_generator(up_to, previously_chosen=set(), *, prune=True):
prime = modprime_at_least(up_to+1)
# Fudge to make it less predictable
fudge_power = 2**randrange(7, 11)
fudge_constant = randrange(prime//2, prime)
fudge_factor = randrange(prime//2, prime)
def permute(x):
permuted = pow(x, fudge_power, prime)
return permuted if 2*x <= prime else prime - permuted
for x in range(prime):
res = (permute(x) + fudge_constant) % prime
res = permute((res * fudge_factor) % prime)
if res in previously_chosen:
if prune:
previously_chosen.remove(res)
elif res < up_to:
yield res
更改就像添加:
一样简单if res in previously_chosen:
previously_chosen.remove(res)
您可以通过添加到您传入的previously_chosen
随时添加到set
。实际上,您也可以从集合中删除以添加回潜在的资源池,尽管这仅在sample_generator
尚未产生或prune=False
跳过它时才有效。
所以有。很容易看出它满足所有要求,并且很容易看出要求是绝对的。请注意,如果您没有设置,它仍会通过将输入转换为集合来满足最坏情况,但会增加开销。
从统计学角度来说,我很好奇这个PRNG实际上有多好。
一些快速搜索引导我创建这三个测试,这些测试似乎都显示出良好的效果!
首先,一些随机数字:
N = 1000000
my_gen = list(sample_generator(N))
target = list(range(N))
random.shuffle(target)
control = list(range(N))
random.shuffle(control)
这些&#34;洗牌&#34;从0
到10⁶-1
的10个数字列表,一个使用我们有趣的Fudged PRNG,另一个使用Mersenne Twister作为基线。第三是控制。
这是一个测试,它查看沿线两个随机数之间的平均距离。将差异与对照进行比较:
from collections import Counter
def birthdat_calc(randoms):
return Counter(abs(r1-r2)//10000 for r1, r2 in zip(randoms, randoms[1:]))
def birthday_compare(randoms_1, randoms_2):
birthday_1 = sorted(birthdat_calc(randoms_1).items())
birthday_2 = sorted(birthdat_calc(randoms_2).items())
return sum(abs(n1 - n2) for (i1, n1), (i2, n2) in zip(birthday_1, birthday_2))
print(birthday_compare(my_gen, target), birthday_compare(control, target))
#>>> 9514 10136
这比每个的方差小。
这是一个测试,它依次获取5个数字并查看元素的顺序。它们应该在所有120个可能的订单之间平均分配。
def permutations_calc(randoms):
permutations = Counter()
for items in zip(*[iter(randoms)]*5):
sorteditems = sorted(items)
permutations[tuple(sorteditems.index(item) for item in items)] += 1
return permutations
def permutations_compare(randoms_1, randoms_2):
permutations_1 = permutations_calc(randoms_1)
permutations_2 = permutations_calc(randoms_2)
keys = sorted(permutations_1.keys() | permutations_2.keys())
return sum(abs(permutations_1[key] - permutations_2[key]) for key in keys)
print(permutations_compare(my_gen, target), permutations_compare(control, target))
#>>> 5324 5368
这再次小于每个方差。
这是一项测试,可以看到&#34;运行多久&#34;是,又名。连续增加或减少的部分。
def runs_calc(randoms):
runs = Counter()
run = 0
for item in randoms:
if run == 0:
run = 1
elif run == 1:
run = 2
increasing = item > last
else:
if (item > last) == increasing:
run += 1
else:
runs[run] += 1
run = 0
last = item
return runs
def runs_compare(randoms_1, randoms_2):
runs_1 = runs_calc(randoms_1)
runs_2 = runs_calc(randoms_2)
keys = sorted(runs_1.keys() | runs_2.keys())
return sum(abs(runs_1[key] - runs_2[key]) for key in keys)
print(runs_compare(my_gen, target), runs_compare(control, target))
#>>> 1270 975
这里的差异非常大,而且在几次执行中,我看起来两者都是均匀的。因此,该测试通过了。
我提到了一个线性同余生成器,因为可能更有成效&#34;。我自己制作了一个严格执行的LCG,看看这是否是一个准确的陈述。
LCG,AFAICT,就像普通的生成器一样,它们不会成为循环。因此,我看过的大多数参考资料,又名。维基百科,仅涵盖了定义期间的内容,而不是如何在特定时期内制定强大的LCG。这可能会影响结果。
这里是:
from operator import mul
from functools import reduce
# Credit http://stackoverflow.com/a/16996439/1763356
# Meta: Also Tobias Kienzler seems to have credit for my
# edit to the post, what's up with that?
def factors(n):
d = 2
while d**2 <= n:
while not n % d:
yield d
n //= d
d += 1
if n > 1:
yield n
def sample_generator3(up_to):
for modulier in count(up_to):
modulier_factors = set(factors(modulier))
multiplier = reduce(mul, modulier_factors)
if not modulier % 4:
multiplier *= 2
if multiplier < modulier - 1:
multiplier += 1
break
x = randrange(0, up_to)
fudge_constant = random.randrange(0, modulier)
for modfact in modulier_factors:
while not fudge_constant % modfact:
fudge_constant //= modfact
for _ in range(modulier):
if x < up_to:
yield x
x = (x * multiplier + fudge_constant) % modulier
我们不再检查质数,但我们确实需要做一些奇怪的事情。
modulier ≥ up_to > multiplier, fudge_constant > 0
a - 1
必须可以被modulier
... fudge_constant
必须 coprime modulier
请注意,这些不是LCG的规则,而是具有完整周期的LCG,显然等于mod
ulier。
我是这样做的:
modulier
up_to
尝试,在条件满足时停止
multiplier
成为
的产品,并删除重复项multiplier
不小于modulier
,请继续下一个modulier
fudge_constant
小于modulier
,随机选择fudge_constant
中的因子
这不是生成它的好方法,但我不知道为什么它会影响数字的质量,除了低fudge_constant
s和{{1}这一事实比这些可能造成的完美发电机更常见。
无论如何,结果是令人震惊的:
multiplier
总之,我的RNG很好,线性同余生成器不是。考虑到Java使用线性同余生成器(尽管它只使用较低的位),我希望我的版本绰绰有余。
答案 2 :(得分:9)
好的,我们走了。这应该是最快的非概率算法。它的运行时间为O(k⋅log²(s) + f⋅log(f)) ⊂ O(k⋅log²(f+k) + f⋅log(f)))
和空格O(k+f)
。 f
是禁止数字的数量,s
是禁止数字最长条纹的长度。对此的期望更复杂,但明显受f
约束。如果您认为s^log₂(s)
大于f
或者对s
再一次是概率性的事实感到不满意,您可以将日志部分更改为{{1}中的二分搜索得到forbidden[pos:]
。
此处的实际实施是O(k⋅log(f+k) + f⋅log(f))
,因为列表O(k⋅(k+f)+f⋅log(f))
中的插入是forbid
。通过使用blist sortedlist替换该列表,可以轻松解决此问题。
我还添加了一些评论,因为这个算法非常复杂。 O(n)
部分与lin
部分相同,但需要log
而不是s
时间。
log²(s)
现在将它与Veedrac提出的“hack”(以及python中的默认实现)进行比较,其中空格import bisect
import random
def sample(k, end, forbid):
forbidden = sorted(forbid)
out = []
# remove the last block from forbidden if it touches end
for end in reversed(xrange(end+1)):
if len(forbidden) > 0 and forbidden[-1] == end:
del forbidden[-1]
else:
break
for i in xrange(k):
v = random.randrange(end - len(forbidden) + 1)
# increase v by the number of values < v
pos = bisect.bisect(forbidden, v)
v += pos
# this number might also be already taken, find the
# first free spot
##### linear
#while pos < len(forbidden) and forbidden[pos] <=v:
# pos += 1
# v += 1
##### log
while pos < len(forbidden) and forbidden[pos] <= v:
step = 2
# when this is finished, we know that:
# • forbidden[pos + step/2] <= v + step/2
# • forbidden[pos + step] > v + step
# so repeat until (checked by outer loop):
# forbidden[pos + step/2] == v + step/2
while (pos + step <= len(forbidden)) and \
(forbidden[pos + step - 1] <= v + step - 1):
step = step << 1
pos += step >> 1
v += step >> 1
if v == end:
end -= 1
else:
bisect.insort(forbidden, v)
out.append(v)
return out
和(O(f+k)
是expected number of “guesses”)时间:< / p>
我n/(n-(f+k))
仅plotted this而k=10
n=10000
(n
只会变得更加极端)。我不得不说:我只是实施了这个,因为它似乎是一个有趣的挑战,但即使我对这是多么极端感到惊讶:
让我们放大一下,看看发生了什么:
是的 - 对于您生成的第9998个数字,猜测更快。请注意,正如您在第一个图中看到的那样,对于较大的f/n
,即使我的单行也可能更快(但对于大n
仍然有相当可怕的空间要求。)
为了让点回家:你在这里花费时间的唯一事情是生成集合,因为这是Veedrac方法中的f
因素。
所以我希望我的时间没有浪费,我设法让你相信Veedrac的方法就是这样。我可以理解为什么概率部分会给你带来麻烦,但也许可以想到这样一个事实:hashmaps(= python dict
s)和其他许多算法都使用类似的方法,它们似乎做得很好。
你可能会害怕重复次数的变化。如上所述,这是geometric distribution p=n-f/n
之后的结果。因此,标准偏差(=您应该“期望”的结果偏离预期平均值)是
这与平均值(√f⋅n < √n² = n
)基本相同。
****编辑**:
我刚才意识到s
实际上也是n/(n-(f+k))
。因此,我的算法的更精确的运行时为O(k⋅log²(n/(n-(f+k))) + f⋅log(f))
。这很好,因为给出了上面的图表,它证明了我的直觉,这比O(k⋅log(f+k) + f⋅log(f))
快得多。但请放心,这也不会改变上述结果,因为f⋅log(f)
是运行时中绝对主导的部分。
答案 3 :(得分:8)
来自OP的读者注意:请考虑查看originally accepted answer以了解逻辑,然后了解此答案。
Aaaaaand为了完整起见:这是necromancer’s answer的概念,但经过调整,因此需要一个禁止数字列表作为输入。这与my previous answer中的代码相同,但我们会在生成数字之前从forbid
构建状态。
O(f+k)
和记忆O(f+k)
。显然,如果没有forbid
(排序/设置)格式的要求,这是最快的事情。我认为这使得它在某种程度上成为赢家^^。forbid
是一个集合,则O(k⋅n/(n-(f+k)))
的重复猜测方法会更快,O(k)
非常接近f+k
而非非常接近n
。forbid
进行了排序,则my ridiculous algorithm会更快:
import random
def sample_gen(n, forbid):
state = dict()
track = dict()
for (i, o) in enumerate(forbid):
x = track.get(o, o)
t = state.get(n-i-1, n-i-1)
state[x] = t
track[t] = x
state.pop(n-i-1, None)
track.pop(o, None)
del track
for remaining in xrange(n-len(forbid), 0, -1):
i = random.randrange(remaining)
yield state.get(i, i)
state[i] = state.get(remaining - 1, remaining - 1)
state.pop(remaining - 1, None)
用法:
gen = sample_gen(10, [1, 2, 4, 8])
print gen.next()
print gen.next()
print gen.next()
print gen.next()
答案 4 :(得分:7)
这是一种不明确构建差异集的方法。但它确实使用了@ Veedrac的“接受/拒绝”逻辑形式。如果你不愿意随意改变基础序列,我担心这是不可避免的:
def sample(n, base, forbidden):
# base is iterable, forbidden is a set.
# Every element of forbidden must be in base.
# forbidden is updated.
from random import random
nusable = len(base) - len(forbidden)
assert nusable >= n
result = []
if n == 0:
return result
for elt in base:
if elt in forbidden:
continue
if nusable * random() < n:
result.append(elt)
forbidden.add(elt)
n -= 1
if n == 0:
return result
nusable -= 1
assert False, "oops!"
这是一个小司机:
base = list(range(100))
forbidden = set()
for i in range(10):
print sample(10, base, forbidden)
答案 5 :(得分:7)
好的,最后一次尝试;-)以改变基本序列为代价,这不需要额外的空间,并且每个n
调用需要与sample(n)
成比例的时间:
class Sampler(object):
def __init__(self, base):
self.base = base
self.navail = len(base)
def sample(self, n):
from random import randrange
if n < 0:
raise ValueError("n must be >= 0")
if n > self.navail:
raise ValueError("fewer than %s unused remain" % n)
base = self.base
for _ in range(n):
i = randrange(self.navail)
self.navail -= 1
base[i], base[self.navail] = base[self.navail], base[i]
return base[self.navail : self.navail + n]
小司机:
s = Sampler(list(range(100)))
for i in range(9):
print s.sample(10)
print s.sample(1)
print s.sample(1)
实际上,这会实现一个可恢复的random.shuffle()
,在选择n
个元素后暂停。 base
没有被破坏,但是被置换了。
答案 6 :(得分:6)
您可以根据Wikipedia's "Fisher--Yates shuffle#Modern method"
实施改组生成器def shuffle_gen(src):
""" yields random items from base without repetition. Clobbers `src`. """
for remaining in xrange(len(src), 0, -1):
i = random.randrange(remaining)
yield src[i]
src[i] = src[remaining - 1]
然后可以使用itertools.islice
切片:
>>> import itertools
>>> sampler = shuffle_gen(range(100))
>>> sample1 = list(itertools.islice(sampler, 10))
>>> sample1
[37, 1, 51, 82, 83, 12, 31, 56, 15, 92]
>>> sample2 = list(itertools.islice(sampler, 80))
>>> sample2
[79, 66, 65, 23, 63, 14, 30, 38, 41, 3, 47, 42, 22, 11, 91, 16, 58, 20, 96, 32, 76, 55, 59, 53, 94, 88, 21, 9, 90, 75, 74, 29, 48, 28, 0, 89, 46, 70, 60, 73, 71, 72, 93, 24, 34, 26, 99, 97, 39, 17, 86, 52, 44, 40, 49, 77, 8, 61, 18, 87, 13, 78, 62, 25, 36, 7, 84, 2, 6, 81, 10, 80, 45, 57, 5, 64, 33, 95, 43, 68]
>>> sample3 = list(itertools.islice(sampler, 20))
>>> sample3
[85, 19, 54, 27, 35, 4, 98, 50, 67, 69]
答案 7 :(得分:6)
这是我的Knuth shuffle版本,由Tim Peters首次发布,由Eric美化,然后由necromancer进行空间优化。
这是基于Eric的版本,因为我确实发现他的代码很漂亮:)。
import random
def shuffle_gen(n):
# this is used like a range(n) list, but we don’t store
# those entries where state[i] = i.
state = dict()
for remaining in xrange(n, 0, -1):
i = random.randrange(remaining)
yield state.get(i,i)
state[i] = state.get(remaining - 1,remaining - 1)
# Cleanup – we don’t need this information anymore
state.pop(remaining - 1, None)
用法:
out = []
gen = shuffle_gen(100)
for n in range(100):
out.append(gen.next())
print out, len(set(out))
答案 8 :(得分:5)
编辑:请参阅@TimPeters和@Chronial下面的更清晰版本。一个小编辑将其推到了顶部。
以下是我认为最有效的增量采样解决方案。调用者要维护的状态包括可供增量采样器使用的字典,以及保留在该范围内的数字计数,而不是先前采样的数字列表。
以下是示范性实施。与其他解决方案相比:
O(log(number_previously_sampled))
O(number_previously_sampled)
代码:
import random
def remove (i, n, state):
if i == n - 1:
if i in state:
t = state[i]
del state[i]
return t
else:
return i
else:
if i in state:
t = state[i]
if n - 1 in state:
state[i] = state[n - 1]
del state[n - 1]
else:
state[i] = n - 1
return t
else:
if n - 1 in state:
state[i] = state[n - 1]
del state[n - 1]
else:
state[i] = n - 1
return i
s = dict()
for n in range(100, 0, -1):
print remove(random.randrange(n), n, s)
答案 9 :(得分:5)
这是@ necromancer的酷解决方案的重写版本。将它包装在一个类中以使其更容易正确使用,并使用更多的dict方法来删除代码行。
from random import randrange
class Sampler:
def __init__(self, n):
self.n = n # number remaining from original range(n)
# i is a key iff i < n and i already returned;
# in that case, state[i] is a value to return
# instead of i.
self.state = dict()
def get(self):
n = self.n
if n <= 0:
raise ValueError("range exhausted")
result = i = randrange(n)
state = self.state
# Most of the fiddling here is just to get
# rid of state[n-1] (if it exists). It's a
# space optimization.
if i == n - 1:
if i in state:
result = state.pop(i)
elif i in state:
result = state[i]
if n - 1 in state:
state[i] = state.pop(n - 1)
else:
state[i] = n - 1
elif n - 1 in state:
state[i] = state.pop(n - 1)
else:
state[i] = n - 1
self.n = n-1
return result
这是一个基本的驱动因素:
s = Sampler(100)
allx = [s.get() for _ in range(100)]
assert sorted(allx) == list(range(100))
from collections import Counter
c = Counter()
for i in range(6000):
s = Sampler(3)
one = tuple(s.get() for _ in range(3))
c[one] += 1
for k, v in sorted(c.items()):
print(k, v)
和样本输出:
(0, 1, 2) 1001
(0, 2, 1) 991
(1, 0, 2) 995
(1, 2, 0) 1044
(2, 0, 1) 950
(2, 1, 0) 1019
通过眼球,这种分布很好(如果你持怀疑态度则进行卡方检验)。这里的一些解决方案并没有给出每个置换的概率相等(即使它们以相等的概率返回n的每个k子集),因此在这方面不同于random.sample()
。
答案 10 :(得分:4)
合理快速的单行(O(n + m)
,n =范围,m =旧样本化):
next_sample = random.sample(set(range(100)).difference(my_sample), 10)
答案 11 :(得分:1)
令人惊讶的是,它还没有在核心功能之一中实现,但是这里是干净的版本,它返回采样值和列表而不进行替换:
def sample_n_points_without_replacement(n, set_of_points):
sampled_point_indices = random.sample(range(len(set_of_points)), n)
sampled_point_indices.sort(reverse=True)
sampled_points = [set_of_points[sampled_point_index] for sampled_point_index in sampled_point_indices]
for sampled_point_index in sampled_point_indices:
del(set_of_points[sampled_point_index])
return sampled_points, set_of_points
答案 12 :(得分:0)
这是一个旁注:假设您要解决完全相同的采样问题,而不在列表上进行替换(我将称其为['a', 'a', 'c', 'ca', 'a', 'b', 'c', 'b', 'a']
1
),但不是在您拥有的所有元素上进行统一采样尚未进行抽样,您将获得初始概率分布sample_space
,该初始概率分布告诉您在整个空间中进行抽样时对分布的p
元素进行抽样的概率。
然后使用numpy的以下实现在数值上是稳定的:
i^th
简短而简洁,我喜欢它。让我知道你们的想法!