在R中进行向量洗牌,但相同元素应具有最小距离

15

我希望对一个向量进行随机排序。该向量包含一些相同的元素,排序后,相同的元素之间应具有至少三个(即相同元素中应该有两个其他元素)的最小距离。

考虑以下 R 代码所示的示例向量:

x <- rep(LETTERS[1:5], 3)  # Create example vector
x
#  [1] "A" "B" "C" "D" "E" "A" "B" "C" "D" "E" "A" "B" "C" "D" "E"

如果我使用“sample”函数洗牌向量,其中一些相同的元素可能会太靠近。例如,如果我使用以下R代码,则元素“C”在位置5和6之后直接出现:

set.seed(53135)
sample(x)                  # sample() function puts same elements too close
#  [1] "B" "A" "E" "D" "C" "C" "E" "A" "B" "C" "D" "E" "A" "D" "B"

我该如何确保相同的元素之间有至少三个单位的距离?


5
你可以始终使用拒绝采样。但根据你的输入向量,这可能非常费时。 - Roland
6
请提供有关您尝试洗牌的向量的更多信息。仅说明“一些向量元素是相同的”是不够的,因为您的采样过程可能会进入死胡同。例如,如果您有一个由9个元素组成的向量,其中4个元素彼此相同,那么您永远无法得到满足位置约束的样本。最少,我们需要知道您的向量中每种元素的频率计数。 - ekoam
@Roland 非常感谢您提供拒绝抽样的提示。虽然这可能有点过度,但我希望有一个具有min.distance参数的函数。无论如何,如果不存在这样的函数,我将尝试使用拒绝抽样来完成它,所以非常感谢您! - Joachim Schork
@ekoam 感谢您回复我!我想将这种“洗牌方法”应用于不同的向量,所以我无法告诉您确切的长度。但是,我可以告诉您,我的向量大约有100-200个元素,并且所有元素都至少有一个双胞胎。出现次数最多的元素将在向量中存在约10次。希望这样能澄清我的问题! - Joachim Schork
@JoachimSchork,答案有什么问题吗?如果有,请随时指出。 - Abdessabour Mtk
@AbdessabourMtk 对不起,回复晚了!(请查看我在您的答案下面的评论) - Joachim Schork
3个回答

6

基本上,我们需要有条件地从x向量中选择一个元素,该元素在min.dist-1次运行中尚未被选择。使用purrr的reduce函数,我们可以实现此操作:

min.dist <- 2
reduce(integer(length(x)-1), ~c(.x, sample(x[!x %in% tail(.x, min.dist)], 1)), .init=sample(x,1))

[1] "A" "E" "D" "B" "A" "D" "E" "C" "D" "A" "C" "E" "B" "A" "E"

打包在函数中

shuffle <- function(x, min.dist=2){
    stopifnot(min.dist < length(unique(x)))
    reduce(integer(length(x)-1), ~c(.x, sample(x[!x %in% tail(.x, min.dist)], 1)), .init=sample(x,1))
}

> shuffle(x, 3)
 [1] "A" "C" "B" "D" "E" "A" "B" "C" "E" "D" "A" "B" "C" "E" "A"
> shuffle(x, 3)
 [1] "A" "B" "D" "E" "C" "A" "B" "D" "E" "C" "A" "D" "E" "C" "A"
> shuffle(x, 4)
 [1] "C" "E" "D" "A" "B" "C" "E" "D" "A" "B" "C" "E" "D" "A" "B"
> shuffle(x, 4)
 [1] "A" "B" "D" "E" "C" "A" "B" "D" "E" "C" "A" "B" "D" "E" "C"
> shuffle(x, 2)
 [1] "E" "A" "D" "E" "B" "D" "A" "E" "C" "D" "A" "E" "C" "A" "B"
> shuffle(x, 2)
 [1] "B" "A" "D" "C" "B" "A" "E" "B" "A" "E" "B" "C" "D" "A" "E"

@27ϕ9评论之后:

shuffle <- function(x, min.dist=2){
    stopifnot(min.dist < length(unique(x)))
    reduce(integer(length(x)-1), ~ c(.x, sample(x[!x %in% tail(.x, min.dist) &( x %in% names(t <- table(x[x%in%.x]) > table(.x))[t] | !x %in% .x)], 1)), .init=sample(x,1))
}
> table(shuffle(rep(LETTERS[1:5], 3),2))

A B C D E 
3 3 3 3 3 
> table(shuffle(rep(LETTERS[1:5], 3),2))
Error in sample.int(length(x), size, replace, prob) : 
  invalid first argument

更新

在经过一些尝试和错误后,考虑到你不总是会有足够的元素来调整min.dist,我想出了一个解决方案,这段代码是上面那些代码中最详细解释的:

