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

Как проверял тестами работу моделей\?

1.7 Middle🔥 161 комментариев
#MLOps и инфраструктура#Опыт и проекты

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

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

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

Тестирование ML моделей: полная стратегия

Тестирование моделей сильно отличается от классического unit-тестирования. Невозможно знать идеальный ответ заранее, поэтому используется вероятностная валидация и статистические тесты.

Уровень 1: Unit тесты для preprocessing

Проверяем, что данные подготавливаются корректно:

import pytest
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler

# Тестируем подготовку данных
class TestDataPreprocessing:
    
    def test_missing_values_handling(self):
        """Проверяем, что пропуски заполняются корректно"""
        data = pd.DataFrame({
            'age': [25, np.nan, 35, 40],
            'income': [50000, 60000, np.nan, 80000]
        })
        
        # Заполняем медианой
        data_filled = data.fillna(data.median())
        
        assert data_filled['age'].isna().sum() == 0
        assert data_filled['income'].isna().sum() == 0
        assert data_filled['age'][1] == 32.5  # медиана
    
    def test_scaling_transforms_correctly(self):
        """Проверяем нормализацию"""
        X = np.array([[1, 2], [3, 4], [5, 6]])
        scaler = StandardScaler()
        X_scaled = scaler.fit_transform(X)
        
        # Проверяем, что масштабирование работает
        assert X_scaled.mean() < 0.01  # близко к 0
        assert X_scaled.std() < 1.01  # близко к 1
    
    def test_categorical_encoding(self):
        """Проверяем кодирование категориальных признаков"""
        data = pd.DataFrame({
            'category': ['A', 'B', 'A', 'C']
        })
        
        encoded = pd.get_dummies(data, columns=['category'])
        
        assert 'category_A' in encoded.columns
        assert 'category_B' in encoded.columns
        assert len(encoded) == 4
        assert encoded['category_A'].sum() == 2
    
    def test_no_data_leakage(self):
        """Проверяем, что нет утечки данных из test в train"""
        from sklearn.model_selection import train_test_split
        
        X = np.random.randn(100, 10)
        y = np.random.randint(0, 2, 100)
        
        X_train, X_test, y_train, y_test = train_test_split(
            X, y, test_size=0.2, random_state=42
        )
        
        # Нет пересечений
        assert len(X_train) == 80
        assert len(X_test) == 20
        assert len(np.intersect1d(X_train, X_test)) == 0

Уровень 2: Integration тесты (обучение-предсказание)

Проверяем, что модель учится и даёт разумные предсказания:

class TestModelTraining:
    
    def test_model_overfitting_detection(self):
        """Проверяем, что модель учится (обучающая точность > тестовая)"""
        from sklearn.ensemble import RandomForestClassifier
        from sklearn.metrics import accuracy_score
        from sklearn.datasets import load_iris
        from sklearn.model_selection import train_test_split
        
        X, y = load_iris(return_X_y=True)
        X_train, X_test, y_train, y_test = train_test_split(
            X, y, test_size=0.3, random_state=42
        )
        
        model = RandomForestClassifier(n_estimators=100, random_state=42)
        model.fit(X_train, y_train)
        
        train_acc = accuracy_score(y_train, model.predict(X_train))
        test_acc = accuracy_score(y_test, model.predict(X_test))
        
        # Модель должна переобучаться немного (train > test)
        assert train_acc > test_acc
        # Но тестовая должна быть разумной
        assert test_acc > 0.7
    
    def test_predictions_in_valid_range(self):
        """Проверяем, что предсказания в корректном диапазоне"""
        from sklearn.linear_model import LogisticRegression
        from sklearn.datasets import load_breast_cancer
        
        X, y = load_breast_cancer(return_X_y=True)
        model = LogisticRegression(max_iter=1000)
        model.fit(X, y)
        
        y_proba = model.predict_proba(X)
        
        # Вероятности должны быть в [0, 1]
        assert np.all(y_proba >= 0)
        assert np.all(y_proba <= 1)
        # Сумма вероятностей = 1
        assert np.allclose(y_proba.sum(axis=1), 1.0)
    
    def test_model_reproducibility(self):
        """Проверяем, что модель воспроизводима"""
        from sklearn.ensemble import GradientBoostingClassifier
        from sklearn.datasets import load_iris
        
        X, y = load_iris(return_X_y=True)
        
        # Первый запуск
        model1 = GradientBoostingClassifier(random_state=42)
        model1.fit(X, y)
        pred1 = model1.predict(X)
        
        # Второй запуск с тем же random_state
        model2 = GradientBoostingClassifier(random_state=42)
        model2.fit(X, y)
        pred2 = model2.predict(X)
        
        # Результаты должны быть идентичны
        assert np.array_equal(pred1, pred2)

