Расчет доверительных интервалов и уровня значимости с Python

Работая над множеством проектов в области машинного обучения и анализа данных, я постоянно сталкиваюсь с необходимостью не просто получить какие-то метрики или показатели, но и оценить, насколько мы можем им доверять.

Представьте ситуацию: вы разработали новую модель машинного обучения, которая показывает accuracy 85%. Звучит неплохо, верно? Но что если этот результат получен на небольшой выборке и при повторном обучении модели на других данных accuracy может существенно отличаться?

Именно здесь на помощь приходят доверительные интервалы, которые позволяют нам оценить диапазон возможных значений для интересующего нас параметра с определенной степенью уверенности. В реальных проектах это может быть:

  • Оценка эффективности A/B-тестов;
  • Анализ точности предсказаний модели;
  • Оценка параметров статистических распределений;
  • Валидация результатов экспериментов.

Математическая основа доверительных интервалов

Прежде чем перейти к практической реализации, важно понять теоретическую базу. Доверительный интервал — это диапазон значений, который с заданной вероятностью (уровнем доверия) содержит истинное значение параметра. Наиболее часто используются следующие уровни доверия:

  • 90% (α = 0.10);
  • 95% (α = 0.05);
  • 99% (α = 0.01).

Код на Python для расчета доверительного интервала для среднего значения выглядит следующим образом:

import numpy as np
from scipy import stats

def calculate_confidence_interval(data, confidence=0.95):
    mean = np.mean(data)
    std_err = stats.sem(data)
    ci = stats.t.interval(confidence, len(data)-1, loc=mean, scale=std_err)
    return ci

# Пример использования
data = np.random.normal(100, 15, size=1000)
ci_90 = calculate_confidence_interval(data, 0.90)
ci_95 = calculate_confidence_interval(data, 0.95)
ci_99 = calculate_confidence_interval(data, 0.99)

print(f"90% CI: {ci_90}")
print(f"95% CI: {ci_95}")
print(f"99% CI: {ci_99}")

90% CI: (99.16643836238909, 100.82560650132034)
95% CI: (99.00723073525228, 100.98481412845715)
99% CI: (98.69561995308172, 101.2964249106277)

Практическое применение доверительных интервалов в машинном обучении

В области машинного обучения доверительные интервалы играют критическую роль при оценке качества моделей. Давайте рассмотрим несколько практических примеров, с которыми я часто сталкиваюсь в своей работе.

Оценка точности классификации с доверительными интервалами

Один из самых частых сценариев — это необходимость оценить, насколько стабильны метрики качества нашей модели. Давайте реализуем функцию, которая будет оценивать accuracy модели с доверительным интервалом используя bootstrap:

from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
import numpy as np

def calculate_model_accuracy_with_ci(model, X, y, n_iterations=1000, confidence=0.95):
    accuracies = []
    
    # Разделяем данные на обучающую и тестовую выборки
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
    
    # Обучаем модель
    model.fit(X_train, y_train)
    
    # Bootstrap для оценки доверительного интервала
    for _ in range(n_iterations):
        # Случайная выборка с возвращением
        indices = np.random.randint(0, len(X_test), len(X_test))
        X_bootstrap = X_test[indices]
        y_bootstrap = y_test[indices]
        
        # Предсказания и подсчет accuracy
        y_pred = model.predict(X_bootstrap)
        accuracy = accuracy_score(y_bootstrap, y_pred)
        accuracies.append(accuracy)
    
    # Расчет доверительного интервала
    lower_percentile = ((1.0-confidence)/2.0) * 100
    upper_percentile = (confidence + ((1.0-confidence)/2.0)) * 100
    
    ci_lower = np.percentile(accuracies, lower_percentile)
    ci_upper = np.percentile(accuracies, upper_percentile)
    mean_accuracy = np.mean(accuracies)
    
    return mean_accuracy, (ci_lower, ci_upper)

# Пример использования
from sklearn.datasets import make_classification

# Генерируем синтетические данные
X, y = make_classification(n_samples=1000, n_features=20, n_informative=15, 
                         n_redundant=5, random_state=42)

# Создаем и оцениваем модель
model = RandomForestClassifier(n_estimators=100, random_state=42)
mean_acc, ci = calculate_model_accuracy_with_ci(model, X, y)

print(f"Mean Accuracy: {mean_acc:.3f}")
print(f"95% CI: ({ci[0]:.3f}, {ci[1]:.3f})")

Mean Accuracy: 0.900
95% CI: (0.855, 0.940)

Интервальные оценки в задачах регрессии

