无需替换的采样算法?

Posted

技术标签:

【中文标题】无需替换的采样算法?【英文标题】:Algorithm for sampling without replacement? 【发布时间】:2010-09-23 15:01:33 【问题描述】:

我正在尝试测试特定数据聚类偶然发生的可能性。一个稳健的方法是蒙特卡罗模拟,其中数据和组之间的关联被随机重新分配大量次(例如 10,000 次),并且使用聚类度量来比较实际数据和模拟以确定 ap价值。

我已经完成了大部分工作,使用将分组映射到数据元素的指针,因此我计划随机重新分配指向数据的指针。问题:什么是无需替换的快速采样方法,以便在复制数据集中随机重新分配每个指针?

例如(这些数据只是一个简化的例子):

数据(n=12 个值) - A 组:0.1、0.2、0.4 / B 组:0.5、0.6、0.8 / C 组:0.4、0.5 / D 组:0.2、0.2、0.3、0.5

对于每个复制数据集,我将拥有相同的集群大小(A=3、B=3、C=2、D=4)和数据值,但会将值重新分配给集群。

为此,我可以生成 1-12 范围内的随机数,分配 A 组的第一个元素,然后生成 1-11 范围内的随机数并分配 A 组的第二个元素,依此类推。指针重新分配很快,我会预先分配所有数据结构,但是没有替换的采样似乎是一个可能已经解决了很多次的问题。

首选逻辑或伪代码。

【问题讨论】:

【参考方案1】:

这是一些基于 Knuth 的书 Seminumeric Algorithms 的算法 3.4.2S 的无替换采样代码。

void SampleWithoutReplacement
(
    int populationSize,    // size of set sampling from
    int sampleSize,        // size of each sample
    vector<int> & samples  // output, zero-offset indicies to selected items
)

    // Use Knuth's variable names
    int& n = sampleSize;
    int& N = populationSize;

    int t = 0; // total input records dealt with
    int m = 0; // number of items selected so far
    double u;

    while (m < n)
    
        u = GetUniform(); // call a uniform(0,1) random number generator

        if ( (N - t)*u >= n - m )
        
            t++;
        
        else
        
            samples[m] = t;
            t++; m++;
        
    

Jeffrey Scott Vitter 在“An Efficient Algorithm for Sequential Random Sampling”中提出了一种更有效但更复杂的方法,ACM Transactions on Mathematical Software, 13(1), March 1987, 58-67。

【讨论】:

我还没有这本书,并且无法向自己证明算法的正确性。我在 java 中实现了它,并检查了人口项目是否以统一的概率进行抽样。结果令人信服。看到这个gist 在 Mathematica 中 Vitter 方法 D 的一个不加批判的实现比内置算法快几个数量级。我在这里描述它:tinyurl.com/lbldlpq @Alban - 我们可以通过考虑第一个元素来查看从 N 的总体中采样 n 个元素的问题。包含此元素的概率为 (n/N):如果是,则问题归结为从剩余的 (N-1) 个元素中采样 (n-1) 个元素;如果不是,那么问题就归结为从剩余的 (N-1) 个元素中采样 (n) 个元素。一些变量变换会表明这是 Knuth 算法的精髓(通过增加 t)。 u 处于开放、半开放还是封闭区间,(0, 1)[0, 1)[0, 1] 是否重要? Knuth 只是说“均匀分布在零和一之间”。【参考方案2】:

基于answer by John D. Cook 的 C++ 工作代码。

#include <random>
#include <vector>

// John D. Cook, https://***.com/a/311716/15485
void SampleWithoutReplacement
(
    int populationSize,    // size of set sampling from
    int sampleSize,        // size of each sample
    std::vector<int> & samples  // output, zero-offset indicies to selected items
)

    // Use Knuth's variable names
    int& n = sampleSize;
    int& N = populationSize;

    int t = 0; // total input records dealt with
    int m = 0; // number of items selected so far

    std::default_random_engine re;
    std::uniform_real_distribution<double> dist(0,1);

    while (m < n)
    
        double u = dist(re); // call a uniform(0,1) random number generator

        if ( (N - t)*u >= n - m )
        
            t++;
        
        else
        
            samples[m] = t;
            t++; m++;
        
    


