Равномерное распределение и его преобразования
Базовый кирпичик всей случайности в программировании — равномерное число из полуинтервала $[0, 1)$. Из этого скромного «сырья» строят броски кубиков, перемешивание колоды, выбор случайного элемента и даже выборки из сложнейших распределений. Понять равномерность — значит получить ключ ко всему остальному.
Равномерное распределение на $[0, 1)$ — распределение, при котором любое число из полуинтервала имеет одинаковую «плотность» появления: ни одна область не предпочтительнее другой, и в среднем точки ложатся ровным ковром по всему отрезку.
В Python источник равномерности — функция random.random(). Она возвращает дробное число u такое, что $0 \le u \lt 1$. «Равномерное» означает, что вероятность попасть в любой подотрезок одинаковой длины одна и та же: в $[0.0, 0.1)$ точка окажется так же часто, как в $[0.5, 0.6)$. Никаких предпочтений, никаких сгущений — именно эта простота делает равномерность идеальным фундаментом.
Почему именно равномерность — фундамент
Может показаться странным, что всё начинается с самого «скучного» распределения. Но в этом и сила. Равномерное число — это чистая, ничем не искажённая случайная «единица». Любое другое распределение — нормальное, экспоненциальное, биномиальное — можно получить, если правильно преобразовать равномерные числа. Генератор умеет выдавать только равномерность; всё остальное — это математические надстройки над ней. Поэтому, освоив преобразования равномерного распределения, вы получаете универсальный инструмент для моделирования чего угодно.
Масштабирование на произвольный отрезок $[a, b]$
Первое и самое частое преобразование — растянуть $[0, 1)$ на нужный отрезок $[a, b]$. Формула предельно проста: $x = a + (b - a) \cdot u$, где $u$ — равномерное число из $[0, 1)$.
Идея геометрическая. Множитель $(b - a)$ растягивает единичный отрезок до нужной ширины, а слагаемое $a$ сдвигает его в нужное место на числовой оси. Проверим граничные случаи: при $u = 0$ получаем $x = a$ (левый край), при u, близком к 1, значение подходит к b, но не достигает его — ровно как и сам полуинтервал $[0, 1)$. Несколько конкретных значений для отрезка $[10, 20]$ (где $a = 10$, $b - a = 10$):
u x = 10 + 10*u ----- ------------- 0.00 10.0 0.25 12.5 0.50 15.0 0.75 17.5 0.90 19.0
Видно, что середине единичного отрезка соответствует середина целевого, а равные шаги по u дают равные шаги по x. Это и есть линейное масштабирование: форма распределения (равномерность) сохраняется, меняются только границы. Кстати, готовая функция random.uniform(a, b) внутри делает ровно это же.
Дискретная случайность: выбор и кубик
Не всегда нужны дробные числа. Часто требуется выбрать один вариант из списка или смоделировать бросок игральной кости. Для этого тоже хватает равномерности — её достаточно «нарезать» на равные кусочки.
Случайный выбор: random.choice
Функция random.choice(seq) возвращает случайный элемент последовательности, причём каждый элемент равновероятен. Под капотом она берёт равномерное число, умножает на длину списка и округляет до индекса — то есть превращает непрерывную равномерность в дискретную. Если в списке три элемента, отрезок $[0, 1)$ делится на три равные трети: $[0, 1/3) \to$ индекс 0, $[1/3, 2/3) \to$ индекс 1, $[2/3, 1) \to$ индекс 2. Каждая треть равна по длине, значит, каждый вариант выпадает с одинаковой вероятностью.
Бросок кубика: randint
Для целых чисел из заданного диапазона есть random.randint(a, b) — она возвращает целое от a до b включительно (в отличие от многих функций Python, верхняя граница тоже входит). Идеально для кубика: random.randint(1, 6) моделирует честную шестигранную кость. Проверим это на большом числе бросков:
import random
random.seed(7)
rolls = [random.randint(1, 6) for _ in range(6000)]
print("Бросков:", len(rolls))
print("Все значения в диапазоне 1..6:", min(rolls) >= 1 and max(rolls) <= 6)
print("Сколько разных граней выпало:", len(set(rolls)))
Вывод:
Бросков: 6000 Все значения в диапазоне 1..6: True Сколько разных граней выпало: 6
Шесть тысяч бросков, и все они честно укладываются в диапазон от 1 до 6, а в выборке встретились все шесть граней — ровно то, чего ждёшь от настоящего кубика. Мы не проверяем здесь точные частоты каждой грани (они слегка «гуляют» от прогона к прогону из-за случайности), но структурные свойства — диапазон и полнота набора граней — гарантированы и устойчивы. На большой выборке частоты, конечно, выровняются примерно по 1000 на грань — это прямое следствие равномерности.
Как работает под капотом
Всё богатство дискретной и масштабированной случайности вырастает из единственного примитива — random.random(), дающего равномерное u из $[0, 1)$. Дальше идут чистые арифметические преобразования:
- Отрезок $[a, b]$: линейное преобразование
a + (b − a)*u— сдвиг и растяжение, сохраняющие равномерность. - Индекс списка:
int(u * n)отображает $[0, 1)$ на целые $0\ldots n-1$, нарезая отрезок наnравных долей. - Целое из $[a, b]$:
a + int(u * (b − a + 1))— то же нарезание, но со сдвигом наaи шириной $b - a + 1$ (поэтомуbвходит включительно).
Ключевая мысль: генератор «умеет» лишь одно — равномерность. Все функции вроде uniform, choice, randint — это тонкие обёртки, которые той или иной арифметикой превращают равномерное число в нужную форму. Именно поэтому равномерное распределение называют фундаментом: на нём, как на бетонной плите, стоит всё здание случайности.
Частые ошибки
- Забывать, что random.random() не включает 1. Полуинтервал $[0, 1)$ означает, что ровно 1.0 не выпадет никогда. При масштабировании
a + (b − a)*uверхняя границаbтоже недостижима — это иногда важно для граничных проверок. - Считать, что randint(a, b) не включает b. В отличие от
rangeи срезов,randintвключает обе границы:randint(1, 6)может вернуть и 1, и 6. Путаница приводит к «потерянной грани» кубика. - Ждать ровных частот на маленькой выборке. Равномерность проявляется лишь «в среднем». На 60 бросках кубика грани могут разойтись очень заметно; ровный расклад по 1000 виден только на тысячах бросков. Это нормальное свойство случайности, а не ошибка.
- Делать собственный выбор через int(u*n) и забывать про крайний случай. Если по недосмотру использовать
roundвместоintили неверную длину, крайние элементы получат искажённую вероятность. Безопаснее звать готовыйrandom.choice. - Путать «равномерное» с «нормальным». Равномерное распределение — это ровный ковёр без пиков; у нормального («колокол») центр гораздо вероятнее краёв. Это совершенно разные формы, и масштабирование одного не превращает его в другое.
Итоги
random.random()даёт равномерное числоuиз полуинтервала $[0, 1)$ — базовый источник случайности, на котором держится всё остальное.- Перенос на отрезок $[a, b]$ — линейное преобразование $x = a + (b - a) \cdot u$: $(b - a)$ растягивает, $a$ сдвигает.
random.choiceравновероятно выбирает элемент списка, нарезая $[0, 1)$ на равные доли;random.randint(a, b)даёт целое из диапазона включая обе границы.- Все эти функции — арифметические обёртки над единственным примитивом-равномерностью.
- Равномерность проявляется лишь в среднем: ровные частоты видны на больших выборках, а не на десятках значений.