В задачах регрессии доверительные интервалы особенно важны, так как они позволяют оценить неопределенность предсказаний. Рассмотрим пример реализации регрессионной модели с оценкой доверительных интервалов для предсказаний:

from sklearn.ensemble import GradientBoostingRegressor
import numpy as np

class RegressionWithConfidenceIntervals:
    def __init__(self, base_model=None, n_estimators=100):
        self.base_model = base_model if base_model else GradientBoostingRegressor(n_estimators=n_estimators)
        self.models = []
        
    def fit(self, X, y, n_bootstrap=100):
        for _ in range(n_bootstrap):
            # Bootstrap выборка
            indices = np.random.randint(0, len(X), len(X))
            X_boot, y_boot = X[indices], y[indices]
            
            # Создаем и обучаем новую модель
            model = clone(self.base_model)
            model.fit(X_boot, y_boot)
            self.models.append(model)
            
    def predict_with_confidence(self, X, confidence=0.95):
        predictions = np.array([model.predict(X) for model in self.models])
        
        # Рассчитываем среднее предсказание и доверительные интервалы
        mean_pred = np.mean(predictions, axis=0)
        lower = np.percentile(predictions, ((1-confidence)/2)*100, axis=0)
        upper = np.percentile(predictions, (confidence+((1-confidence)/2))*100, axis=0)
        
        return mean_pred, lower, upper

# Пример использования
from sklearn.datasets import make_regression

# Генерируем данные
X, y = make_regression(n_samples=1000, n_features=10, noise=0.1, random_state=42)

# Создаем и обучаем модель
model = RegressionWithConfidenceIntervals()
model.fit(X, y)

# Получаем предсказания с доверительными интервалами
mean_pred, lower, upper = model.predict_with_confidence(X[:5])

for i in range(5):
    print(f"Prediction {i+1}:")
    print(f"Mean: {mean_pred[i]:.2f}")
    print(f"95% CI: ({lower[i]:.2f}, {upper[i]:.2f})")

Уровень значимости и проверка гипотез

В своей практике я часто сталкиваюсь с необходимостью принимать решения на основе данных: является ли разница между двумя группами статистически значимой? Действительно ли новая версия алгоритма работает лучше предыдущей? Именно здесь ключевую роль играет правильное понимание и использование уровня значимости (p-value).

Читайте также:  Как предсказать отток клиентов с помощью машинного обучения

Давайте рассмотрим несколько наиболее часто используемых статистических тестов и их реализацию на Python. В своей работе я создал удобную обертку для проведения различных статистических тестов:

from scipy import stats
import numpy as np
import pandas as pd
from typing import Tuple, Optional

class StatisticalTester:
    def __init__(self, alpha: float = 0.05):
        self.alpha = alpha
        
    def test_normality(self, data: np.ndarray) -> Tuple[float, bool]:
        """Проверка на нормальность распределения (тест Шапиро-Уилка)"""
        statistic, p_value = stats.shapiro(data)
        is_normal = p_value > self.alpha
        return p_value, is_normal
    
    def compare_two_samples(self, 
                           sample1: np.ndarray, 
                           sample2: np.ndarray,
                           paired: bool = False) -> dict:
        """
        Сравнение двух выборок с автоматическим выбором подходящего теста
        """
        # Проверяем нормальность обеих выборок
        _, is_normal1 = self.test_normality(sample1)
        _, is_normal2 = self.test_normality(sample2)
        
        results = {}
        
        if paired:
            if is_normal1 and is_normal2:
                # Парный t-тест
                t_stat, p_value = stats.ttest_rel(sample1, sample2)
                test_name = "Paired t-test"
            else:
                # Тест Уилкоксона
                t_stat, p_value = stats.wilcoxon(sample1, sample2)
                test_name = "Wilcoxon signed-rank test"
        else:
            if is_normal1 and is_normal2:
                # Непарный t-тест
                t_stat, p_value = stats.ttest_ind(sample1, sample2)
                test_name = "Independent t-test"
            else:
                # Тест Манна-Уитни
                t_stat, p_value = stats.mannwhitneyu(sample1, sample2)
                test_name = "Mann-Whitney U test"
        
        results['test_name'] = test_name
        results['statistic'] = t_stat
        results['p_value'] = p_value
        results['significant'] = p_value < self.alpha return results def calculate_effect_size(self, sample1: np.ndarray, sample2: np.ndarray) -> float:
        """Расчет размера эффекта (Cohen's d)"""
        n1, n2 = len(sample1), len(sample2)
        var1, var2 = np.var(sample1, ddof=1), np.var(sample2, ddof=1)
        
        # Pooled standard deviation
        pooled_se = np.sqrt(((n1 - 1) * var1 + (n2 - 1) * var2) / (n1 + n2 - 2))
        
        # Cohen's d
        cohen_d = (np.mean(sample1) - np.mean(sample2)) / pooled_se
        
        return cohen_d

