Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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
like image 323
ip696 Avatar asked Nov 03 '17 18:11

ip696


2 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).

like image 169
Stuart Marks Avatar answered Nov 01 '22 09:11

Stuart Marks


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.

like image 24
jmj Avatar answered Nov 01 '22 09:11

jmj