Distribution des derniers chiffres des nombres aléatoires en Python

24

Il existe deux façons évidentes de générer un chiffre aléatoire de 0 à 9 en Python. On pourrait générer un nombre à virgule flottante aléatoire entre 0 et 1, multiplier par 10 et arrondir vers le bas. Alternativement, on pourrait utiliser la random.randintméthode.

import random

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

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

J'étais curieux de savoir ce qui se passerait si l'on générait un nombre aléatoire entre 0 et 1 et conservait le dernier chiffre. Je ne m'attendais pas nécessairement à une distribution uniforme, mais j'ai trouvé le résultat assez surprenant.

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)

Production:

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

Un histogramme est illustré ci-dessous. Notez que 0 n'apparaît pas, car les zéros de fin sont tronqués. Mais quelqu'un peut-il expliquer pourquoi les chiffres 4, 5 et 6 sont plus courants que les autres? J'ai utilisé Python 3.6.10, mais les résultats étaient similaires dans Python 3.8.0a4.

Distribution des derniers chiffres des flotteurs aléatoires

Dave Radcliffe
la source
4
Cela a à voir avec la façon dont les représentations de chaînes de flottants sont calculées en Python. Voir docs.python.org/3/tutorial/floatingpoint.html . Vous obtiendriez des résultats bien plus uniformes si vous utilisiez le dixième chiffre (le premier après la décimale) plutôt que le dernier chiffre.
Dennis
1
Nous stockons des flottants en représentation binaire (puisque notre mémoire est également binaire). strle convertit en base-10 qui est susceptible de causer des problèmes. par exemple une mantisse flottante à 1 bit b0 -> 1.0et b1 -> 1.5. Le "dernier chiffre" sera toujours 0ou 5.
Mateen Ulhaq
1
random.randrange(10)est encore plus évident, à mon humble avis. random.randint(qui appelle random.randrangesous le capot) a été ajouté ultérieurement au randommodule pour les personnes qui ne comprennent pas comment les plages fonctionnent en Python. ;)
PM 2Ring
2
@ PM2Ring: randrangeest arrivé en deuxième position, après avoir décidé que l' randintinterface était une erreur.
user2357112 prend en charge Monica
@ user2357112supportsMonica Oh, ok. Je me suis trompé. J'étais sûr que randrange était 1er, mais ma mémoire n'est plus aussi bonne qu'avant. ;)
PM 2Ring

Réponses:

21

Ce n'est pas "le dernier chiffre" du nombre. C'est le dernier chiffre de la chaîne que strvous avez donné lorsque vous avez passé le numéro.

Lorsque vous appelez strun flottant, Python vous donne suffisamment de chiffres pour que l'appel floatsur la chaîne vous donne le flottant d'origine. À cette fin, un 1 ou 9 de fin est moins susceptible d'être nécessaire que d'autres chiffres, car un 1 ou 9 de fin signifie que le nombre est très proche de la valeur que vous obtiendriez en arrondissant ce chiffre. Il y a de fortes chances qu'aucun autre flotteur ne soit plus proche, et si c'est le cas, ce chiffre peut être jeté sans sacrifier le float(str(original_float))comportement.

S'il strvous a donné suffisamment de chiffres pour représenter exactement l'argument, le dernier chiffre serait presque toujours 5, sauf lorsque random.random()renvoie 0,0, auquel cas le dernier chiffre serait 0. (Les flottants ne peuvent représenter que des logiques dyadiques et le dernier chiffre décimal non nul de un rationnel dyadique non entier est toujours 5.) Les sorties seraient également extrêmement longues, ressemblant à

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

ce qui est l'une des raisons pour lesquelles strcela ne se produit pas.

Si strvous vous donnait exactement 17 chiffres significatifs (assez pour distinguer toutes les valeurs flottantes les unes des autres, mais parfois plus de chiffres que nécessaire), alors l'effet que vous voyez disparaîtrait. Il y aurait une distribution presque uniforme des chiffres de fin (y compris 0).

(De plus, vous avez oublié que strparfois renvoie une chaîne en notation scientifique, mais c'est un effet mineur, car il y a une faible probabilité d'obtenir un flotteur d'où cela se produirait random.random().)

user2357112 prend en charge Monica
la source
5

TL; DR Votre exemple ne regarde pas réellement le dernier chiffre. Le dernier chiffre d'une mantisse finie représentée binaire convertie en base 10 doit toujours être 0ou 5.


Jetez un œil à 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);

    // ...
}

Et maintenant à 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;
    // ...
}

Wikipédia le confirme:

La précision de la signification de 53 bits donne une précision de 15 à 17 chiffres décimaux significatifs (2 -53 ≈ 1,11 × 10 -16 ). Si une chaîne décimale avec au plus 15 chiffres significatifs est convertie en représentation double précision IEEE 754, puis reconvertie en une chaîne décimale avec le même nombre de chiffres, le résultat final doit correspondre à la chaîne d'origine. Si un nombre double précision IEEE 754 est converti en une chaîne décimale avec au moins 17 chiffres significatifs, puis reconverti en représentation double précision, le résultat final doit correspondre au nombre d'origine.

Ainsi, lorsque nous utilisons str(ou repr), nous ne représentons que 17 chiffres significatifs en base-10. Cela signifie qu'une partie du nombre à virgule flottante sera tronquée. En fait, pour obtenir la représentation exacte, vous avez besoin d'une précision de 53 chiffres significatifs! Vous pouvez le vérifier comme suit:

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

Maintenant, en utilisant la précision maximale, voici la bonne façon de trouver le "dernier chiffre":

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

REMARQUE: Comme indiqué par user2357112, les implémentations correctes à examiner sont PyOS_double_to_stringet format_float_short, mais je laisserai les actuelles car elles sont plus intéressantes sur le plan pédagogique.

Mateen Ulhaq
la source
"Ainsi, lorsque nous utilisons str (ou repr), nous ne représentons que 17 chiffres significatifs en base-10." - 17 est le maximum. S'il s'agissait en fait d'un nombre fixe de 17 chiffres, l'effet dans la question n'apparaîtrait pas. L'effet dans la question provient des str(some_float)utilisations de l' arrondi juste assez de chiffres pour l'aller-retour .
user2357112 prend en charge Monica
1
Vous regardez la mauvaise mise en œuvre de PyOS_double_to_string. Cette implémentation est prétraitée en faveur de celle-ci
user2357112 prend en charge Monica
Concernant le premier commentaire: Comme mentionné, la représentation exacte d'un nombre à virgule flottante (EDIT: avec un exposant de 0) nécessite 53 chiffres significatifs, bien que 17 soit suffisant pour garantir float(str(x)) == x. La plupart du temps, cette réponse était juste pour montrer que l'hypothèse ("dernier chiffre de la représentation exacte") faite dans la question était fausse, car le résultat correct est juste 5s (et peu probable 0).
Mateen Ulhaq
53 chiffres décimaux significatifs ne suffisent pas. Voici un exemple qui en prend beaucoup plus.
user2357112 prend en charge Monica
@ user2357112supportsMonica Désolé, je voulais dire avec un exposant de 0. (Ce qui est nécessaire pour garantir l'uniformité dans l'intervalle [0, 1].)
Mateen Ulhaq