← Назад к вопросам

Что такое stratified sampling и когда его использовать?

2.0 Middle🔥 111 комментариев
#Статистика и математика

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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 или неравномерное распределение!