Verteilung der letzten Ziffern von Zufallszahlen in Python

24

Es gibt zwei offensichtliche Möglichkeiten, in Python eine zufällige Ziffer von 0 bis 9 zu generieren. Man könnte eine zufällige Gleitkommazahl zwischen 0 und 1 erzeugen, mit 10 multiplizieren und abrunden. Alternativ könnte man die random.randintMethode verwenden.

import random

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

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

Ich war gespannt, was passieren würde, wenn man eine Zufallszahl zwischen 0 und 1 generiert und die letzte Ziffer behält . Ich hatte nicht unbedingt erwartet, dass die Verteilung einheitlich sein würde, aber ich fand das Ergebnis ziemlich überraschend.

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)

Ausgabe:

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

Ein Histogramm ist unten gezeigt. Beachten Sie, dass 0 nicht angezeigt wird, da nachgestellte Nullen abgeschnitten werden. Aber kann jemand erklären, warum die Ziffern 4, 5 und 6 häufiger sind als die anderen? Ich habe Python 3.6.10 verwendet, aber die Ergebnisse in Python 3.8.0a4 waren ähnlich.

Verteilung der letzten Ziffern zufälliger Floats

Dave Radcliffe
quelle
4
Dies hängt damit zusammen, wie die Zeichenfolgendarstellungen von Floats in Python berechnet werden. Siehe docs.python.org/3/tutorial/floatingpoint.html . Sie würden viel gleichmäßigere Ergebnisse erzielen, wenn Sie die Zehntelstelle (zuerst nach der Dezimalstelle) anstelle der letzten Ziffer verwenden würden.
Dennis
1
Wir speichern Floats in binärer Darstellung (da unser Speicher auch binär ist). strkonvertiert es in Basis-10, was Probleme verursachen kann. zB eine 1-Bit-Float-Mantisse b0 -> 1.0und b1 -> 1.5. Die "letzte Ziffer" ist immer 0oder 5.
Mateen Ulhaq
1
random.randrange(10)ist noch offensichtlicher, IMHO. random.randint(was random.randrangeunter der Haube aufgerufen wird) war eine spätere Ergänzung des randomModuls für Leute, die nicht verstehen, wie Bereiche in Python funktionieren. ;)
PM 2Ring
2
@ PM2Ring: randrangekam tatsächlich an zweiter Stelle, nachdem sie entschieden hatten, dass die randintSchnittstelle ein Fehler war.
user2357112 unterstützt Monica
@ user2357112supportsMonica Oh, ok. Ich stehe korrigiert. Ich war mir sicher, dass Randrange der 1. war, aber mein Gedächtnis ist nicht mehr so ​​gut wie früher. ;)
PM 2Ring

Antworten:

21

Das ist nicht "die letzte Ziffer" der Nummer. Das ist die letzte Ziffer der Zeichenfolge, strdie Sie erhalten haben, als Sie die Nummer übergeben haben.

Wenn Sie streinen Float aufrufen , gibt Python Ihnen genügend Ziffern, damit Sie beim Aufrufen floatder Zeichenfolge den ursprünglichen Float erhalten. Zu diesem Zweck ist eine nachfolgende 1 oder 9 weniger wahrscheinlich als andere Ziffern erforderlich, da eine nachfolgende 1 oder 9 bedeutet, dass die Zahl sehr nahe an dem Wert liegt, den Sie durch Abrunden dieser Ziffer erhalten würden. Es besteht eine gute Chance, dass keine anderen Floats näher sind, und wenn ja, kann diese Ziffer verworfen werden, ohne das float(str(original_float))Verhalten zu beeinträchtigen .

Wenn strSie genügend Ziffern hätten, um das Argument genau darzustellen, wäre die letzte Ziffer fast immer 5, außer wenn random.random()0.0 zurückgegeben wird. In diesem Fall wäre die letzte Ziffer 0. (Floats können nur dyadische Rationalitäten darstellen und die letzte Dezimalstelle ungleich Null von Ein nicht ganzzahliger dyadischer Rational ist immer 5.) Die Ausgaben wären auch extrem lang und würden so aussehen

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

Das ist einer der Gründe, warum strdas nicht so ist.

