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 тестов.