#include <iostream>
int main(int,char**)

  const size_t sz = 10;
  std::vector< int > samples(sz);
  SampleWithoutReplacement(10*sz,sz,samples);
  for (size_t i = 0; i < sz; i++ ) 
    std::cout << samples[i] << "\t";
  

  return 0;

【讨论】:

我编辑了您的答案,因此由于 GCC 和其他常见编译器中的线程保护,它不会太慢。但是,根据我的comment on John's answer,我不知道间隔应该是开放的、半开放的还是封闭的。这目前是半开放的。【参考方案3】:

查看我对这个问题的回答Unique (non-repeating) random numbers in O(1)?。相同的逻辑应该可以完成您想要做的事情。

【讨论】:

太棒了!抱歉,当我搜索 SO(用于无替换抽样、统计、算法等)时,我没有看到该答案。也许这会像一个元问题一样引导像我这样的人找到你的原始答案。干杯!【参考方案4】:

受@John D. Cook's answer 的启发,我用 Nim 编写了一个实现。起初我很难理解它是如何工作的,所以我进行了广泛的评论,还包括一个例子。也许它有助于理解这个想法。另外,我稍微更改了变量名称。

iterator uniqueRandomValuesBelow*(N, M: int) =
  ## Returns a total of M unique random values i with 0 <= i < N
  ## These indices can be used to construct e.g. a random sample without replacement
  assert(M <= N)

  var t = 0 # total input records dealt with
  var m = 0 # number of items selected so far

  while (m < M):
    let u = random(1.0) # call a uniform(0,1) random number generator

    # meaning of the following terms:
    # (N - t) is the total number of remaining draws left (initially just N)
    # (M - m) is the number how many of these remaining draw must be positive (initially just M)
    # => Probability for next draw = (M-m) / (N-t)
    #    i.e.: (required positive draws left) / (total draw left)
    #
    # This is implemented by the inequality expression below:
    # - the larger (M-m), the larger the probability of a positive draw
    # - for (N-t) == (M-m), the term on the left is always smaller => we will draw 100%
    # - for (N-t) >> (M-m), we must get a very small u
    #
    # example: (N-t) = 7, (M-m) = 5
    # => we draw the next with prob 5/7
    #    lets assume the draw fails
    # => t += 1 => (N-t) = 6
    # => we draw the next with prob 5/6
    #    lets assume the draw succeeds
    # => t += 1, m += 1 => (N-t) = 5, (M-m) = 4
    # => we draw the next with prob 4/5
    #    lets assume the draw fails
    # => t += 1 => (N-t) = 4
    # => we draw the next with prob 4/4, i.e.,
    #    we will draw with certainty from now on
    #    (in the next steps we get prob 3/3, 2/2, ...)
    if (N - t)*u >= (M - m).toFloat: # this is essentially a draw with P = (M-m) / (N-t)
      # no draw -- happens mainly for (N-t) >> (M-m) and/or high u
      t += 1
    else:
      # draw t -- happens when (M-m) gets large and/or low u
      yield t # this is where we output an index, can be used to sample
      t += 1
      m += 1

# example use
for i in uniqueRandomValuesBelow(100, 5):
  echo i

【讨论】:

【参考方案5】:

当总体规模远大于样本规模时,上述算法变得低效,因为它们具有复杂度O(n), n em> 是人口规模。

当我还是个学生的时候,我写了一些无放回均匀采样的算法,平均复杂度O(s log s),其中 s 是样本大小。这是二叉树算法的代码,平均复杂度 O(s log s),在 R 中:

