Python 中随机数的最后一位数字的分布

31

在Python中生成0到9之间的随机数字有两种明显的方法。一种是生成0到1之间的随机浮点数,乘以10并向下取整。另一种是使用random.randint方法。

import random

def random_digit_1():
    return int(10 * random.random())

def random_digit_2():
    return random.randint(0, 9)

我很好奇如果在0和1之间生成一个随机数字,并且保留最后一位数字,会发生什么。我并没有期望分布是均匀的,但结果让我感到非常惊讶。

from random import random, seed
from collections import Counter

seed(0)
counts = Counter(int(str(random())[-1]) for _ in range(1_000_000))
print(counts)

输出:

Counter({1: 84206,
         5: 130245,
         3: 119433,
         6: 129835,
         8: 101488,
         2: 100861,
         9: 84796,
         4: 129088,
         7: 120048})

下面显示一个直方图。请注意,0不会出现,因为尾随的零会被截断。但是,有人能解释一下为什么数字4、5和6比其他数字更常见吗?我使用了Python 3.6.10,但在Python 3.8.0a4中结果类似。

Distribution of final digits of random floats


4
这与Python中计算浮点数的字符串表示方式有关。请参见https://docs.python.org/3/tutorial/floatingpoint.html。如果使用小数点后第一位(十分位)而不是最后一位,将会得到更加均匀的结果。 - Dennis
1
我们以二进制表示形式存储浮点数(因为我们的内存也是二进制的)。str将其转换为十进制,这可能会导致问题。例如,一个1位浮点数尾数b0 -> 1.0b1 -> 1.5。 "最后一位数字"始终为05 - Mateen Ulhaq
1
在我看来,random.randrange(10) 更加明显。random.randint(在幕后调用 random.randrange)是 random 模块的后期添加,供那些不理解 Python 中范围工作原理的人使用。 ;) - PM 2Ring
2
@PM2Ring:实际上,在他们决定randint接口是一个错误之后,randrange才出现。 - user2357112
@user2357112supportsMonica 哦,好的。我改口了。我本来以为randrange是第一个,但我的记忆力不如以前了。;) - PM 2Ring
请注意,random.randrange(10) 保证每个数字出现的机会相等,而 int(10*random.random()) 可能具有微妙的不同机会。 - Marius Gedminas
2个回答

25

那不是这个数字的“最后一位数”。那是当传递该数字时,str给出的字符串的最后一位数。

当你对浮点数调用str时,Python会给你足够多的数字,以便在字符串上调用float将会得到原始的浮点数。为此,尾部为1或9的情况不太可能发生,因为这意味着该数字非常接近舍入该数字的值。很可能没有其他浮点数更接近,如果有的话,可以舍弃该位数字而不损失float(str(original_float))的行为。

如果str给出了足够的数字来精确表示参数,则最后一位数字几乎总是5,除非random.random()返回0.0,在这种情况下,最后一位数字将为0。(浮点数只能表示二进有理数,非整数二进有理数的最后一个非零小数位始终为5)。输出也会非常长,看起来像

>>> import decimal, random
>>> print(decimal.Decimal(random.random()))
0.29711195452007921335990658917580731213092803955078125
这也是为什么str不这样做的原因之一。如果str返回恰好17个有效数字(足以将所有浮点值彼此区分开来,但有时比必要的位数多),那么您所看到的效果将会消失。会有一个近乎均匀的尾数分布(包括0)。另外,你忘了str有时会返回科学计数法表示的字符串,但这只是一个次要的影响,因为从random.random()得到需要这样表示的浮点数的概率很低。

7

TL;DR:您的示例实际上并没有看到最后一位数字。将有限二进制表示的尾数转换为十进制时,最后一位数字始终应该是05


请查看cpython/pystrtod.c中的评论:

