Excel UDF 加权 RANDBETWEEN()

Excel UDF weighted RANDBETWEEN()

其实不是 RANDBETWEEN()。我正在尝试创建一个 UDF 以 return 数组中数字的索引,其中数字越大,被选中的可能性就越大。

我知道如何将概率分配给工作中的随机数sheet(即对概率总和使用 MATCH(),SO 上有很多内容对此进行了解释),但我想要一个 UDF,因为我将一个特殊的输入数组传递给函数——而不仅仅是一个选定的范围。

我的问题是,权重已关闭,数组中后面的数字比数组中前面的数字更有可能被 returned,我看不到我的代码中的哪个位置出错了。到目前为止,这是 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() 中数字的相对大小并随机选择但偏向于较大的数字。 例如。 {1,0,0,1}outputArray() 应该分别分配 {50%,0%,0%,50%} 的概率 但是,当我测试 outputArray() 时,对于 1000 个样本和 100 次迭代,并绘制了项目 1 或项目 4 在数组被 returned,我得到了这个结果:

大约 20%:80% 分布。绘图 {1,1,1,1}(所有人都应该有平等的机会)给出了 10%:20%:30%:40% 的分布

我知道我遗漏了一些明显的东西,但我不知道是什么,有什么帮助吗?

更新

有些人要完整的代码,就在这里。

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()段用于规范不同的输入法。 IE。用户可以混合输入值、单元格 references/ranges 和数组,该函数可以应付。例如{=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) 或者我没有计数 1,而是计数 2、3、4 等每个可能的 UDF 输出 并制作了一个简短的宏来重新计算 sheet 100 次,每次将 countif 的结果复制到 table 中以进行绘图。我不能准确地说出我是怎么做到的,因为我把这一切都留在了工作中,但我会在周一更新。

这是我按照你的逻辑建造的东西。它工作得很好,提供了不同的结果。

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 子。它甚至会生成结果摘要。 如果您将变体更改为 varLngInputChances = Array(1, 1, 0, 0, 1),它会给出:

a ===> 329 b ===> 351 e ===> 320

这是相当好的随机:)你可以在这里改变样本的数量: For lng = 1 To 1000,它工作得相当快。我刚刚尝试了 100,000 次测试。

试试这个:

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} 和 return 1, 2, 3 or 4 中的任何一个。

另请注意:如果将 Application.Sum(v) 的值保存在变量中而不是对数组 v.[=26= 中的每个值执行计算,则可能会更快一些]

更新
该函数现在将 v 作为参数——就像您的代码一样。我还对其进行了一些调整,以便它可以处理具有任何基数的 v,这意味着您也可以从工作表中 运行 它:例如 =Probable({1,0,0,1})

看来我犯了一个悲惨的错误。我的代码很好,我的计数不太好。我在我的图形中使用 SUMIF() 而不是 COUNTIF(),导致数组中后来的对象(具有更高的 Index - 我是 UDF 的输出应该计算但不是求和)得到与其位置成比例的权重。

回想起来,我认为比我聪明得多的人可能已经从所提供的信息中推断出这一点。我说{1,1,1,1}有一个{10%:20%:30%:40%},也就是{1:2:3:4}的比例,刚好和outputs的index是一样的,推导:outputs是相加不计算的。

类似地,具有 {20%:0%:0%:80%} 输出的 {1,0,0,1} 图表,我们将每个百分比除以它的索引 (20%/1, 80%/4) 和 嘿 Presto {20%:0%:0%:20%},或者我预期的 1:1 比率。

有些烦人但令人满意 - 知道答案一直就在那里。我想这一切可能都有道理。至少 post 可以作为一个警告,提醒初出茅庐的 VBA 人检查他们的算法。