# The Tree growing algorithm for uniform sampling without replacement
# by Pavel Ruzankin 
quicksample = function (n,size)
# n - the number of items to choose from
# size - the sample size

  s=as.integer(size)
  if (s>n) 
    stop("Sample size is greater than the number of items to choose from")
  
  # upv=integer(s) #level up edge is pointing to
  leftv=integer(s) #left edge is poiting to; must be filled with zeros
  rightv=integer(s) #right edge is pointig to; must be filled with zeros
  samp=integer(s) #the sample
  ordn=integer(s) #relative ordinal number

  ordn[1L]=1L #initial value for the root vertex
  samp[1L]=sample(n,1L) 
  if (s > 1L) for (j in 2L:s) 
    curn=sample(n-j+1L,1L) #current number sampled
    curordn=0L #currend ordinal number
    v=1L #current vertice
    from=1L #how have come here: 0 - by left edge, 1 - by right edge
    repeat 
      curordn=curordn+ordn[v]
      if (curn+curordn>samp[v])  #going down by the right edge
        if (from == 0L) 
          ordn[v]=ordn[v]-1L
        
        if (rightv[v]!=0L) 
          v=rightv[v]
          from=1L
         else  #creating a new vertex
          samp[j]=curn+curordn
          ordn[j]=1L
          # upv[j]=v
          rightv[v]=j
          break
        
       else  #going down by the left edge
        if (from==1L) 
          ordn[v]=ordn[v]+1L
        
        if (leftv[v]!=0L) 
          v=leftv[v]
          from=0L
         else  #creating a new vertex
          samp[j]=curn+curordn-1L
          ordn[j]=-1L
          # upv[j]=v
          leftv[v]=j
          break
        
      
    
  
  return(samp)  

该算法的复杂性在以下内容中进行了讨论: 鲁赞金,PS; Voytishek, A. V. 关于随机选择算法的成本。蒙特卡罗方法应用程序。 5 (1999), 没有。 1,39-54。 http://dx.doi.org/10.1515/mcma.1999.5.1.39

如果您觉得该算法有用,请提供参考。

另请参阅: P. Gupta, G. P. Bhattacharjee。 (1984) 一种有效的无放回随机抽样算法。国际计算机数学杂志 16:4,第 201-209 页。 DOI:10.1080/00207168408803438

Teuhola, J. 和 Nevalainen, O. 1982 年。两种有效的无放回随机抽样算法。 /IJCM/, 11(2): 127–140。 DOI:10.1080/00207168208803304

在上一篇论文中,作者使用哈希表并声称他们的算法具有 O(s) 复杂度。还有一种更快的哈希表算法,很快就会在 pqR(pretty quick R)中实现: https://stat.ethz.ch/pipermail/r-devel/2017-October/075012.html

【讨论】:

【参考方案6】:

here 描述了另一种无需替换的采样算法。

这与 John D. Cook 在他的回答和 Knuth 中描述的相似,但它有不同的假设:总体规模未知,但样本可以放入内存中。这个叫做“Knuth's algorithm S”。

引用rosettacode文章:

    选择可用的前 n 个项目作为样本; 对于 i > n 的第 i 个项目,有 n/i 个随机机会保留它。如果没有这个机会,样品将保持不变。如果 不是,让它随机 (1/n) 替换先前选择的 n 之一 样品的项目。 对任何后续项目重复 #2。

【讨论】:

Rosettacode 的算法名称错误:它应该是“算法 R”或“水库采样”。 “算法 S”(又名“选择抽样技术”)需要提前知道人口规模。 TAOCP - Vol 2 - §3.4.2 中描述了这两种算法【参考方案7】:

我写了一个survey of algorithms for sampling without replacement。我可能有偏见,但我推荐我自己的算法,在下面用 C++ 实现,为许多 k、n 值提供最佳性能,并为其他值提供可接受的性能。假设randbelow(i) 返回一个公平选择的小于i 的随机非负整数。

void cardchoose(uint32_t n, uint32_t k, uint32_t* result) 
    auto t = n - k + 1;
    for (uint32_t i = 0; i < k; i++) 
        uint32_t r = randbelow(t + i);
        if (r < t) 
            result[i] = r;
         else 
            result[i] = result[r - t];
        
    
    std::sort(result, result + k);
    for (uint32_t i = 0; i < k; i++) 
        result[i] += i;
    

【讨论】:

与 std::sample 和 range::sample 相比如何? 这将取决于您的特定 C++ 标准库如何实现它。在这两种情况下,文档都说“这个函数可以实现选择采样或水库采样”,所以它的执行可能与我对其中一种算法的实现类似,但您必须自己测试才能确定。

以上是关于无需替换的采样算法?的主要内容,如果未能解决你的问题,请参考以下文章

用向量中的随机值填充数据框中的 NA 值(无需替换)

在 R 中复制不替换的分层随机抽样

用重采样替换变量

从 TensorFlow 中给定的非均匀分布进行无替换采样

从数据框中的列中采样唯一行而不进行替换

通过组替换引导,但为重新采样的单元创建新标识符