char * PyOS_double_to_string(double val,
                                         char format_code,
                                         int precision,
                                         int flags,
                                         int *type)
{
    char format[32];
    Py_ssize_t bufsize;
    char *buf;
    int t, exp;
    int upper = 0;

    /* Validate format_code, and map upper and lower case */
    switch (format_code) {
    // ...
    case 'r':          /* repr format */
        /* Supplied precision is unused, must be 0. */
        if (precision != 0) {
            PyErr_BadInternalCall();
            return NULL;
        }
        /* The repr() precision (17 significant decimal digits) is the
           minimal number that is guaranteed to have enough precision
           so that if the number is read back in the exact same binary
           value is recreated.  This is true for IEEE floating point
           by design, and also happens to work for all other modern
           hardware. */
        precision = 17;
        format_code = 'g';
        break;
    // ...
}

维基百科 证实:

53位有效数字的精度能提供15到17位有效十进制数字的精度 (2-53 ≈ 1.11 × 10-16)。如果将一个最多有15位有效数字的十进制字符串转换为IEEE 754双精度表示,然后将其转换为具有相同位数的十进制字符串,则最终结果应与原始字符串匹配。如果将IEEE 754双精度数转换为至少有17个有效数字的十进制字符串,然后将其转换回双精度表示,最终结果必须与原始数字匹配。

因此,当我们使用 str(或 repr)时,我们只以十进制17位有效数字的形式表示。这意味着一些浮点数将被截断。实际上,要获得精确的表示,您需要53位有效数字的精度!您可以如下验证:

>>> counts = Counter(
...     len(f"{random():.99f}".lstrip("0.").rstrip("0"))
...     for _ in range(1000000)
... )
>>> counts
Counter({53: 449833,
         52: 270000,
         51: 139796,
         50: 70341,
         49: 35030,
         48: 17507,
         47: 8610,
         46: 4405,
         45: 2231,
         44: 1120,
         43: 583,
         42: 272,
         41: 155,
         40: 60,
         39: 25,
         38: 13,
         37: 6,
         36: 5,
         35: 4,
         34: 3,
         32: 1})
>>> max(counts)
53

现在使用最高精度,这是找到“最后一位数字”的正确方法:

>>> counts = Counter(
...     int(f"{random():.53f}".lstrip("0.").rstrip("0")[-1])
...     for _ in range(1000000)
... )
>>> counts
Counter({5: 1000000})

因此,最后一位数字始终为5。(或在非常罕见的情况下为0。)这是有道理的,因为:
2**0  == 1.0
2**-1 == 0.5
2**-2 == 0.25
2**-3 == 0.125
2**-4 == 0.0625
2**-5 == 0.03125
2**-6 == 0.015625
2**-7 == 0.0078125
2**-8 == 0.00390625
2**-9 == 0.001953125
...
2**-k == 0.[k-1 digits]5

所有尾数都是这些系数的某些部分和。


注意:正如用户2357112指出的那样,要查看正确的实现,请参阅 PyOS_double_to_stringformat_float_short,但我将保留当前的内容,因为它更具教育意义。


因此,当我们使用str(或repr)时,我们仅在十进制的17个有效数字中进行表示。 17是最大值。如果实际上是固定的17位数字,则问题中的效果将不会出现。问题中的效果来自于刚好足够的数字来回舍入str(some_float)使用。 - user2357112
1
你正在查看错误的PyOS_double_to_string实现方式。这个实现方式已被预处理掉,换成了这个实现方式 - user2357112
关于第一个评论:如上所述,浮点数(编辑:指数为0)的精确表示需要53个有效数字,但17个足以保证float(str(x)) == x。大多数情况下,这个答案只是为了表明问题中假设的(“精确表示的最后一位数字”)是错误的,因为正确的结果只是5(和一个不太可能的0)。 - Mateen Ulhaq
53个十进制数字是不够的。这里有一个需要更多的示例。 - user2357112
@user2357112supportsMonica 抱歉,我的意思是以0为指数。(这是确保在区间[0,1]内均匀性所必需的。) - Mateen Ulhaq

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接