shuffle <- function(x, min.dist=2){
    stopifnot(min.dist < length(unique(x)))
    reduce(integer(length(x)-1), function(.x, ...){
        # whether the value is in the tail of the aggregated vector
        in.tail <- x %in% tail(.x, min.dist)
        # whether a value still hasn't reached the max frequency
        freq.got <- x %in% names(t<-table(x[x%in%.x]) > table(.x))[t]
        # whether a value isn't in the aggregated vector
        yet <- !x %in% .x
        # the if is there basically to account for the cases when we don't have enough vars to space out the vectors
         c(.x, if(any((!in.tail & freq.got) | yet )) sample(x[(!in.tail & freq.got) | yet ], 1) else  x[which(freq.got)[1]] )
    }, .init=sample(x,1))
}

现在运行table(shuffle(rep(LETTERS[1:5], 3),2))总是为所有变量返回3,并且我们可以有一定的把握地说,在向量中,变量之间的最小距离为2.保证没有元素重复的唯一方法是使用min.dist=length(unique(x))-1,否则就会出现最多r < min.dist元素与它们上次出现的位置之间的距离不足min.dist,如果存在这样的元素,则它们将位于生成向量的length(x) + 1 - 1:min.dist子集中。

为了确保完全确定,可以使用循环来检查输出向量的尾部是否具有唯一值:(删除我用于演示目的的打印语句)

shuffler <- function(x, min.dist=2){
    while(!length(unique(print(tail(l<-shuffle(x, min.dist=min.dist), min.dist+1))))==min.dist+1){}
    l
}

table(print(shuffler(rep(LETTERS[1:5], 3),2)))
 [1] "A" "B" "C" "E" "B" "C" "D" "A" "C" "D" "A" "E" "B" "D" "E"

A B C D E 
3 3 3 3 3 

table(print(shuffler(rep(LETTERS[1:5], 3),2)))
[1] "D" "C" "C"
[1] "C" "C" "E"
[1] "C" "A" "C"
[1] "D" "B" "D"
[1] "B" "E" "D"
 [1] "C" "A" "E" "D" "A" "B" "C" "E" "A" "B" "D" "C" "B" "E" "D"

A B C D E 
3 3 3 3 3 

更新:

shuffler <- function(x, min.dist=2){
    while(any(unlist(lapply(unique(tl<-tail(l<-shuffle(x, min.dist=min.dist), 2*min.dist)), function(x) diff(which(tl==x))<=min.dist)))){}
    l
}

这个新版本在测试矢量尾部元素的min.dist时更为严格,而上一个版本只适用于min.dist=2,然而这个新版本进行了更好的测试。


@27ϕ9,感谢您的评论,一开始我没有注意到。您能检查一下编辑吗? - Abdessabour Mtk
通过 shuffle(rep(letters[1:2], c(2, 3)), 1) 进行测试,有时会得到无效的 "ababb";唯一可能的排列应该是 "babab"。 - Mikko Marttila
@MikkoMarttila 感谢您的评论,您所需要做的就是用“shuffler”替换调用。 - Abdessabour Mtk
1
@AbdessabourMtk 很抱歉回复晚了!我想要将你的代码应用到我的实际数据上,所以需要进行一些准备工作。现在我已经将你的代码应用到我的数据上,它完美地运行了。非常感谢你的所有努力、测试和代码开发! - Joachim Schork

6
如果您的数据很大,那么使用概率来完成此类任务可能会更快。以下是一个示例:
prob_shuffler = function(x, min.dist = 2){
    n = length(x)
    res = sample(x)
    OK = FALSE
    
    # We loop until we have a solution
    while(!OK){
        OK = TRUE
        for(i in 1:min.dist){
            # We check if identical elements are 'i' steps away
            pblm = res[1:(n-i)] == res[-(1:i)]
            if(any(pblm)){
                if(sum(pblm) >= (n - i)/2){
                    # back to square 1
                    res = sample(x)
                } else {
                    # we pair each identical element with 
                    # an extra one
                    extra = sample(which(!pblm), sum(pblm))
                    id_reshuffle = c(which(pblm), extra)
                    res[id_reshuffle] = sample(res[id_reshuffle])
                }

                # We recheck from the beginning
                OK = FALSE
                break
            }
        }
    }

    res
}

虽然while循环看起来令人生畏,但实际上收敛速度很快。当然,两个字符之间距离为min.dist的概率越低,收敛速度越快。

目前@Abdessabour Mtk和@Carles Sans Fuentes提供的解决方案可以工作,但是根据输入数据的大小,很快就会变得难以承受。以下是一个基准测试:

library(microbenchmark)

