Excel VBA 的 Rnd() 真的有这么糟糕吗?

Is Excel VBA's Rnd() really this bad?

我需要一个用于 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。

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

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

这似乎平均需要 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

为了在速度和优点之间取得平衡,我正在考虑像

那样将它们结合起来
for...
  z = [rand()] ' good but slow.
  for .. ' just a few
     t = z + rnd()
     t = t - int(t)
     ...

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

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

看完这个问题我很好奇,找到了这篇论文 “Assessing Excel VBA Suitability for Monte Carlo Simulation" by Alexei Botchkarev that is available here。不推荐 RAND 和 RND 函数,但正如论文中指出的那样,Jerry Wang 在 VBA 中已经实现了 Mersenne Twister。

快速搜索让我找到了这个评论很好的版本,该版本已于 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

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

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

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