Уровень 3: Метрики и производительность

Проверяем, что модель достигает целевых метрик:

class TestModelMetrics:
    
    def test_model_meets_business_requirements(self):
        """Проверяем, что метрики соответствуют бизнес-требованиям"""
        from sklearn.ensemble import RandomForestClassifier
        from sklearn.metrics import recall_score, precision_score, roc_auc_score
        from sklearn.datasets import load_breast_cancer
        from sklearn.model_selection import train_test_split
        
        X, y = load_breast_cancer(return_X_y=True)
        X_train, X_test, y_train, y_test = train_test_split(
            X, y, test_size=0.3, random_state=42
        )
        
        model = RandomForestClassifier(n_estimators=100, random_state=42)
        model.fit(X_train, y_train)
        
        y_pred = model.predict(X_test)
        y_proba = model.predict_proba(X_test)[:, 1]
        
        # Требование: recall >= 0.90 (ловим 90% болезней)
        recall = recall_score(y_test, y_pred)
        assert recall >= 0.90, f"Recall {recall:.2f} ниже требуемого 0.90"
        
        # Требование: precision >= 0.85
        precision = precision_score(y_test, y_pred)
        assert precision >= 0.85, f"Precision {precision:.2f} ниже требуемого 0.85"
        
        # Требование: ROC-AUC >= 0.95
        roc_auc = roc_auc_score(y_test, y_proba)
        assert roc_auc >= 0.95, f"ROC-AUC {roc_auc:.2f} ниже требуемого 0.95"
    
    def test_baseline_comparison(self):
        """Проверяем, что модель лучше baseline"""
        from sklearn.ensemble import RandomForestClassifier
        from sklearn.linear_model import LogisticRegression
        from sklearn.metrics import roc_auc_score
        from sklearn.datasets import load_iris
        from sklearn.model_selection import cross_val_score
        
        X, y = load_iris(return_X_y=True)
        
        # Baseline: логистическая регрессия
        baseline = LogisticRegression(max_iter=1000)
        baseline_score = cross_val_score(
            baseline, X, y, cv=5, scoring='roc_auc'
        ).mean()
        
        # Предложенная модель
        model = RandomForestClassifier(n_estimators=100, random_state=42)
        model_score = cross_val_score(
            model, X, y, cv=5, scoring='roc_auc'
        ).mean()
        
        # Должна быть лучше baseline
        assert model_score > baseline_score, \
            f"Model ROC-AUC {model_score:.3f} <= Baseline {baseline_score:.3f}"
    
    def test_no_data_leakage_in_validation(self):
        """Проверяем отсутствие утечки при кросс-валидации"""
        from sklearn.model_selection import StratifiedKFold
        from sklearn.ensemble import RandomForestClassifier
        from sklearn.datasets import load_iris
        
        X, y = load_iris(return_X_y=True)
        kfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
        
        train_indices_list = []
        test_indices_list = []
        
        for train_idx, test_idx in kfold.split(X, y):
            train_indices_list.append(set(train_idx))
            test_indices_list.append(set(test_idx))
            
            # Проверяем, что нет пересечений
            assert len(train_idx) + len(test_idx) == len(X)
            assert len(set(train_idx) & set(test_idx)) == 0

Уровень 4: Статистические тесты

Проверяем значимость результатов:

from scipy import stats

class TestStatisticalSignificance:
    
    def test_cross_validation_stability(self):
        """Проверяем, что результаты кросс-валидации стабильны"""
        from sklearn.model_selection import cross_val_score
        from sklearn.ensemble import RandomForestClassifier
        from sklearn.datasets import load_iris
        
        X, y = load_iris(return_X_y=True)
        model = RandomForestClassifier(n_estimators=100, random_state=42)
        
        scores = cross_val_score(model, X, y, cv=10, scoring='accuracy')
        
        # Результаты должны быть стабильны (низкий std)
        assert scores.std() < 0.1, f"Нестабильные результаты: std={scores.std():.3f}"
        
        # Среднее значение > 0.9
        assert scores.mean() > 0.9, f"Низкая точность: mean={scores.mean():.3f}"
    
    def test_significance_of_improvement(self):
        """Проверяем статистическую значимость улучшения"""
        from sklearn.model_selection import cross_val_score
        from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
        from sklearn.datasets import load_iris
        
        X, y = load_iris(return_X_y=True)
        
        # Две модели
        rf = RandomForestClassifier(n_estimators=100, random_state=42)
        gb = GradientBoostingClassifier(random_state=42)
        
        # Кросс-валидация
        scores_rf = cross_val_score(rf, X, y, cv=10, scoring='accuracy')
        scores_gb = cross_val_score(gb, X, y, cv=10, scoring='accuracy')
        
        # t-тест (значимо ли различие?)
        t_stat, p_value = stats.ttest_ind(scores_gb, scores_rf)
        
        # p < 0.05 означает значимое различие
        # Если различие есть, то модель действительно лучше
        if p_value < 0.05:
            assert scores_gb.mean() > scores_rf.mean()