x = rep(c(letters, LETTERS), 10)
length(x)
#> [1] 520

microbenchmark(prob_shuffler(x, 1), shuffler_am(x, 1), shuffler_csf(x, 1), times = 10)
#> Unit: microseconds
#>                 expr       min        lq       mean    median        uq        max neval
#>  prob_shuffler(x, 1)    87.001   111.501    155.071   131.801   192.401    264.401    10
#>    shuffler_am(x, 1) 17218.100 18041.900  20324.301 18740.351 22296.301  26495.200    10
#>   shuffler_csf(x, 1) 86771.401 88550.501 118185.581 95582.001 98781.601 341826.701    10

microbenchmark(prob_shuffler(x, 2), shuffler_am(x, 2), shuffler_csf(x, 2), times = 10)
#> Unit: microseconds
#>                 expr     min        lq       mean    median        uq        max neval
#>  prob_shuffler(x, 2)   140.1   195.201   236.3312   245.252   263.202    354.101    10
#>    shuffler_am(x, 2) 18886.2 19526.901 22967.6409 21021.151 26758.800  29133.400    10
#>   shuffler_csf(x, 2) 86078.1 92209.901 97151.0609 97612.251 99850.101 107981.401    10

microbenchmark(prob_shuffler(x, 3), shuffler_am(x, 3), shuffler_csf(x, 3), times = 10)
#> Unit: microseconds
#>                 expr       min        lq        mean     median       uq        max neval
#>  prob_shuffler(x, 3)   318.001   450.402    631.5312    573.352    782.2   1070.401    10
#>    shuffler_am(x, 3) 19003.501 19622.300  23314.4808  20784.551  28281.5  32885.101    10
#>   shuffler_csf(x, 3) 87692.701 96152.202 101233.5411 100925.201 108034.7 113814.901    10

我们可以指出两个事情: a)在所有逻辑中,prob_shuffler的速度取决于min.dist,而其他方法则不太依赖,b)prob_shuffler仅使用520个观测数据就快了近100倍(并且可以扩展)。
当然,如果min.dist处有两个相同字符的概率非常高,则递归方法应该更快。但在大多数实际情况下,概率方法更快。

1
这是一种非常有趣的方法!但是,也许可以增加一个 max.iter 或类似的参数,以防止在输入无法收敛的情况下出现死循环。 - Mikko Marttila
@LaurentBergé 非常感谢您提供这种替代方法!实际上,我注意到即使我的数据还不是很大,当我增加了min.dist时,其他方法也需要一些时间。因此,我认为您的方法对于拥有更大数据的每个人都非常棒。 - Joachim Schork
@MikkoMarttila:当然,算法可以改进,但这只是为了提供一个快速而简单的替代方案。 - Laurent Bergé

3

我希望这个答案对您有用。 它使用基本的R语言编写,但是它是有效的。 如果您想逐行检查,我将保留打印格式:

x <- rep(LETTERS[1:5], 3)  # Create example vector


shuffle <- function(x, min_dist=3){
  #init variables   
  result<-c() # result vector
  count<-0
  vec_use<-x
  vec_keep<-c()
  for(i in 1:length(x)){
#    print(paste0("iteration =", i))
    if (count>min_dist){
      valback<-vec_keep[1]
#      print(paste0("value to be returned:",  valback))
      ntimes_valback<-(table(vec_keep)[valback])
      vec_use<- c(vec_use,rep(valback,ntimes_valback))
#      print(paste0("vec_use after giving back valbak =", valback))
#      print(paste0(vec_use,","))
      vec_keep <- vec_keep[!vec_keep %in% valback]
#      print(paste0("vec_keep after removing valback =", valback))
#      print(paste0(vec_keep,","))
    }
    val<-sample(vec_use,1)
#    print(paste0("val = ",val))#remove value
    vec_keep<- c(vec_keep,x[x %in% val])
    vec_keep<-vec_keep[1:(length(vec_keep)-1)]#removing 1 letter
#    print(paste0("vec_keep ="))
#    print(paste0(vec_keep,","))
    vec_use <- vec_use[!vec_use %in% val]
#    print(paste0("vec_use ="))
#    print(paste0(vec_use,","))
    result[i]<-val
    count<-count+1
    }
  return(result)
}
shuffle(x)
"C" "D" "B" "E" "C" "A" "B" "D" "E" "A" "C" "D" "B" "E" "C"

非常感谢您的代码。它与我的数据完美地配合!我选择了Abdessabour Mtk的“被接受的答案”,因为他回答得最快。但是您的解决方案也同样有效。 - Joachim Schork
1
没问题!我认为这个逻辑更清晰(但很高兴能帮助!) - Carles

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接