Distribuição dos dígitos finais dos números aleatórios em Python

24

Existem duas maneiras óbvias de gerar um dígito aleatório de 0 a 9 no Python. Pode-se gerar um número de ponto flutuante aleatório entre 0 e 1, multiplicar por 10 e arredondar para baixo. Alternativamente, pode-se usar o random.randintmétodo

import random

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

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

Eu estava curioso sobre o que aconteceria se alguém gerasse um número aleatório entre 0 e 1 e mantivesse o último dígito. Eu não esperava necessariamente que a distribuição fosse uniforme, mas achei o resultado bastante surpreendente.

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)

Resultado:

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

Um histograma é mostrado abaixo. Observe que 0 não aparece, pois os zeros à direita são truncados. Mas alguém pode explicar por que os dígitos 4, 5 e 6 são mais comuns que o resto? Eu usei o Python 3.6.10, mas os resultados foram semelhantes no Python 3.8.0a4.

Distribuição dos dígitos finais dos carros alegóricos aleatórios

Dave Radcliffe
fonte
4
Isso tem a ver com a maneira como as representações de strings de carros alegóricos são calculadas em Python. Veja docs.python.org/3/tutorial/floatingpoint.html . Você obteria resultados muito mais uniformes se usasse o décimo dígito (primeiro após o decimal) em vez do último dígito.
Dennis
11
Armazenamos carros alegóricos em representação binária (já que nossa memória também é binária). strconverte-o para a base-10, que provavelmente causará problemas. por exemplo, uma mantissa flutuante de 1 bit b0 -> 1.0e b1 -> 1.5. O "último dígito" será sempre 0ou 5.
Mateen Ulhaq 25/04
11
random.randrange(10)é ainda mais óbvio, IMHO. random.randint(que chama por random.randrangebaixo do capô) foi uma adição posterior ao randommódulo para pessoas que não entendem como os intervalos funcionam no Python. ;)
PM 2Ring
2
@ PM2Ring: randrangena verdade, ficou em segundo lugar, depois que eles decidiram que a randintinterface era um erro.
user2357112 suporta Monica em 25/04
@ user2357112supportsMonica Oh, ok. Eu estou corrigido. Eu tinha certeza que randrange era o primeiro, mas minha memória não é tão boa quanto costumava ser. ;)
PM 2Ring

Respostas:

21

Esse não é "o último dígito" do número. Esse é o último dígito da stringstr quando você passa o número.

Quando você chama strum float, o Python fornece dígitos suficientes para que o chamar floatna string forneça o float original. Para isso, é menos provável que um 1 ou 9 à direita seja necessário que outros dígitos, porque 1 ou 9 à direita significa que o número está muito próximo do valor que você obteria ao arredondar esse dígito. Há uma boa chance de nenhum outro carro alegórico estar mais próximo e, nesse caso, esse dígito pode ser descartado sem sacrificar o float(str(original_float))comportamento.

Se strvocê forneceu dígitos suficientes para representar exatamente o argumento, o último dígito quase sempre seria 5, exceto quando random.random()retorna 0,0, nesse caso o último dígito seria 0. (Os flutuadores podem representar apenas racionais diádicos e o último dígito decimal diferente de zero de um racional diádico não inteiro é sempre 5.) As saídas também seriam extremamente longas, parecendo

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

qual é uma das razões strnão faz isso.

Se strvocê fornecesse exatamente 17 dígitos significativos (o suficiente para distinguir todos os valores flutuantes um do outro, mas às vezes mais dígitos do que o necessário), o efeito que você está vendo desapareceria. Haveria uma distribuição quase uniforme dos dígitos à direita (incluindo 0).

(Além disso, você esqueceu que stràs vezes retorna uma seqüência de caracteres em notação científica, mas isso é um efeito menor, porque há uma baixa probabilidade de obter uma flutuação de onde isso aconteceria random.random().)

user2357112 suporta Monica
fonte
5

TL; DR Seu exemplo não está realmente olhando para o último dígito. O último dígito de uma mantissa finita representada em binário convertida em base-10 deve sempre ser 0ou 5.


Dê uma olhada em 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 agora em 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;
    // ...
}

A Wikipedia confirma isso:

A precisão de 53 bits do significando fornece de 15 a 17 precisão de dígitos decimais significativos (2 -53 ± 1,11 × 10-16 ). Se uma sequência decimal com no máximo 15 dígitos significativos for convertida em representação de precisão dupla IEEE 754 e depois convertida novamente em uma sequência decimal com o mesmo número de dígitos, o resultado final deverá corresponder à sequência original. Se um número de precisão dupla IEEE 754 for convertido em uma sequência decimal com pelo menos 17 dígitos significativos e depois convertido novamente em representação de precisão dupla, o resultado final deverá corresponder ao número original.

Assim, quando usamos str(ou repr), estamos representando apenas 17 dígitos significativos na base 10. Isso significa que parte do número de ponto flutuante será truncado. De fato, para obter a representação exata, você precisa de uma precisão de 53 dígitos significativos! Você pode verificar isso da seguinte maneira:

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

Agora, usando a precisão máxima, eis a maneira correta de encontrar o "último dígito":

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

NOTA: Conforme indicado por user2357112, as implementações corretas a serem observadas são PyOS_double_to_stringe format_float_short, mas deixarei as atuais porque são mais interessantes em termos pedagógicos.

Mateen Ulhaq
fonte
"Assim, quando usamos str (ou repr), estamos representando apenas 17 dígitos significativos na base 10". - 17 é o máximo. Se na verdade fossem 17 dígitos fixos, o efeito na pergunta não apareceria. O efeito na pergunta vem dos str(some_float)usos de arredondamento de dígitos suficientes para a viagem de ida e volta .
user2357112 suporta Monica em 25/04
11
Você está olhando para a implementação errada de PyOS_double_to_string. Essa implementação é pré-processada em favor desta
user2357112 suporta Monica
Quanto ao primeiro comentário: Como mencionado, a representação exata de um número de ponto flutuante (EDIT: com um expoente de 0) requer 53 dígitos significativos, embora 17 sejam suficientes para garantir float(str(x)) == x. Principalmente, essa resposta foi apenas para mostrar que a suposição ("último dígito da representação exata") feita na pergunta estava errada, uma vez que o resultado correto é apenas 5s (e um improvável 0).
Mateen Ulhaq 25/04
53 dígitos decimais significativos não são suficientes. Aqui está um exemplo que requer muito mais.
user2357112 suporta Monica em 25/04
@ user2357112supportsMonica Desculpe, eu quis dizer com um expoente de 0. (O que é necessário para garantir uniformidade dentro do intervalo [0, 1].)
Mateen Ulhaq