การแจกแจงตัวเลขสุดท้ายของตัวเลขสุ่มใน Python


24

มีสองวิธีที่ชัดเจนในการสร้างตัวเลขสุ่มจาก 0 ถึง 9 ใน Python เราสามารถสร้างจำนวนจุดลอยตัวแบบสุ่มระหว่าง 0 ถึง 1 คูณด้วย 10 และปัดเศษลง อีกวิธีหนึ่งสามารถใช้random.randintวิธีการ

import random

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

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

ฉันอยากรู้ว่าจะเกิดอะไรขึ้นถ้ามีคนสร้างตัวเลขสุ่มระหว่าง 0 ถึง 1 และเก็บตัวเลขสุดท้ายไว้ ฉันไม่ได้คาดหวังว่าการแจกจ่ายจะเหมือนกัน แต่ฉันพบว่าผลลัพธ์ค่อนข้างน่าประหลาดใจ

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)

เอาท์พุท:

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

ฮิสโตแกรมแสดงไว้ด้านล่าง โปรดทราบว่า 0 ไม่ปรากฏเนื่องจากศูนย์ต่อท้ายถูกตัดทอน แต่ทุกคนสามารถอธิบายได้ไหมว่าทำไมตัวเลข 4, 5 และ 6 ถึงเป็นสามัญมากกว่าที่เหลือ? ฉันใช้ Python 3.6.10 แต่ผลลัพธ์คล้ายกันใน Python 3.8.0a4

การกระจายของตัวเลขสุดท้ายของการสุ่มลอยตัว


4
สิ่งนี้เกี่ยวข้องกับการคำนวณสตริงของการลอยตัวใน Python ดูdocs.python.org/3/tutorial/floatingpoint.html คุณจะได้ผลลัพธ์ที่ดียิ่งขึ้นหากคุณใช้หลักสิบตามหลักสิบ (แรกหลังจุดทศนิยม) แทนที่จะเป็นหลักสุดท้าย
Dennis

1
เราเก็บลอยในรูปแบบไบนารี่แทน (เนื่องจากหน่วยความจำของเราเป็นแบบไบนารี่ด้วย) strแปลงเป็นฐาน 10 ซึ่งถูกผูกไว้เพื่อทำให้เกิดปัญหา เช่น 1 บิตลอย mantissa และb0 -> 1.0 b1 -> 1.5"การหลักสุดท้าย" จะเป็นหรือ0 5
Mateen Ulhaq

1
random.randrange(10)ชัดเจนยิ่งขึ้น IMHO random.randint(ซึ่งเรียกrandom.randrangeภายใต้ประทุน) เป็นส่วนเพิ่มเติมในภายหลังrandomสำหรับผู้ที่ไม่เข้าใจวิธีการทำงานของช่วงใน Python ;)
PM 2Ring

2
@ PM2Ring: randrangeจริง ๆ แล้วมาสองหลังจากพวกเขาตัดสินใจว่าrandintอินเทอร์เฟซเป็นข้อผิดพลาด
user2357112 รองรับโมนิก้า

@ user2357112 ผู้สนับสนุนโมนิก้าโอ้โห ฉันยืนแก้ไขแล้ว ฉันแน่ใจว่า randrange เป็นที่ 1 แต่ความจำของฉันไม่ดีเท่าที่เคยเป็นมา ;)
PM 2Ring

คำตอบ:


21

นั่นไม่ใช่ "หลักสุดท้าย" ของตัวเลข นั่นคือตัวเลขหลักสุดท้ายของสตริงที่strให้คุณเมื่อผ่านหมายเลข

เมื่อคุณโทรหาstrทุ่นหลาม Python จะให้ตัวเลขที่เพียงพอแก่การเรียกใช้floatบนสตริงจะให้ทริปดั้งเดิมกับคุณ สำหรับจุดประสงค์นี้การต่อท้าย 1 หรือ 9 มีความจำเป็นน้อยกว่าตัวเลขอื่น ๆ เนื่องจากการลาก 1 หรือ 9 หมายถึงตัวเลขนั้นใกล้เคียงกับค่าที่คุณจะได้รับจากการปัดเศษตัวเลขนั้น มีโอกาสที่ดีที่ไม่มีสิ่งใดลอยอยู่ใกล้และถ้าเป็นเช่นนั้นตัวเลขนั้นสามารถถูกทิ้งได้โดยไม่ต้องเสียสละfloat(str(original_float))พฤติกรรม

หากstrคุณให้ตัวเลขเพียงพอที่จะเป็นตัวแทนของการโต้แย้งหลักสุดท้ายเกือบจะเป็น 5 เสมอยกเว้นเมื่อrandom.random()ส่งคืน 0.0 ซึ่งในกรณีนี้ตัวเลขหลักสุดท้ายจะเป็น 0 (ลอยสามารถเป็นตัวแทนปันส่วน dyadicและหลักทศนิยมที่ไม่ใช่ศูนย์ของ เหตุผล dyadic ที่ไม่ใช่จำนวนเต็มอยู่เสมอ 5. ) ผลลัพธ์จะยาวมากเช่นกัน

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

