How to generate random values where a percentage of them are 0?

Viewed 2941

I create a random stream

Random random = new Random();
Stream<Integer> boxed = random.ints(0, 100000000).boxed();

But I need 60% of the numbers generated to be 0, while the remaining can be truly random. How can I do it?

EDIT:

And I need only positive numbers and between 0-100

1
2
0
0
9
0
0
1
12
8 Answers

I'll assume the OP wants approximately 60% of the generated values to be zero, and the remaining approximate 40% to be (pseudo-)random values in the range 1-100, inclusive.

The JDK library makes it easy to generate a stream of N different values. Since there are 100 values in the range [1,100], and this represents 40% of the output, there need to be 150 values that map to zero to cover the remaining 60%. Thus N is 250.

We can create a stream of ints in the rage [0,249] (inclusive) and map the lowest 150 values in this range to zero, leaving the remainder in the range [1,100]. Here's the code:

    IntStream is = random.ints(0, 250)
                         .map(i -> Math.max(i-149, 0));

UPDATE

If the task is to produce exactly 60% zeroes, there's a way to do it, using a variation of an algorithm in Knuth, TAOCP Vol 2, sec 3.4.2, Random Sampling and Shuffling, Algorithm S. (I explain this algorithm in a bit more detail in this other answer.) This algorithm lets one choose n elements at random from a collection of N total elements, making a single pass over the collection.

In this case we're not selecting elements from a collection. Instead, we're emitting a known quantity of numbers, requiring some subset of them to be zeroes, with the remainder being random numbers from some range. The basic idea is that, as you emit numbers, the probability of emitting a zero depends on the quantity of zeroes remaining to be emitted vs. the quantity of numbers remaining to be emitted. Since this is a stream of fixed size, and it has a bit of state, I've opted to implement it using a Spliterator:

static IntStream randomWithPercentZero(int count, double pctZero, int range) {
    return StreamSupport.intStream(
        new Spliterators.AbstractIntSpliterator(count, Spliterator.SIZED) {
            int remainingInts = count;
            int remainingZeroes = (int)Math.round(count * pctZero);
            Random random = new Random();

            @Override
            public boolean tryAdvance(IntConsumer action) {
                if (remainingInts == 0)
                    return false;

                if (random.nextDouble() < (double)remainingZeroes / remainingInts--) {
                    remainingZeroes--;
                    action.accept(0);
                } else {
                    action.accept(random.nextInt(range) + 1);
                }
                return true;
            }
        },
        false);
}

There's a fair bit of boilerplate, but you can see the core of the algorithm within tryAdvance. If no numbers are remaining, it returns false, signaling the end of the stream. Otherwise, it emits a number, with a certain probability (starting at 60%) of it being a zero, otherwise a random number in the desired range. As more zeroes are emitted, the numerator drops toward zero. If enough zeroes have been emitted, the fraction becomes zero and no more zeroes are emitted.

If few zeroes are emitted, the denominator drops until it gets closer to the numerator, increasing the probability of emitting a zero. If few enough zeroes are emitted, eventually the required quantity of zeroes equals the quantity of numbers remaining, so the value of the fraction becomes 1.0. If this happens, the rest of the stream is zeroes, so enough zeroes will always be emitted to meet the requirement. The nice thing about this approach is that there is no need to collect all the numbers in an array and shuffle them, or anything like that.

Call the method like this:

IntStream is = randomWithPercentZero(1_000_000, 0.60, 100);

This gets a stream of 1,000,000 ints, 60% of which are zeroes, and the remainder are in the range 1-100 (inclusive).

Since the size of the target interval is divisible by ten, you can count on the last digit of generated numbers being uniformly distributed. Hence, this simple approach should work:

  • Generate numbers in the range 0..1000
  • If the last digit of the random number r is 0..5, inclusive, return zero
  • Otherwise, return r / 10

Here is this approach in code:

Stream<Integer> boxed = random.ints(0, 1000).map(r -> r%10 < 6 ? 0 : r/10).boxed();

Demo.

You can use IntStream.map and re-use your Random instance to generate a random number from 0 to 9 inclusive, returning zero if it's in the first 60%, else the generated number:

Stream<Integer> boxed = random.ints(0, 100)
                              .map(i -> (random.nextInt(10) < 6) ? 0 : i)
                              .boxed();

Construct 10 objects. 6 of them returns 0 all the time. and rest 4 returns random based on your specs.

Now randomly select one of the object and call

List<Callable<Integer>> callables = new ArrayList<>(10);
for (int i = 0; i < 6; i++) {
    callables.add(() -> 0);
}
Random rand = new Random();
for (int i = 6; i < 10; i++) {          
    callables.add(() -> rand.nextInt());
}

callables.get(rand.nextInt(10)).call();

This is simpler way to implement it. You can optimize it further.

If you want exactly 60% of zeros and 40% of strictly positive numbers, you can simply use a modulus check:

Stream<Integer> boxed = IntStream.range(0, 100_000_000)
                            .map(i -> (i % 10 < 6) ? 0 : r.nextInt(Integer.MAX_VALUE) + 1)
                            .boxed();

You may want to "shuffle" the stream after that to avoid having 6 zeros in a row every ten positions.

Why not generate an array of those 60% (those are zero values to start with) and just randomly generate the other 40%:

List<Integer> toShuffle = IntStream
            .concat(Arrays.stream(new int[60_000_000]),
                    random.ints(40_000_000, 0, Integer.MAX_VALUE))
            .boxed()
            .collect(Collectors.toCollection(() -> new ArrayList<>(100_000_000)));

    Collections.shuffle(toShuffle);

You can controll your stream to generate exactly 60% of zero by counting the generated values and producing zero to reach the desired 60%.

public class RandomGeneratorSample {
    public static void main(String... strings) {
        Random random = new Random();
        Controll60PercentOfZero controll60 = new Controll60PercentOfZero();

        Stream<Integer> boxed = random.ints(0, 100).map(x -> controll60.nextValueMustBeZero(x) ? 0 : x).boxed();

        boxed.forEach(System.out::println);
    }

    static class Controll60PercentOfZero {
        private long count_zero = 1;
        private long count_not_zero = 1;

        public boolean nextValueMustBeZero(int x) {

            if (x == 0) {
                count_zero++;
            } else {
                count_not_zero++;
            }           
            boolean nextValueMustBeZero= (count_zero * 100 / count_not_zero) < 60;
            if(nextValueMustBeZero){
                count_zero++;
            }
            return nextValueMustBeZero;
        }
    }

}

Interesting question.
Most of other answers are very relevant and each one handles it from a different point of view.
I would like to contribute.

To get a Collection (and not a stream) with exactly 60% of 0 and that these appear "pseudo-randomly", you could :

  • declare and instantiate a List

  • then loop on the number of elements that you want to add it

  • inside it, every 6 iterations on 10, you add 0 at a random index in the List. Otherwise, you add a random value at a random index in the List.

The drawback is that about 40 % of the time, nextInt() is invoked twice: once to generate the value and another one to generate the index where inserting the value in the List.

Here is a sample code that generate 1000 elements from 0 to 1000 which exactly 60% are 0 :

Random random = new Random();
List<Integer> values = new ArrayList<>();

for (int i = 0; i < 1000; i++) {
    int nextValue = i % 10 < 6 ? 0 : random.nextInt(1000) + 1;
    int indexInList = values.size() <= 1 ? 0 : random.nextInt(values.size() - 1);
    values.add(indexInList, nextValue);
}
Related