Distribusi angka akhir angka acak dalam Python

24

Ada dua cara yang jelas untuk menghasilkan angka acak dari 0 hingga 9 dengan Python. Seseorang dapat menghasilkan angka floating point acak antara 0 dan 1, kalikan dengan 10, dan bulatkan. Atau, seseorang dapat menggunakan random.randintmetode ini.

import random

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

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

Saya ingin tahu tentang apa yang akan terjadi jika seseorang menghasilkan angka acak antara 0 dan 1, dan menyimpan digit terakhir . Saya tidak selalu berharap distribusinya seragam, tetapi saya menemukan hasilnya cukup mengejutkan.

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)

Keluaran:

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

Histogram ditunjukkan di bawah ini. Perhatikan bahwa 0 tidak muncul, karena trailing nol terpotong. Tetapi adakah yang bisa menjelaskan mengapa angka 4, 5, dan 6 lebih umum daripada yang lainnya? Saya menggunakan Python 3.6.10, tetapi hasilnya serupa di Python 3.8.0a4.

Distribusi digit akhir float acak

Dave Radcliffe
sumber
4
Ini ada hubungannya dengan cara bahwa representasi string float dihitung dengan Python. Lihat docs.python.org/3/tutorial/floatingpoint.html . Anda akan mendapatkan lebih banyak hasil genap jika Anda menggunakan digit kesepuluh (pertama setelah desimal) daripada digit terakhir.
Dennis
1
Kami menyimpan float dalam representasi biner (karena ingatan kami juga biner). strmengubahnya menjadi basis-10 yang pasti akan menimbulkan masalah. misalnya 1-bit mengambang mantissa b0 -> 1.0dan b1 -> 1.5. "Digit terakhir" akan selalu 0atau 5.
Mateen Ulhaq
1
random.randrange(10)bahkan lebih jelas, IMHO. random.randint(yang memanggil di random.randrangebawah tenda) adalah tambahan nanti untuk randommodul untuk orang yang tidak mengerti bagaimana rentang bekerja dengan Python. ;)
PM 2Ring
2
@ PM2Ring: randrangesebenarnya berada di urutan kedua, setelah mereka memutuskan bahwa randintantarmuka adalah kesalahan.
user2357112 mendukung Monica
@ user2357112supportsMonica Oh, ok. Saya berdiri dikoreksi. Saya yakin randrange adalah yang pertama, tetapi ingatan saya tidak sebagus dulu. ;)
PM 2Ring

Jawaban:

21

Itu bukan "digit terakhir" dari nomor tersebut. Digit terakhir dari string strmemberi Anda ketika melewati nomor tersebut.

Saat Anda memanggil strfloat, Python memberi Anda cukup digit yang memanggil floatstring akan memberi Anda float asli. Untuk tujuan ini, trailing 1 atau 9 cenderung kurang diperlukan daripada digit lainnya, karena trailing 1 atau 9 berarti angka tersebut sangat dekat dengan nilai yang Anda dapatkan dengan membulatkan angka itu. Ada kemungkinan bagus tidak ada pelampung lain yang lebih dekat, dan jika demikian, angka itu dapat dibuang tanpa mengorbankan float(str(original_float))perilaku.

Jika strmemberi Anda cukup digit untuk secara tepat mewakili argumen, digit terakhir hampir selalu menjadi 5, kecuali ketika random.random()mengembalikan 0,0, dalam hal ini digit terakhir adalah 0. (Mengapung hanya dapat mewakili rasional dyadic , dan angka desimal bukan nol terakhir dari rasional dyadic non-integer selalu 5.) Outputnya juga akan sangat panjang, terlihat seperti

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

yang merupakan salah satu alasan strtidak melakukan itu.

Jika strmemberi Anda tepat 17 digit signifikan (cukup untuk membedakan semua nilai float satu sama lain, tetapi terkadang lebih banyak digit dari yang diperlukan), maka efek yang Anda lihat akan hilang. Akan ada distribusi angka trailing yang hampir seragam (termasuk 0).

(Juga, Anda lupa bahwa strkadang - kadang mengembalikan string dalam notasi ilmiah, tapi itu efek kecil, karena ada kemungkinan rendah mendapatkan pelampung di mana itu akan terjadi random.random().)

user2357112 mendukung Monica
sumber
5

TL; DR Contoh Anda sebenarnya tidak melihat angka terakhir. Digit terakhir mantissa terwakili biner terbatas yang dikonversi ke basis-10 harus selalu 0atau 5.


Lihatlah 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);

    // ...
}

Dan sekarang di 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 menegaskan hal ini:

Presisi signifikan dan 53-bit menghasilkan dari 15 hingga 17 presisi angka desimal yang signifikan (2 -53 ≈ 1,11 × 10 -16 ). Jika string desimal dengan maksimum 15 digit dikonversi ke representasi presisi ganda IEEE 754, dan kemudian dikonversi kembali ke string desimal dengan jumlah digit yang sama, hasil akhir harus cocok dengan string asli. Jika nomor presisi ganda IEEE 754 dikonversi ke string desimal dengan setidaknya 17 digit signifikan, dan kemudian dikonversi kembali ke representasi presisi ganda, hasil akhir harus cocok dengan nomor aslinya.

Jadi, ketika kita menggunakan str(atau repr), kita hanya mewakili 17 digit signifikan pada basis-10. Ini berarti beberapa nomor floating point akan terpotong. Bahkan, untuk mendapatkan representasi yang tepat, Anda membutuhkan ketepatan 53 digit signifikan! Anda dapat memverifikasi ini sebagai berikut:

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

Sekarang menggunakan presisi maksimum, inilah cara yang tepat untuk menemukan "digit terakhir":

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

CATATAN: Seperti yang ditunjukkan oleh user2357112, implementasi yang benar untuk dilihat adalah PyOS_double_to_stringdan format_float_short, tapi saya akan membiarkan yang saat ini karena mereka lebih menarik secara pedagogis.

Mateen Ulhaq
sumber
"Jadi, ketika kita menggunakan str (atau repr), kita hanya mewakili 17 digit signifikan di basis-10." - 17 adalah maksimum. Jika itu sebenarnya 17 digit tetap, efek dalam pertanyaan tidak akan muncul. Efek dalam pertanyaan tersebut berasal dari str(some_float)penggunaan pembulatan angka hingga cukup untuk pulang pergi .
user2357112 mendukung Monica
1
Anda melihat implementasi yang salah dari PyOS_double_to_string. Implementasi itu telah
disiapkan untuk yang
Mengenai komentar pertama: Seperti disebutkan, representasi tepat dari angka floating point (EDIT: dengan eksponen 0) membutuhkan 53 digit signifikan, meskipun 17 cukup untuk menjamin float(str(x)) == x. Sebagian besar, jawaban ini hanya untuk menunjukkan asumsi ("digit terakhir dari representasi persis") yang dibuat dalam pertanyaan itu salah, karena hasil yang benar adalah hanya 5s (dan tidak mungkin 0).
Mateen Ulhaq
53 angka desimal yang signifikan tidak cukup. Inilah contoh yang membutuhkan lebih banyak.
user2357112 mendukung Monica
@ user2357112supportsMonica Maaf, maksud saya dengan eksponen 0. (Yang diperlukan untuk menjamin keseragaman dalam interval [0, 1].)
Mateen Ulhaq