Distribuzione delle cifre finali di numeri casuali in Python


24

Esistono due modi ovvi per generare una cifra casuale da 0 a 9 in Python. Si potrebbe generare un numero in virgola mobile casuale compreso tra 0 e 1, moltiplicarlo per 10 e arrotondare per difetto. In alternativa, si potrebbe usare il random.randintmetodo.

import random

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

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

Ero curioso di sapere cosa accadrebbe se si generasse un numero casuale compreso tra 0 e 1 e si mantenesse l' ultima cifra. Non mi aspettavo necessariamente che la distribuzione fosse uniforme, ma ho trovato il risultato abbastanza 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)

Produzione:

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

Di seguito è mostrato un istogramma. Si noti che 0 non viene visualizzato, poiché gli zeri finali vengono troncati. Qualcuno può spiegare perché le cifre 4, 5 e 6 sono più comuni delle altre? Ho usato Python 3.6.10, ma i risultati erano simili in Python 3.8.0a4.

Distribuzione delle cifre finali di galleggianti casuali


4
Ciò ha a che fare con il modo in cui le rappresentazioni di stringa dei float vengono calcolate in Python. Vedi docs.python.org/3/tutorial/floatingpoint.html . Otterresti risultati molto più uniformi se utilizzassi la decima cifra (prima dopo il decimale) anziché l'ultima cifra.
Dennis,

1
Conserviamo i float nella rappresentazione binaria (poiché la nostra memoria è anche binaria). strlo converte in base-10 che è destinato a causare problemi. ad es. una mantissa float a 1 bit b0 -> 1.0e b1 -> 1.5. L '"ultima cifra" sarà sempre 0o 5.
Mateen Ulhaq,

1
random.randrange(10)è ancora più ovvio, IMHO. random.randint(che chiama random.randrangesotto il cofano) è stata un'aggiunta successiva al randommodulo per le persone che non capiscono come funzionano i range in Python. ;)
PM 2Ring

2
@ PM2Ring: in randrangerealtà è arrivato secondo, dopo aver deciso che l' randintinterfaccia era un errore.
user2357112 supporta Monica il

@ user2357112supportsMonica Oh, ok. Sono corretto. Ero sicuro che randrange fosse il 1 °, ma la mia memoria non è più buona come una volta. ;)
PM 2Ring

Risposte:


21

Questa non è "l'ultima cifra" del numero. Questa è l'ultima cifra della stringa che strti è stata data quando hai passato il numero.

Quando chiami strun float, Python ti dà abbastanza cifre che chiamare floatla stringa ti darà il float originale. A tal fine, è meno probabile che un 1 o 9 finale sia necessario rispetto ad altre cifre, poiché un 1 o 9 finale indica che il numero è molto vicino al valore che otterresti arrotondando quella cifra. C'è una buona probabilità che nessun altro galleggiante sia più vicino e, in tal caso, quella cifra può essere scartata senza sacrificare il float(str(original_float))comportamento.

Se strti fornissero cifre sufficienti per rappresentare esattamente l'argomento, l'ultima cifra sarebbe quasi sempre 5, tranne quando random.random()restituisce 0,0, nel qual caso l'ultima cifra sarebbe 0. (I float possono rappresentare solo razionali diadici e l'ultima cifra decimale diversa da zero di un razionale diadico non intero è sempre 5.) Anche gli output sarebbero estremamente lunghi, simili a

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

che è uno dei motivi per cui strnon lo fa.

Se strti fornissi esattamente 17 cifre significative (abbastanza per distinguere tutti i valori float l'uno dall'altro, ma a volte più cifre del necessario), l'effetto che stai vedendo scomparirebbe. Ci sarebbe una distribuzione quasi uniforme delle cifre finali (incluso 0).

(Inoltre, hai dimenticato che a strvolte restituisce una stringa in notazione scientifica, ma questo è un effetto minore, perché c'è una bassa probabilità di ottenere un galleggiante da cui ciò potrebbe accadere random.random().)


5

TL; DR Il tuo esempio in realtà non sta guardando l'ultima cifra. L'ultima cifra di una mantissa rappresentata binaria finita convertita in base-10 dovrebbe essere sempre 0o 5.


Dai un'occhiata 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);

    // ...
}

E ora a 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 lo conferma:

La precisione del significato a 53 bit fornisce da 15 a 17 una precisione decimale significativa (2 -53 ≈ 1,11 × 10 -16 ). Se una stringa decimale con al massimo 15 cifre significative viene convertita in rappresentazione a doppia precisione IEEE 754 e quindi riconvertita in una stringa decimale con lo stesso numero di cifre, il risultato finale deve corrispondere alla stringa originale. Se un numero IEEE 754 a precisione doppia viene convertito in una stringa decimale con almeno 17 cifre significative, quindi convertito nuovamente in rappresentazione a precisione doppia, il risultato finale deve corrispondere al numero originale.

Pertanto, quando utilizziamo str(o repr), rappresentiamo solo 17 cifre significative in base-10. Ciò significa che parte del numero in virgola mobile verrà troncato. In effetti, per ottenere la rappresentazione esatta, è necessaria una precisione di 53 cifre significative! Puoi verificarlo come segue:

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

Ora usando la massima precisione, ecco il modo corretto per trovare "l'ultima cifra":

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

NOTA: come sottolineato da user2357112, le implementazioni corrette da guardare sono PyOS_double_to_stringe format_float_short, ma lascerò quelle attuali perché sono più pedagogicamente interessanti.


"Quindi, quando usiamo str (o repr), rappresentiamo solo 17 cifre significative in base-10." - 17 è il massimo. Se in realtà fosse un numero fisso di 17 cifre, l'effetto nella domanda non apparirebbe. L'effetto nella domanda proviene dagli str(some_float)utilizzi di arrotondamento delle cifre sufficienti per il round trip .
user2357112 supporta Monica il

1
Stai guardando l'implementazione sbagliata di PyOS_double_to_string. Tale implementazione è stata preelaborata a favore di questa
user2357112 supporta Monica il

Per quanto riguarda il primo commento: come detto, l'esatta rappresentazione di un numero in virgola mobile (EDIT: con un esponente di 0) richiede 53 cifre significative, sebbene 17 sia sufficiente per garantire float(str(x)) == x. Per lo più, questa risposta era solo per mostrare l'assunto ("ultima cifra della rappresentazione esatta") fatto nella domanda era sbagliato, poiché il risultato corretto è solo 5s (e un improbabile 0).
Mateen Ulhaq,

53 cifre decimali significative non sono sufficienti. Ecco un esempio che richiede molto di più.
user2357112 supporta Monica il

@ user2357112supportsMonica Siamo spiacenti, intendevo con un esponente di 0. (Il che è necessario per garantire l'uniformità nell'intervallo [0, 1].)
Mateen Ulhaq
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.