Если ты умеешь генерировать случайные числа в numpy — ты умеешь делать две вещи, за которые на собеседовании дают плюс: разбивать пользователей на группы A/B честно и проверять метрику симуляцией, не дожидаясь реальных данных. Этот урок про модуль np.random: как зафиксировать seed, чтобы результат повторялся, как раскидать юзеров по группам с нужными весами и как сгенерировать «фейковую выручку», которая ведёт себя как настоящая.
Случайность для аналитика — это не казино. Это инструмент. Рандомизация в A/B-тесте убирает скрытые перекосы между группами. Симуляция позволяет ответить на вопрос «а что если?» до того, как ты потратишь две недели на сбор реальных цифр. И почти всегда тебе нужно, чтобы случайность была воспроизводимой: коллега запустил твой код — получил ровно те же числа. За это отвечает np.random.seed.
Мы идём дальше в серии numpy с нуля. В прошлых частях были массивы, индексация, агрегации и broadcasting. Сегодня добавляем последний кирпич базового набора — генерацию случайных данных. После этого урока ты сможешь написать мини-симуляцию A/B-теста в несколько строк, и это уже уровень, на котором задают вопросы Senior-кандидатам.
np.random.seed: почему случайность должна повторяться
Компьютер не умеет в настоящую случайность. Он генерирует псевдослучайные числа по формуле, которая стартует с некоторого начального значения — его называют seed (зерно). Зафиксируешь seed — получишь одну и ту же «случайную» последовательность каждый раз.
Зачем это аналитику? Чтобы результат был воспроизводимым. Ты показываешь коллеге симуляцию A/B-теста, он запускает твой ноутбук и видит ровно те же числа. Без фиксации seed каждый запуск дал бы новые значения, и проверить тебя было бы невозможно.
import numpy as np
np.random.seed(0)
print(np.random.rand(3))
# [0.5488135 0.71518937 0.60276338]
np.random.seed(0) # тот же seed — та же последовательность
print(np.random.rand(3))
# [0.5488135 0.71518937 0.60276338]
Обрати внимание: первый и второй вывод одинаковые, потому что перед каждым мы сбросили seed на 0. Если бы вызвали np.random.rand(3) второй раз без сброса — получили бы следующие три числа из той же последовательности, а не повтор.
Правило простое: ставь np.random.seed(КАКОЕ-ТО ЧИСЛО) в начале любого кода, где есть случайность и тебе важна воспроизводимость. Число — любое, обычно берут 0, 42, 7. Само значение seed на «качество» случайности не влияет, это просто точка старта.
rand, randn, randint: три способа получить случайные числа
Это три рабочие лошадки. Запомни разницу между ними — на этом строится почти всё.
np.random.rand(...) — равномерное распределение на отрезке [0, 1). Каждое число от 0 до 1, все одинаково вероятны. Удобно для долей, вероятностей, флагов «попал/не попал».
np.random.seed(0)
print(np.random.rand(2, 3))
# [[0.5488135 0.71518937 0.60276338]
# [0.54488318 0.4236548 0.64589411]]
Аргументы rand — это форма массива. rand(3) даст вектор из 3 чисел, rand(2, 3) — матрицу 2 на 3.
np.random.randn(...) — нормальное (гауссово) распределение со средним 0 и стандартным отклонением 1. Числа группируются вокруг нуля, бывают и отрицательные. Это основа для симуляции «естественных» величин: рост, время на сайте, отклонение метрики.
np.random.seed(1)
print(np.random.randn(4))
# [ 1.62434536 -0.61175641 -0.52817175 -1.07296862]
np.random.randint(low, high, size=...) — случайные целые числа от low (включительно) до high (НЕ включительно). Идеально для симуляции счётчиков: число заказов, кликов, кубик.
np.random.seed(10)
print(np.random.randint(1, 7, size=5)) # бросок кубика 5 раз
# [2 6 5 1 2]
Запомни границу: randint(1, 7) даёт числа 1, 2, 3, 4, 5, 6 — семёрка не входит. Это частая ловушка.
Попробуй сам
Подсказка к решению — раскрыть только если застрял
np.concatenate([conv_a, conv_b]) склеивает массивы. np.random.permutation(arr) возвращает перемешанную копию. Для выбора без возврата: np.random.choice(conv_b, size=3, replace=False). Не забудь сбросить seed перед каждым блоком.np.random.choice: выбор из списка, веса и возврат
np.random.choice — самая полезная функция для аналитика, потому что именно ей раскидывают пользователей по группам A/B. Она выбирает случайные элементы из массива.
Простой случай — выбрать size элементов (с возможным повторением):
np.random.seed(3)
print(np.random.choice([10, 20, 30, 40], size=3))
# [30 10 20]
По умолчанию выбор идёт С ВОЗВРАТОМ (replace=True): один и тот же элемент может выпасть несколько раз. Это правильно для бросков кубика или симуляции независимых событий.
Если нужно выбрать БЕЗ возврата (каждый элемент максимум один раз) — ставь replace=False. Так делают выборку респондентов или случайную подвыборку строк:
np.random.seed(3)
print(np.random.choice([10, 20, 30, 40], size=3, replace=False))
# [40 20 10]
Самое важное для A/B — параметр p, веса (вероятности). Им задают, какая доля юзеров пойдёт в какую группу. Сумма весов должна быть равна 1:
np.random.seed(5)
# 80% в группу A (контроль), 20% в группу B (новая фича)
print(np.random.choice(['A', 'B'], size=10, p=[0.8, 0.2]))
# ['A' 'B' 'A' 'B' 'A' 'A' 'A' 'A' 'A' 'A']
Из 10 пользователей в B попали двое — примерно 20%, как и просили. На больших числах (10000+ юзеров) доля будет почти ровно 0.2. Так делают неравномерный сплит: например, выкатывают новую фичу сначала на 10% аудитории, чтобы снизить риск.
permutation и shuffle: честное перемешивание для рандомизации
В A/B-тесте критично, чтобы разбиение на группы было случайным, без скрытого порядка. Если ты возьмёшь первые 50% пользователей в контроль, а вторые 50% в тест — может оказаться, что в начале списка сидят старые лояльные юзеры, а в конце новички. Группы получатся неравными ещё ДО эксперимента. Решение — перемешать.
np.random.shuffle(arr) перемешивает массив НА МЕСТЕ. Он меняет сам arr и ничего не возвращает (вернёт None):
np.random.seed(2)
a = np.array([1, 2, 3, 4, 5])
np.random.shuffle(a)
print(a)
# [3 5 2 4 1]
np.random.permutation(arr) делает то же самое, но НЕ трогает оригинал — возвращает новый перемешанный массив. Исходный остаётся как был. Это безопаснее, и чаще используют именно его:
np.random.seed(2)
print(np.random.permutation([1, 2, 3, 4, 5]))
# [3 5 2 4 1]
Заметь: при одинаковом seed shuffle и permutation дают одинаковый порядок — это один и тот же алгоритм.
Типичный приём рандомизации: перемешать индексы пользователей и разрезать пополам.
np.random.seed(0)
user_ids = np.arange(1000) # id от 0 до 999
shuffled = np.random.permutation(user_ids)
group_a = shuffled[:500] # первая половина — контроль
group_b = shuffled[500:] # вторая — тест
print(len(group_a), len(group_b))
# 500 500
Теперь обе группы — случайная смесь, без перекоса по порядку регистрации.
Попробуй сам
Подсказка к решению — раскрыть только если застрял
np.random.randint(0, 10, size=10) даёт целые 0..9 (десятка не входит). orders >= 5 даёт массив True/False, np.sum по нему считает True. Для сплита: np.random.choice(['A','B'], size=8, p=[0.7, 0.3]).Нормальная выборка: симулируем метрику до сбора данных
Главный навык урока. np.random.normal(loc, scale, size) генерирует числа из нормального распределения, где loc — среднее, scale — стандартное отклонение (разброс), size — сколько чисел. В отличие от randn, тут ты сам задаёшь среднее и разброс под свою метрику.
Допустим, ты знаешь, что средний чек в магазине около 1500 рублей с разбросом примерно 300. Можно сгенерировать 10000 «чеков» и работать с ними как с реальными:
np.random.seed(0)
checks = np.random.normal(loc=1500, scale=300, size=5)
print(np.round(checks, 2))
# [2029.22 1620.05 1793.62 2172.27 2060.27]
Построив большую выборку, можно проверять гипотезы. Например — какая доля чеков превысит 1800 рублей? Это вопрос «сколько юзеров попадут в премиум-сегмент», и симуляция отвечает на него мгновенно:
np.random.seed(0)
checks = np.random.normal(1500, 300, size=100000)
share = np.mean(checks > 1800) # доля чеков выше 1800
print(round(float(share), 3))
# 0.158
Около 16% — это и есть ответ симуляции. Так же симулируют A/B: генерируешь две выборки метрики с разным средним (контроль 1500, тест 1560), считаешь разницу средних и смотришь, не случайна ли она. Это фундамент power-анализа и оценки sample size — тем, которые отделяют Middle от Senior на собеседовании.
Ключевая идея: симуляция = генерируем правдоподобные данные → считаем нужную метрику → повторяем много раз. numpy делает это за миллисекунды на миллионах строк, тогда как обычный Python-цикл захлебнулся бы.
Сэмплирование: выборка приближает истину
ИСТИННОЕ РАСПРЕДЕЛЕНИЕ ВЫБОРКА (size=N)
средний чек ~1500, разброс 300 np.random.normal(1500,300,N)
* * ┌───────────────────────┐
* * берём N │ 2029 1620 1793 ... │
* * ───────────────► │ 2172 2060 1488 ... │
* * │ ... ... ... │
* * └───────────┬───────────┘
───┴──────┬───────┴─── │ .mean()
1200 1500 1800 ▼
▲ ┌──────────┐
│ истинное μ = 1500 │ ~1499 │
└──────────── приближается ◄────── │ оценка μ │
чем больше N └──────────┘
Из распределения с истинным средним μ берём случайную выборку N значений (np.random.normal). Среднее выборки .mean() приближает μ — и чем больше N, тем точнее. Так аналитик оценивает метрику симуляцией до сбора реальных данных.
Чек-лист и частые ошибки
Короткая шпаргалка, чтобы не путаться.
np.random.seed(42) # фиксируем воспроизводимость
np.random.rand(3) # равномерно [0, 1)
np.random.randn(3) # нормально, среднее 0, разброс 1
np.random.randint(0, 10, 3) # целые от 0 до 9
np.random.choice(arr, size=5) # выбор с возвратом
np.random.choice(arr, 5, replace=False) # без возврата
np.random.choice(['A','B'], 100, p=[0.7, 0.3]) # с весами
np.random.permutation(arr) # новый перемешанный (оригинал цел)
np.random.shuffle(arr) # на месте (меняет arr, вернёт None)
np.random.normal(1500, 300, 1000) # своя метрика
Частые ошибки новичка:
- Забыл seed — каждый запуск даёт новые числа, проверить результат невозможно. Ставь seed в начале.
randint(1, 7)ждёшь семёрку — верхняя граница НЕ включается. Это диапазон 1..6.shuffleприсвоил результат —a = np.random.shuffle(a)сломает всё: shuffle возвращаетNone, и вaзапишетсяNone. Просто вызывайnp.random.shuffle(a)без присваивания, либо бериpermutation.- Веса
pне дают в сумме 1 — будет ошибка. Проверь, чтоsum(p) == 1. replace=False, аsizeбольше длины массива — без возврата нельзя выбрать больше элементов, чем есть. Ошибка.
И помни про назначение: rand/normal — для непрерывных величин (доли, выручка, время), randint — для счётчиков, choice — для выбора из категорий и сплита на группы, permutation/shuffle — для честной рандомизации перед разбиением.
Попробуй сам
Подсказка к решению — раскрыть только если застрял
np.random.normal(loc=1500, scale=300, size=10000). Среднее: checks.mean(). Доля выше порога: np.mean(checks > 1800) даёт долю от 0 до 1, умножь на 100 для процентов. round(x, 1) округляет до одного знака.Главное
- np.random.seed(N) фиксирует случайность: тот же seed — та же последовательность, результат воспроизводим у тебя и у коллеги
- rand даёт равномерные [0,1), randn — нормальные вокруг 0, randint — целые (верхняя граница НЕ включается)
- np.random.choice раскидывает по группам: replace=False для выборки без повторов, p=[...] для неравномерного A/B-сплита
- permutation возвращает перемешанную копию (оригинал цел), shuffle перемешивает на месте и возвращает None — не присваивай его
- np.random.normal(среднее, разброс, size) генерирует правдоподобную метрику для симуляции до сбора реальных данных — основа power-анализа
Частые вопросы
Зачем нужен np.random.seed и почему без него результаты разные?
Генератор случайных чисел на самом деле псевдослучайный: он идёт по детерминированной последовательности от стартовой точки. seed фиксирует эту точку, поэтому при каждом запуске получаются одни и те же числа. Это нужно, чтобы симуляция и A/B-разбивка были воспроизводимыми — коллега запустит код и увидит ровно ваши цифры. Без сида последовательность стартует с системного состояния и меняется от запуска к запуску.
Чем rand отличается от randn и randint?
rand даёт равномерное распределение на полуинтервале от 0 до 1, randn — стандартное нормальное (среднее 0, стандартное отклонение 1, есть отрицательные значения), а randint — целые числа в заданном диапазоне. Для нормальной выборки с нужными параметрами берут randn и масштабируют, либо сразу np.random.normal(loc, scale, size).
Как случайно разбить пользователей на группы A и B?
Три рабочих способа: np.random.choice(['A','B'], size=n) для присвоения метки каждому, np.random.shuffle или np.random.permutation чтобы перемешать индексы и разрезать пополам. Перед разбивкой поставьте seed, чтобы сплит был воспроизводимым. Логику самих A/B-метрик потом удобно закрепить на задачах из банка вопросов.
Что такое симуляция Монте-Карло для A/B-теста?
Это когда вы много тысяч раз прогоняете виртуальный эксперимент со случайными данными и смотрите на распределение исхода. Например, генерируете конверсии при известном эффекте, тысячу раз считаете, «поймал» ли тест значимость, и так оцениваете мощность. Симуляции полезны, когда аналитическая формула сложна или её нет — численный ответ всегда можно получить перебором.
Стоит ли использовать старый np.random или новый default_rng?
В современном коде рекомендуют создавать генератор через rng = np.random.default_rng(seed) и звать методы от него: rng.random(), rng.normal(), rng.choice(). Это изолированный поток случайности, он не конфликтует с чужим кодом, который тоже дёргает глобальный np.random. Старый интерфейс (np.random.seed + np.random.rand) до сих пор работает и его полно в туториалах, но для новых проектов лучше default_rng.