VBA - How to tell 13 combinatios by randomize to remain under 100? - excel

I would like to randomize 13 variables which in sum have to be 100%.
For a better understanding I show you what I mean by a snippet. From column F to column R all variables have to understand that in sum (column S) these are not allowed to be over 100.
Each row shall be another scenario, so the rows by itself are independent.
My approach was like the following but unfortunately nothing happens. Anyone an idea? Thanks a lot.
Sub Zufall()
Dim k As Integer
Application.ScreenUpdating = False
Range("F11:R55").ClearContents
DoEvents
Do Until WorksheetFunction.Sum(Range("S11:S55")) = 100
Randomize
For k = Columns(19) To Columns(6) Step -1
Cells(11, k).Formula = Int(Rnd() * 100)
Next k
Range("R55:F11").Value = WorksheetFunction.Sum(Range("S11:S55"))
DoEvents
Loop
Application.ScreenUpdating = True
End Sub

The following code shows how to generate a set of n random single precision numbers between 0 and 1 which sum to 1.
' Returns an array of random numbers (0 <= n < 1) which sum to 1.
' Size is the size of the array to return.
Function GenerateRandomNumbers(ByVal size As Integer) As Variant
ReDim vals(1 To size) As Single
Dim idx As Integer
Dim sum As Single
For idx = 1 To size
vals(idx) = Rnd
sum = sum + vals(idx)
Next idx
For idx = 1 To size
vals(idx) = vals(idx) / sum
Next idx
GenerateRandomNumbers = vals
End Function
You use it like this.
Sub Randomm()
Dim row As Long
Dim col As Long
Dim thirteenRandomNumbersWhichSumToOne As Variant
With Worksheets("Sheet1")
For row = 6 To 50
thirteenRandomNumbersWhichSumToOne = GenerateRandomNumbers(13)
For col = 6 To 18 ' F to R
.Cells(row, col).Value = thirteenRandomNumbersWhichSumToOne(col - 5)
.Cells(row, col).NumberFormat = "0%"
Next col
.Cells(row, 19).FormulaR1C1 = "=SUM(RC6:RC18)"
.Cells(row, 19).NumberFormat = "0%"
Next row
End With
End Sub

This will generate 13 values from A1 through A13:
Sub Randomm()
Dim wf As WorksheetFunction, rng As Range
Set wf = Application.WorksheetFunction
Set rng = Range("A1:A13")
rng.Formula = "=RAND()"
rng.Value = rng.Value
zum = wf.Sum(rng)
For i = 1 To 13
Cells(i, 1).Value = Cells(i, 1).Value / zum
Next i
rng.NumberFormat = "0.00%"
End Sub

this is the current and last code. Missing:
idx values in sum do not limit up to 100% in column S
Changed:
column S was implied by using the easy sum calcluation in Excel
the % wasnt shown right this is why i used it by Format in Excel
Sub Randomm()
Dim wf As WorksheetFunction, rng As Range, zum As Single, i As Integer
Set wf = Application.WorksheetFunction
Set rng = Range("F6:R50")
Dim totalRow As Single
rng.Formula = "=RANDBETWEEN(5,25)/100"
rng.Value = rng.Value
zum = wf.sum(rng)
For i = 6 To 50
Cells(i, 6).Value = Cells(i, 13).Value
Next i
End Sub
Function GenerateRandomNumbers(ByVal size As Integer) As Variant
ReDim vals(1 To size) As Single
Dim idx As Integer
Dim sum As Single
For idx = 1 To size
vals(idx) = Rnd
Next idx
For idx = 1 To size
vals(idx) = vals(idx)
Next idx
GenerateRandomNumbers = vals
End Function

Related

How to limit all variables in one row that the sum is 100%?