# Пример использования
tester = StatisticalTester(alpha=0.05)

# Генерируем тестовые данные
np.random.seed(42)
control_group = np.random.normal(100, 15, 100)
treatment_group = np.random.normal(105, 15, 100)  # Небольшой эффект воздействия

# Проводим статистический анализ
results = tester.compare_two_samples(control_group, treatment_group)
effect_size = tester.calculate_effect_size(control_group, treatment_group)

print(f"Test used: {results['test_name']}")
print(f"P-value: {results['p_value']:.4f}")
print(f"Statistically significant: {results['significant']}")
print(f"Effect size (Cohen's d): {effect_size:.4f}")

Test used: Independent t-test
P-value: 0.0006
Statistically significant: True
Effect size (Cohen’s d): -0.4934

Множественное тестирование и поправка на множественные сравнения

Одна из частых ошибок, которую я встречаю в работе других специалистов – это игнорирование проблемы множественных сравнений. Когда мы проводим несколько статистических тестов одновременно, возрастает вероятность получить ложноположительный результат. Давайте рассмотрим, как правильно учитывать это:

from scipy.stats import multipletests
import numpy as np
import pandas as pd

class MultipleTester:
    def __init__(self, method='bonferroni'):
        """
        method: метод коррекции p-value
        Доступные методы: 'bonferroni', 'sidak', 'holm-sidak', 'holm', 
                         'simes-hochberg', 'hommel', 'fdr_bh', 'fdr_by'
        """
        self.method = method
        
    def test_multiple_hypotheses(self, 
                               data: pd.DataFrame,
                               control_col: str,
                               test_cols: list) -> pd.DataFrame:
        """
        Проведение множественных сравнений с контрольной группой
        
        Parameters:
        -----------
        data: DataFrame с данными
        control_col: название столбца с контрольной группой
        test_cols: список столбцов для сравнения с контролем
        """
        results = []
        p_values = []
        
        # Проводим попарные сравнения
        for col in test_cols:
            test_result = stats.ttest_ind(data[control_col], data[col])
            results.append({
                'comparison': f'{control_col} vs {col}',
                'statistic': test_result.statistic,
                'p_value': test_result.pvalue
            })
            p_values.append(test_result.pvalue)
        
        # Применяем коррекцию на множественные сравнения
        rejected, p_adjusted, _, _ = multipletests(p_values, 
                                                 method=self.method)
        
        # Формируем результаты
        results_df = pd.DataFrame(results)
        results_df['p_adjusted'] = p_adjusted
        results_df['significant'] = rejected
        
        return results_df
    
    def control_fdr(self, p_values: np.ndarray, q: float = 0.05) -> tuple:
        """
        Контроль FDR (False Discovery Rate) с использованием метода 
        Benjamini-Hochberg
        
        Parameters:
        -----------
        p_values: массив p-значений
        q: желаемый уровень FDR
        
        Returns:
        --------
        mask: булев массив значимых результатов
        critical_values: критические значения для каждого теста
        """
        p_values = np.array(p_values)
        n = len(p_values)
        indices = np.arange(1, n + 1)
        critical_values = q * indices / n
        
        # Сортируем p-значения
        sorted_indices = np.argsort(p_values)
        sorted_p_values = p_values[sorted_indices]
        
        # Находим последнее p-значение, меньшее критического
        mask = sorted_p_values <= critical_values
        if not np.any(mask):
            return np.zeros_like(p_values, dtype=bool), critical_values
        
        last_true = np.where(mask)[0][-1]
        threshold = critical_values[last_true]
        
        # Возвращаем маску для исходного порядка p-значений
        mask = p_values <= threshold
        
        return mask, critical_values

# Пример использования
np.random.seed(42)

# Генерируем данные для множественного сравнения
n_samples = 100
n_tests = 10
data = pd.DataFrame({
    'control': np.random.normal(100, 15, n_samples)
})

# Добавляем тестовые группы (некоторые с эффектом, некоторые без)
for i in range(n_tests):
    effect = 5 if i < 3 else 0  # Только первые 3 группы имеют реальный эффект
    data[f'test_{i}'] = np.random.normal(100 + effect, 15, n_samples)

# Проводим множественное тестирование
tester = MultipleTester(method='fdr_bh')
test_cols = [f'test_{i}' for i in range(n_tests)]
results = tester.test_multiple_hypotheses(data, 'control', test_cols)

