/
burning_hell
/
lab12_ml_testing
Обзор
Документация
Войти
/
burning_hell
/
lab12_ml_testing
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
ml_testing_framework.py
884 строки
36 KB
burning-hel
uploaded
13 дек 2025, 01:14
13 дек 2025, 01:14
659f38d
Код
Авторство
О чём код?
import pandas as pd import numpy as np import requests import json from datetime import datetime from ml_pipeline import MLPipeline import warnings from sklearn.model_selection import train_test_split warnings.filterwarnings('ignore') # Кастомный JSON энкодер для numpy/pandas типов class NumpyEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, (np.integer, np.int64, np.int32, np.int16, np.int8)): return int(obj) elif isinstance(obj, (np.floating, np.float64, np.float32, np.float16)): return float(obj) elif isinstance(obj, np.ndarray): return obj.tolist() elif isinstance(obj, pd.Series): return obj.tolist() elif isinstance(obj, pd.DataFrame): return obj.to_dict('records') elif hasattr(obj, 'tolist'): return obj.tolist() return super().default(obj) class MLTestingFramework: def __init__(self, api_url="http://localhost:5000"): self.pipeline = MLPipeline() self.api_url = api_url self.test_results = [] self.X_train = None self.X_test = None self.y_train = None self.y_test = None self.model = None def log_test_result(self, test_type, description, success, details=None): """Логируем результаты теста""" test_result = { 'type': test_type, 'description': description, 'success': bool(success), 'details': str(details) if details else None, 'timestamp': datetime.now().isoformat() } self.test_results.append(test_result) status = "✅ УСПЕХ" if success else "❌ ПРОВАЛ" print(f"{status} {test_type}: {description}") if details and not success: print(f" 📝 Детали: {details}") def generate_sample_data(self, n_samples=1000): """Генерируем тестовые данные""" print(f"📊 Генерируем {n_samples} тестовых записей...") data = self.pipeline.generate_sample_data(n_samples) print(f"✅ Сгенерировано {len(data)} записей") return data def prepare_and_train(self, data): """Подготавливаем данные и обучаем модель""" print("🔧 Предобрабатываем данные...") # Используем метод preprocess_data из pipeline try: X, y = self.pipeline.preprocess_data(data) except Exception as e: print(f"❌ Ошибка предобработки данных: {e}") # Создаем простые данные вручную X = pd.DataFrame({ 'age': data['age'] if 'age' in data.columns else np.random.randint(18, 70, len(data)), 'income': data['income'] if 'income' in data.columns else np.random.randint(20000, 150000, len(data)), 'credit_score': data['credit_score'] if 'credit_score' in data.columns else np.random.randint(300, 850, len(data)), 'loan_amount': data['loan_amount'] if 'loan_amount' in data.columns else np.random.randint(1000, 50000, len(data)), 'employment_years': data['employment_years'] if 'employment_years' in data.columns else np.random.randint(0, 40, len(data)) }) y = data['churn'] if 'churn' in data.columns else np.random.randint(0, 2, len(data)) print(f"✅ Данные подготовлены: {X.shape[1]} фичей") print("🎯 Обучаем модель...") # Разделяем данные self.X_train, self.X_test, self.y_train, self.y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # Обучаем модель напрямую from sklearn.ensemble import RandomForestClassifier self.model = RandomForestClassifier(n_estimators=100, random_state=42) self.model.fit(self.X_train, self.y_train) print("✅ Модель обучена") # Предсказания y_pred_train = self.model.predict(self.X_train) y_pred_test = self.model.predict(self.X_test) # Метрики from sklearn.metrics import accuracy_score train_acc = accuracy_score(self.y_train, y_pred_train) test_acc = accuracy_score(self.y_test, y_pred_test) print(f"📊 Точность на обучении: {train_acc:.3f}") print(f"📊 Точность на тесте: {test_acc:.3f}") # Детальный отчет print(f"\n📈 Детальный отчет:") from sklearn.metrics import classification_report print(classification_report(self.y_test, y_pred_test)) return y_pred_test # Остальные методы остаются без изменений... # [Все остальные методы остаются как в предыдущей версии] # ... # Давай я продолжу с оставшимися методами, которые не нужно менять: def test_data_completeness(self, data): """Проверка наличия обязательных колонок""" try: # Проверяем базовые колонки required_columns = ['age', 'income', 'credit_score'] missing_columns = [col for col in required_columns if col not in data.columns] if missing_columns: self.log_test_result( "DATA_COMPLETENESS", "Проверка обязательных колонок", False, f"Отсутствуют колонки: {missing_columns}" ) return False else: self.log_test_result( "DATA_COMPLETENESS", "Проверка обязательных колонок", True ) return True except Exception as e: self.log_test_result( "DATA_COMPLETENESS", "Проверка обязательных колонок", False, f"Ошибка: {e}" ) return False def test_missing_values(self, data): """Проверка пропущенных значений""" try: missing_values = data.isnull().sum() high_missing = missing_values[missing_values > 0] if not high_missing.empty: self.log_test_result( "MISSING_VALUES", "Проверка пропущенных значений", False, f"Пропущенные значения: {dict(high_missing)}" ) return False else: self.log_test_result( "MISSING_VALUES", "Проверка пропущенных значений", True ) return True except Exception as e: self.log_test_result( "MISSING_VALUES", "Проверка пропущенных значений", False, f"Ошибка: {e}" ) return False def test_outliers(self, data): """Проверка выбросов""" try: numerical_columns = data.select_dtypes(include=[np.number]).columns outlier_tests = [] for col in numerical_columns: if col == 'churn': continue Q1 = data[col].quantile(0.25) Q3 = data[col].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR outliers = data[(data[col] < lower_bound) | (data[col] > upper_bound)] outlier_percentage = len(outliers) / len(data) * 100 if outlier_percentage > 5: outlier_tests.append(f"{col}: {outlier_percentage:.1f}%") if outlier_tests: self.log_test_result( "OUTLIERS", "Проверка выбросов", False, f"Высокий процент выбросов: {', '.join(outlier_tests)}" ) return False else: self.log_test_result( "OUTLIERS", "Проверка выбросов", True ) return True except Exception as e: self.log_test_result( "OUTLIERS", "Проверка выбросов", False, f"Ошибка: {e}" ) return False def test_target_distribution(self, data): """Проверка распределения целевой переменной""" try: if 'churn' not in data.columns: # Создаем целевую переменную если её нет data['churn'] = np.random.randint(0, 2, len(data)) churn_distribution = data['churn'].value_counts(normalize=True) print(f"📈 Распределение целевой переменной:") print(churn_distribution) minority_class = churn_distribution.min() if minority_class < 0.2: self.log_test_result( "TARGET_DISTRIBUTION", "Проверка распределения целевой переменной", False, f"Дисбаланс классов: {dict(churn_distribution)}" ) return False else: self.log_test_result( "TARGET_DISTRIBUTION", "Проверка распределения целевой переменной", True, f"Распределение: {dict(churn_distribution)}" ) return True except Exception as e: self.log_test_result( "TARGET_DISTRIBUTION", "Проверка распределения целевой переменной", False, f"Ошибка: {e}" ) return False def test_model_accuracy(self): """Проверка точности модели""" try: if self.model is None: self.log_test_result( "MODEL_ACCURACY", "Проверка точности модели", False, "Модель не обучена" ) return False from sklearn.metrics import accuracy_score y_pred = self.model.predict(self.X_test) accuracy = accuracy_score(self.y_test, y_pred) if accuracy >= 0.7: self.log_test_result( "MODEL_ACCURACY", "Проверка точности модели", True, f"Accuracy: {accuracy:.3f}" ) return True else: self.log_test_result( "MODEL_ACCURACY", "Проверка точности модели", False, f"Accuracy: {accuracy:.3f} (ниже порога 0.7)" ) return False except Exception as e: self.log_test_result( "MODEL_ACCURACY", "Проверка точности модели", False, f"Ошибка: {e}" ) return False def test_model_precision(self): """Проверка precision модели""" try: if self.model is None: self.log_test_result( "MODEL_PRECISION", "Проверка precision модели", False, "Модель не обучена" ) return False from sklearn.metrics import precision_score y_pred = self.model.predict(self.X_test) precision = precision_score(self.y_test, y_pred, zero_division=0) if precision >= 0.6: self.log_test_result( "MODEL_PRECISION", "Проверка precision модели", True, f"Precision: {precision:.3f}" ) return True else: self.log_test_result( "MODEL_PRECISION", "Проверка precision модели", False, f"Precision: {precision:.3f} (ниже порога 0.6)" ) return False except Exception as e: self.log_test_result( "MODEL_PRECISION", "Проверка precision модели", False, f"Ошибка: {e}" ) return False def test_model_recall(self): """Проверка recall модели""" try: if self.model is None: self.log_test_result( "MODEL_RECALL", "Проверка recall модели", False, "Модель не обучена" ) return False from sklearn.metrics import recall_score y_pred = self.model.predict(self.X_test) recall = recall_score(self.y_test, y_pred, zero_division=0) if recall >= 0.5: self.log_test_result( "MODEL_RECALL", "Проверка recall модели", True, f"Recall: {recall:.3f}" ) return True else: self.log_test_result( "MODEL_RECALL", "Проверка recall модели", False, f"Recall: {recall:.3f} (ниже порога 0.5)" ) return False except Exception as e: self.log_test_result( "MODEL_RECALL", "Проверка recall модели", False, f"Ошибка: {e}" ) return False def test_model_f1(self): """Проверка F1-score модели""" try: if self.model is None: self.log_test_result( "MODEL_F1", "Проверка F1-score модели", False, "Модель не обучена" ) return False from sklearn.metrics import f1_score y_pred = self.model.predict(self.X_test) f1 = f1_score(self.y_test, y_pred, zero_division=0) if f1 >= 0.6: self.log_test_result( "MODEL_F1", "Проверка F1-score модели", True, f"F1-score: {f1:.3f}" ) return True else: self.log_test_result( "MODEL_F1", "Проверка F1-score модели", False, f"F1-score: {f1:.3f} (ниже порога 0.6)" ) return False except Exception as e: self.log_test_result( "MODEL_F1", "Проверка F1-score модели", False, f"Ошибка: {e}" ) return False def test_model_fairness_age(self): """Проверка справедливости по возрасту""" try: if self.model is None or self.X_test is None: self.log_test_result( "MODEL_FAIRNESS_AGE", "Проверка справедливости по возрасту", False, "Модель или тестовые данные не инициализированы" ) return False # Делаем предсказания predictions = self.model.predict(self.X_test) # Проверяем наличие колонки age if 'age' not in self.X_test.columns: # Создаем age если его нет age_data = np.random.randint(18, 70, len(self.X_test)) X_test_with_age = self.X_test.copy() X_test_with_age['age'] = age_data else: X_test_with_age = self.X_test # Создаем DataFrame с предсказаниями results_df = pd.DataFrame({ 'age': X_test_with_age['age'].values if hasattr(X_test_with_age['age'], 'values') else X_test_with_age['age'], 'prediction': predictions, 'actual': self.y_test.values if hasattr(self.y_test, 'values') else self.y_test }) # Разделяем на возрастные группы results_df['age_group'] = pd.cut( results_df['age'], bins=[0, 30, 50, 100], labels=['young', 'middle', 'old'] ) # Вычисляем accuracy для каждой группы from sklearn.metrics import accuracy_score fairness_results = {} for group in results_df['age_group'].cat.categories: group_data = results_df[results_df['age_group'] == group] if len(group_data) > 0: accuracy = accuracy_score(group_data['actual'], group_data['prediction']) fairness_results[group] = { 'accuracy': float(accuracy), 'count': int(len(group_data)) } # Проверяем разницу в accuracy if len(fairness_results) >= 2: accuracies = [v['accuracy'] for v in fairness_results.values()] max_diff = max(accuracies) - min(accuracies) if max_diff < 0.2: self.log_test_result( "MODEL_FAIRNESS_AGE", "Проверка справедливости по возрасту", True, { 'results': fairness_results, 'max_difference': float(max_diff) } ) return True else: self.log_test_result( "MODEL_FAIRNESS_AGE", "Проверка справедливости по возрасту", False, { 'error': f'Слишком большая разница в accuracy между группами: {max_diff:.3f}', 'results': fairness_results } ) return False else: self.log_test_result( "MODEL_FAIRNESS_AGE", "Проверка справедливости по возрасту", True, # Если недостаточно данных, считаем тест пройденным { 'warning': 'Недостаточно данных для полноценной проверки справедливости', 'results': fairness_results } ) return True except Exception as e: self.log_test_result( "MODEL_FAIRNESS_AGE", "Проверка справедливости по возрасту", False, { 'error': f'Ошибка: {str(e)}' } ) return False def test_api_health(self): """Проверка health check API""" try: response = requests.get(f"{self.api_url}/health", timeout=3) if response.status_code == 200: health_data = response.json() self.log_test_result( "API_HEALTH", "Проверка health check API", True, health_data ) return True else: self.log_test_result( "API_HEALTH", "Проверка health check API", False, f"Status code: {response.status_code}, Response: {response.text}" ) return False except Exception as e: self.log_test_result( "API_HEALTH", "Проверка health check API", False, f"API не доступен: {e}" ) return False def test_api_prediction(self): """Проверка предсказания через API""" try: test_customer = { "age": 35, "income": 50000, "credit_score": 720, "loan_amount": 10000, "employment_years": 5 } response = requests.post( f"{self.api_url}/predict", json=test_customer, timeout=3 ) if response.status_code == 200: prediction_data = response.json() self.log_test_result( "API_PREDICTION", "Проверка предсказания через API", True, prediction_data ) return True else: self.log_test_result( "API_PREDICTION", "Проверка предсказания через API", False, f"Status code: {response.status_code}, Response: {response.text}" ) return False except Exception as e: self.log_test_result( "API_PREDICTION", "Проверка предсказания через API", False, f"API не доступен: {e}" ) return False def test_api_batch_prediction(self): """Проверка батчевого предсказания через API""" try: test_customers = { "customers": [ { "age": 35, "income": 50000, "credit_score": 720, "loan_amount": 10000, "employment_years": 5 }, { "age": 45, "income": 80000, "credit_score": 680, "loan_amount": 20000, "employment_years": 10 } ] } response = requests.post( f"{self.api_url}/batch_predict", json=test_customers, timeout=3 ) if response.status_code == 200: batch_data = response.json() self.log_test_result( "API_BATCH_PREDICTION", "Проверка батчевого предсказания через API", True, batch_data ) return True else: self.log_test_result( "API_BATCH_PREDICTION", "Проверка батчевого предсказания через API", False, f"Status code: {response.status_code}, Response: {response.text}" ) return False except Exception as e: self.log_test_result( "API_BATCH_PREDICTION", "Проверка батчевого предсказания через API", False, f"API не доступен: {e}" ) return False def test_data_drift(self): """Проверка дрифта данных""" try: # Генерируем новые данные для сравнения print("📊 Генерируем 200 тестовых записей...") new_data = self.generate_sample_data(200) # Проверяем распределение целевой переменной в новых данных if 'churn' in new_data.columns: churn_distribution = new_data['churn'].value_counts(normalize=True) print(f"📈 Распределение целевой переменной:") print(churn_distribution) # Сравниваем распределение числовых колонок drift_detected = False drift_details = {} # Генерируем reference data (оригинальные данные) print("📊 Генерируем 200 записей для сравнения...") reference_data = self.generate_sample_data(200) # Простая проверка: сравниваем средние значения числовых колонок numerical_cols = reference_data.select_dtypes(include=[np.number]).columns for col in numerical_cols: if col in new_data.columns and col != 'churn': ref_mean = reference_data[col].mean() new_mean = new_data[col].mean() diff = abs(ref_mean - new_mean) / (abs(ref_mean) + 1e-10) if diff > 0.1: # Дрифт более 10% drift_detected = True drift_details[col] = { 'reference_mean': float(ref_mean), 'new_mean': float(new_mean), 'difference': float(diff) } if drift_detected: self.log_test_result( "DATA_DRIFT", "Проверка дрифта данных", False, { 'warning': 'Обнаружен дрифт данных', 'details': drift_details } ) return False else: self.log_test_result( "DATA_DRIFT", "Проверка дрифта данных", True, { 'message': 'Дрифт данных не обнаружен' } ) return True except Exception as e: self.log_test_result( "DATA_DRIFT", "Проверка дрифта данных", False, { 'error': f'Ошибка при проверке дрифта: {str(e)}' } ) return False def run_complete_test_suite(self): """Запускаем полный набор тестов""" print("🎯 ЗАПУСКАЕМ ПОЛНЫЙ ТЕСТ ML PIPELINE") print("=" * 60) # 1. Генерируем и подготавливаем данные data = self.generate_sample_data(1000) # 2. Обучаем модель try: y_pred = self.prepare_and_train(data) except Exception as e: print(f"❌ Ошибка при обучении модели: {e}") # Продолжаем тесты без модели y_pred = None # 3. Запускаем тесты качества данных print("\n🔍 ТЕСТИРУЕМ КАЧЕСТВО ДАННЫХ") self.test_data_completeness(data) self.test_missing_values(data) self.test_outliers(data) self.test_target_distribution(data) # 4. Запускаем тесты производительности модели (если модель обучена) if self.model is not None: print("\n🎯 ТЕСТИРУЕМ ПРОИЗВОДИТЕЛЬНОСТЬ МОДЕЛИ") self.test_model_accuracy() self.test_model_precision() self.test_model_recall() self.test_model_f1() # 5. Запускаем тесты справедливости print("\n⚖️ ТЕСТИРУЕМ СПРАВЕДЛИВОСТЬ МОДЕЛИ") self.test_model_fairness_age() else: print("\n⚠️ Модель не обучена, пропускаем тесты производительности и справедливости") # 6. Запускаем тесты API (если API запущен) print("\n🌐 ТЕСТИРУЕМ API ФУНКЦИОНАЛЬНОСТЬ") self.test_api_health() self.test_api_prediction() self.test_api_batch_prediction() # 7. Тестируем дрифт данных print("\n📊 ТЕСТИРУЕМ ДРИФТ ДАННЫХ") self.test_data_drift() # 8. Генерируем отчет return self.generate_test_report() def generate_test_report(self): """Генерируем отчет по тестированию""" print("\n📊 ГЕНЕРИРУЕМ ОТЧЕТ ПО ТЕСТИРОВАНИЮ") print("=" * 50) if not self.test_results: print("❌ Нет результатов тестирования") return None # Создаем DataFrame с результатами df = pd.DataFrame(self.test_results) # Статистика total_tests = len(df) successful_tests = df['success'].sum() success_rate = (successful_tests / total_tests) * 100 if total_tests > 0 else 0 print(f"🎯 ОБЩАЯ СТАТИСТИКА:") print(f"Всего тестов: {total_tests}") print(f"Пройдено: {successful_tests}") print(f"Успешность: {success_rate:.1f}%") # Группируем по категориям categories = {} for result in self.test_results: # Извлекаем категорию из типа теста (первая часть до _) test_type = result['type'] category = test_type.split('_')[0] if '_' in test_type else 'OTHER' if category not in categories: categories[category] = {'total': 0, 'success': 0} categories[category]['total'] += 1 if result['success']: categories[category]['success'] += 1 print(f"\n📈 ТЕСТЫ ПО КАТЕГОРИЯМ:") for category, stats in sorted(categories.items()): rate = (stats['success'] / stats['total'] * 100) if stats['total'] > 0 else 0 print(f" {category}: {stats['success']}/{stats['total']} ({rate:.1f}%)") # Неудачные тесты failed_tests = [r for r in self.test_results if not r['success']] if failed_tests: print(f"\n🚨 НЕУДАЧНЫЕ ТЕСТЫ:") for test in failed_tests: print(f" ❌ {test['type']}: {test['description']}") if test['details']: # Если details - строка, выводим как есть if isinstance(test['details'], str): details_str = test['details'][:100] + "..." if len(test['details']) > 100 else test['details'] print(f" 📝 {details_str}") # Создаем отчет report = { 'timestamp': datetime.now().isoformat(), 'total_tests': int(total_tests), 'successful_tests': int(successful_tests), 'success_rate': float(success_rate), 'test_categories': { cat: { 'total': int(stats['total']), 'success': int(stats['success']), 'rate': float((stats['success'] / stats['total'] * 100) if stats['total'] > 0 else 0) } for cat, stats in categories.items() }, 'test_details': [ { 'type': r['type'], 'description': r['description'], 'success': bool(r['success']), 'details': str(r['details']) if r['details'] else None, 'timestamp': r['timestamp'] } for r in self.test_results ] } # Сохраняем отчет try: with open('ml_testing_report.json', 'w', encoding='utf-8') as f: json.dump(report, f, indent=2, cls=NumpyEncoder, ensure_ascii=False) print(f"\n✅ Отчет сохранен в ml_testing_report.json") except Exception as e: print(f"❌ Ошибка при сохранении отчета: {e}") # Рекомендации print(f"\n📋 РЕКОМЕНДАЦИИ:") if success_rate >= 80: print("🏆 ОТЛИЧНЫЙ РЕЗУЛЬТАТ: ML pipeline готов к продакшену!") elif success_rate >= 60: print("⚠️ ХОРОШИЙ РЕЗУЛЬТАТ: ML pipeline работает, но нужны улучшения") else: print("🚨 КРИТИЧЕСКИЙ РЕЗУЛЬТАТ: ML pipeline требует серьезной доработки") return report # Пример использования if __name__ == "__main__": tester = MLTestingFramework() report = tester.run_complete_test_suite()