Розподіл кінцевих цифр випадкових чисел у 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 . Ви отримаєте набагато більш рівномірні результати, якщо б використовували десяту цифру (першу після десяткової), а не останню цифру.
Денніс

1
Ми зберігаємо поплавці у двійковому поданні (оскільки наша пам'ять також є бінарною). strперетворює його в базу-10, що може спричинити проблеми. наприклад, 1-бітна поплавкова мантія b0 -> 1.0і b1 -> 1.5. "Остання цифра" завжди буде 0або 5.
Mateen Ulhaq

1
random.randrange(10)ще очевидніше, ІМХО. random.randint(який дзвінки random.randrangeпід капотом) було пізнішим доповненням до randomмодуля для людей, які не розуміють, як працюють діапазони в Python. ;)
PM 2Ring

2
@ PM2Ring: randrangeнасправді прийшов другий, після того як вони вирішили, що randintінтерфейс був помилкою.
user2357112 підтримує Моніку

@ user2357112підтримкаMonica О, добре. Я стою виправлений. Я був впевнений, що ранжинг був 1-м, але моя пам’ять не така хороша, як раніше. ;)
PM 2Ring

Відповіді:


21

Це не "остання цифра" числа. Ось остання цифра рядка strдала вам, коли передали число.

Коли ви зателефонуєте strна флоат, Python дає вам достатньо цифр, що виклик floatрядка дасть вам оригінальний поплавок. З цією метою менша ймовірність знаходження контуру 1 або 9, ніж інші цифри, тому що пробіл 1 або 9 означає, що число дуже близьке до значення, яке ви отримаєте, округлюючи цю цифру. Є хороший шанс, що інші плавці не знаходяться ближче, і якщо так, ця цифра може бути відкинута без шкоди для float(str(original_float))поведінки.

Якщо б strвам дали достатньо цифр, щоб точно відобразити аргумент, остання цифра майже завжди була б 5, за винятком випадків, коли random.random()повертає 0,0, і в цьому випадку остання цифра буде дорівнює 0. (Поплавці можуть представляти лише діадичні раціональні показники , а остання ненульова цифра - десяткову цифру неціле діадічне раціональне завжди 5.) Виходи також були б надзвичайно довгими, схожими

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

що є однією з причин strцього не робити.

Якщо б strви дали рівно 17 значущих цифр (достатньо, щоб відрізнити всі знаки плавучих значень один від одного, але іноді більше цифр, ніж потрібно), ефект, який ви бачите, зникне. Існує майже рівномірний розподіл проміжних цифр (включаючи 0).

(Крім того, ви забули, що strіноді повертає рядок у науковій нотації, але це незначний ефект, тому що низька ймовірність отримати поплавок, де це станеться random.random().)


5

TL; DR Ваш приклад насправді не дивиться на останню цифру. Остання цифра кінцевої бінарної мантіси, перетвореної на базу-10, завжди повинна бути 0або 5.


Погляньте на 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;
    // ...
}

Вікіпедія підтверджує це:

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

Примітка: Як зазначив user2357112, правильні реалізації , щоб подивитися на це PyOS_double_to_stringі format_float_short, але я залишу поточні з них, тому що вони більш педагогічно цікаво.


"Таким чином, коли ми використовуємо str (або repr), ми представляємо лише 17 значущих цифр у base-10." - 17 - це максимум. Якби це були фіксовані 17 цифр, ефект у питанні не з’явився б. Ефект у питанні виникає завдяки str(some_float)використанню для округлення достатньо лише цифр, які використовуються для заокруглення .
user2357112 підтримує Моніку

1
Ви дивитесь на неправильну реалізацію PyOS_double_to_string. Ця реалізація попередньо оброблена на користь цього
користувач2357112 підтримує Моніку

Щодо першого коментаря: Як уже згадувалося, точне подання числа з плаваючою комою (EDIT: з коефіцієнтом 0) вимагає 53 значущих цифр, хоча 17 достатньо, щоб гарантувати float(str(x)) == x. Здебільшого ця відповідь полягала лише у тому, щоб показати припущення ("остання цифра точного подання"), зроблене у запитанні, було помилковим, оскільки правильний результат - це просто 5s (і малоймовірно 0).
Mateen Ulhaq


@ user2357112supportsMonica Вибачте, я мав на увазі показник 0. (Що необхідно для гарантування рівномірності в інтервалі [0, 1].)
Mateen Ulhaq,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.