print("Results after FDR correction:")
print(results[['comparison', 'p_value', 'p_adjusted', 'significant']])

Продвинутые методы оценки неопределенности

В современном машинном обучении классические методы оценки неопределенности часто дополняются более продвинутыми подходами. Давайте рассмотрим некоторые из них.

Читайте также:  Как правильно оценивать результаты A/B тестов с помощью Python

Байесовские методы оценки неопределенности

В своей практике я все чаще обращаюсь к байесовским методам, особенно когда требуется более гибкий подход к оценке неопределенности. Байесовский подход позволяет естественным образом учитывать предварительные знания и обновлять их по мере поступления новых данных. Вот пример кода реализации байесовской регрессии с оценкой неопределенности:

import pymc3 as pm
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler

class BayesianRegression:
    def __init__(self, n_samples=2000):
        self.n_samples = n_samples
        self.model = None
        self.trace = None
        self.scaler_X = StandardScaler()
        self.scaler_y = StandardScaler()
        
    def fit(self, X, y):
        # Стандартизация данных
        X_scaled = self.scaler_X.fit_transform(X)
        y_scaled = self.scaler_y.fit_transform(y.reshape(-1, 1)).ravel()
        
        # Создание и обучение байесовской модели
        with pm.Model() as self.model:
            # Априорные распределения для параметров
            alpha = pm.Normal('alpha', mu=0, sd=10)
            beta = pm.Normal('beta', mu=0, sd=10, shape=X.shape[1])
            sigma = pm.HalfNormal('sigma', sd=1)
            
            # Линейное предсказание
            mu = alpha + pm.math.dot(X_scaled, beta)
            
            # Правдоподобие
            y_obs = pm.Normal('y_obs', mu=mu, sd=sigma, observed=y_scaled)
            
            # Семплирование из апостериорного распределения
            self.trace = pm.sample(self.n_samples, cores=2, return_inferencedata=False)
            
    def predict(self, X_new, return_ci=True, ci_level=0.95):
        X_new_scaled = self.scaler_X.transform(X_new)
        
        # Получаем samples предсказаний
        alpha_samples = self.trace['alpha']
        beta_samples = self.trace['beta']
        
        # Генерируем предсказания для каждого сэмпла
        predictions = np.array([
            alpha + np.dot(X_new_scaled, beta)
            for alpha, beta in zip(alpha_samples, beta_samples)
        ])
        
        # Обратная трансформация
        predictions = self.scaler_y.inverse_transform(predictions)
        
        # Рассчитываем среднее предсказание и доверительные интервалы
        mean_pred = predictions.mean(axis=0)
        
        if return_ci:
            lower = np.percentile(predictions, ((1-ci_level)/2)*100, axis=0)
            upper = np.percentile(predictions, (1+ci_level)/2*100, axis=0)
            return mean_pred, lower, upper
        
        return mean_pred

# Пример использования
np.random.seed(42)
X = np.random.randn(100, 3)
true_beta = [1.5, -0.5, 0.8]
y = np.dot(X, true_beta) + np.random.normal(0, 0.5, size=100)

model = BayesianRegression()
model.fit(X, y)

# Делаем предсказания с доверительными интервалами
X_test = np.random.randn(5, 3)
mean_pred, lower, upper = model.predict(X_test)

for i in range(5):
    print(f"\nПредсказание {i+1}:")
    print(f"Среднее: {mean_pred[i]:.2f}")
    print(f"95% CI: ({lower[i]:.2f}, {upper[i]:.2f})")

Продвинутые методы бутстрапа для сложных случаев

В реальных проектах часто возникают ситуации, когда классические методы бутстрапа не подходят из-за специфики данных или задачи. Я разработал несколько модифицированных подходов для таких случаев:

