【问题标题】:Is Excel VBA's Rnd() really this bad?Excel VBA Rnd() 真的这么糟糕吗?
【发布时间】:2016-12-17 21:15:49
【问题描述】:

我需要一个用于 2D Monte Carlo 模拟的伪随机数生成器,它不具备使用简单 LCG 获得的特征超平面。我使用以下代码在 Excel 2013 中测试了随机数生成器 Rnd()(运行大约需要 5 秒):

Sub ZoomRNG()

Randomize
For i = 1 To 1000
    Found = False
    Do
        x = Rnd()   ' 2 random numbers between 0.0 and 1.0
        y = Rnd()
        If ((x > 0.5) And (x < 0.51)) Then
            If ((y > 0.5) And (y < 0.51)) Then
                ' Write if both x & y in a narrow range
                Cells(i, 1) = i
                Cells(i, 2) = x
                Cells(i, 3) = y
                Found = True
            End If
        End If
    Loop While (Not Found)
Next i

End Sub

这是运行上述代码后 x 与 y 的简单图

它不仅看起来不是很随机,而且比臭名昭著的 RANDU 算法在 2D 中具有更明显的超平面。基本上,我是否错误地使用了该函数,或者 VBA 中的 Rnd() 函数实际上一点也不可用?

作为比较,这是我在 C++ 中使用 Mersenne Twister MT19937 得到的结果。

【问题讨论】:

  • Randomize 函数接受一个参数,例如Randomize Now() 但实际上似乎对你的情节没有影响

标签: excel random excel-2013 montecarlo vba


【解决方案1】:

所有 LCG 都会生成超平面。 LCG 的质量随着这些超平面之间距离的减小而增加。因此,拥有比 RANDU 更多的超平面是一件好事。

MT 图看起来好多了,因为它不是 LCG。事实上,任何非 LCG pRNG 都可能有一个随机的情节,但仍然很糟糕。

为避免二维相关问题,您可以对 x 和 y 使用相同的 LCG,但对 x 和 y 使用不同的种子。当然,这不适用于 RND,因为您不能有两个单独的流。您将需要一个 LCG pRNG,它通过引用将种子作为参数。