does anyone has an idea how to command that the sum of each row is always 100%? Thant includes that the random variables within the row have to know the values of the cells in the same row or?
Sub Randomm()
Dim wf As WorksheetFunction, rng As Range, zum As Single, i As Integer
Set wf = Application.WorksheetFunction
Set rng = Range("F6:R50")
Dim totalRow As Single
rng.Formula = "=RAND()"
rng.Value = rng.Value
zum = wf.sum(rng)
For i = 6 To 50
Cells(i, 6).Value = Cells(i, 13).Value / zum
Next i
rng.NumberFormat = "0.00%"
End Sub
Function GenerateRandomNumbers(ByVal size As Integer) As Variant
ReDim vals(1 To size) As Single
Dim idx As Integer
Dim sum As Single
For idx = 1 To size
vals(idx) = Rnd
sum = sum + vals(idx)
Next idx
For idx = 1 To size
vals(idx) = vals(idx) / sum
Next idx
GenerateRandomNumbers = vals
End Function
Well, you could sample from Dirichlet distribution, which returns array of values summed to 1. Then you could scale it to 100% or to whatever value you need.
I have little knowledge about how VBA works, so this is just sketch of the code
Function GenerateRandomNumbers(ByVal size As Integer) As Variant
ReDim vals(1 To size) As Single
Dim idx As Integer
Dim sum As Single
For idx = 1 To size
vals(idx) = -Log(1.0-Rnd) 'sample as exponential
sum = sum + vals(idx) 'find total sum
Next idx
For idx = 1 To size
vals(idx) = vals(idx) / sum 'normalization
Next idx
GenerateRandomNumbers = vals
End Function
For all who have the same question at any time, the Dirichlet Distribution function by #Severin Pappadeux works with the Sub below mentioned (reference: sub Solution by another user from stackoverflow)
Sub Random3()
Dim row As Long
Dim col As Long
Dim thirteenRandomNumbersWhichSumToOne As Variant
With Worksheets("Optimierung2")
For row = 11 To 55
thirteenRandomNumbersWhichSumToOne = GenerateRandomNumbers(13)
For col = 6 To 18 ' F to R
.Cells(row, col).Value = thirteenRandomNumbersWhichSumToOne(col - 5)
.Cells(row, col).NumberFormat = "0%"
Next col
.Cells(row, 19).FormulaR1C1 = "=SUM(RC6:RC18)"
.Cells(row, 19).NumberFormat = "0%"
Next row
End With
End Sub
Function GenerateRandomNumbers(ByVal size As Integer) As Variant
ReDim vals(1 To size) As Single
Dim idx As Integer
Dim sum As Single
For idx = 1 To size
vals(idx) = -Log(1# - Rnd) 'sample as exponential
sum = sum + vals(idx) 'find total sum
Next idx
For idx = 1 To size
vals(idx) = vals(idx) / sum 'normalization
Next idx
GenerateRandomNumbers = vals
End Function

How do I convert the Excel formula =STDEV.S(IF(FREQUENCY(range,range),range)) into VBA code?

I have an Excel formula that operates on a pre-existing range of data.
The Excel formula is: =STDEV.S(IF(FREQUENCY(range,range),range)) , where "range" is the aforementioned range of data.
My goal is to convert this formula into VBA code.
The following code is my attempt at trying to convert the formula into VBA, as well as my visualization of the process to try and understand why it is not putting out the same result:
Private Sub CommandButton1_Click()
Dim diffArray() As Variant
Dim i As Integer
Dim x As Integer
Dim array1() As Variant, size As Integer, j As Integer
Dim freqArray1() As Variant
Dim freqArray2() As Variant, size2 As Integer, j2 As Integer
'assigns the data values to array1
size = 0
j = 0
ReDim array1(size)
For i = 3 To 15
size = size + 1
ReDim Preserve array1(size)
array1(j) = Cells(i, 2)
j = j + 1
Next i
Cells(20, 2).Value = UBound(array1)
Cells(21, 2).Value = LBound(array1)
If UBound(array1) > 1 Then Cells(19, 2).Value = WorksheetFunction.StDev_S(array1)
'setting freqArray1 to frequency(array1, array1)
freqArray1 = WorksheetFunction.Frequency(array1, array1)
Cells(20, 3).Value = UBound(freqArray1)
Cells(21, 3).Value = LBound(freqArray1)
For i = LBound(freqArray1) To (UBound(freqArray1))
Cells(2 + LBound(freqArray1) + i, 3).Value = freqArray1(i, 1)
Next i
If UBound(freqArray1) > 1 Then Cells(19, 3).Value = WorksheetFunction.StDev_S(freqArray1)
'setting freqArray2 to if(frequency(array1, array1), array1)
size2 = 0
j2 = 0
ReDim freqArray2(size2)
For i = LBound(freqArray1) To (UBound(freqArray1))
If freqArray1(i, 1) Then
size2 = size2 + 1
ReDim Preserve freqArray2(size2)
freqArray2(j2) = freqArray1(i, 1)
j2 = j2 + 1
End If
Next i
Cells(20, 4).Value = UBound(freqArray2)
Cells(21, 4).Value = LBound(freqArray2)
For i = (LBound(freqArray2)) To UBound(freqArray2)
Cells(2 + LBound(freqArray2) + i, 4).Value = freqArray2(i)
Next i
'takes the standard deviation of if(frequency(array1, array1), array1)
If UBound(freqArray2) > 1 Then Cells(19, 4).Value = WorksheetFunction.StDev_S(freqArray2)
End Sub
The data values being operated on are in the orange cells column B(array1).
The array 'frequency(array1, array1)' is in the yellow cells column C.
The array 'if(frequency(array1, array1), array1)' is in the green cells column D.
The goal is for the values in the two blue cells(B18 and D19) to be the same.
I don't understand two things:
Why are the values in the blue cells(B18 and D19) not the same?
Why do the indices of the arrays change?
One starts at '0', the next starts at '1', and the last starts at '-1'?
use a dictionary to create a unique list and use that in the StDev_S
Private Sub CommandButton1_Click()
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
Dim rngArray As Variant
rngArray = ActiveSheet.Range("B3:B15")
Dim i As Long
For i = LBound(rngArray, 1) To UBound(rngArray, 1)
On Error Resume Next
dict.Add rngArray(i, 1), rngArray(i, 1)
On Error Resume Next
Next i
If dict.Count > 0 Then
Dim unqArr As Variant
ReDim unqArr(1 To dict.Count) As Variant
i = 1
Dim key As Variant
For Each key In dict.Keys
unqArr(i) = key
i = i + 1
Next key
ActiveSheet.Cells(19, 4).Value = Application.WorksheetFunction.StDev_S(unqArr)
End If
End Sub

Looping through an array in Excel

Trying to loop through a sheets"data".Range"AM1:AS12" and copy the data to range beginning at BD1 as long as the data doesn't equal "#N/A"
My code works with copying the first column, but doesn't do anything with the data after that. Where am I going wrong?
Set S2 = Sheets("data").Range("AM:AM")
Set S3 = Sheets("data").Range("BD:BD")
Dim i As Integer, j As Integer
j = 1
For i = 1 To 12
If S2.Cells(i, 1).Value <> "#N/A" Then
S3.Cells(j, 2).Value = S2.Cells(i, 1).Value
j = j + 1
End If
Next i
Replace:
<> "#N/A"
By:
Not(Application.WorksheetFunction.IfNa(...))
This works when i tested it.
Sub CopyCell()
Set S2 = Sheets("data").Range("A:A")
Set S3 = Sheets("data").Range("M:M")
Dim i As Integer, j As Integer
For j = 1 To 2
For i = 1 To 12
If S2.Cells(i, j).Value <> "#N/A" Then
S3.Cells(i, j).Value = S2.Cells(i, j).Value
End If
Next i
Next j
Call DeleteBlank
End Sub
Sub DeleteBlank()
Dim x As Integer
Dim y As Integer
For y = 13 To 16 'Range numbers for the columns the data is copied to
For x = 1 To 10 ' Number of cells of data you want to loop through
If Cells(x, y).Value = "" Then
Cells(x, y).Delete Shift:=xlUp
End If
Next x
Next y
End Sub
the best thing to is not to check if it is equal to "#N/A"
The best is to check if it is an error : If Not (IsError(S2.Cells(i, 1).Value)) Then

Unique Random Numbers using VBA

I am trying to create a series of unique (non-duplicating) random numbers within a user defined range. I have managed to create the random numbers, but I am getting duplicate values. How can I ensure that the random numbers will never be a duplicate?
Sub GenerateCodesUser()
Application.ScreenUpdating = False
Worksheets("Users").Activate
Dim MINNUMBER As Long
Dim MAXNUMBER As Long
MINNUMBER = 1000
MAXNUMBER = 9999999
Dim Row As Integer
Dim Number As Long
Dim high As Double
Dim Low As Double
Dim i As Integer
If (CustomCodes.CardNumberMin.Value = "") Then
MsgBox ("Fill Card Number Field!")
Exit Sub
ElseIf (CustomCodes.CardNumberMin.Value < MINNUMBER) Then
MsgBox ("Card Number Value must be equal or higher then" & MINNUMBER)
Exit Sub
End If
If (CustomCodes.CardNumberMax.Value = "") Then
MsgBox ("Fill Card Number Field!")
Exit Sub
ElseIf (CustomCodes.CardNumberMax.Value > MAXNUMBER) Then
MsgBox ("Card Number Value must be equal or higher then " & MAXNUMBER)
Exit Sub
End If
Low = CustomCodes.CardNumberMin.Value
high = CustomCodes.CardNumberMax.Value '<<< CHANGE AS DESIRED
If (Low < 1000) Then
'break
End If
For i = 1 To Cells(1, 1).End(xlToRight).Column
If InStr(Cells(1, i), "CardNumber") Then
Row = 2
While Cells(Row, 1) <> 0
Do
Number = ((high - Low + 1) * Rnd() + Low)
Loop Until Number > Low
Cells(Row, i) = Number
Row = Row + 1
Wend
End If
Next
Application.ScreenUpdating = True
End Sub
Here's a method of guaranteeing unique integer random numbers. Inline comments describe the method.
Function UniuqeRandom(Mn As Long, Mx As Long, Sample As Long) As Long()
Dim dat() As Long
Dim i As Long, j As Long
Dim tmp As Long
' Input validation checks here
If Mn > Mx Or Sample > (Mx - Mn + 1) Then
' declare error to suit your needs
Exit Function
End If
' size array to hold all possible values
ReDim dat(0 To Mx - Mn)
' Fill the array
For i = 0 To UBound(dat)
dat(i) = Mn + i
Next
' Shuffle array, unbiased
For i = UBound(dat) To 1 Step -1
tmp = dat(i)
j = Int((i + 1) * Rnd)
dat(i) = dat(j)
dat(j) = tmp
Next
'original biased shuffle
'For i = 0 To UBound(dat)
' tmp = dat(i)
' j = Int((Mx - Mn) * Rnd)
' dat(i) = dat(j)
' dat(j) = tmp
'Next
' Return sample
ReDim Preserve dat(0 To Sample - 1)
UniuqeRandom = dat
End Function
use it like this
Dim low As Long, high As Long
Dim rng As Range
Dim dat() As Long
Set rng = Range(Cells(1, 1), Cells(1, 1).End(xlToRight))
dat = UniuqeRandom(low, high, rng.Columns.Count)
rng.Offset(1, 0) = dat
Note: see this Wikipedia article regarding shuffle bias
The edit fixed one source of bias. The inherent limitations of Rnd (based on a 32 bit seed) and Modulo bias remain.
I see you have an accepted answer, but for whatever it's worth here is my stab at this question. This one uses a boolean function instead of numerical arrays. It's very simple yet fast. The advantage of it, which I'm not saying is perfect, is an effective solution for numbers in a long range because you only ever check the numbers you have already picked and saved and don't need a potentially large array to hold the values you have rejected so it won't cause memory problems because of the size of the array.
Sub UniqueRandomGenerator()
Dim N As Long, MaxNum As Long, MinNum As Long, Rand As Long, i As Long
MinNum = 1 'Put the input of minimum number here
MaxNum = 100 'Put the input of maximum number here
N = MaxNum - MinNum + 1
ReDim Unique(1 To N, 1 To 1)
For i = 1 To N
Randomize 'I put this inside the loop to make sure of generating "good" random numbers
Do
Rand = Int(MinNum + N * Rnd)
If IsUnique(Rand, Unique) Then Unique(i, 1) = Rand: Exit Do
Loop
Next
Sheet1.[A1].Resize(N) = Unique
End Sub
Function IsUnique(Num As Long, Data As Variant) As Boolean
Dim iFind As Long
On Error GoTo Unique
iFind = Application.WorksheetFunction.Match(Num, Data, 0)
If iFind > 0 Then IsUnique = False: Exit Function
Unique:
IsUnique = True
End Function
It Works perfectly:
Option Base 1
Public Function u(a As Variant, b As Variant) As Variant
Application.Volatile
Dim k%, p As Double, flag As Boolean, x() As Variant
k = 1
flag = False
ReDim x(1)
x(1) = Application.RandBetween(a, b)
Do Until k = b - a + 1
Do While flag = False
Randomize
p = Application.RandBetween(a, b)
'Debug.Assert p = 2
resultado = Application.Match(p, x, False)
If IsError(resultado) Then
k = k + 1
ReDim Preserve x(k)
x(k) = p
flag = True
Else
flag = False
End If
Loop
flag = False
Loop
u = x
End Function

Implement Structured Reference in VBA Function

I am using this VBA function, and right now it works fine as the format of my spreadsheet has not changed. But I will be adapting it for another use, and the order is likely going to be changing a bit more often. The table header names that I need for this function will not change their names though. My whole spreadsheet equations already uses structured references, but I am struggling to wrap my head around how exactly I eliminate the remnants of the explicit references to the column position in the VBA code.
The row that states If data(i, 2).Value = expNum Then and
If data(i, 14).Value <> Empty Then
curEnergy = data(i, 14).Value`
Are the ones in question. Column 2 corresponds to a column named "Exp #", and Column 14 corresponds to a column name "G (kcal/mol)" I've attached the full function below, just in case there are critical pieces in other sections that I'm not aware of. I would like to replace those references to 2 and 14 with the structured reference or something robust to withstand reordering of column positions.
Function BoltzmannEnergy(expNum As String) As Double
Application.Volatile
Worksheets("Raw Values").Activate
Dim data As Range, curCell As Range
Dim numRows As Integer, arrayCount As Integer, arraySize As Integer
Dim energies() As Double, curEnergy As Double, minEnergy As Double, RT As Double, BoltzTop As Double, BoltzBtm As Double
Dim expFound As Boolean
Const T As Double = 298
Const R As Double = 0.001985
RT = R * T
BoltzTop = 0
BoltzBtm = 0
expFound = False
arraySize = 5
minEnergy = 0
ReDim energies(0 To arraySize)
Set data = Range("RawValues")
arrayCount = 0
numRows = data.Rows.Count
For i = 1 To numRows
If data(i, 2).Value = expNum Then
If arrayCount = UBound(energies) - 1 Then
ReDim Preserve energies(0 To arrayCount + arraySize + 1)
End If
expFound = True
If data(i, 14).Value <> Empty Then
curEnergy = data(i, 14).Value
If curEnergy <> 0 Then
If curEnergy < minEnergy Then
minEnergy = curEnergy
End If
energies(arrayCount) = curEnergy
arrayCount = arrayCount + 1
End If
End If
ElseIf expFound = True Then
Exit For
End If
Next i
For i = 0 To arrayCount - 1
BoltzTop = BoltzTop + energies(i) * Exp(-(energies(i) - minEnergy) / RT)
BoltzBtm = BoltzBtm + Exp(-(energies(i) - minEnergy) / RT)
Next i
BoltzmannEnergy = BoltzTop / BoltzBtm
End Function
If you know that header names will not change you can replace those number with two variables each referring to a find range.columns.
Something like:
Dim intColumn1, intColumn2 As Integer
Dim rngTemp As Range
'assuming your header row is row 1
Set rngTemp = Worksheets("Raw Values").Rows(1).Find(what:="Header_1_value")
'check if you actually found a cell
If Not rngTemp Is Nothing Then
intColumn1 = rngTemp.Column
End If
'assuming your header row is row 1
Set rngTemp = Worksheets("Raw Values").Rows(1).Find(what:="Header_2_value")
'check if you actually found a cell
If Not rngTemp Is Nothing Then
intColumn2 = rngTemp.Column
End If

Resources