Уровень 5: Regression тесты и мониторинг

Проверяем, что модель не деградирует со временем:

class TestModelDegradation:
    
    def test_model_performance_on_new_data(self):
        """Проверяем, что модель работает на новых данных"""
        from sklearn.ensemble import RandomForestClassifier
        from sklearn.metrics import roc_auc_score
        from sklearn.datasets import load_iris
        from sklearn.model_selection import train_test_split
        
        X, y = load_iris(return_X_y=True)
        
        # Тренируем на старых данных
        X_train, X_old_test, y_train, y_old_test = train_test_split(
            X, y, test_size=0.2, random_state=42
        )
        
        model = RandomForestClassifier(n_estimators=100, random_state=42)
        model.fit(X_train, y_train)
        
        old_auc = roc_auc_score(y_old_test, model.predict_proba(X_old_test)[:, 1])
        
        # Проверяем на новых данных (симулируем)
        X_new_test, y_new_test = load_iris(return_X_y=True)
        X_train, X_new_test, y_train, y_new_test = train_test_split(
            X_new_test, y_new_test, test_size=0.2, random_state=43
        )
        
        new_auc = roc_auc_score(y_new_test, model.predict_proba(X_new_test)[:, 1])
        
        # Проверяем, что деградация не слишком сильная
        degradation = old_auc - new_auc
        assert degradation < 0.1, \
            f"Слишком сильная деградация: {degradation:.3f}"
    
    def test_feature_importance_stability(self):
        """Проверяем стабильность важности признаков"""
        from sklearn.ensemble import RandomForestClassifier
        from sklearn.datasets import load_iris
        from sklearn.model_selection import train_test_split
        
        X, y = load_iris(return_X_y=True)
        X_train, X_test, y_train, y_test = train_test_split(
            X, y, test_size=0.3, random_state=42
        )
        
        # Тренируем две модели с разными random_state
        model1 = RandomForestClassifier(n_estimators=100, random_state=42)
        model1.fit(X_train, y_train)
        importance1 = model1.feature_importances_
        
        model2 = RandomForestClassifier(n_estimators=100, random_state=43)
        model2.fit(X_train, y_train)
        importance2 = model2.feature_importances_
        
        # Корреляция важности признаков должна быть высокой
        correlation = np.corrcoef(importance1, importance2)[0, 1]
        assert correlation > 0.8, \
            f"Нестабильная важность признаков: корреляция={correlation:.3f}"

Полный пример с pytest

# conftest.py - фикстуры
import pytest
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

@pytest.fixture
def iris_data():
    """Фикстура для загрузки Iris датасета"""
    X, y = load_iris(return_X_y=True)
    return train_test_split(X, y, test_size=0.3, random_state=42)

@pytest.fixture
def trained_model(iris_data):
    """Фикстура для обученной модели"""
    from sklearn.ensemble import RandomForestClassifier
    X_train, X_test, y_train, y_test = iris_data
    model = RandomForestClassifier(n_estimators=100, random_state=42)
    model.fit(X_train, y_train)
    return model, X_test, y_test

# Использование
def test_model_with_fixture(trained_model):
    model, X_test, y_test = trained_model
    assert model.score(X_test, y_test) > 0.8

Запуск тестов

# Все тесты
pytest tests/ -v

# Тесты с покрытием
pytest tests/ --cov=src --cov-report=html

# Только медленные тесты
pytest tests/ -m slow

# Параллельно
pytest tests/ -n auto

# С вывод всех print
pytest tests/ -s

Ключевые принципы

  1. Unit тесты: логика обработки данных
  2. Integration тесты: обучение и предсказание
  3. Метрики: соответствие бизнес-требованиям
  4. Статистика: значимость результатов
  5. Мониторинг: отсутствие деградации
  6. Воспроизводимость: контроль random_state
  7. Data leakage: проверка отсутствия утечек
Как проверял тестами работу моделей\? | PrepBro