【讨论】:

    【解决方案2】:

    为了产生更好的随机生成器并使其性能更快,我修改了您的代码,如下所示:

    Const N = 1000           'Put this on top of your code module
    Sub ZoomRNG()
    
    Dim RandXY(1 To N, 1 To 3) As Single, i As Single, x As Single, y As Single
    
    For i = 1 To N
        Randomize            'Put this in the loop to generate a better random numbers
        Do
            x = Rnd
            y = Rnd
            If x > 0.5 And x < 0.51 Then
                If y > 0.5 And y < 0.51 Then
                    RandXY(i, 1) = i
                    RandXY(i, 2) = x
                    RandXY(i, 3) = y
                    Exit Do
                End If
            End If
        Loop
    Next
    Cells(1, 9).Resize(N, 3) = RandXY
    End Sub
    

    绘制结果后得到这个

    结果看起来比您的代码输出更好。把上面的代码稍微修改成这样

    Const N = 1000
    Sub ZoomRNG()
    
    Dim RandXY(1 To N, 1 To 3) As Single, i As Single, x As Single, y As Single
    
    For i = 1 To N
        Randomize
        Do
            x = Rnd
            If x > 0.5 And x < 0.51 Then
                y = Rnd
                If y > 0.5 And y < 0.51 Then
                    RandXY(i, 1) = i
                    RandXY(i, 2) = x
                    RandXY(i, 3) = y
                    Exit Do
                End If
            End If
        Loop
    Next
    Cells(1, 9).Resize(N, 3) = RandXY
    End Sub
    

    比上一个效果更好

    当然,C++ 中的 Mersenne Twister MT19937 仍然更好,但最后的结果对于进行 Monte-Carlo 模拟来说非常好。 FWIW,你可能有兴趣阅读这篇论文:On the accuracy of statistical procedures in Microsoft Excel 2010

    【讨论】:

    • 感谢@Anastasiya - 参考使事情变得清晰。 excel 工作表函数 RAND() 和 VBA 中的生成器 Rnd() 不同,VBA 的生成器明显逊色。我认为 MS 改进了 excel 中的 RNG,但可能只有 RAND() 之一。不幸的是,对于 MonteCarlo 模拟,在 VBA 中使用生成器会很方便。
    • @XYZ 我怀疑它们都很相似,因为 VBA 中没有 Application.WorksheetFunction.Rand()
    • @XYZ 我刚刚发现,如果你在我的第一个代码中放入一个介于 x = Rndy = Rnd 之间的虚拟变量 z = Rnd 会使结果更好。
    • 我测试了代码,虽然它看起来确实好一些,但当我对值进行排序时,在 1000 个点中,只有 388 个是唯一的(其中大约 60% 是重复的)。
    【解决方案3】:

    读完这个问题后,我很好奇,找到了这篇论文 “Assessing Excel VBA Suitability for Monte Carlo Simulation”由 Alexei Botchkarev 提供,here。不推荐同时使用 RAND 和 RND 函数,但正如 Jerry Wang 在论文中指出的那样,Mersenne Twister 已在 VBA 中实现。

    快速搜索后,我找到了这个在 2015 年 2 月 28 日更新的评论很好的版本:http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/VERSIONS/BASIC/MTwister.xlsb

    来源:http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/VERSIONS/BASIC/basic.html

    【讨论】:

      【解决方案4】:

      作为速度和善良之间的平衡,我正在考虑将它们组合起来像

      for...
        z = [rand()] ' good but slow.
        for .. ' just a few
           t = z + rnd()
           t = t - int(t)
           ...
      

      请记住,好熵 + 坏熵 = 更好的熵。

      也就是说,每个 [rand()] 只有 0.05 毫秒。

      【讨论】:

        【解决方案5】:

        这似乎平均需要 1000 * 100 * 100 次迭代才能完成,而且 VBA 通常比原生 Excel 公式要慢一些。考虑这个例子

        Sub ZoomRNG()
            t = Timer
            [a1:a1000] = "=ROW()"
            [b1:c1000] = "=RAND()/100+0.5"
            [a1:c1000] = [A1:C1000].Value
            Debug.Print CDbl(Timer - t) ' 0.0546875 seconds
        End Sub
        

        更新

        一点也不差!即使没有Randomize,这也可以工作

        Sub ZoomRNGs() ' VBA.Rnd returns Single
            t = Timer
            For i = 1 To 1000
                Cells(i, 1) = i
                Cells(i, 2) = Rnd / 100 + 0.5
                Cells(i, 3) = Rnd / 100 + 0.5
            Next i
            Debug.Print Timer - t ' 0.25 seconds
        End Sub
        
        Sub ZoomRNGd() ' the Excel Function RAND() returns Double
            t = Timer
            For i = 1 To 1000
                Cells(i, 1) = i
                Cells(i, 2) = [RAND()] / 100 + 0.5
                Cells(i, 3) = [RAND()] / 100 + 0.5
            Next i
            Debug.Print Timer - t ' 0.625 seconds
        End Sub
        

        Single 的精度大约是Double 的一半:

        s = Rnd: d = [RAND()]
        Debug.Print s; d; Len(Str(s)); Len(Str(d)) ' " 0.2895625  0.580839555868045  9  17 "
        

        更新 2

        我发现了与 VBA Rnd 一样快的 C 替代方案。
        C:\Windows\System32\msvcrt.dll 是 Microsoft C 运行时库:

        Declare Function rand Lib "msvcrt" () As Long ' this in a VBA module
        

        然后你可以在你的代码中像x = rand / 32767这样使用它:

        Sub ZoomRNG()
            t = Timer
            Dim i%, x#, y#, Found As Boolean
            For i = 1 To 1000
                Found = False
                Do
                    x = rand / 32767 ' RAND_MAX = 32,767
                    y = rand / 32767
                    If ((x > 0.5) And (x < 0.51)) Then
                        If ((y > 0.5) And (y < 0.51)) Then
                            ' Write if both x & y in a narrow range
                            Cells(i, 1) = i
                            Cells(i, 2) = x
                            Cells(i, 3) = y
                            Found = True
                        End If
                    End If
                Loop While (Not Found)
            Next i
            Debug.Print Timer - t ' 2.875 seconds
        End Sub
        

        【讨论】:

        • 谢谢@Slai。不过,使用 1000 x 100 x 100 迭代是有意的——我想看看在查看整体输出的一小部分时输出是否仍然看起来是随机的。但是,当我使用您调用 [RAND()] 的建议时,我得到了看起来像高质量的随机数,但获取所有 1000 个值要慢得多。
        • 我的结论是 [RAND()] = 可能是 Mersenne Twister 输出的高质量,但速度非常慢。 Rnd = 令人尴尬的糟糕,应该带有警告标签。
        • @XYZ 比这要复杂一些。 Excel 函数 RAND 似乎比 VBA Rnd 快,但评估它 [] 是缓慢的部分。 [RAND()]Evaluate("RAND()") 的 VBA 快捷方式。我还尝试了x = WorksheetFunction.RandBetween(0, 1234567890) / 1234567890,它有点快,但结果和Rnd 一样糟糕。我的建议是像我的第一个示例一样使用 Excel 公式进行大部分计算,并使用 VBA 来处理 Excel 无法完成的事情。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-12-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-03-04
        • 2011-08-10
        相关资源
        最近更新 更多