Distribución de dígitos finales de números aleatorios en Python

24

Hay dos formas obvias de generar un dígito aleatorio de 0 a 9 en Python. Se podría generar un número aleatorio de coma flotante entre 0 y 1, multiplicar por 10 y redondear hacia abajo. Alternativamente, uno podría usar el random.randintmétodo.

import random

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

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

Tenía curiosidad sobre lo que sucedería si uno generara un número aleatorio entre 0 y 1, y mantuviera el último dígito. No esperaba necesariamente que la distribución fuera uniforme, pero el resultado me pareció bastante sorprendente.

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)

Salida:

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

A continuación se muestra un histograma. Tenga en cuenta que 0 no aparece, ya que los ceros finales se truncan. Pero, ¿alguien puede explicar por qué los dígitos 4, 5 y 6 son más comunes que el resto? Usé Python 3.6.10, pero los resultados fueron similares en Python 3.8.0a4.

Distribución de dígitos finales de carrozas aleatorias

Dave Radcliffe
fuente
44
Esto tiene que ver con la forma en que las representaciones de cadenas de flotadores se calculan en Python. Ver docs.python.org/3/tutorial/floatingpoint.html . Obtendría resultados mucho más uniformes si usara el décimo dígito (primero después del decimal) en lugar del último dígito.
Dennis
1
Almacenamos flotantes en representación binaria (ya que nuestra memoria también es binaria). strlo convierte a base-10, lo que seguramente causará problemas. por ejemplo, una mantisa flotante de 1 bit b0 -> 1.0y b1 -> 1.5. El "último dígito" siempre será 0o 5.
Mateen Ulhaq
1
random.randrange(10)es aún más obvio, en mi humilde opinión. random.randint(que llama random.randrangebajo el capó) fue una adición posterior al randommódulo para personas que no entienden cómo funcionan los rangos en Python. ;)
PM 2Ring
2
@ PM2Ring: en randrangerealidad llegó en segundo lugar, después de que decidieron que la randintinterfaz era un error.
user2357112 es compatible con Monica el
@ user2357112supportsMonica Oh, está bien. Estoy corregido. Estaba seguro de que randrange era el primero, pero mi memoria no es tan buena como solía ser. ;)
PM 2Ring

Respuestas:

21

Ese no es "el último dígito" del número. Ese es el último dígito de la cadena que strle dio cuando pasó el número.

Cuando llama stra un flotante, Python le da suficientes dígitos que al llamar floata la cadena le dará el flotante original. Para este propósito, es menos probable que sea necesario un 1 o 9 al final que otros dígitos, porque un 1 o 9 al final significa que el número está muy cerca del valor que obtendría al redondear ese dígito. Hay una buena posibilidad de que no haya otros flotadores más cerca, y si es así, ese dígito puede descartarse sin sacrificar el float(str(original_float))comportamiento.

Si strle da suficientes dígitos para representar exactamente el argumento, el último dígito casi siempre será 5, excepto cuando random.random()devuelve 0.0, en cuyo caso el último dígito sería 0. (Los flotantes solo pueden representar racionales diádicos y el último dígito decimal distinto de cero de un racional diádico no entero es siempre 5.) Las salidas también serían extremadamente largas, como

>>> import decimal, random
>>> print(decimal.Decimal(random.random()))
0.29711195452007921335990658917580731213092803955078125

cuál es una de las razones por las strque no hace eso.

Si strle dio exactamente 17 dígitos significativos (suficiente para distinguir todos los valores flotantes entre sí, pero a veces más dígitos de los necesarios), entonces el efecto que está viendo desaparecería. Habría una distribución casi uniforme de los dígitos finales (incluido 0).

(Además, olvidó que a strveces devuelve una cadena en notación científica, pero ese es un efecto menor, porque hay una baja probabilidad de obtener un flotador de donde eso sucedería random.random()).

user2357112 es compatible con Monica
fuente
5

TL; DR Su ejemplo no está realmente mirando el último dígito. El último dígito de una mantisa finita binaria representada convertida a base-10 siempre debe ser 0o 5.


Echa un vistazo a cpython/floatobject.c:

static PyObject *
float_repr(PyFloatObject *v)
{
    PyObject *result;
    char *buf;

    buf = PyOS_double_to_string(PyFloat_AS_DOUBLE(v),
                                'r', 0,
                                Py_DTSF_ADD_DOT_0,
                                NULL);

    // ...
}

Y ahora en 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;
    // ...
}

Wikipedia confirma esto:

La precisión significativa de 53 bits proporciona una precisión significativa de 15 a 17 dígitos decimales (2 -53 ≈ 1.11 × 10 -16 ). Si una cadena decimal con un máximo de 15 dígitos significativos se convierte en una representación de doble precisión IEEE 754, y luego se vuelve a convertir en una cadena decimal con el mismo número de dígitos, el resultado final debe coincidir con la cadena original. Si un número IEEE 754 de doble precisión se convierte en una cadena decimal con al menos 17 dígitos significativos, y luego se vuelve a convertir en una representación de doble precisión, el resultado final debe coincidir con el número original.

Por lo tanto, cuando usamos str(o repr), solo representamos 17 dígitos significativos en base-10. Esto significa que parte del número de coma flotante se truncará. De hecho, para obtener la representación exacta, ¡necesita una precisión de 53 dígitos significativos! Puede verificar esto de la siguiente manera:

>>> 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

Ahora, usando la máxima precisión, esta es la forma correcta de encontrar el "último dígito":

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

NOTA: Como lo señaló user2357112, las implementaciones correctas a considerar son PyOS_double_to_stringy format_float_short, pero dejaré las actuales porque son más pedagógicamente interesantes.

Mateen Ulhaq
fuente
"Por lo tanto, cuando usamos str (o repr), solo representamos 17 dígitos significativos en base-10". - 17 es el máximo. Si en realidad se tratara de 17 dígitos fijos, el efecto en la pregunta no aparecería. El efecto en la pregunta proviene de los str(some_float)usos de redondeo de dígitos suficientes para el viaje de ida y vuelta .
user2357112 es compatible con Monica el
1
Estás viendo la implementación incorrecta de PyOS_double_to_string. Esa implementación está preprocesada a favor de esta
user2357112 es compatible con Monica el
Con respecto al primer comentario: Como se mencionó, la representación exacta de un número de coma flotante (EDITAR: con un exponente de 0) requiere 53 dígitos significativos, aunque 17 es suficiente para garantizar float(str(x)) == x. Principalmente, esta respuesta fue solo para mostrar que la suposición ("último dígito de representación exacta") hecha en la pregunta era incorrecta, ya que el resultado correcto es solo 5s (y poco probable 0).
Mateen Ulhaq
53 dígitos decimales significativos no son suficientes. Aquí hay un ejemplo que toma mucho más.
user2357112 es compatible con Monica el
@ user2357112supportsMonica Lo siento, quise decir con un exponente de 0. (que es necesario para garantizar la uniformidad dentro del intervalo [0, 1].)
Mateen Ulhaq