Excel UDF взвешенный RANDBETWEEN()
Ну не действительно RANDBETWEEN()
. Я пытаюсь создать UDF, чтобы вернуть индекс числа в массив, где чем больше число, тем вероятнее, что он будет выбран.
Я знаю, как назначать вероятности случайным числам на листе (т.е. используя MATCH()
в сумме вероятностей, там много материала на SO, объясняющем это), но я хочу UDF, потому что я передаю специальный входной массив в функцию - не только выбранный диапазон.
Моя проблема в том, что взвешивание выключено, более вероятно, что числа, полученные позже в массиве, будут возвращены, чем предыдущие в массиве, и я не вижу, где в моем коде я ошибался. Здесь UDF пока:
Public Function PROBABLE(ParamArray inputArray() As Variant) As Long
'Takes a set of relative or absolute probabilities and ranks a random number within them
Application.Volatile (True)
Dim outputArray() As Variant
Dim scalar As Single
Dim rankNum As Single
Dim runningTot As Single
'''''
'Here I take inputArray() and convert to outputArray(),
'which is fed into the probability code below
'''''
scalar = 1 / WorksheetFunction.Sum(outputArray)
rankNum = Rnd()
runningTot = 0
For i = 0 To UBound(outputArray)
runningTot = runningTot + outputArray(i)
If runningTot * scalar >= rankNum Then
PROBABLE = i + 1
Exit Function
End If
Next i
End Function
Функция должна смотреть на относительные размеры чисел в outputArray()
и выбирать случайным образом, но взвешивать по отношению к большему числу.
Например. outputArray()
of {1,0,0,1}
должен присваивать вероятности соответственно {50%,0%,0%,50%}
. Однако, когда я тестировал, что outputArray()
, для 1000 выборок и 100 итераций, и нарисовал, как часто возвращался элемент 1 или элемент 4 в массиве, я получил этот результат: ![Graph]()
Примерно 20%: 80% распределения. График {1,1,1,1}
(все должны иметь равный шанс) дал 10%: 20%: 30%: 40% распределение
Я знаю, что мне не хватает чего-то очевидного, но я не могу сказать, что, любая помощь?
UPDATE
Некоторые люди спрашивали полный код, вот он.
Public Function PROBABLE(ParamArray inputArray() As Variant) As Long
'Takes a set of relative or absolute probabilities and ranks a random number within them
Application.Volatile (True) 'added some dimensions up here
Dim outputArray() As Variant
Dim inElement As Variant
Dim subcell As Variant
Dim scalar As Single
Dim rankNum As Single
Dim runningTot As Single
'convert ranges to values
'creating a new array from the mixture of ranges and values in the input array
''''
'This is where I create outputArray() from inputArray()
''''
ReDim outputArray(0)
For Each inElement In inputArray
'Normal values get copied from the input UDF to an output array, ranges get split up then appended
If TypeName(inElement) = "Range" Or TypeName(inElement) = "Variant()" Then
For Each subcell In inElement
outputArray(UBound(outputArray)) = subcell
ReDim Preserve outputArray(UBound(outputArray) + 1)
Next subcell
'Stick the element on the end of an output array
Else
outputArray(UBound(outputArray)) = inElement
ReDim Preserve outputArray(UBound(outputArray) + 1)
End If
Next inElement
ReDim Preserve outputArray(UBound(outputArray) - 1)
''''
'End of new code, the rest is as before
''''
scalar = 1 / WorksheetFunction.Sum(outputArray)
rankNum = Rnd()
runningTot = 0
For i = 0 To UBound(outputArray)
runningTot = runningTot + outputArray(i)
If runningTot * scalar >= rankNum Then
PROBABLE = i + 1
Exit Function
End If
Next i
End Function
Начальный раздел inputArray()
🡒 outputArray()
используется для стандартизации различных методов ввода. То есть пользователь может ввести смесь значений, ссылок/диапазонов ячеек и массивов, и функция может справиться. например {=PROBABLE(A1,5,B1:C15,IF(ISTEXT(D1:D3),LEN(D1:D3),0))}
(вы получаете изображение) должен работать так же хорошо, как =PROBABLE(A1:A3)
. Я просматриваю подэлементы inputArray() и помещаю их в свой outputArray(). Я вполне уверен, что с этой частью кода ничего не случилось.
Затем, чтобы получить мои результаты, я скопировал UDF в A1:A1000
, использовал COUNTIF(A1:A1000,1)
или вместо count 1, я сделал счет 2, 3, 4 и т.д. для каждого из возможных выходов UDF и сделал короткий макрос пересчитать лист 100 раз, каждый раз копируя результат countif в таблицу на график. Я не могу точно сказать, как я это сделал, потому что я оставил все это на работе, но я обновлю в понедельник.
Ответы
Ответ 1
Кажется, я совершил трагическую ошибку. Мой код был в порядке, мой подсчет был не так хорош. Я использовал SUMIF()
вместо COUNTIF()
в моем графике, в результате чего в массиве появились более поздние объекты (с более высоким индексом - вывод UDF, который я должен был считать, но вместо этого суммировал), получая взвешивание, пропорциональное их положение.
В ретроспективе я думаю, что кто-то гораздо более умный, чем я, вероятно, мог бы вывести это из приведенной информации. Я сказал, что {1,1,1,1}
имеет a {10%:20%:30%:40%}
, что отношение a {1: 2: 3: 4}, которое является точно таким же соотношением, как и индексы выходов, вычет: выходы суммированы не считаются.
Аналогично, график {1,0,0,1}
с выходом {20%:0%:0%:80%}
, делящий каждый процент на него индекс (20%/1, 80%/4) и Hey Presto {20%:0%:0%:20%}
, или соотношение 1:1, которое я ожидал.
Что-то раздражающее, но удовлетворяющее в этом - знание ответа было все время. Полагаю, во всем этом, вероятно, есть мораль. По крайней мере, сообщение может служить предупреждением начинающим VBAers, чтобы проверить их арифметику.
Ответ 2
Попробуйте следующее:
Function Probable(v As Variant) As Long
Application.Volatile 'remove this if you don't want a volatile function
Dim v2 As Variant
ReDim v2(LBound(v) To UBound(v) + 1)
v2(LBound(v2)) = 0
Dim i As Integer
For i = LBound(v) To UBound(v)
v2(i + 1) = v2(i) + v(i) / Application.Sum(v)
Next i
Probable = Application.WorksheetFunction.Match(Rnd(), v2, 1)
End Function
Массив v
по существу является вашим outputArray
.
Код принимает такой массив, как {1,0,0,1}
, и преобразует его в {0,0.5,0.5,1}
(обратите внимание на 0
в начале), после чего вы можете сделать MATCH
, как вы предложили получить либо 1 or 4
с равной вероятностью.
Аналогично, если вы должны начать с {1,1,1,1}
, он будет преобразован в {0,0.25,0.5,0.75,1}
и с равной вероятностью вернет любой из 1, 2, 3 or 4
.
Также обратите внимание: вы могли бы сделать это немного быстрее, если вы сохраните значение Application.Sum(v)
в переменной, а не выполните вычисление для каждого значения в массиве v
.
Обновление
Функция теперь принимает v
как параметр - как ваш код. Я также немного изменил его, чтобы иметь дело с v
, имеющим любую базу, что означает, что вы также можете запустить его с листа: =Probable({1,0,0,1})
например
Ответ 3
Это то, что я построил, следуя вашей логике. Он работает вполне нормально, обеспечивая разные результаты.
Option Explicit
Public Function TryMyRandom() As String
Dim lngTotalChances As Long
Dim i As Long
Dim previousValue As Long
Dim rnd As Long
Dim result As Variant
Dim varLngInputArray As Variant
Dim varLngInputChances As Variant
Dim varLngChancesReedit As Variant
varLngInputChances = Array(1, 2, 3, 4, 5)
varLngInputArray = Array("a", "b", "c", "d", "e")
lngTotalChances = Application.WorksheetFunction.Sum(varLngInputChances)
rnd = Application.WorksheetFunction.RandBetween(1, lngTotalChances)
ReDim varLngChancesReedit(UBound(varLngInputChances))
For i = LBound(varLngInputChances) To UBound(varLngInputChances)
varLngChancesReedit(i) = varLngInputChances(i) + previousValue
previousValue = varLngChancesReedit(i)
If rnd <= varLngChancesReedit(i) Then
result = varLngInputArray(i)
Exit For
End If
Next i
TryMyRandom = result
End Function
Public Sub TestMe()
Dim lng As Long
Dim i As Long
Dim dict As Object
Dim key As Variant
Dim res As String
Set dict = CreateObject("Scripting.Dictionary")
For lng = 1 To 1000
res = TryMyRandom
If dict.Exists(res) Then
dict(res) = dict(res) + 1
Else
dict(res) = 1
End If
Next lng
For Each key In dict.Keys
Debug.Print key & " ===> " & dict(key)
Next
End Sub
Что касается вашего случая, убедитесь, что массив отсортирован. Например, в моем случае речь идет о varLngInputChances
. Я не посмотрел на угловые случаи, возможно, там может быть ошибка.
Запустите TestMe
sub. Это сгенерирует даже краткое изложение результатов.
Если вы измените варианты на varLngInputChances = Array(1, 1, 0, 0, 1)
, это даст:
a ===> 329
b ===> 351
e ===> 320
что довольно неплохо.:) Здесь вы можете изменить номер выборки:
For lng = 1 To 1000
, он работает довольно быстро. Я только что попробовал его с 100 000 тестов.