I am wondering what would be the best way (e.g. in Java) to generate random numbers within a particular range where each number has a certain probability to occur or not?
Here is the python code even though you ask for java, but it's very similar.
# weighted probability
theta = np.array([0.1,0.25,0.6,0.05])
print(theta)
sample_axis = np.hstack((np.zeros(1), np.cumsum(theta)))
print(sample_axis)
[0. 0.1 0.35 0.95 1. ]. This represent the cumulative distribution.
you can use a uniform distribution to draw an index in this unit range.
def binary_search(axis, q, s, e):
if e-s <= 1:
print(s)
return s
else:
m = int( np.around( (s+e)/2 ) )
if q < axis[m]:
binary_search(axis, q, s, m)
else:
binary_search(axis, q, m, e)
range_index = np.random.rand(1)
print(range_index)
q = range_index
s = 0
e = sample_axis.shape[0]-1
binary_search(sample_axis, q, 0, e)
Yours is a pretty good way already and works well with any range.
Just thinking: another possibility is to get rid of the fractions by multiplying with a constant multiplier, and then build an array with the size of this multiplier. Multiplying by 10 you get
P(1) = 2
P(2) = 3
P(3) = 5
Then you create an array with the inverse values -- '1' goes into elements 1 and 2, '2' into 3 to 6, and so on:
P = (1,1, 2,2,2, 3,3,3,3,3);
and then you can pick a random element from this array instead.
(Add.) Using the probabilities from the example in kiruwka's comment:
int[] numsToGenerate = new int[] { 1, 2, 3, 4, 5 };
double[] discreteProbabilities = new double[] { 0.1, 0.25, 0.3, 0.25, 0.1 };
the smallest multiplier that leads to all-integers is 20, which gives you
2, 5, 6, 5, 2
and so the length of numsToGenerate
would be 20, with the following values:
1 1
2 2 2 2 2
3 3 3 3 3 3
4 4 4 4 4
5 5
The distribution is exactly the same: the chance of '1', for example, is now 2 out of 20 -- still 0.1.
This is based on your original probabilities all adding up to 1. If they do not, multiply the total by this same factor (which is then going to be your array length as well).
Your approach is fine for the specific numbers you picked, although you could reduce storage by using an array of 10 instead of an array of 100. However, this approach doesn't generalize well to large numbers of outcomes or outcomes with probabilities such as 1/e
or 1/PI
.
A potentially better solution is to use an alias table. The alias method takes O(n)
work to set up the table for n
outcomes, but then is constant time to generate regardless of how many outcomes there are.
there is one more effective way rather than getting into fractions or creating big arrays or hard coding range to 100
in your case array becomes int[]{2,3,5} sum = 10 just take sum of all the probablity run random number generator on it result = New Random().nextInt(10)
iterate over array elements from index 0 and calculate sum and return when sum is greater than return element of that index as a output
i.e if result is 6 then it will return index 2 which is no 5
this solution will scale irrespective of having big numbers or size of the range
Written this class for interview after referencing the paper pointed by pjs in another post , the population of base64 table can be further optimized. The result is amazingly fast, initialization is slightly expensive, but if the probabilities are not changing often, this is a good approach.
*For duplicate key, the last probability is taken instead of being combined (slightly different from EnumeratedIntegerDistribution behaviour)
public class RandomGen5 extends BaseRandomGen {
private int[] t_array = new int[4];
private int sumOfNumerator;
private final static int DENOM = (int) Math.pow(2, 24);
private static final int[] bitCount = new int[] {18, 12, 6, 0};
private static final int[] cumPow64 = new int[] {
(int) ( Math.pow( 64, 3 ) + Math.pow( 64, 2 ) + Math.pow( 64, 1 ) + Math.pow( 64, 0 ) ),
(int) ( Math.pow( 64, 2 ) + Math.pow( 64, 1 ) + Math.pow( 64, 0 ) ),
(int) ( Math.pow( 64, 1 ) + Math.pow( 64, 0 ) ),
(int) ( Math.pow( 64, 0 ) )
};
ArrayList[] base64Table = {new ArrayList<Integer>()
, new ArrayList<Integer>()
, new ArrayList<Integer>()
, new ArrayList<Integer>()};
public int nextNum() {
int rand = (int) (randGen.nextFloat() * sumOfNumerator);
for ( int x = 0 ; x < 4 ; x ++ ) {
if (rand < t_array[x])
return x == 0 ? (int) base64Table[x].get(rand >> bitCount[x])
: (int) base64Table[x].get( ( rand - t_array[x-1] ) >> bitCount[x]) ;
}
return 0;
}
public void setIntProbList( int[] intList, float[] probList ) {
Map<Integer, Float> map = normalizeMap( intList, probList );
populateBase64Table( map );
}
private void clearBase64Table() {
for ( int x = 0 ; x < 4 ; x++ ) {
base64Table[x].clear();
}
}
private void populateBase64Table( Map<Integer, Float> intProbMap ) {
int startPow, decodedFreq, table_index;
float rem;
clearBase64Table();
for ( Map.Entry<Integer, Float> numObj : intProbMap.entrySet() ) {
rem = numObj.getValue();
table_index = 3;
for ( int x = 0 ; x < 4 ; x++ ) {
decodedFreq = (int) (rem % 64);
rem /= 64;
for ( int y = 0 ; y < decodedFreq ; y ++ ) {
base64Table[table_index].add( numObj.getKey() );
}
table_index--;
}
}
startPow = 3;
for ( int x = 0 ; x < 4 ; x++ ) {
t_array[x] = x == 0 ? (int) ( Math.pow( 64, startPow-- ) * base64Table[x].size() )
: ( (int) ( ( Math.pow( 64, startPow-- ) * base64Table[x].size() ) + t_array[x-1] ) );
}
}
private Map<Integer, Float> normalizeMap( int[] intList, float[] probList ) {
Map<Integer, Float> tmpMap = new HashMap<>();
Float mappedFloat;
int numerator;
float normalizedProb, distSum = 0;
//Remove duplicates, and calculate the sum of non-repeated keys
for ( int x = 0 ; x < probList.length ; x++ ) {
mappedFloat = tmpMap.get( intList[x] );
if ( mappedFloat != null ) {
distSum -= mappedFloat;
} else {
distSum += probList[x];
}
tmpMap.put( intList[x], probList[x] );
}
//Normalise the map to key -> corresponding numerator by multiplying with 2^24
sumOfNumerator = 0;
for ( Map.Entry<Integer, Float> intProb : tmpMap.entrySet() ) {
normalizedProb = intProb.getValue() / distSum;
numerator = (int) ( normalizedProb * DENOM );
intProb.setValue( (float) numerator );
sumOfNumerator += numerator;
}
return tmpMap;
}
}
If you are not against adding a new library in your code, this feature is already implemented in MockNeat, check the probabilities() method.
Some examples directly from the wiki:
String s = mockNeat.probabilites(String.class)
.add(0.1, "A") // 10% chance
.add(0.2, "B") // 20% chance
.add(0.5, "C") // 50% chance
.add(0.2, "D") // 20% chance
.val();
Or if you want to generate numbers within given ranges with a given probability you can do something like:
Integer x = m.probabilites(Integer.class)
.add(0.2, m.ints().range(0, 100))
.add(0.5, m.ints().range(100, 200))
.add(0.3, m.ints().range(200, 300))
.val();
Disclaimer: I am the author of the library, so I might be biased when I am recommending it.