class AdvancedBootstrap:
    def __init__(self, n_iterations=1000, block_size=None):
        self.n_iterations = n_iterations
        self.block_size = block_size
    
    def block_bootstrap(self, data, func):
        """
        Блочный бутстрап для временных рядов
        """
        n = len(data)
        if self.block_size is None:
            # Оптимальный размер блока по правилу большого пальца
            self.block_size = int(np.ceil(np.sqrt(n)))
        
        results = []
        for _ in range(self.n_iterations):
            # Генерируем случайные начальные точки для блоков
            n_blocks = int(np.ceil(n / self.block_size))
            starts = np.random.randint(0, n - self.block_size + 1, size=n_blocks)
            
            # Собираем bootstrap выборку из блоков
            bootstrap_sample = []
            for start in starts:
                block = data[start:start + self.block_size]
                bootstrap_sample.extend(block)
            
            # Обрезаем до нужной длины и вычисляем статистику
            bootstrap_sample = bootstrap_sample[:n]
            results.append(func(bootstrap_sample))
        
        return np.array(results)
    
    def weighted_bootstrap(self, data, weights, func):
        """
        Взвешенный бутстрап для несбалансированных данных
        """
        n = len(data)
        results = []
        
        for _ in range(self.n_iterations):
            # Генерируем выборку с учетом весов
            indices = np.random.choice(n, size=n, p=weights/np.sum(weights))
            bootstrap_sample = data[indices]
            results.append(func(bootstrap_sample))
        
        return np.array(results)
    
    def stratified_bootstrap(self, data, strata, func):
        """
        Стратифицированный бутстрап для сохранения структуры данных
        """
        unique_strata = np.unique(strata)
        n = len(data)
        results = []
        
        for _ in range(self.n_iterations):
            bootstrap_sample = []
            
            # Проводим бутстрап отдельно для каждой страты
            for stratum in unique_strata:
                stratum_mask = strata == stratum
                stratum_data = data[stratum_mask]
                stratum_size = len(stratum_data)
                
                # Генерируем индексы для текущей страты
                stratum_indices = np.random.choice(stratum_size, 
                                                 size=stratum_size, 
                                                 replace=True)
                
                bootstrap_sample.extend(stratum_data[stratum_indices])
            
            results.append(func(bootstrap_sample))
        
        return np.array(results)

# Пример использования
bootstrap = AdvancedBootstrap(n_iterations=1000)

# Пример для временного ряда
time_series = np.random.randn(1000)
mean_func = lambda x: np.mean(x)
block_bootstrap_results = bootstrap.block_bootstrap(time_series, mean_func)

# Рассчитываем доверительный интервал
ci_lower = np.percentile(block_bootstrap_results, 2.5)
ci_upper = np.percentile(block_bootstrap_results, 97.5)
print(f"\nBlock Bootstrap 95% CI: ({ci_lower:.3f}, {ci_upper:.3f})")

# Пример для взвешенного бутстрапа
weights = np.random.uniform(0.1, 1.0, size=1000)
weighted_bootstrap_results = bootstrap.weighted_bootstrap(time_series, 
                                                       weights, 
                                                       mean_func)

# Пример для стратифицированного бутстрапа
strata = np.random.choice(['A', 'B', 'C'], size=1000)
stratified_results = bootstrap.stratified_bootstrap(time_series, 
                                                  strata, 
                                                  mean_func)

Практические рекомендации по выбору методов

На основе своего опыта работы с различными проектами, я выработал следующие рекомендации по выбору методов оценки неопределенности:

Читайте также:  Как находить точки роста онлайн-бизнеса с помощью Python

Для небольших выборок (n < 30):

  • Используйте t-распределение вместо нормального;
  • Применяйте непараметрические методы;
  • Рассмотрите возможность использования байесовских методов.

Для временных рядов:

  • Используйте блочный бутстрап;
  • Учитывайте автокорреляцию при расчете доверительных интервалов;
  • Применяйте методы, специфичные для временных рядов (например, HAC-оценки).

Для несбалансированных данных:

  • Применяйте взвешенный бутстрап;
  • Используйте стратификацию;
  • Корректируйте доверительные интервалы с учетом дисбаланса.

Для больших наборов данных:

  • Используйте ресурсоэкономные методы вычисления и алгоритмы;
  • Применяйте параллельные вычисления для бутстрапа;
  • Рассмотрите возможность использования подвыборок (subsampling).

Заключение

В этой статье мы подробно рассмотрели различные аспекты расчета доверительных интервалов и уровня значимости с использованием Python.

Основные выводы:

  1. Правильная оценка неопределенности критически важна для принятия обоснованных решений в data science проектах;
  2. Современная экосистема Python предоставляет богатый инструментарий для статистического анализа, от базовых методов до продвинутых байесовских подходов;
  3. При работе с реальными данными важно учитывать их специфику и выбирать подходящие методы оценки неопределенности;
  4. Использование продвинутых методов, таких как байесовский подход и различные модификации бутстрапа, позволяет получать более надежные результаты в сложных случаях.

Для бизнеса правильная оценка неопределенности позволяет:

  1. Принимать более обоснованные решения;
  2. Лучше управлять рисками и оптимизировать ресурсы;
  3. Повышать эффективность A/B-тестирования;
  4. Улучшать качество прогнозных моделей.