I have a Monte Carlo simulation in VBA. The client wants (dont question why) to fix random number sequence, i.e. every time you run the model, sequence shall stay the same. I managed to fix random seed as described here. BUT it is not the same on different PCs. Any idea why and how can I also fix it on different machines?
You can use the rnd function with a negative argument to achieve a repeating list of random numbers.
Here is a link to the documentation:
http://office.microsoft.com/en-us/access-help/rnd-function-HA001228901.aspx
Note To repeat sequences of random numbers, call Rnd with a negative argument immediately before using Randomize with a numeric argument. Using Randomize with the same value for number does not repeat the previous sequence.
Sub TestRandomNumberSequence()
rnd (-10)
For i = 1 To 5
Randomize 10
MsgBox BetweenRange(1, 20, rnd)
Next i
'always returns the following sequence
'5
'18
'19
'6
'17
End Sub
Function BetweenRange(min As Integer, max As Integer, ByVal rnd As Double) As Integer
BetweenRange = Int((max - min + 1) * rnd + min)
End Function
As per your request, please checkout the following link:
Wabash College Download
Related
I'm looking for a random number generator that I can use to alter the size of a picture box on my form. Currently, I have this solution found here on StackOverflow:
Public Function GetRandomNumber(ByVal Min As Integer, ByVal Max As Integer) As Integer
Static lcGenerator As System.Random = New System.Random()
Return lcGenerator.Next(Min, Max)
End Function
As it stands, the function will generate a random integer between the range specified. This is great, but I am noticing that often the picture box doesn't change in size with a decent amount of variation. The following code shows how I am calculating variance in size:
Dim lcModifier As Decimal = (GetRandomNumber(-20, 11) * 0.01)
pbForeground.Width = CInt((0.4 - lcModifier) * pbBackground.Width)
pbForeground.Height = CInt((0.4 - lcModifier) * pbBackground.Height)
The idea is that I generate a random number between -20 and 11. This would yield a 20% increase or 10% decrease, as the minimum is inclusive but the maximum is exclusive, and I am subtracting the modifier.
I had the idea to use a Single value in place of the integer value. The Single type gave me more "room" away from 0 than a Double would, but the Next method only uses Integer values, and we're back to my initial problem.
The NextDouble method works differently to the Next method, and I am unable to specify Min and Max values, nor can I generate a negative value.
Is there any way I can generate a value using Minimum and Maximum bounds that is not close to zero?
Thanks,
sunnyCr
The NextDouble return a percentage. You just need to multiply by the range of value and then translate to the desired starting position.
Function GetRandomNumber(ByVal low As Double, high As Double) As Double
Static rng As New Random
Return (rng.NextDouble() * (high - low)) + low
End Function
In trying to solve this question, I wrote the following in an attempt to implement the Box-Muller transform to generate random normal variables in pure VBA:
Function RandNorm(Optional mean As Double = 0, Optional sd As Double = 1) As Double
Dim s As Double
s = Sqr(-2 * Log(Rnd())) * Cos(6.283185307 * Rnd()) '6.28 etc. is 2*pi
RandNorm = mean + sd * s
End Function
The following somewhat weak test always works, returning a number close to 0:
Sub test1()
Randomize
Dim s As Double
Dim i As Long
For i = 1 To 17000000
s = s + RandNorm()
Next i
Debug.Print s / 17000000
End Sub
On the other hand, the following test never works (because it tries to take the log of 0, which is undefined):
Sub test2()
Randomize
Dim s As Double
Dim i As Long
Debug.Print Rnd() 'just to clock it
For i = 1 To 17000000
s = s + RandNorm()
Next i
Debug.Print s / 17000000
End Sub
The problem is that rnd() returns 0 on average once out of every 2^24 (a bit less than 17,000,000) calls. It is of course easy enough to tweak the definition of RandNorm to avoid the zero (see the linked-to question), but I am still puzzled by the above code. It would make perfect sense to me if each test failed half the time (when the zero is fed into Log()) and worked half the time (when the zero is fed into Cos()). It seems that Randomize avoids at least half of the possible seeds.
Why does Randomize behave this way? Is there a way to seed the random number generator so that all possible states of the random number generator can occur?
On Edit
If I define the following sub:
Sub ReRandomize()
Dim r As Double
Randomize
If Rnd() > 0.5 Then r = Rnd()
End Sub
And modify test1 and test2 above to use ReRandomize instead of Randomize, both of the test subs will fail 50% of the time, so that might answer the part of the question about if there is "a way to seed the random number generator so that all possible states of the random number generator can occur?" It is still mysterious as to why Randomize behaves the way that it does. This is the second time that an Excel VBA question made me realize that Randomize is a weird sub. None of this matters very much for typical use of rnd(), but it does underscore that it is a somewhat low quality random number generator which shouldn't be used for serious statistical work.
I simply modified the Rnd calc to not include 0 or 1. You have to remember that the Rnd Function can produce a number (of type double) in the range of 0 or 1. Therefore, it's chances of having a duplicate number are pretty low.
dbl1stRnd = Rnd()
dblRnd = (0.9999 - 0.0001) * dbl1stRnd + 0.0001
s = Sqr(-2 * Log(dblRnd)) * Cos(6.283185307 * dblRnd) '6.28 etc. is 2*pi
Some example outputs of the regular Rnd() function with Randomize:
3.633606E-02
0.2324036
0.3460443
0.5870923
5.553758E-02
0.2629338
0.2400494
0.1982901
0.5923058
0.7915452
0.4874671
0.2062811
0.5676001
0.1178594
1.932621E-03
0.4326598
0.8291379
I hope this explains some and is what you are looking for.
In response to this question I ran the following VBA experiment:
Sub Test()
Dim i As Long, A As Variant
Dim count1 As Long, count2 As Long
ReDim A(1 To 10000)
For i = 1 To 10000
Randomize
A(i) = IIf(Rnd() < 0.5, 0, 1)
Next i
'count how often A(i) = A(i+1)
For i = 1 To 9999
If A(i) = A(i + 1) Then count1 = count1 + 1
Next i
For i = 1 To 10000
A(i) = IIf(Rnd() < 0.5, 0, 1)
Next i
'count how often A(i) = A(i+1)
For i = 1 To 9999
If A(i) = A(i + 1) Then count2 = count2 + 1
Next i
Debug.Print "First Loop: " & count1
Debug.Print "Second Loop: " & count2 & vbCrLf
End Sub
When I saw output like this:
First Loop: 5550
Second Loop: 4976
I was pretty sure that I knew what was happening: VBA was converting the system clock into something of lower resolution (perhaps microsecond) which as a consequence would lead to Randomize sometimes producing identical seeds in two or more passes through the loop. In my original answer I even confidently asserted this. But then I ran the code some more and noticed that the output was sometimes like this:
First Loop: 4449
Second Loop: 5042
The overseeding is still causing a noticeable autocorrelation -- but in the opposite (and unexpected) direction. Successive passes through the loop with the same seed should produce identical outputs, hence we should see successive values agreeing more often than chance would predict, not disagreeing more often than chance would predict.
Curious now, I modified the code to:
Sub Test2()
Dim i As Long, A As Variant
Dim count1 As Long, count2 As Long
ReDim A(1 To 10000)
For i = 1 To 10000
Randomize
A(i) = Rnd()
Next i
'count how often A(i) = A(i+1)
For i = 1 To 9999
If A(i) = A(i + 1) Then count1 = count1 + 1
Next i
For i = 1 To 10000
A(i) = Rnd()
Next i
'count how often A(i) = A(i+1)
For i = 1 To 9999
If A(i) = A(i + 1) Then count2 = count2 + 1
Next i
Debug.Print "First Loop: " & count1
Debug.Print "Second Loop: " & count2 & vbCrLf
End Sub
Which always gives the following output:
First Loop: 0
Second Loop: 0
It seems that it isn't the case that successive calls to Randomize sometimes returns the same seed (at least not often enough to make a difference).
But if that isn't the source of the autocorrelation -- what is? And -- why does it sometimes manifest itself as a negative rather than a positive autocorrelation?
Partial answer only, fell free to edit and complete.
Well, there is clearly a correlation when you overuse the Randomize function.
I tried the following code, with a conditional formatting (black fill for values >0.5), and there is clearly patterns appearing (try to comment the Randomize to see a more "random" pattern. (best seen with 20 pt columns and 10% zoom)
Function Rndmap()
Dim i As Long, j As Long
Dim bmp(1 To 512, 1 To 512) As Long
For i = 1 To 512
For j = 1 To 512
' Rnd -1 ' uncomment this line to get a big white and black lines pattern.
Randomize 'comment this line to have a random pattern
bmp(i, j) = IIf(Rnd() < 0.5, 0, 1)
Next j
Next i
Range(Cells(1, 1), Cells(512, 512)) = bmp
End Function
So while the MSDN states that "Using Randomize with the same value for number does not repeat the previous sequence.", implying that if the Timer returns twice the same value, the Rnd should keep on the same random sequence without reseting, there is still some behind the scene link..
Some screenshots:
Rnd() only:
Using Randomize:
Using Rnd -1 and Randomize:
The Randomize method initialises the Rnd function with the current system time as it's seed, you can also specify a number with Randomize to be used as the seed.
I decided to test how long a sequence continues before repeating itself:
Sub randomRepeatTest()
For i = 1 To 100000
Randomize
randomThread = randomThread & Int(9 * Rnd + 1)
If i Mod 2 = 0 Then
If Left(randomThread, i / 2) = Right(randomThread, i / 2) Then
Debug.Print i / 2
Exit Sub
End If
End If
Next i
End Sub
This sub generates a random sequence of the digits 0 - 9, and as the sequence becomes an even length it is tested to see if the first half of the sequence matches the second half, and if so it outputs the length the sequence reached before repeating. After running it a number of times, and discounting where a digit is repeated twice at the beginning, the result comes out at 256 (nice).
Providing any value to Randomize will still return a result of 256.
We're randomizing Rnd every loop, so what's going on here?
Well as I said at the beginning, if no value is given to Randomize, it will use the system time as the seed. The resolution of this time is something I can't seem to find sourced, however I believe it to be low.
I have tested using the value of timer which returns the time of day in seconds to 2 decimal places (e.g. 60287.81). I have also tried GetTickCount which returns the system active time (starts counting at boot) in milliseconds. Both of these still result in the 256 sequence limit.
So, why when we're randomizing every loop does the sequence repeat? Well the reality is, the code is executed within a millisecond. Essentially, we're providing the same number to randomize every loop, and so we're not actually shuffling the seed.
So, is Rnd more random without Randomize?
I ran the above sub again without Randomize; nothing returned. I upped the loop count to 2,000,000; still nothing.
I've managed to source the algorithm used by the workbook Rand formula, which I believe is the same as Rnd with no initialised seed:
C IX, IY, IZ SHOULD BE SET TO INTEGER VALUES BETWEEN 1 AND 30000 BEFORE FIRST ENTRY
IX = MOD(171 * IX, 30269)
IY = MOD(172 * IY, 30307)
IZ = MOD(170 * IZ, 30323)
RANDOM = AMOD(FLOAT(IX) / 30269.0 + FLOAT(IY) / 30307.0 + FLOAT(IZ) / 30323.0, 1.0)
It is an iterative function which uses the result of the previous call to generate a new number. Referenced as the Wichman-Hill procedure, it guarantees that more than 10^13 numbers will be generated before the sequence repeats itself.
The problem with Rnd
For the algorithm to work, it first needs to be initialised with values for IX, IY & IZ. The problem we have here is that we can't initialise the algorithm with random variables, as it is this algorithm we need in order to get random values, so the only option is to provide some static values to get it going.
I have tested this and it seems to be the case. Opening a fresh instance of Excel, ? Rnd() returns 0.70554. Doing the same again returns the exact same number.
So the problem we have is Rnd without using Randomize gives us a much longer sequence of random numbers, however that sequence will start at the same place each time we open Excel. Where functions are dependant on random generation, such as password generation, this doesn't suffice as we will get the same repeated results each time we open Excel.
The solution
Here's a function I have come up with and it seems to work well:
Public Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal Milliseconds As LongPtr)
Public Declare Function GetTickCount Lib "kernel32" () As Long
Public randomCount As Long
Function getRandom()
If randomCount Mod 255 = 0 Then
Sleep 1
End If
Randomize GetTickCount
getRandom = Rnd()
randomCount = randomCount + 1
End Function
It makes use of the GetTickCount function as the Randomize seed. Each call adds 1 to a randomCount variable, and after every 255 runs the macro is forced to sleep for 1 millisecond (although this actually works out at around 15 on my system) so that the seed of GetTickCount will be changed, and so a new sequence of numbers will be returned by Rnd
This of course will return the same sequence if by chance it is used at the same system time, however for most cases it will be a sufficient method for generating more random numbers. If not, it would need some fancy work using something like the Random.Org API.
I'm trying to randomize a number in VB.NET 3 times. And each time I randomize a number it should be different from the other two numbers.
For example I have 3 integers. Int1,Int2 and Int3. I will randomize Int1 between 1-10 , and then I will randomize Int2 between 1-10 however the value shouldn't be equal to the value I randomized in Int1 and the same goes for Int3 it shouldn't equal to Int1 and Int2.
I have figured out how to randomize a number, this is the code I'm using:
Dim RndNumber As Random
Dim num,num2 As Integer
RndNumber = New Random
num = RndNumber.Next(1, 11)
num2 = RndNumber.Next(1, 11)
Now I'm stuck on how I make num2 randomize a number between 1-10 that is not equals to num.
I appreciate any help, thanks.
In all the examples, RNG is a random number generator created from the NET Random class:
Private RNG = New Random()
Linq
If you only need two or three, you could loop until the current pick is not in the result set. But this is even simpler using some extension methods:
Dim nums = Enumerable.Range(1, 10).
OrderBy(Function(r) RNG.Next).
Take(3).
ToArray()
This starts with all numbers between 1 and 10, puts them in random order, takes the first 3 and stores them in the nums array. I used the multiline form, breaking after the .s to illustrate the steps.
Just change the range, size/count and Take() element as needed. For instance, for something like a lottery with 5 unique numbers 1-69 (condensed form):
Dim winners = Enumerable.Range(1, 69).OrderBy(Function(r) RNG.Next()).Take(5).ToArray()
Dim powerball = Enumerable.Range(1, 26).OrderBy(Function(r) RNG.Next()).Take(1).First
Since the Powerball can be a repeat of the first numbers, it comes from its own pool. Since we only want one, we dont need an array, just the First().
Manual
It is good to know the logic for these things, so this shows a manual version. This does it differently, by picking and actually checking random values:
' picked value storage
Dim picks As New List(Of Int32)
Dim pick As Int32 ' current candidate
Do
pick = RNG.Next(1, 11)
If picks.Contains(pick) = False Then
picks.Add(pick)
End If
Loop Until picks.Count = 3
Rather than loose vars, this uses a list to hold the picks. This makes it easy to see if the current pick has already been selected. For more than just a few values, use a HashSet(Of Int32) rather than a List for performance.
Random Pairs
To create a random sets of numbers with 2 of each, such as for a matching game, just double up the base pool of values then put them in random order:
' create pool of 2 values each for 1-13
Dim nums = Enumerable.Range(1, 13).ToArray()
' concat the set to make 2 of each value, randomize
Dim pool = nums.Concat(nums).OrderBy(Function(r) RNG.Next).ToArray()
For a manual method you would have to check the count of each value in the loop.
'Use up' Picks
One more variation is when you need a pool of randoms used periodically, but you don't know how many you will need in advance. Examples would be the balls for a BINGO game or a deck of cards.
Rather than a global indexer pointing to the last slot used (or next slot to use), a Stack(Of T) (or a Queue) will "use up" values as you need them:
' create, randomize pool of 100 ints
Dim nums = Enumerable.Range(1, 100).OrderBy(Function(r) RNG.Next).ToArray
' use array to create Stack<T>
Dim shoe As New Stack(Of Int32)(nums)
' same as:
Dim shoe = New Stack(Of Int32)(Enumerable.Range(1, 100).
OrderBy(Function(r) RNG.Next).ToArray())
This starts basically the same with 100 integers, randomized and stored in an array, but there is no Take(n) because we want them all. They values are then stored in a stack collection. Using it:
Console.WriteLine(shoe.Count)
For n As Int32 = 1 To 3
Console.WriteLine("Picked #{0}", shoe.Pop)
Next
Console.WriteLine(shoe.Count)
When you Pop a value it is removed from the collection automatically. If you use a lot of values from the shoe, you will want to check the count to make sure it is not empty.
100
Picked #12
Picked #69
Picked #53
97
After drawing 3 values, the shoe has only 97 values remaining.
Random Notes
In all cases your Random generator should be a form level object which you create once. Never create them in a loop or you will likely get the same value over and over.
The OrderBy(Function(r) RNG.Next) method of randomizing is usually good enough for casual use, but it is inefficient. If you will be randomizing large sets and/or using it frequently you should consider using a proper shuffle such as the Fisher-Yates shuffle shown here.
I'll try and keep this as basic and to the point as possible.
Basically, I have weights/probabilities associated with a certain range of numbers. For example :
0: 10%
1: 50%
2: 15%
3: 25%
This then translates into cumulative probabilities :
0: 10%
1: 60%
2: 75%
3: 100%
Using a uniform RNG in VBA, the program generates numbers between 0 and 1, or whatever inferior limit it is set to. Using the same values as the previous example, but only generating numbers greater than 60% (and <= 100%), this results in numbers between 0.6 - 1.0.
This is where I'm stuck. I need to convert these random numbers very efficiently into their "corresponding values".
All of it is stored in VBA variables and needless to say, I don't want to have to write a Select Case for every situation since they're actually 120 different variables and weights.
As of right now, this is what I have to generate those numbers:
RandomNumber = LowerLimit + Rnd() * (1 - LowerLimit)
Thanks is advance for all your help! If I missed a post that was discussing this particular issue please feel free to refer me to it but I really didn't find anything relating to corresponding random numbers.
Place the following function into a public module. You would call it like so mynumber = WeightedRnd(Array(0, 1, 2, 3), Array(0.1, 0.5, 0.15, 0.25)).
Public Function WeightedRnd(values As Variant, weights As Variant) As Double
'First, calculate the cumulative weights
Dim cumulativeWeight As Double
For i = 0 To UBound(weights)
weights(i) = weights(i) + cumulativeWeight
cumulativeWeight = weights(i)
Next
'Next, generate our random number
Dim randomNumber As Double
randomNumber = Rnd()
'Finally, figure out which "bucket" it falls into
For i = 0 To UBound(weights)
If randomNumber <= weights(i) Then
WeightedRnd = values(i)
Exit Function
End If
Next
End Function