0
\$\begingroup\$

I originally posted this on StackOverflow but was requested to post it here instead as it relates to optimization/performance of the code rather than a specific problem.

TL;DR: Get-Random produces an even distribution of numbers where every number in the pool has an even chance of appearing. I'm looking for a way to generate random numbers where every individual number appears with a frequency that I myself specify.


If I run

for($i=1;$i -le 1000;$i++){
    Get-Random -Minimum 0 -Maximum 10
}

I get a very evenly distributed count for each number (roughly 100 for each number between 0 and 9). Using Get-Random allows me to get a random number every time but on average, every individual result will appear roughly an equal amount of times. I want to decide the frequency for how often any specific number appears, and then generate random numbers that fit that distribution. As an example:

Number   Probability
0        10
1        11
2        19
3        12
4        3
5        10
6        6
7        7
8        4
9        18

I'm looking for a way to use the above list to randomly generate a number between 0 to 9, but with the probability of each individual number appearing using the Probability column.

My very hard-coded and not so generic solution so far is that I thought of doing something like adding a cumulative percentage column:

Number   Probability   CumulativeProbability
0        10            10
1        11            21
2        19            40
3        12            52
4        3             55
5        10            65
6        6             71
7        7             78
8        4             82
9        18            100

And from here, run the object through a filter. Something like:

$RandomNumber = Get-Random -Minimum 0 -Maximum 100
$MyProbabilityObject | Where-Object {$RandomNumber -ge $_.CumulativeProbability}

This gives me all numbers with a lower Cumulative probability than the random number. Let's say the $RandomNumber was 42, that would result in:

Number   Probability   CumulativeProbability
0        10            10
1        11            21
2        19            40

From here, I could pipe the result to

Select-Object CumulativeProbability | Measure-Object -Property CumulativeProbability -Maximum 

Which gives me the highest value, and then use that column as a reference to find the number with

Where-Object {$_.CumulativeProbability -eq $TheNumberIGetAbove}

While this kinda works, it feels like I'm doing several laps around a problem that should be easier and more straightforward to solve. Are there any better ways of generating random numbers that fit a distribution you specify yourself instead of using an even distribution such as Get-Random?

\$\endgroup\$
1
  • 1
    \$\begingroup\$ you could try breaking your range into batches and then generating numbers for each new sub-range. ///// as an aside, the -Maximum parameter is EXCLUSIVE. that means -min 1 -max 5 will NEVER give you a 5 since the inclusive range is 1..4. also, using -InputObject $Range is faster ... and is inclusive. [grin] \$\endgroup\$
    – Lee_Dailey
    Apr 16, 2021 at 13:47

1 Answer 1

2
\$\begingroup\$

Since you tagged your question with Performance, my example is not totally idiomatic PowerShell.

You could build a lookup array with each number as many times as you have defined the probability for them.

Afterwards you can easily index into it using one or an array of random (index) numbers.

$propabilityDefinition = @{
    0 = 10
    1 = 11
    2 = 19
    3 = 12
    4 = 3
    5 = 10
    6 = 6
    7 = 7
    8 = 4
    9 = 18
}

# build a lookup array with each number as many times as defined as probability
$numberLookup = foreach ($entry in $propabilityDefinition.GetEnumerator()) { 
    # using foreach with Linq.Enumerable.Repeat is the fastest way I found so far
    [System.Linq.Enumerable]::Repeat($entry.Key, $entry.Value)
}

# get x random numbers within the scope of the array indexes. 
# with PS7+ we can also use the Count parameter of Get-Random, but don't use "Get-Random" within a loop as it hits the performance quite hard
#   PS7 variant: "$randomIndex = (Get-Random -Minimum 0 -Maximum $numberLookup.Count -Count 1000)"
# if you only want to get one random number, you can always use "$randomIndex = Get-Random -Minimum 0 -Maximum $numberLookup.Count"
$random = [System.Random]::new()
$randomIndex = foreach ($null in 0..10000) {
    $random.Next(0, $numberLookup.Count)
}


# we can use the entiry array of random numbers at once for the index operator
$numberLookup[$randomIndex] | Group-Object -NoElement | Sort-Object Count -Descending

Output:

Count Name
----- ----
 1896 2
 1821 9
 1194 3
 1141 1
  983 0
  979 5
  694 7
  596 6
  393 8
  304 4
\$\endgroup\$
1
  • \$\begingroup\$ Wow, thank you! I'm still fairly new to programming in general and powershell in particular so the [System.Linq.Enumerable] was complete news to me. \$\endgroup\$ May 10, 2021 at 1:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.