Равномерное распределение и его преобразования
Базовый кирпичик всей случайности в программировании — равномерное число из полуинтервала [0, 1). Из этого скромного «сырья» строят броски кубиков, перемешивание колоды, выбор случайного элемента и даже выборки из сложнейших распределений. Понять равномерность — значит получить ключ ко всему остальному.
Равномерное распределение на [0, 1) — распределение, при котором любое число из полуинтервала имеет одинаковую «плотность» появления: ни одна область не предпочтительнее другой, и в среднем точки ложатся ровным ковром по всему отрезку.
В Python источник равномерности — функция random.random(). Она возвращает дробное число u такое, что 0 ≤ u < 1. «Равномерное» означает, что вероятность попасть в любой подотрезок одинаковой длины одна и та же: в [0.0, 0.1) точка окажется так же часто, как в [0.5, 0.6). Никаких предпочтений, никаких сгущений — именно эта простота делает равномерность идеальным фундаментом.
Почему именно равномерность — фундамент
Может показаться странным, что всё начинается с самого «скучного» распределения. Но в этом и сила. Равномерное число — это чистая, ничем не искажённая случайная «единица». Любое другое распределение — нормальное, экспоненциальное, биномиальное — можно получить, если правильно преобразовать равномерные числа. Генератор умеет выдавать только равномерность; всё остальное — это математические надстройки над ней. Поэтому, освоив преобразования равномерного распределения, вы получаете универсальный инструмент для моделирования чего угодно.
Масштабирование на произвольный отрезок [a, b]
Первое и самое частое преобразование — растянуть [0, 1) на нужный отрезок [a, b]. Формула предельно проста: x = a + (b − a) * 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) → индекс 0, [1/3, 2/3) → индекс 1, [2/3, 1) → индекс 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…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)*u:(b − a)растягивает,aсдвигает. random.choiceравновероятно выбирает элемент списка, нарезая [0, 1) на равные доли;random.randint(a, b)даёт целое из диапазона включая обе границы.- Все эти функции — арифметические обёртки над единственным примитивом-равномерностью.
- Равномерность проявляется лишь в среднем: ровные частоты видны на больших выборках, а не на десятках значений.