在保持项目排序的同时从列表中获取随机样本?

Posted

技术标签:

【中文标题】在保持项目排序的同时从列表中获取随机样本?【英文标题】:Get random sample from list while maintaining ordering of items? 【发布时间】:2011-09-22 21:32:22 【问题描述】:

我有一个排序列表,比方说:(它不仅仅是数字,它是一个使用复杂耗时的算法排序的对象列表)

mylist = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ,9  , 10 ]

是否有一些 python 函数可以给我 N 个项目,但会保持顺序?

例子:

randomList = getRandom(mylist,4)
# randomList = [ 3 , 6 ,7 , 9 ]
randomList = getRandom(mylist,4)
# randomList = [ 1 , 2 , 4 , 8 ]

等等……

【问题讨论】:

你为什么不想random.sample然后排序? 它是用非平凡的算法排序的......它不仅仅是数字 对丹尼尔的评论稍作改动:对[0,count)的范围进行采样,对样本进行排序(范围内的数字具有自然顺序),然​​后根据mylist提取值指数。使用zip 可以实现相同的效果,但机制略有不同。 好的,我可以得到一个答案+例子,所以我有什么可以接受的吗? :) 【参考方案1】:

以下代码将生成大小为 4 的随机样本:

import random

sample_size = 4
sorted_sample = [
    mylist[i] for i in sorted(random.sample(range(len(mylist)), sample_size))
]

