Here's a hack, though realize that there's never a guarantee that it'll work.
#' Random sampling of data
#'
#' Return a sample of the dataset's rows where the sum of 'fld' values
#' is between the two numbers of 'sumbetween'.
#'
#' @param dat data.frame
#' @param fld character, the name of one of the fields in 'dat'
#' @param sumbetween numeric, length 2, the two ends of the range of
#' desired sum
#' @param suggestn integer, a suggestion for 'n' around which sample
#' sizes are based; the actual samples attempted will vary between
#' 0.5 and 1.5 times this value; if 'NA' (the default), then it
#' defaults naively to 'mean(sumbetween) / median(dat[[fld]])'
#' @param iters integer, number of samples to attempt before
#' "giving up" (otherwise this might run forever)
#' @return data.frame, a sample of the original dataset; regardless of
#' success, two attributes are included, 'mu' and 'sigma',
#' indicating the mean and standard deviation of the samples tested
random_sample <- function(dat, fld, sumbetween, suggestn = NA, iters = 100) {
stopifnot(fld %in% names(dat), is.numeric(dat[[fld]]), is.numeric(sumbetween))
if (is.na(suggestn)) {
suggestn <- mean(sumbetween) / median(dat[[fld]])
}
suggestn <- min(suggestn, nrow(dat))
mu <- NA
Sn <- 0
ind <- FALSE
n <- 0L
while ((is.na(iters) || n < iters) && !ind) {
n <- n + 1L
size <- min(nrow(dat), sample(seq(max(1, floor(suggestn/2)), ceiling(suggestn*1.5)), size = 1))
rows <- sample(nrow(dat), size = size)
s <- sum(dat[[fld]][rows])
ind <- sumbetween[1] <= s & s <= sumbetween[2]
# incremental mean and almost-variance of the samples
# http://datagenetics.com/blog/november22017/index.html
lastmu <- mu
mu <- sum(s, (n-1)*mu, na.rm = TRUE)/n
Sn <- Sn + sum(s, -lastmu, na.rm = TRUE)*sum(s, -mu, na.rm = TRUE)
}
out <- if (ind) dat[rows,] else NA
if (!ind) warning("unable to find a successful sample after ", n, " iterations")
# actual mean and variance of samples, successful or not
attr(out, "mu") <- mu
attr(out, "sigma") <- sqrt(Sn / n)
return(out)
}
And its use is below. I use str
here to demonstrate one feature: the addition of the all tested samples' means and deviations as attributes. If success, the attributes are not shown (print.data.frame
by default shows no attributes), but if it fails then a warning will be given, and NA
returned with the same attributes.
set.seed(42)
str(random_sample(mtcars, "mpg", c(90,100), iters=20))
# Warning in random_sample(mtcars, "mpg", c(90, 100), iters = 20) :
# unable to find a successful sample after 20 iterations
# logi NA
# - attr(*, "mu")= num 106
# - attr(*, "sigma")= num 37.9
str(random_sample(mtcars, "mpg", c(90,100), iters=20))
# 'data.frame': 5 obs. of 12 variables:
# $ mpg : num 33.9 14.3 14.7 18.1 17.3
# $ cyl : num 4 8 8 6 8
# $ disp: num 71.1 360 440 225 275.8
# $ hp : num 65 245 230 105 180
# $ drat: num 4.22 3.21 3.23 2.76 3.07
# $ wt : num 1.83 3.57 5.34 3.46 3.73
# $ qsec: num 19.9 15.8 17.4 20.2 17.6
# $ vs : num 1 0 0 1 0
# $ am : num 1 0 0 0 0
# $ gear: num 4 3 3 3 3
# $ carb: num 1 4 4 1 3
# $ new1: num 75.1 368 448 231 283.8
# - attr(*, "mu")= num 96.1
# - attr(*, "sigma")= num 42.1
The intent of the returns mean/deviation is to help the user determine if the suggestn
(suggestion for a starting sample size) is mis-placed, or if iters
is just too small and we quit too early (such as when the intended range is well within mu +/- sigma
).
This uses iters
to prevent an infinite loop. You can disable it (off to the races!) at your own peril.
This makes no promises that a feasible solution can be found. Imagine all values are multiples of 20, and the desired range is only 10 wide. There are certainly other conditions that are heuristically difficult to "know" with certainty to know if a solution exists.