Wenn strSie genau 17 signifikante Ziffern erhalten (genug, um alle Gleitkommawerte voneinander zu unterscheiden, aber manchmal mehr Ziffern als erforderlich), verschwindet der Effekt, den Sie sehen. Es würde eine nahezu gleichmäßige Verteilung der nachfolgenden Ziffern (einschließlich 0) geben.

(Außerdem haben Sie vergessen, dass strmanchmal eine Zeichenfolge in wissenschaftlicher Notation zurückgegeben wird, aber das ist ein geringfügiger Effekt, da die Wahrscheinlichkeit gering ist, dass ein Float entsteht, bei dem dies passieren würde random.random().)

user2357112 unterstützt Monica
quelle
5

TL; DR In Ihrem Beispiel wird nicht die letzte Ziffer angezeigt. Die letzte Ziffer einer endlichen binär dargestellten Mantisse, die in Basis 10 umgewandelt wurde, sollte immer 0oder sein 5.


Schauen Sie sich an 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);

    // ...
}

Und jetzt bei 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 bestätigt dies:

Die 53-Bit-Signifikanzgenauigkeit ergibt eine Genauigkeit von 15 bis 17 signifikanten Dezimalstellen (2 -53 ≈ 1,11 × 10 -16 ). Wenn eine Dezimalzeichenfolge mit höchstens 15 signifikanten Stellen in eine IEEE 754-Darstellung mit doppelter Genauigkeit konvertiert und dann wieder in eine Dezimalzeichenfolge mit derselben Anzahl von Stellen konvertiert wird, sollte das Endergebnis mit der ursprünglichen Zeichenfolge übereinstimmen. Wenn eine IEEE 754-Zahl mit doppelter Genauigkeit in eine Dezimalzeichenfolge mit mindestens 17 signifikanten Stellen konvertiert und dann wieder in eine Darstellung mit doppelter Genauigkeit konvertiert wird, muss das Endergebnis mit der ursprünglichen Zahl übereinstimmen.

Wenn wir also str(oder repr) verwenden, repräsentieren wir nur 17 signifikante Ziffern in Basis-10. Dies bedeutet, dass ein Teil der Gleitkommazahl abgeschnitten wird. Um die genaue Darstellung zu erhalten, benötigen Sie eine Genauigkeit von 53 signifikanten Stellen! Sie können dies wie folgt überprüfen:

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

Wenn Sie nun die maximale Genauigkeit verwenden, finden Sie hier den richtigen Weg, um die "letzte Ziffer" zu finden:

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

HINWEIS: Wie von user2357112 hervorgehoben, sind PyOS_double_to_stringund die richtigen Implementierungen zu betrachten format_float_short, aber ich werde die aktuellen Implementierungen belassen, da sie pädagogisch interessanter sind.

Mateen Ulhaq
quelle
"Wenn wir also str (oder repr) verwenden, repräsentieren wir nur 17 signifikante Stellen in Base-10." - 17 ist das Maximum. Wenn es tatsächlich eine feste 17-stellige Zahl wäre, würde der Effekt in der Frage nicht erscheinen. Der Effekt in der Frage ergibt sich aus der str(some_float)Verwendung von Rundungen mit gerade genug Ziffern für die Rundreise .
user2357112 unterstützt Monica
1
Sie sehen die falsche Implementierung von PyOS_double_to_string. Diese Implementierung ist zugunsten dieser
user2357112 unterstützt Monica
Zum ersten Kommentar: Wie bereits erwähnt, erfordert die genaue Darstellung einer Gleitkommazahl (BEARBEITEN: mit einem Exponenten von 0) 53 signifikante Stellen, obwohl 17 ausreichen, um dies zu gewährleisten float(str(x)) == x. Meistens sollte diese Antwort nur zeigen, dass die in der Frage gemachte Annahme ("letzte Ziffer der exakten Darstellung") falsch war, da das richtige Ergebnis nur 5s (und unwahrscheinlich 0) ist.
Mateen Ulhaq
53 signifikante Dezimalstellen reichen nicht aus. Hier ist ein Beispiel, das viel mehr braucht.
user2357112 unterstützt Monica
@ user2357112supportsMonica Entschuldigung, ich meinte mit einem Exponenten von 0. (Dies ist notwendig, um die Gleichmäßigkeit innerhalb des Intervalls [0, 1] zu gewährleisten.)
Mateen Ulhaq