(注意:对于 Python 2,最好使用 xrange 而不是 range

说明

random.sample(range(len(mylist)), sample_size)

生成原始列表的索引的随机样本。

然后对这些索引进行排序以保留原始列表中元素的顺序。

最后,列表推导式根据采样索引从原始列表中提取实际元素。

【讨论】:

【参考方案2】:

简单的代码 O(#picks*log(#picks)) 方式

在不替换索引的情况下随机抽取样本,对索引进行排序,然后从原始样本中获取。

indices = random.sample(range(len(myList)), K)
[myList[i] for i in sorted(indices)]

random.sample(seq, K) 将从seq 的总体中随机同时选择 K 个元素,无需替换。当我们使用range 执行此操作时,每个样本都是 O(1),因为 python 中的 range 对象是稀疏的,实际上并没有构造一个完整的列表(特别是 cpython 实现调用len(seq) 和后来的seq[i]位于范围对象上,这些对象是虚拟化/伪造的,因此是 O(1))。然后查找随机索引(按顺序)。

如果您有迭代器(例如生成器表达式),您可以考虑先转换为列表,然后执行上述答案。如果您的迭代器是无限长度的,您可以使用下一节中的技术,它的性能要低得多,但可能在智力上很有趣(例如,如果您正在使用尚不支持索引的函数式语言中的小型有界列表,或超过 RAM 和磁盘大小的巨型流):

(也是来自 cmets 中用户 tegan 的有用说明:如果这是 python2,您将像往常一样使用 xrange。否则,您将使用 O(N) 而不是 O(#picks) 算法。)


用于不可索引序列的慢速/可流式 O(arrayLen)-时间、O(1)-辅助空间方式

您也可以使用数学技巧,从左到右迭代地遍历myList,选择具有动态变化概率(N-numbersPicked)/(total-numbersVisited) 的数字。这种方法是O(N),因为它访问所有内容一次(比排序更快,即 O(N log(N)),但比我们在上一节中直接索引 K 选择要慢得多(即 O(K log( K)) 排序后)。

from __future__ import division

def orderedSampleWithoutReplacement(seq, k):
    if not 0<=k<=len(seq):
        raise ValueError('Required that 0 <= sample_size <= population_size')

    numbersPicked = 0
    for i,number in enumerate(seq):
        prob = (k-numbersPicked)/(len(seq)-i)
        if random.random() < prob:
            yield number
            numbersPicked += 1

证明:考虑到从大小为len(seq) 的总体seq 中挑选k 的子集的均匀分布(无替换),我们可以考虑将任意点i 的分区划分为“左” (0,1,...,i-1) 和“右”(i,i+1,...,len(seq))。鉴于我们从左侧已知子集中选择numbersPicked,剩余的必须来自右侧未知子集的相同均匀分布,尽管参数现在不同。特别是,seq[i] 包含所选元素的概率为#remainingToChoose/#remainingToChooseFrom(k-numbersPicked)/(len(seq)-i),因此我们对其进行模拟并对结果进行递归。 (这必须终止,因为如果 #remainingToChoose == #remainingToChooseFrom,则所有剩余概率均为 1。)这类似于碰巧动态生成的概率树。基本上,您可以通过以先验选择为条件来模拟均匀概率分布(当您生长概率树时,您选择当前分支的概率,使其与先验叶子后验相同,即以先验选择为条件;这将起作用,因为这个概率一致地恰好是 N/k)。

(人们可能会查看这篇文章的编辑历史,以找到一个精心制作的模拟“证明”,这在以前是必要的,因为一些反对意见。)

下面是另一种编码方式,具有更多语义命名的变量。

from __future__ import division
import random

def orderedSampleWithoutReplacement(seq, sampleSize):
    totalElems = len(seq)
    if not 0<=sampleSize<=totalElems:
        raise ValueError('Required that 0 <= sample_size <= population_size')
    
    picksRemaining = sampleSize
    for elemsSeen,element in enumerate(seq):
        elemsRemaining = totalElems - elemsSeen
        prob = picksRemaining/elemsRemaining
        if random.random() < prob:
            yield element
            picksRemaining -= 1

from collections import Counter         
Counter(
    tuple(orderedSampleWithoutReplacement([0,1,2,3], 2))
    for _ in range(10**5)
)

edit:Timothy Shields 提到了Reservoir Sampling,这有点像这种方法(但从候选选择开始并随机交换它们),并且在len(seq) 未知时很有用(例如带有生成器表达式)。具体来说,被称为“算法 R”的那个是 O(N) 和 O(1) 辅助空间,如果就地完成的话;它涉及获取前 K 个元素并慢慢替换它们(还给出了归纳证明的提示)。在***页面上还可以找到有用的水库采样变体。这个想法是你预先填充一个候选返回值列表(我们假设它适合在 RAM 或磁盘上),并在你遍历列表时概率性地将它们交换出来(它可能比你的 RAM 或磁盘任意大)。


可索引序列的最佳 O(#picks * (1+log(N/#picks)))-time, O(1)-aux-space 方式

一个值得注意的算法在 Reservoir Sampling 文章中(ctrl-F Algorithm L section“最优算法”):它是一种竞争因素最优算法,(与原始解决方案一样)数量为 O(k)样本数量,而不是列表元素数量的 O(n)。

这里的直觉是我们可以跳过列表的任意部分,甚至不必访问它们,因为选择之间的元素数量不依赖于我们在列表中看到的数据。

与依赖于上述超几何分布不同,水库预先填充了候选解决方案(前 k 个项目)并定期换出这一事实,这使其看起来更像是一个具有几何等待时间的过程.这是一篇被广泛引用的论文,但我无法通过它来判断证明对于大 N 是渐近正确的,还是对所有 N 都有效。

从文章中不清楚是否可以在开始时不知道序列长度的情况下使用此算法(在这种情况下,可能只使用此答案第一部分中的原始方法)。

【讨论】:

@pst:没有劣势,只是提速了O(N) 而不是O(N log(N)) 非常好,我也想知道如何使用这种线性方法。这个公式有***页面吗? :) 我很惊讶这个答案没有更多的赞成票,它实际上解释了解决方案的工作原理(并提供了另一个解决方案!),而不是第一个答案,它只是一个单行 sn -p-- 让我不知道为什么或如何工作。 不错的解决方案 ninjagecko。如果有人有兴趣把它写出来,你的解决方案有一个很好的归纳证明。 不错的解决方案!不要忘记为运行 Python 2 的用户添加 from __future__ import division【参考方案3】:

也许您可以只生成索引样本,然后从列表中收集项目。

randIndex = random.sample(range(len(mylist)), sample_size)
randIndex.sort()
rand = [mylist[i] for i in randIndex]

【讨论】:

【参考方案4】:

显然random.sample是在python 2.3中引入的

所以对于那个版本,我们可以使用随机播放(例如 4 个项目):

myRange =  range(0,len(mylist)) 
shuffle(myRange)
coupons = [ bestCoupons[i] for i in sorted(myRange[:4]) ]

【讨论】:

您使用的是 Python 2.2?!你应该升级......这已经过时了。 好吧,它是我们在服务器上拥有的......进行系统范围的更新太官僚了【参考方案5】:

random.sample 实现它。

>>> random.sample([1, 2, 3, 4, 5],  3)   # Three samples without replacement
[4, 1, 5]

【讨论】:

那不是订购的。

以上是关于在保持项目排序的同时从列表中获取随机样本?的主要内容,如果未能解决你的问题,请参考以下文章

如何在c#中保持内部顺序的同时将2个排序的列表合并到一个随机列表中

LINQ,按属性分组,同时保持其他属性排序

抓取随机行,同时保持其他表的顺序

在 Flutter 中保持滚动视图偏移的同时添加列表视图项

单行删除重复项,保持列表排序 [重复]

如何在 Redshift 中做分层随机样本?