Что такое stratified sampling и когда его использовать?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Stratified Sampling: определение и применение в аналитике
Stratified sampling (стратифицированная выборка) — это техника случайной выборки, при которой генеральная совокупность делится на подгруппы (страты), и затем из каждой страты случайно выбирается пропорциональное или оптимальное количество элементов. Главная цель: уменьшить дисперсию оценок и обеспечить репрезентативность всех важных групп в выборке.
Простой пример
Есть 10000 пользователей:
- 8000 из США (80%)
- 1500 из Европы (15%)
- 500 из Азии (5%)
Обычная случайная выборка (простая): n=500
Ожидаем: 400 USA, 75 Europe, 25 Asia
Но может выпасть: 420 USA, 60 Europe, 20 Asia (смещение!)
Stratified sampling: выбираем из каждой страты пропорционально
n=500 → 400 USA, 75 Europe, 25 Asia (ГАРАНТИРОВАНО)
Типы stratified sampling
1. Proportional Allocation (пропорциональное распределение)
Размер страты в выборке совпадает с её долей в генеральной совокупности.
from pandas import DataFrame
# Генеральная совокупность
data = DataFrame({
'user_id': range(10000),
'country': ['USA']*8000 + ['Europe']*1500 + ['Asia']*500
})
# Простое stratified sampling: пропорциональное
strata_sizes = data['country'].value_counts()
proportions = strata_sizes / len(data)
sample_size = 500
strata_sample_sizes = (proportions * sample_size).astype(int)
# Result: USA=400, Europe=75, Asia=25
samples = []
for country in data['country'].unique():
country_data = data[data['country'] == country]
n = strata_sample_sizes[country]
samples.append(country_data.sample(n=n, random_state=42))
stratified_sample = pd.concat(samples)
print(stratified_sample['country'].value_counts())
# USA: 400, Europe: 75, Asia: 25
Когда использовать: когда хотим сохранить репрезентативность всех групп в исходных пропорциях.
2. Optimal Allocation (оптимальное распределение)
Размер страты зависит от её дисперсии. Страты с высокой дисперсией получают больше элементов.
# Если в США конверсия очень волатильна (дисперсия высокая),
# выбираем там больше пользователей
strata_variance = data.groupby('country')['conversion'].var()
# Neyman allocation
weights = strata_sizes * strata_variance ** 0.5
optimal_allocations = (weights / weights.sum()) * sample_size
# Может получиться:
# USA: 300 (high variance, нужна большая выборка)
# Europe: 150 (lower variance)
# Asia: 50 (low variance, мало пользователей нужно)
Когда использовать: когда нам важна точность оценок, а не репрезентативность. Нужно минимизировать стандартную ошибку.
3. Stratified Cluster Sampling (комбинированный подход)
Сначала выделяем страты, потом внутри каждой страты выбираем кластеры.
# Пример: нас интересуют все транзакции пользователей,
# но мы хотим убедиться, что все девайсы представлены
# Страты: Desktop, Mobile, Tablet
# Кластеры внутри страты: пользователи
for device in ['Desktop', 'Mobile', 'Tablet']:
device_users = data[data['device'] == device]['user_id'].unique()
# Выбираем 30% пользователей из этого девайса
selected_users = np.random.choice(device_users,
size=int(0.3*len(device_users)),
replace=False)
# Берём ВСЕ транзакции этих пользователей
transactions = data[data['user_id'].isin(selected_users)]
Когда использовать stratified sampling
Сценарий 1: Неравномерное распределение важных групп
Продукт используется в 100 странах. 90% трафика из США. Нас интересуют insights для всех стран, но простая случайная выборка даст мало данных из других стран.
✅ Решение: stratified sampling по странам
-- Гарантируем минимум данных из каждой страны
WITH strata AS (
SELECT country, COUNT(*) as total
FROM events
GROUP BY country
),
sampling_rates AS (
SELECT
country,
-- Минимум 100 примеров из каждой страны
CASE
WHEN total < 100 THEN 1.0 -- берём всех
ELSE LEAST(100.0 / total, 0.1) -- или 10% максимум
END as rate
FROM strata
)
SELECT *
FROM events e
WHERE RANDOM() < (SELECT rate FROM sampling_rates WHERE country = e.country);
Сценарий 2: Высокая дисперсия внутри групп
A/B тестируем новую функцию. Конверсия зависит от:
- Типа пользователя (free/premium) — разные базовые конверсии
- Платформы (web/mobile) — разное поведение
- Региона (США/EU) — разные регуляции
Если тестируем на простой выборке, дисперсия будет завышена, и тест потребует больше пользователей.
✅ Решение: stratified sampling по типам/платформам/регионам
# Ожидаемые конверсии по стратам:
strata_conversion_rates = {
('free', 'web', 'US'): 0.05,
('free', 'web', 'EU'): 0.04,
('free', 'mobile', 'US'): 0.02,
('premium', 'web', 'US'): 0.15,
# ...
}
# Дисперсия в каждой страте вычисляется как p*(1-p)
strata_variances = {
strata: p*(1-p)
for strata, p in strata_conversion_rates.items()
}
# Optimal allocation: страты с высокой дисперсией получают больше
strata_weights = {
strata: np.sqrt(strata_variances[strata])
for strata in strata_variances
}
total_weight = sum(strata_weights.values())
strata_sample_sizes = {
strata: int(10000 * strata_weights[strata] / total_weight)
for strata in strata_weights
}
print(strata_sample_sizes)
# ('free', 'web', 'US'): 1200
# ('free', 'mobile', 'US'): 800 (высокая дисперсия 0.02*0.98)
# ('premium', 'web', 'US'): 400 (низкая дисперсия 0.15*0.85)
Сценарий 3: Ищем эффект в меньшинстве
Частая проблема: хотим понять, влияет ли фича на пользователей с мобильного (10% базы), но основной трафик с десктопа (90%). Простая выборка даст мало мобильных.
✅ Решение: oversampling (увеличенная выборка из мобильной страты)
# Нормальное распределение: 90% desktop, 10% mobile
# Oversampling: берём 50% мобильных и 30% десктопных
desktop_sample = data[data['platform'] == 'desktop'].sample(frac=0.3)
mobile_sample = data[data['platform'] == 'mobile'].sample(frac=0.5)
stratified_sample = pd.concat([desktop_sample, mobile_sample])
# Теперь в выборке ~40% мобильных!
print(stratified_sample['platform'].value_counts(normalize=True))
# desktop: 0.60, mobile: 0.40
# При анализе нужно использовать веса для восстановления генеральной совокупности
weights = stratified_sample['platform'].map({
'desktop': 0.9 / 0.6, # в популяции 90%, в выборке 60%
'mobile': 0.1 / 0.4 # в популяции 10%, в выборке 40%
})
# Взвешенная средняя конверсия
weighted_conversion = (stratified_sample['conversion'] * weights).sum() / weights.sum()
Сценарий 4: Мониторинг по сегментам (когортный анализ)
Хотим отслеживать здоровье продукта по когортам: по дате регистрации, плану подписки, источнику трафика.
✅ Решение: stratified sampling по когортам
-- Каждый день случайная выборка из каждой когорты
WITH daily_strata AS (
SELECT
signup_cohort,
COUNT(*) as cohort_size,
-- Выбираем не менее 100 пользователей из каждой когорты
GREATEST(100, (cohort_size * 0.1)::INT) as target_sample_size
FROM users
WHERE DATE(TODAY()) = @today
GROUP BY signup_cohort
)
SELECT u.user_id, u.signup_cohort
FROM users u
JOIN daily_strata d ON u.signup_cohort = d.signup_cohort
WHERE
DATE(u.created_at) = @today
AND RANDOM() < (d.target_sample_size::FLOAT / d.cohort_size)
ORDER BY u.signup_cohort;
Сравнение с simple random sampling
┌─────────────────────┬────────────────────┬──────────────────────┐
│ Характеристика │ Simple Random │ Stratified │
├─────────────────────┼────────────────────┼──────────────────────┤
│ Дисперсия │ Выше │ Ниже ✅ │
│ Репрезентативность │ Может быть смещена │ Гарантирована ✅ │
│ Нужный размер │ Больше │ Меньше ✅ │
│ Сложность │ Простая │ Средняя │
│ Когда использовать? │ Однородная база │ Неоднородная база ✅ │
└─────────────────────┴────────────────────┴──────────────────────┘
Расчёт минимального размера выборки
Для простой случайной выборки:
n = (Z² * σ²) / E²
Где:
Z = 1.96 (для 95% confidence)
σ = стандартное отклонение
E = желаемая погрешность
Для stratified sampling:
n = (Z² * Σ(Nh * σh)²) / (N² * E²)
Где:
Nh = размер страты h
σh = стандартное отклонение в страте h
N = общий размер генеральной совокупности
Пример:
import numpy as np
from scipy import stats
# Параметры
z_score = 1.96 # 95% confidence
margin_of_error = 0.05 # 5% погрешность
# Страты
strata = {
'USA': {'size': 8000, 'std': 0.15},
'EU': {'size': 1500, 'std': 0.12},
'ASIA': {'size': 500, 'std': 0.20},
}
total_population = sum(s['size'] for s in strata.values())
# Stratified sample size (optimal allocation)
variance_sum = sum(
strata[s]['size'] * strata[s]['std']
for s in strata
)
n_stratified = (z_score * variance_sum / (total_population * margin_of_error)) ** 2
n_stratified = int(np.ceil(n_stratified))
print(f"Stratified sample size: {n_stratified}") # ~350
# Для сравнения, простая случайная выборка
n_simple = (z_score ** 2 * 0.25) / (margin_of_error ** 2)
n_simple = int(np.ceil(n_simple)) # ~385
print(f"Simple sample size: {n_simple}")
print(f"Экономия: {((n_simple - n_stratified) / n_simple * 100):.1f}%")
Ошибки при использовании stratified sampling
❌ Ошибка 1: Забыть про веса при анализе
Если применяли oversampling, нужно использовать веса при анализе!
# Выборка: 40% mobile, 60% desktop
# Генеральная совокупность: 10% mobile, 90% desktop
# Неправильно: просто считаем среднее
mean_wrong = data['conversion'].mean() # смещено в сторону mobile
# Правильно: взвешиваем
weights = data['platform'].map({
'mobile': 0.1 / 0.4,
'desktop': 0.9 / 0.6
})
mean_correct = (data['conversion'] * weights).sum() / weights.sum()
❌ Ошибка 2: Создать слишком много страт
Если создадите 100 страт, а выборка всего 1000, в каждой страте будет по 10 элементов. Это не поможет, а усложнит анализ.
# Плохо: 5 факторов × 4 уровня = 20 страт
strata = ['device', 'os', 'country', 'subscription', 'cohort']
# Хорошо: только самые важные
strata = ['subscription', 'country'] # 2 × 5 = 10 комбинаций
❌ Ошибка 3: Не проверить качество стратификации
После стратификации проверьте, что стратирование сработало:
# Check: распределение в выборке совпадает с генеральной совокупностью
sample_dist = stratified_sample['country'].value_counts(normalize=True)
population_dist = data['country'].value_counts(normalize=True)
print(sample_dist)
print(population_dist)
# Должны быть близки (или ровно совпадать при proportional allocation)
Реальный пример: анализ retention по странам
-- Задача: рассчитать 30-day retention по каждой стране
-- Проблема: 95% трафика из США, 5% из остального
-- Решение: stratified sampling
WITH strata_allocation AS (
SELECT
country,
COUNT(DISTINCT user_id) as users_in_strata,
-- Для каждой страны: мин 500 пользователей
GREATEST(500, COUNT(DISTINCT user_id) / 10) as target_sample
FROM users
WHERE signup_date BETWEEN '2024-01-01' AND '2024-02-01'
GROUP BY country
)
SELECT
u.country,
COUNT(DISTINCT u.user_id) as sampled_users,
SUM(CASE WHEN DATE(COALESCE(r.day_30_active, u.created_at + INTERVAL '30 days')) >= u.created_at + INTERVAL '30 days' THEN 1 ELSE 0 END)::FLOAT / COUNT(DISTINCT u.user_id) as day_30_retention
FROM users u
JOIN strata_allocation s ON u.country = s.country
LEFT JOIN retention r ON u.user_id = r.user_id
WHERE
u.signup_date BETWEEN '2024-01-01' AND '2024-02-01'
AND RANDOM() < (s.target_sample::FLOAT / s.users_in_strata)
GROUP BY u.country
ORDER BY sampled_users DESC;
Итог
Stratified sampling — мощный инструмент для Product Analysts. Используйте его когда:
✅ База неоднородна (разные страны, устройства, типы пользователей) ✅ Важно обеспечить данные для всех сегментов ✅ Дисперсия внутри групп выше, чем между группами ✅ Нужно минимизировать размер выборки (экономия на сбор данных)
Главное: не забывайте про веса при анализе, если применяли oversampling или неравномерное распределение!