ซึ่งเป็นหนึ่งในเหตุผลที่strไม่ทำเช่นนั้น

หากstrให้ตัวเลขที่สำคัญกับคุณถึง 17 หลัก (เพียงพอที่จะแยกความแตกต่างของค่าทศนิยมทั้งหมดจากกัน แต่บางครั้งก็มีจำนวนมากกว่าจำนวนที่จำเป็น) จากนั้นเอฟเฟกต์ที่คุณเห็นจะหายไป จะมีการแจกแจงหลักที่ใกล้เคียงกันเกือบทั้งหมด (รวมถึง 0)

(นอกจากนี้คุณลืมว่าstrบางครั้งส่งคืนสตริงในสัญกรณ์ทางวิทยาศาสตร์ แต่นั่นเป็นผลกระทบเล็กน้อยเนื่องจากมีความน่าจะเป็นต่ำที่จะได้รับทุ่นที่จะเกิดขึ้นจากrandom.random())


5

TL; DRตัวอย่างของคุณไม่ได้ดูที่ตัวเลขสุดท้าย หลักสุดท้ายของ mantissa ไบนารีตัวแทน จำกัด แปลงฐาน 10 ควรจะเป็นหรือ05


ดูที่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);

    // ...
}

และตอนนี้ที่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ยืนยันสิ่งนี้:

ความแม่นยำ 53 บิตให้ความแม่นยำทศนิยม 15 ถึง 17 หลัก (2 -53 ≈ 1.11 × 10 -16 ) หากสตริงทศนิยมที่มีตัวเลขนัยสำคัญไม่เกิน 15 หลักจะถูกแปลงเป็นการแทนค่าความแม่นยำสองเท่าของ IEEE 754 จากนั้นแปลงกลับเป็นสตริงทศนิยมที่มีจำนวนหลักเท่ากันผลลัพธ์สุดท้ายควรตรงกับสตริงต้นฉบับ หากหมายเลขที่มีความแม่นยำสองเท่าของ IEEE 754 ถูกแปลงเป็นสตริงทศนิยมที่มีตัวเลขนัยสำคัญอย่างน้อย 17 หลักจากนั้นแปลงกลับเป็นตัวแทนที่มีความแม่นยำสองเท่าผลลัพธ์สุดท้ายต้องตรงกับหมายเลขดั้งเดิม

ดังนั้นเมื่อเราใช้str(หรือrepr) เราจะแสดงเพียง 17 หลักสำคัญในฐาน -10 ซึ่งหมายความว่าจำนวนจุดลอยตัวบางส่วนจะถูกตัดทอน ในความเป็นจริงเพื่อให้ได้การแทนที่แน่นอนคุณต้องมีความแม่นยำถึง 53 หลักสำคัญ! คุณสามารถตรวจสอบได้ดังนี้

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

ตอนนี้ใช้ความแม่นยำสูงสุดต่อไปนี้เป็นวิธีที่เหมาะสมในการค้นหา "หลักสุดท้าย":

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

หมายเหตุ:ตามที่ผู้ใช้ระบุไว้ 2,357112 การใช้งานที่ถูกต้องเพื่อดูเป็นPyOS_double_to_stringและformat_float_shortแต่ฉันจะออกจากคนปัจจุบันในเพราะพวกเขาน่าสนใจมากขึ้นในการสอน


"ดังนั้นเมื่อเราใช้ str (หรือ repr) เราจะเป็นเพียง 17 หลักสำคัญในฐาน -10" - 17 คือสูงสุด หากเป็นจริง 17 หลักคงที่ผลกระทบของคำถามจะไม่ปรากฏขึ้น ผลกระทบในคำถามที่มาจากเพียงแค่พอหลักต่อการเดินทางรอบปัดเศษstr(some_float)ใช้
user2357112 รองรับ Monica

1
PyOS_double_to_stringคุณกำลังมองหาที่การดำเนินงานที่ไม่ถูกต้องของ การใช้งานนั้นได้รับการประมวลผลล่วงหน้าสำหรับผู้ใช้รายนี้
2,235,712 สนับสนุนโมนิก้า

เกี่ยวกับความคิดเห็นแรก: ตามที่กล่าวถึงการแสดงที่แน่นอนของจำนวนจุดลอยตัว (แก้ไข: ด้วยเลขชี้กำลังเป็น 0) ต้องการ 53 หลักสำคัญแม้ว่า 17 จะเพียงพอที่จะรับประกันfloat(str(x)) == xได้ ส่วนใหญ่คำตอบนี้เป็นเพียงการแสดงสมมติฐาน ("ตัวเลขสุดท้ายของการเป็นตัวแทนที่แน่นอน") ที่ทำในคำถามนั้นผิดเนื่องจากผลลัพธ์ที่ถูกต้องเป็นเพียงแค่5s (และไม่น่าเป็นไปได้0)
Mateen Ulhaq


@ user2357112supportsMonica ขออภัยฉันหมายถึงเลขยกกำลังเป็น 0 (ซึ่งจำเป็นต่อการรับประกันความสม่ำเสมอภายในช่วงเวลา [0, 1].)
Mateen Ulhaq
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.