Spaces:
Sleeping
Sleeping
| # app_pectin.py | |
| """ | |
| Pectin Models Demo - Interactive Pectin Production Predictor | |
| Run: streamlit run app_pectin.py | |
| """ | |
| import os | |
| import time | |
| import pandas as pd | |
| import numpy as np | |
| import streamlit as st | |
| import joblib | |
| import pickle | |
| from pathlib import Path | |
| from typing import Dict, List, Optional, Any, Tuple | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| from huggingface_hub import snapshot_download, hf_hub_download | |
| import json | |
| import io | |
| import xgboost | |
| import traceback | |
| from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score | |
| from scipy import stats | |
| import warnings | |
| warnings.filterwarnings('ignore') | |
| # Page config | |
| st.set_page_config( | |
| page_title="Pectin Production Predictor", | |
| page_icon="🧪", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # CSS styling | |
| st.markdown( | |
| """ | |
| <style> | |
| .main-header { | |
| font-size: 2.5rem; | |
| color: #2E8B57; | |
| text-align: center; | |
| margin-bottom: 1.5rem; | |
| font-weight: bold; | |
| } | |
| .section-header { | |
| font-size: 1.5rem; | |
| color: #2E8B57; | |
| margin: 1.5rem 0 1rem 0; | |
| border-bottom: 2px solid #2E8B57; | |
| padding-bottom: 0.5rem; | |
| } | |
| .prediction-box { | |
| background: #f0f8f4; | |
| border-radius: 10px; | |
| padding: 1.5rem; | |
| border-left: 5px solid #2E8B57; | |
| margin: 1rem 0; | |
| } | |
| .metric-box { | |
| background: white; | |
| border-radius: 8px; | |
| padding: 1rem; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| margin: 0.5rem 0; | |
| } | |
| .model-comparison { | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| padding: 1rem; | |
| margin: 0.5rem 0; | |
| } | |
| .info-box { | |
| background: #e7f3ff; | |
| border-radius: 8px; | |
| padding: 1rem; | |
| margin: 1rem 0; | |
| border-left: 4px solid #1890ff; | |
| } | |
| .error-box { | |
| background: #ffe7e7; | |
| border-radius: 8px; | |
| padding: 1rem; | |
| margin: 1rem 0; | |
| border-left: 4px solid #ff4d4f; | |
| } | |
| .comparison-header { | |
| background: #2E8B57; | |
| color: white; | |
| padding: 0.5rem 1rem; | |
| border-radius: 5px; | |
| margin: 1rem 0; | |
| } | |
| .metric-card { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border-radius: 10px; | |
| padding: 1rem; | |
| margin: 0.5rem 0; | |
| text-align: center; | |
| } | |
| .metric-value { | |
| font-size: 1.8rem; | |
| font-weight: bold; | |
| margin: 0.5rem 0; | |
| } | |
| .metric-name { | |
| font-size: 0.9rem; | |
| opacity: 0.9; | |
| } | |
| .good-metric { background: linear-gradient(135deg, #2E8B57 0%, #3CB371 100%); } | |
| .medium-metric { background: linear-gradient(135deg, #FFA500 0%, #FF8C00 100%); } | |
| .poor-metric { background: linear-gradient(135deg, #FF6B6B 0%, #EE5A52 100%); } | |
| </style> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| # Configuration | |
| REPO_ID = "arabovs-ai-lab/PectinProductionModels" | |
| # Available models configuration | |
| # Available models configuration - ОБНОВЛЕННАЯ ВЕРСИЯ | |
| AVAILABLE_MODELS = { | |
| "best_model": { | |
| "subfolder": "best_model", | |
| "description": "🎯 Best overall model (Gradient Boosting) - оптимальная производительность", | |
| "color": "#FF6B6B" | |
| }, | |
| "gradient_boosting": { | |
| "subfolder": "gradient_boosting", | |
| "description": "📈 Gradient Boosting - лучшая для многозадачной регрессии", | |
| "color": "#4ECDC4" | |
| }, | |
| "random_forest": { | |
| "subfolder": "random_forest", | |
| "description": "🌲 Random Forest - надежная и стабильная", | |
| "color": "#45B7D1" | |
| }, | |
| "xgboost": { | |
| "subfolder": "xgboost", | |
| "description": "⚡ XGBoost - высокая производительность на табличных данных", | |
| "color": "#96CEB4" | |
| }, | |
| "linear_regression": { | |
| "subfolder": "linear_regression", | |
| "description": "📊 Linear Regression - базовая линейная модель", | |
| "color": "#FECA57" | |
| }, | |
| # ДОБАВЛЕННЫЕ МОДЕЛИ | |
| "extra_trees": { | |
| "subfolder": "extra_trees", | |
| "description": "🌳 Extra Trees - экстремальные случайные леса", | |
| "color": "#FF9FF3" | |
| }, | |
| "k_neighbors": { | |
| "subfolder": "k-neighbors", | |
| "description": "📏 K-Neighbors - метод ближайших соседей", | |
| "color": "#54A0FF" | |
| }, | |
| "lasso_regression": { | |
| "subfolder": "lasso_regression", | |
| "description": "🎯 Lasso Regression - L1-регуляризация", | |
| "color": "#5F27CD" | |
| }, | |
| "multilayer_perceptron": { | |
| "subfolder": "multilayer_perceptron", | |
| "description": "🧠 Neural Network MLP - многослойный перцептрон", | |
| "color": "#00D2D3" | |
| }, | |
| "ridge_regression": { | |
| "subfolder": "ridge_regression", | |
| "description": "🏔️ Ridge Regression - L2-регуляризация", | |
| "color": "#FF9F43" | |
| }, | |
| "support_vector_regression": { | |
| "subfolder": "support_vector_regression", | |
| "description": "🔗 Support Vector Regression - метод опорных векторов", | |
| "color": "#A3CB38" | |
| } | |
| } | |
| # Sample types available | |
| SAMPLE_TYPES = ["ЯП(М)", "Абр.", "КрП", "Айв.", "Ткв", "Рв.", "ЯП(Ф)"] | |
| # Column mapping for Russian to English | |
| COLUMN_MAPPING = { | |
| 'Образец \nпектина': 'sample', | |
| 't, мин': 'time_min', | |
| 'T, °C': 'temperature_c', | |
| 'P, атм': 'pressure_atm', | |
| 'pH': 'ph', | |
| 'Т:Ж': 'solid_liquid_ratio', | |
| 'ПВ, %': 'pectin_yield', | |
| 'ГК, %': 'galacturonic_acid', | |
| 'Mw, Д': 'molecular_weight', | |
| 'СЭ, %': 'esterification_degree' | |
| } | |
| # Target descriptions | |
| TARGET_DESCRIPTIONS = { | |
| 'pectin_yield': { | |
| 'name': 'Выход пектина', | |
| 'description': 'Процент выхода пектина из сырья', | |
| 'unit': '%', | |
| 'range': (0, 100) | |
| }, | |
| 'galacturonic_acid': { | |
| 'name': 'Галактуроновая кислота', | |
| 'description': 'Содержание галактуроновой кислоты в пектине', | |
| 'unit': '%', | |
| 'range': (0, 100) | |
| }, | |
| 'molecular_weight': { | |
| 'name': 'Молекулярная масса', | |
| 'description': 'Молекулярная масса пектина', | |
| 'unit': 'Da', | |
| 'range': (0, 500000) | |
| }, | |
| 'esterification_degree': { | |
| 'name': 'Степень этерификации', | |
| 'description': 'Степень этерификации пектина', | |
| 'unit': '%', | |
| 'range': (0, 100) | |
| } | |
| } | |
| # Metric descriptions | |
| METRIC_DESCRIPTIONS = { | |
| 'mae': { | |
| 'name': 'MAE (Средняя абсолютная ошибка)', | |
| 'description': 'Среднее абсолютное значение разницы между предсказанием и истинным значением', | |
| 'better_lower': True, | |
| 'perfect_value': 0 | |
| }, | |
| 'mse': { | |
| 'name': 'MSE (Средняя квадратичная ошибка)', | |
| 'description': 'Среднее квадратов разниц между предсказанием и истинным значением', | |
| 'better_lower': True, | |
| 'perfect_value': 0 | |
| }, | |
| 'rmse': { | |
| 'name': 'RMSE (Среднеквадратичная ошибка)', | |
| 'description': 'Корень из средней квадратичной ошибки, имеет ту же размерность что и целевая переменная', | |
| 'better_lower': True, | |
| 'perfect_value': 0 | |
| }, | |
| 'r2': { | |
| 'name': 'R² (Коэффициент детерминации)', | |
| 'description': 'Доля дисперсии зависимой переменной, объясняемая моделью', | |
| 'better_lower': False, | |
| 'perfect_value': 1 | |
| }, | |
| 'mape': { | |
| 'name': 'MAPE (Средняя абсолютная процентная ошибка)', | |
| 'description': 'Средняя абсолютная ошибка в процентах от истинных значений', | |
| 'better_lower': True, | |
| 'perfect_value': 0 | |
| }, | |
| 'correlation': { | |
| 'name': 'Корреляция Пирсона', | |
| 'description': 'Мера линейной зависимости между предсказаниями и истинными значениями', | |
| 'better_lower': False, | |
| 'perfect_value': 1 | |
| } | |
| } | |
| # Helper function to download and cache model files | |
| def load_pectin_model(model_name: str): | |
| """Load pectin model and supporting files from Hugging Face Hub""" | |
| try: | |
| model_info = AVAILABLE_MODELS[model_name] | |
| subfolder = model_info["subfolder"] | |
| st.info(f"🔄 Попытка загрузки модели {model_name} из репозитория {REPO_ID}") | |
| # Download model file | |
| try: | |
| model_path = hf_hub_download( | |
| repo_id=REPO_ID, | |
| filename=f"{subfolder}/model.pkl", | |
| repo_type="model" | |
| ) | |
| st.success(f"✅ Модель {model_name} загружена успешно") | |
| except Exception as e: | |
| st.error(f"❌ Ошибка загрузки модели {model_name}: {str(e)}") | |
| st.markdown('<div class="error-box">', unsafe_allow_html=True) | |
| st.error("**Возможные причины ошибки 403:**") | |
| st.error("1. Репозиторий не существует или приватный") | |
| st.error("2. Нет доступа к интернету") | |
| st.error("3. Проблемы с авторизацией Hugging Face") | |
| st.error("4. Файлы модели отсутствуют в репозитории") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| return {"status": "error", "error": str(e)} | |
| # Download supporting files | |
| try: | |
| scaler_path = hf_hub_download( | |
| repo_id=REPO_ID, | |
| filename="scaler.pkl", | |
| repo_type="model" | |
| ) | |
| st.success("✅ Scaler загружен успешно") | |
| except Exception as e: | |
| st.error(f"❌ Ошибка загрузки scaler: {str(e)}") | |
| return {"status": "error", "error": f"Scaler error: {str(e)}"} | |
| try: | |
| encoder_path = hf_hub_download( | |
| repo_id=REPO_ID, | |
| filename="label_encoder.pkl", | |
| repo_type="model" | |
| ) | |
| st.success("✅ Label encoder загружен успешно") | |
| except Exception as e: | |
| st.error(f"❌ Ошибка загрузки label encoder: {str(e)}") | |
| return {"status": "error", "error": f"Encoder error: {str(e)}"} | |
| try: | |
| metadata_path = hf_hub_download( | |
| repo_id=REPO_ID, | |
| filename="model_metadata.json", | |
| repo_type="model" | |
| ) | |
| st.success("✅ Метаданные загружены успешно") | |
| except Exception as e: | |
| st.error(f"❌ Ошибка загрузки метаданных: {str(e)}") | |
| return {"status": "error", "error": f"Metadata error: {str(e)}"} | |
| # Load all artifacts | |
| try: | |
| model = joblib.load(model_path) | |
| scaler = joblib.load(scaler_path) | |
| with open(encoder_path, 'rb') as f: | |
| label_encoder = pickle.load(f) | |
| with open(metadata_path, 'r', encoding='utf-8') as f: | |
| metadata = json.load(f) | |
| st.success(f"✅ Все артефакты модели {model_name} загружены и готовы к использованию") | |
| return { | |
| "model": model, | |
| "scaler": scaler, | |
| "label_encoder": label_encoder, | |
| "metadata": metadata, | |
| "status": "success" | |
| } | |
| except Exception as e: | |
| st.error(f"❌ Ошибка загрузки артефактов модели: {str(e)}") | |
| return {"status": "error", "error": f"Loading error: {str(e)}"} | |
| except Exception as e: | |
| st.error(f"❌ Критическая ошибка загрузки модели {model_name}: {str(e)}") | |
| st.markdown('<div class="error-box">', unsafe_allow_html=True) | |
| st.error("**Детали ошибки:**") | |
| st.code(traceback.format_exc()) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| return {"status": "error", "error": str(e)} | |
| # Fallback to demo mode if models cannot be loaded | |
| def create_demo_model(): | |
| """Create demo model for testing when real models are unavailable""" | |
| st.warning("🎭 Используется демо-режим с тестовыми данными") | |
| class DemoModel: | |
| def predict(self, X): | |
| # Generate realistic demo predictions based on input features | |
| n_samples = len(X) | |
| predictions = [] | |
| for i in range(n_samples): | |
| # Base values with some variation | |
| pectin_yield = 20 + np.random.normal(0, 3) | |
| galacturonic_acid = 60 + np.random.normal(0, 5) | |
| molecular_weight = 120000 + np.random.normal(0, 20000) | |
| esterification_degree = 70 + np.random.normal(0, 5) | |
| predictions.append([pectin_yield, galacturonic_acid, molecular_weight, esterification_degree]) | |
| return np.array(predictions) | |
| class DemoEncoder: | |
| def __init__(self): | |
| self.classes_ = SAMPLE_TYPES | |
| def transform(self, x): | |
| return [self.classes_.index(val) if val in self.classes_ else 0 for val in x] | |
| class DemoScaler: | |
| def transform(self, X): | |
| return X # Identity transform for demo | |
| return { | |
| "model": DemoModel(), | |
| "scaler": DemoScaler(), | |
| "label_encoder": DemoEncoder(), | |
| "metadata": { | |
| 'feature_columns': ['time_min', 'temperature_c', 'pressure_atm', 'ph', 'sample_encoded', 'method_encoded'], | |
| 'target_columns': ['pectin_yield', 'galacturonic_acid', 'molecular_weight', 'esterification_degree'] | |
| }, | |
| "status": "demo" | |
| } | |
| # Preprocessing functions | |
| def preprocess_single_input(input_data: Dict, model_artifacts: Dict) -> Optional[np.ndarray]: | |
| """Preprocess single input record for prediction""" | |
| try: | |
| # Create DataFrame | |
| df = pd.DataFrame([input_data]) | |
| # Encode sample type | |
| label_encoder = model_artifacts["label_encoder"] | |
| sample_value = input_data['sample'] | |
| if sample_value in label_encoder.classes_: | |
| df['sample_encoded'] = label_encoder.transform([sample_value])[0] | |
| else: | |
| st.warning(f"⚠️ Тип сырья '{sample_value}' не найден в энкодере. Используется значение по умолчанию.") | |
| default_class = label_encoder.classes_[0] | |
| df['sample_encoded'] = label_encoder.transform([default_class])[0] | |
| # Create method_encoded feature | |
| df['method_encoded'] = 1 if input_data['time_min'] <= 15 else 0 | |
| # Get feature columns from metadata | |
| feature_columns = model_artifacts["metadata"].get('feature_columns', []) | |
| # Ensure all required features are present | |
| for col in feature_columns: | |
| if col not in df.columns: | |
| df[col] = 0 # Fill missing with default | |
| # Select features in correct order | |
| X_processed = df[feature_columns] | |
| # Scale features | |
| scaler = model_artifacts["scaler"] | |
| X_scaled = scaler.transform(X_processed) | |
| return X_scaled | |
| except Exception as e: | |
| st.error(f"❌ Ошибка предобработки данных: {e}") | |
| st.code(traceback.format_exc()) | |
| return None | |
| def predict_single(input_data: Dict, model_artifacts: Dict) -> Optional[Dict]: | |
| """Make prediction for single input""" | |
| try: | |
| # Preprocess input | |
| processed_data = preprocess_single_input(input_data, model_artifacts) | |
| if processed_data is None: | |
| return None | |
| # Make prediction | |
| model = model_artifacts["model"] | |
| predictions = model.predict(processed_data) | |
| # Format results | |
| target_columns = model_artifacts["metadata"].get('target_columns', []) | |
| results = {} | |
| for i, target in enumerate(target_columns): | |
| if i < predictions.shape[1]: | |
| results[target] = float(predictions[0, i]) | |
| return results | |
| except Exception as e: | |
| st.error(f"❌ Ошибка предсказания: {e}") | |
| st.code(traceback.format_exc()) | |
| return None | |
| def detect_file_encoding(uploaded_file): | |
| """Определяет кодировку файла""" | |
| try: | |
| # Пробуем разные кодировки | |
| for encoding in ['utf-8', 'cp1251', 'iso-8859-1', 'windows-1251']: | |
| try: | |
| content = uploaded_file.getvalue().decode(encoding) | |
| uploaded_file.seek(0) # Сбрасываем позицию | |
| return encoding, content | |
| except UnicodeDecodeError: | |
| continue | |
| return 'utf-8', uploaded_file.getvalue().decode('utf-8', errors='ignore') | |
| except Exception as e: | |
| st.error(f"❌ Ошибка определения кодировки: {e}") | |
| return 'utf-8', uploaded_file.getvalue().decode('utf-8', errors='ignore') | |
| def parse_russian_number(value): | |
| """Парсит русские числа с запятыми как десятичными разделителями""" | |
| if pd.isna(value) or value == '': | |
| return 0.0 | |
| if isinstance(value, (int, float)): | |
| return float(value) | |
| if isinstance(value, str): | |
| # Заменяем запятые на точки и убираем пробелы | |
| cleaned = value.replace(',', '.').replace(' ', '') | |
| try: | |
| return float(cleaned) | |
| except ValueError: | |
| # Если не получается, возвращаем 0 | |
| return 0.0 | |
| return 0.0 | |
| def read_flexible_pectin_file(uploaded_file, skiprows=None): | |
| """Универсальная функция для чтения файлов пектина с разными форматами""" | |
| try: | |
| file_extension = uploaded_file.name.lower() | |
| st.info(f"📂 Чтение файла: {uploaded_file.name}, расширение: {file_extension}") | |
| if file_extension.endswith('.xlsx'): | |
| # Для Excel файлов | |
| if skiprows is None: | |
| # Пробуем разные варианты пропуска строк | |
| for skip in [0, 1, 2, 3]: | |
| try: | |
| df = pd.read_excel(uploaded_file, skiprows=skip, engine='openpyxl') | |
| if len(df.columns) > 3: # Если есть достаточно колонок | |
| st.info(f"✅ Найдена структура данных с пропуском {skip} строк") | |
| return df | |
| except Exception as e: | |
| st.warning(f"⚠️ Не удалось прочитать с пропуском {skip} строк: {e}") | |
| continue | |
| # Если ничего не сработало, читаем без пропуска | |
| return pd.read_excel(uploaded_file, engine='openpyxl') | |
| else: | |
| return pd.read_excel(uploaded_file, skiprows=skiprows, engine='openpyxl') | |
| elif file_extension.endswith('.csv') or file_extension.endswith('.txt'): | |
| # Для CSV/TXT файлов | |
| encoding, content = detect_file_encoding(uploaded_file) | |
| st.info(f"📖 Определена кодировка: {encoding}") | |
| # Пробуем разные разделители | |
| separators = ['\t', ',', ';', '|'] | |
| for sep in separators: | |
| try: | |
| if skiprows is not None: | |
| df = pd.read_csv(io.StringIO(content), sep=sep, skiprows=skiprows, encoding=encoding) | |
| else: | |
| df = pd.read_csv(io.StringIO(content), sep=sep, encoding=encoding) | |
| if len(df.columns) > 3: # Если есть достаточно колонок | |
| st.info(f"✅ Найден разделитель: '{sep}' с пропуском {skiprows or 0} строк") | |
| return df | |
| except Exception as e: | |
| st.warning(f"⚠️ Не удалось прочитать с разделителем '{sep}': {e}") | |
| continue | |
| # Если ничего не сработало, пробуем без указания разделителя | |
| try: | |
| return pd.read_csv(io.StringIO(content), encoding=encoding) | |
| except Exception as e: | |
| st.error(f"❌ Не удалось прочитать файл: {e}") | |
| return None | |
| else: | |
| st.error("❌ Неподдерживаемый формат файла") | |
| return None | |
| except Exception as e: | |
| st.error(f"❌ Ошибка чтения файла: {e}") | |
| st.code(traceback.format_exc()) | |
| return None | |
| def validate_file_structure(uploaded_file, skiprows=None): | |
| """Проверяет структуру файла перед обработкой""" | |
| try: | |
| st.info("🔍 Начинаем проверку структуры файла...") | |
| df = read_flexible_pectin_file(uploaded_file, skiprows) | |
| if df is None or df.empty: | |
| st.error("❌ Файл пустой или не содержит данных") | |
| return False, None | |
| st.success(f"✅ Файл успешно прочитан! Размер: {df.shape}") | |
| st.info("📋 Структура файла (первые 5 строк):") | |
| st.dataframe(df.head(), width='stretch') | |
| st.info(f"📊 Колонки файла: {list(df.columns)}") | |
| # Проверяем наличие необходимых колонок (русских или английских) | |
| required_russian = ['Образец \nпектина', 't, мин', 'T, °C', 'P, атм', 'pH'] | |
| required_english = ['sample', 'time_min', 'temperature_c', 'pressure_atm', 'ph'] | |
| has_russian = any(col in df.columns for col in required_russian) | |
| has_english = any(col in df.columns for col in required_english) | |
| if not has_russian and not has_english: | |
| st.error(f"❌ Отсутствуют обязательные колонки. Нужны либо русские: {required_russian}, либо английские: {required_english}") | |
| return False, df | |
| st.success("✅ Все необходимые колонки присутствуют") | |
| return True, df | |
| except Exception as e: | |
| st.error(f"❌ Не удалось прочитать файл: {e}") | |
| st.code(traceback.format_exc()) | |
| return False, None | |
| def calculate_regression_metrics(y_true, y_pred, target_name): | |
| """Вычисляет метрики регрессии""" | |
| try: | |
| # Удаляем NaN значения | |
| mask = ~(np.isnan(y_true) | np.isnan(y_pred)) | |
| y_true_clean = y_true[mask] | |
| y_pred_clean = y_pred[mask] | |
| if len(y_true_clean) == 0: | |
| return None | |
| metrics = {} | |
| # Базовые метрики | |
| metrics['mae'] = mean_absolute_error(y_true_clean, y_pred_clean) | |
| metrics['mse'] = mean_squared_error(y_true_clean, y_pred_clean) | |
| metrics['rmse'] = np.sqrt(metrics['mse']) | |
| # R² score | |
| if len(y_true_clean) > 1: | |
| metrics['r2'] = r2_score(y_true_clean, y_pred_clean) | |
| else: | |
| metrics['r2'] = np.nan | |
| # MAPE (Mean Absolute Percentage Error) | |
| if np.all(y_true_clean != 0): | |
| metrics['mape'] = np.mean(np.abs((y_true_clean - y_pred_clean) / y_true_clean)) * 100 | |
| else: | |
| metrics['mape'] = np.nan | |
| # Корреляция | |
| if len(y_true_clean) > 1: | |
| correlation, p_value = stats.pearsonr(y_true_clean, y_pred_clean) | |
| metrics['correlation'] = correlation | |
| metrics['correlation_p_value'] = p_value | |
| else: | |
| metrics['correlation'] = np.nan | |
| metrics['correlation_p_value'] = np.nan | |
| # Количество образцов | |
| metrics['n_samples'] = len(y_true_clean) | |
| # Диапазон истинных значений | |
| metrics['true_min'] = np.min(y_true_clean) | |
| metrics['true_max'] = np.max(y_true_clean) | |
| metrics['true_mean'] = np.mean(y_true_clean) | |
| metrics['true_std'] = np.std(y_true_clean) | |
| return metrics | |
| except Exception as e: | |
| st.error(f"❌ Ошибка вычисления метрик для {target_name}: {e}") | |
| return None | |
| def get_metric_quality(metric_name, value, target_name): | |
| """Определяет качество метрики""" | |
| if np.isnan(value): | |
| return "unknown" | |
| metric_info = METRIC_DESCRIPTIONS.get(metric_name, {}) | |
| perfect_value = metric_info.get('perfect_value', 0) | |
| better_lower = metric_info.get('better_lower', True) | |
| if metric_name == 'r2': | |
| if value >= 0.9: | |
| return "good" | |
| elif value >= 0.7: | |
| return "medium" | |
| else: | |
| return "poor" | |
| elif metric_name == 'correlation': | |
| if value >= 0.9: | |
| return "good" | |
| elif value >= 0.7: | |
| return "medium" | |
| else: | |
| return "poor" | |
| elif metric_name == 'mape': | |
| if value <= 10: | |
| return "good" | |
| elif value <= 20: | |
| return "medium" | |
| else: | |
| return "poor" | |
| elif metric_name in ['mae', 'mse', 'rmse']: | |
| # Оцениваем относительно диапазона целевой переменной | |
| target_range = TARGET_DESCRIPTIONS[target_name]['range'][1] - TARGET_DESCRIPTIONS[target_name]['range'][0] | |
| relative_error = value / target_range * 100 | |
| if relative_error <= 5: | |
| return "good" | |
| elif relative_error <= 15: | |
| return "medium" | |
| else: | |
| return "poor" | |
| return "unknown" | |
| def create_metric_card(metric_name, value, target_name, width=200): | |
| """Создает карточку с метрикой""" | |
| quality = get_metric_quality(metric_name, value, target_name) | |
| metric_info = METRIC_DESCRIPTIONS.get(metric_name, {}) | |
| if metric_name in ['r2', 'correlation']: | |
| display_value = f"{value:.3f}" | |
| elif metric_name == 'mape': | |
| display_value = f"{value:.1f}%" | |
| elif metric_name in ['mae', 'mse', 'rmse']: | |
| display_value = f"{value:.2f}" | |
| else: | |
| display_value = f"{value:.2f}" | |
| quality_class = { | |
| "good": "good-metric", | |
| "medium": "medium-metric", | |
| "poor": "poor-metric", | |
| "unknown": "" | |
| }.get(quality, "") | |
| card_html = f""" | |
| <div class="metric-card {quality_class}" style="width: {width}px; display: inline-block; margin: 5px;"> | |
| <div class="metric-name">{metric_info.get('name', metric_name)}</div> | |
| <div class="metric-value">{display_value}</div> | |
| <div class="metric-name">{target_name}</div> | |
| </div> | |
| """ | |
| return card_html | |
| def process_batch_file_single_model(uploaded_file, model_artifacts, model_name, skiprows=None): | |
| """Обработка файла с одной моделью""" | |
| try: | |
| st.info(f"🔄 Начинаем обработку файла с моделью {model_name}...") | |
| # Читаем файл | |
| df = read_flexible_pectin_file(uploaded_file, skiprows) | |
| if df is None or df.empty: | |
| st.error("❌ Файл пустой или не содержит данных") | |
| return None | |
| st.success(f"✅ Файл загружен: {len(df)} записей") | |
| # Применяем маппинг русских названий на английские | |
| columns_renamed = [] | |
| for rus_name, eng_name in COLUMN_MAPPING.items(): | |
| if rus_name in df.columns: | |
| df = df.rename(columns={rus_name: eng_name}) | |
| columns_renamed.append(f"{rus_name} -> {eng_name}") | |
| if columns_renamed: | |
| st.info(f"✅ Переименованы колонки: {columns_renamed}") | |
| # Преобразуем числовые колонки с русскими форматами | |
| numeric_columns = ['time_min', 'temperature_c', 'pressure_atm', 'ph', | |
| 'pectin_yield', 'galacturonic_acid', 'molecular_weight', 'esterification_degree'] | |
| for col in numeric_columns: | |
| if col in df.columns: | |
| df[col] = df[col].apply(parse_russian_number) | |
| st.info(f"✅ Преобразованы числовые данные в колонке: {col}") | |
| # Проверяем необходимые колонки | |
| required_columns = ['sample', 'time_min', 'temperature_c', 'pressure_atm', 'ph'] | |
| missing_columns = [col for col in required_columns if col not in df.columns] | |
| if missing_columns: | |
| st.error(f"❌ Отсутствуют обязательные колонки: {missing_columns}") | |
| st.info("📋 Доступные колонки в файле:") | |
| st.write(list(df.columns)) | |
| return None | |
| # Обрабатываем данные | |
| predictions = [] | |
| processed_data = [] | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| successful_predictions = 0 | |
| for idx, row in df.iterrows(): | |
| try: | |
| status_text.text(f"Обработка записи {idx+1} из {len(df)}...") | |
| progress_bar.progress((idx + 1) / len(df)) | |
| # Преобразуем строку в словарь | |
| input_data = row.to_dict() | |
| # Убираем ненужные колонки | |
| for col in list(input_data.keys()): | |
| if col not in required_columns and not col.startswith('predicted_'): | |
| if col != 'solid_liquid_ratio': # Сохраняем Т:Ж если есть | |
| del input_data[col] | |
| # Проверяем и очищаем данные | |
| for key in list(input_data.keys()): | |
| if pd.isna(input_data[key]) or input_data[key] == '': | |
| if key in required_columns: | |
| st.warning(f"⚠️ Строка {idx+1}: пропущено значение в колонке '{key}'") | |
| del input_data[key] | |
| # Проверяем наличие обязательных полей | |
| if all(field in input_data for field in required_columns): | |
| prediction = predict_single(input_data, model_artifacts) | |
| if prediction: | |
| predictions.append(prediction) | |
| processed_data.append(input_data) | |
| successful_predictions += 1 | |
| else: | |
| predictions.append({}) | |
| processed_data.append(input_data) | |
| else: | |
| missing = [field for field in required_columns if field not in input_data] | |
| st.warning(f"⚠️ Строка {idx+1}: отсутствуют поля {missing}") | |
| predictions.append({}) | |
| processed_data.append(input_data) | |
| except Exception as e: | |
| st.error(f"❌ Ошибка обработки строки {idx+1}: {e}") | |
| predictions.append({}) | |
| processed_data.append(row.to_dict() if 'row' in locals() else {}) | |
| status_text.text("✅ Обработка завершена!") | |
| progress_bar.empty() | |
| st.info(f"📊 Успешно обработано: {successful_predictions} из {len(df)} записей") | |
| # Создаем DataFrame с результатами | |
| result_df = pd.DataFrame(processed_data) | |
| # Добавляем предсказания с префиксом модели | |
| target_columns = model_artifacts["metadata"].get('target_columns', []) | |
| for i, target in enumerate(target_columns): | |
| result_df[f'{model_name}_{target}'] = [pred.get(target, None) for pred in predictions] | |
| # Сохраняем оригинальные данные если они есть | |
| original_columns = ['pectin_yield', 'galacturonic_acid', 'molecular_weight', 'esterification_degree'] | |
| for col in original_columns: | |
| if col in df.columns: | |
| result_df[f'original_{col}'] = df[col] | |
| return result_df | |
| except Exception as e: | |
| st.error(f"❌ Критическая ошибка обработки файла: {e}") | |
| st.markdown('<div class="error-box">', unsafe_allow_html=True) | |
| st.error("**Детали ошибки:**") | |
| st.code(traceback.format_exc()) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| return None | |
| def process_batch_file_multiple_models(uploaded_file, selected_models, skiprows=None): | |
| """Обработка файла с несколькими моделями и сравнение результатов""" | |
| try: | |
| st.info("🔄 Начинаем обработку файла с несколькими моделями...") | |
| # Сначала читаем и предобрабатываем файл один раз | |
| df = read_flexible_pectin_file(uploaded_file, skiprows) | |
| if df is None or df.empty: | |
| st.error("❌ Файл пустой или не содержит данных") | |
| return None, {}, {}, {} | |
| st.success(f"✅ Файл загружен: {len(df)} записей") | |
| # Применяем маппинг русских названий на английские | |
| for rus_name, eng_name in COLUMN_MAPPING.items(): | |
| if rus_name in df.columns: | |
| df = df.rename(columns={rus_name: eng_name}) | |
| # Преобразуем числовые колонки с русскими форматами | |
| numeric_columns = ['time_min', 'temperature_c', 'pressure_atm', 'ph', | |
| 'pectin_yield', 'galacturonic_acid', 'molecular_weight', 'esterification_degree'] | |
| for col in numeric_columns: | |
| if col in df.columns: | |
| df[col] = df[col].apply(parse_russian_number) | |
| # Проверяем необходимые колонки | |
| required_columns = ['sample', 'time_min', 'temperature_c', 'pressure_atm', 'ph'] | |
| missing_columns = [col for col in required_columns if col not in df.columns] | |
| if missing_columns: | |
| st.error(f"❌ Отсутствуют обязательные колонки: {missing_columns}") | |
| return None, {}, {}, {} | |
| # Создаем базовый DataFrame с исходными данными | |
| base_columns = required_columns.copy() | |
| if 'solid_liquid_ratio' in df.columns: | |
| base_columns.append('solid_liquid_ratio') | |
| result_df = df[base_columns].copy() | |
| # Добавляем оригинальные целевые переменные если есть | |
| original_columns = ['pectin_yield', 'galacturonic_acid', 'molecular_weight', 'esterification_degree'] | |
| has_ground_truth = False | |
| for col in original_columns: | |
| if col in df.columns: | |
| result_df[f'original_{col}'] = df[col] | |
| has_ground_truth = True | |
| # Обрабатываем данные для каждой модели | |
| all_predictions = {} | |
| model_results = {} | |
| all_metrics = {} | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| for model_idx, model_name in enumerate(selected_models): | |
| try: | |
| status_text.text(f"🔄 Обработка моделью {model_name} ({model_idx+1}/{len(selected_models)})...") | |
| progress_bar.progress(model_idx / len(selected_models)) | |
| # Загружаем модель | |
| model_artifacts = load_pectin_model(model_name) | |
| if model_artifacts["status"] not in ["success", "demo"]: | |
| st.error(f"❌ Не удалось загрузить модель {model_name}") | |
| continue | |
| # Получаем предсказания для всех строк | |
| model_predictions = [] | |
| successful_predictions = 0 | |
| for idx, row in df.iterrows(): | |
| try: | |
| input_data = row[required_columns].to_dict() | |
| prediction = predict_single(input_data, model_artifacts) | |
| if prediction: | |
| model_predictions.append(prediction) | |
| successful_predictions += 1 | |
| else: | |
| model_predictions.append({}) | |
| except Exception as e: | |
| st.error(f"❌ Ошибка предсказания моделью {model_name} для строки {idx+1}: {e}") | |
| model_predictions.append({}) | |
| # Сохраняем предсказания | |
| all_predictions[model_name] = model_predictions | |
| model_results[model_name] = { | |
| 'successful': successful_predictions, | |
| 'total': len(df), | |
| 'artifacts': model_artifacts | |
| } | |
| # Добавляем предсказания в результат | |
| target_columns = model_artifacts["metadata"].get('target_columns', []) | |
| for target in target_columns: | |
| result_df[f'{model_name}_{target}'] = [pred.get(target, None) for pred in model_predictions] | |
| # Вычисляем метрики если есть ground truth | |
| model_metrics = {} | |
| if has_ground_truth: | |
| for target in target_columns: | |
| original_col = f'original_{target}' | |
| predicted_col = f'{model_name}_{target}' | |
| if original_col in result_df.columns and predicted_col in result_df.columns: | |
| y_true = result_df[original_col].values | |
| y_pred = result_df[predicted_col].values | |
| metrics = calculate_regression_metrics(y_true, y_pred, target) | |
| if metrics: | |
| model_metrics[target] = metrics | |
| all_metrics[model_name] = model_metrics | |
| st.success(f"✅ Модель {model_name}: {successful_predictions}/{len(df)} успешных предсказаний") | |
| except Exception as e: | |
| st.error(f"❌ Ошибка обработки моделью {model_name}: {e}") | |
| continue | |
| progress_bar.progress(1.0) | |
| status_text.text("✅ Все модели обработаны!") | |
| return result_df, all_predictions, model_results, all_metrics | |
| except Exception as e: | |
| st.error(f"❌ Критическая ошибка обработки файла: {e}") | |
| st.markdown('<div class="error-box">', unsafe_allow_html=True) | |
| st.error("**Детали ошибки:**") | |
| st.code(traceback.format_exc()) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| return None, {}, {}, {} | |
| # Обновите функцию create_comparison_visualizations следующим образом: | |
| def create_comparison_visualizations(result_df, selected_models, target_columns, all_metrics): | |
| """Создает визуализации для сравнения моделей""" | |
| try: | |
| st.markdown("### 📊 Визуальное сравнение моделей") | |
| # Создаем вкладки для разных типов визуализаций | |
| viz_tab1, viz_tab2, viz_tab3, viz_tab4 = st.tabs([ | |
| "📈 Сравнение по целевым переменным", | |
| "📋 Сводная статистика", | |
| "🔍 Детальный анализ", | |
| "📉 Метрики качества" | |
| ]) | |
| with viz_tab1: | |
| # Графики для каждой целевой переменной | |
| for target in target_columns: | |
| st.markdown(f"#### {TARGET_DESCRIPTIONS[target]['name']}") | |
| # Собираем данные для графика | |
| plot_data = [] | |
| for model in selected_models: | |
| col_name = f'{model}_{target}' | |
| if col_name in result_df.columns: | |
| values = result_df[col_name].dropna() | |
| if len(values) > 0: | |
| plot_data.append({ | |
| 'model': model, | |
| 'values': values, | |
| 'mean': values.mean(), | |
| 'std': values.std() | |
| }) | |
| if len(plot_data) > 0: | |
| # Создаем subplot для разных типов графиков | |
| fig = make_subplots( | |
| rows=1, cols=2, | |
| subplot_titles=('Распределение предсказаний', 'Сравнение средних значений'), | |
| specs=[[{"type": "box"}, {"type": "bar"}]] | |
| ) | |
| # Box plot | |
| for i, data in enumerate(plot_data): | |
| fig.add_trace( | |
| go.Box( | |
| y=data['values'], | |
| name=data['model'], | |
| marker_color=AVAILABLE_MODELS[data['model']]['color'], | |
| showlegend=False | |
| ), | |
| row=1, col=1 | |
| ) | |
| # Bar plot со средними значениями | |
| models = [data['model'] for data in plot_data] | |
| means = [data['mean'] for data in plot_data] | |
| stds = [data['std'] for data in plot_data] | |
| fig.add_trace( | |
| go.Bar( | |
| x=models, | |
| y=means, | |
| error_y=dict(type='data', array=stds, visible=True), | |
| marker_color=[AVAILABLE_MODELS[model]['color'] for model in models], | |
| showlegend=False | |
| ), | |
| row=1, col=2 | |
| ) | |
| fig.update_layout( | |
| height=400, | |
| title_text=f"Сравнение моделей: {TARGET_DESCRIPTIONS[target]['name']}", | |
| showlegend=False | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.info(f"ℹ️ Нет данных для отображения по целевой переменной {target}") | |
| with viz_tab2: | |
| # Сводная статистика по моделям | |
| st.markdown("#### 📋 Сводная статистика по моделям") | |
| stats_data = [] | |
| for target in target_columns: | |
| for model in selected_models: | |
| col_name = f'{model}_{target}' | |
| if col_name in result_df.columns: | |
| values = result_df[col_name].dropna() | |
| if len(values) > 0: | |
| stats_data.append({ | |
| 'Модель': model, | |
| 'Целевая переменная': TARGET_DESCRIPTIONS[target]['name'], | |
| 'Среднее': float(values.mean()), | |
| 'Стандартное отклонение': float(values.std()), | |
| 'Минимум': float(values.min()), | |
| 'Максимум': float(values.max()), | |
| 'Количество предсказаний': int(len(values)) | |
| }) | |
| if stats_data: | |
| stats_df = pd.DataFrame(stats_data) | |
| # Форматируем числовые колонки | |
| formatted_stats = stats_df.copy() | |
| numeric_columns = ['Среднее', 'Стандартное отклонение', 'Минимум', 'Максимум'] | |
| for col in numeric_columns: | |
| if col in formatted_stats.columns: | |
| if col == 'Стандартное отклонение': | |
| formatted_stats[col] = formatted_stats[col].apply(lambda x: f"{x:.4f}") | |
| else: | |
| formatted_stats[col] = formatted_stats[col].apply(lambda x: f"{x:.2f}") | |
| st.dataframe(formatted_stats, width='stretch') | |
| # Pivot таблица для удобного сравнения | |
| try: | |
| pivot_mean = stats_df.pivot_table( | |
| index='Модель', | |
| columns='Целевая переменная', | |
| values='Среднее', | |
| aggfunc='first' | |
| ) | |
| # Форматируем pivot таблицу | |
| formatted_pivot = pivot_mean.copy() | |
| for col in formatted_pivot.columns: | |
| formatted_pivot[col] = formatted_pivot[col].apply(lambda x: f"{float(x):.2f}" if pd.notna(x) else "N/A") | |
| st.markdown("#### 📊 Сравнение средних значений") | |
| st.dataframe(formatted_pivot, width='stretch') | |
| except Exception as e: | |
| st.warning(f"Не удалось создать сводную таблицу: {e}") | |
| else: | |
| st.info("ℹ️ Нет данных для статистического анализа") | |
| with viz_tab3: | |
| # Детальный анализ | |
| st.markdown("#### 🔍 Детальный анализ предсказаний") | |
| # Выбор строки для детального анализа | |
| if len(result_df) > 0: | |
| # Создаем описания для выпадающего списка | |
| options = [] | |
| for i in range(len(result_df)): | |
| try: | |
| sample = str(result_df.iloc[i]['sample']) | |
| time_min = float(result_df.iloc[i]['time_min']) | |
| temperature_c = float(result_df.iloc[i]['temperature_c']) | |
| description = f"Запись {i+1}: {sample} - t={time_min:.1f}мин, T={temperature_c:.1f}°C" | |
| options.append((i, description)) | |
| except (KeyError, ValueError, TypeError): | |
| options.append((i, f"Запись {i+1}")) | |
| selected_row_idx = st.selectbox( | |
| "Выберите запись для детального анализа:", | |
| options=[opt[0] for opt in options], | |
| format_func=lambda x: next((opt[1] for opt in options if opt[0] == x), f"Запись {x+1}") | |
| ) | |
| if selected_row_idx is not None: | |
| row_data = result_df.iloc[selected_row_idx] | |
| # Создаем график сравнения предсказаний для выбранной строки | |
| comparison_data = [] | |
| for target in target_columns: | |
| for model in selected_models: | |
| col_name = f'{model}_{target}' | |
| if col_name in result_df.columns: | |
| value = row_data[col_name] | |
| if pd.notna(value): | |
| try: | |
| comparison_data.append({ | |
| 'Модель': model, | |
| 'Целевая переменная': TARGET_DESCRIPTIONS[target]['name'], | |
| 'Значение': float(value), | |
| 'Цвет': AVAILABLE_MODELS[model]['color'] | |
| }) | |
| except (TypeError, ValueError): | |
| continue | |
| if comparison_data: | |
| comp_df = pd.DataFrame(comparison_data) | |
| fig = px.bar( | |
| comp_df, | |
| x='Целевая переменная', | |
| y='Значение', | |
| color='Модель', | |
| barmode='group', | |
| title=f"Сравнение предсказаний для записи {selected_row_idx+1}", | |
| color_discrete_map={model: AVAILABLE_MODELS[model]['color'] for model in selected_models} | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # Показываем детальные значения | |
| st.markdown("#### 📋 Численные значения предсказаний") | |
| detail_data = [] | |
| for target in target_columns: | |
| try: | |
| row_detail = {'Целевая переменная': TARGET_DESCRIPTIONS[target]['name']} | |
| for model in selected_models: | |
| col_name = f'{model}_{target}' | |
| if col_name in result_df.columns: | |
| value = row_data[col_name] | |
| if pd.notna(value): | |
| row_detail[model] = f"{float(value):.2f}" | |
| else: | |
| row_detail[model] = 'N/A' | |
| # Добавляем оригинальное значение если есть | |
| orig_col = f'original_{target}' | |
| if orig_col in result_df.columns and pd.notna(row_data[orig_col]): | |
| row_detail['Оригинальное значение'] = f"{float(row_data[orig_col]):.2f}" | |
| detail_data.append(row_detail) | |
| except (KeyError, TypeError, ValueError): | |
| continue | |
| if detail_data: | |
| detail_df = pd.DataFrame(detail_data) | |
| st.dataframe(detail_df, width='stretch') | |
| with viz_tab4: | |
| # Вкладка с метриками качества | |
| st.markdown("#### 📉 Метрики качества моделей") | |
| if not all_metrics or not any(all_metrics.values()): | |
| st.info(""" | |
| ℹ️ **Для вычисления метрик необходимы оригинальные значения (ground truth) в загруженном файле** | |
| Убедитесь, что ваш файл содержит колонки с настоящими значениями: | |
| - `pectin_yield` (ПВ, %) | |
| - `galacturonic_acid` (ГК, %) | |
| - `molecular_weight` (Mw, Д) | |
| - `esterification_degree` (СЭ, %) | |
| """) | |
| return | |
| # Создаем таблицу с метриками для каждой модели и целевой переменной | |
| metrics_data = [] | |
| for model_name, model_metrics in all_metrics.items(): | |
| for target_name, metrics in model_metrics.items(): | |
| for metric_name, metric_value in metrics.items(): | |
| if metric_name in ['mae', 'mse', 'rmse', 'r2', 'mape', 'correlation']: | |
| try: | |
| # Преобразуем значение в float для безопасного форматирования | |
| float_value = float(metric_value) if metric_value is not None else float('nan') | |
| metrics_data.append({ | |
| 'Модель': model_name, | |
| 'Целевая переменная': TARGET_DESCRIPTIONS[target_name]['name'], | |
| 'Метрика': METRIC_DESCRIPTIONS[metric_name]['name'], | |
| 'Значение': float_value, | |
| 'metric_type': metric_name, | |
| 'target_type': target_name | |
| }) | |
| except (TypeError, ValueError): | |
| continue | |
| if metrics_data: | |
| metrics_df = pd.DataFrame(metrics_data) | |
| # Pivot таблица для удобного сравнения | |
| try: | |
| pivot_metrics = metrics_df.pivot_table( | |
| index=['Модель', 'Целевая переменная'], | |
| columns='Метрика', | |
| values='Значение', | |
| aggfunc='first' | |
| ).reset_index() | |
| # Форматируем числовые колонки | |
| formatted_pivot = pivot_metrics.copy() | |
| for col in formatted_pivot.columns: | |
| if col not in ['Модель', 'Целевая переменная']: | |
| formatted_pivot[col] = formatted_pivot[col].apply( | |
| lambda x: f"{float(x):.4f}" if pd.notna(x) else "N/A" | |
| ) | |
| st.dataframe(formatted_pivot, width='stretch') | |
| except Exception as e: | |
| st.warning(f"Не удалось создать сводную таблицу метрик: {e}") | |
| st.dataframe(metrics_df, width='stretch') | |
| # Визуализация метрик | |
| st.markdown("#### 📊 Визуализация метрик качества") | |
| # Выбор метрики для визуализации | |
| available_metrics = [m for m in metrics_df['metric_type'].unique() if m in METRIC_DESCRIPTIONS] | |
| if available_metrics: | |
| selected_metric = st.selectbox( | |
| "Выберите метрику для визуализации:", | |
| options=available_metrics, | |
| format_func=lambda x: METRIC_DESCRIPTIONS.get(x, {}).get('name', x) | |
| ) | |
| if selected_metric: | |
| # Фильтруем данные для выбранной метрики | |
| metric_data = metrics_df[metrics_df['metric_type'] == selected_metric] | |
| if len(metric_data) > 0: | |
| # Преобразуем значения в float для построения графика | |
| metric_data = metric_data.copy() | |
| metric_data['Числовое значение'] = metric_data['Значение'].apply( | |
| lambda x: float(x) if pd.notna(x) else float('nan') | |
| ) | |
| fig = px.bar( | |
| metric_data, | |
| x='Модель', | |
| y='Числовое значение', | |
| color='Целевая переменная', | |
| barmode='group', | |
| title=f"{METRIC_DESCRIPTIONS[selected_metric]['name']} по моделям", | |
| labels={'Числовое значение': METRIC_DESCRIPTIONS[selected_metric]['name']} | |
| ) | |
| # Добавляем линию идеального значения | |
| perfect_value = METRIC_DESCRIPTIONS[selected_metric].get('perfect_value', 0) | |
| better_lower = METRIC_DESCRIPTIONS[selected_metric].get('better_lower', True) | |
| if not better_lower: | |
| # Для метрик где лучше большее значение (R², корреляция) | |
| fig.add_hline( | |
| y=perfect_value, | |
| line_dash="dash", | |
| line_color="green", | |
| annotation_text="Идеальное значение" | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.info("ℹ️ Нет доступных метрик для визуализации") | |
| else: | |
| st.info("ℹ️ Нет данных метрик для отображения") | |
| except Exception as e: | |
| st.error(f"❌ Ошибка создания визуализаций: {str(e)}") | |
| st.markdown('<div class="error-box">', unsafe_allow_html=True) | |
| st.error("**Детали ошибки:**") | |
| st.code(traceback.format_exc()) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Также обновите функцию display_metrics_dashboard: | |
| def display_metrics_dashboard(all_metrics, selected_models): | |
| """Создает дашборд с метриками качества""" | |
| try: | |
| st.markdown("### 📊 Дашборд метрик качества") | |
| if not all_metrics or not any(all_metrics.values()): | |
| st.info(""" | |
| ℹ️ **Для отображения метрик качества необходимы оригинальные значения в загруженном файле** | |
| Убедитесь, что ваш файл содержит колонки с настоящими значениями: | |
| - `pectin_yield` (ПВ, %) | |
| - `galacturonic_acid` (ГК, %) | |
| - `molecular_weight` (Mw, Д) | |
| - `esterification_degree` (СЭ, %) | |
| """) | |
| return | |
| # Создаем вкладки для разных представлений метрик | |
| metric_tab1, metric_tab2, metric_tab3 = st.tabs(["🎯 Карточки метрик", "📈 Сравнительные графики", "📋 Детальная таблица"]) | |
| with metric_tab1: | |
| st.markdown("#### 🎯 Основные метрики качества") | |
| # Показываем карточки с метриками для каждой модели и целевой переменной | |
| for model_name in selected_models: | |
| if model_name in all_metrics and all_metrics[model_name]: | |
| st.markdown(f"##### 🧮 Модель: {model_name}") | |
| model_metrics = all_metrics[model_name] | |
| cols = st.columns(4) | |
| for idx, (target_name, metrics) in enumerate(model_metrics.items()): | |
| with cols[idx % 4]: | |
| # Показываем основные метрики в карточках | |
| for metric_name in ['r2', 'mae', 'rmse', 'mape']: | |
| if metric_name in metrics and metrics[metric_name] is not None: | |
| try: | |
| value = float(metrics[metric_name]) | |
| card_html = create_metric_card( | |
| metric_name, | |
| value, | |
| target_name | |
| ) | |
| st.markdown(card_html, unsafe_allow_html=True) | |
| except (TypeError, ValueError): | |
| continue | |
| with metric_tab2: | |
| st.markdown("#### 📈 Сравнение метрик между моделями") | |
| # Создаем сравнительные графики для основных метрик | |
| comparison_metrics = ['r2', 'mae', 'rmse', 'mape'] | |
| for metric in comparison_metrics: | |
| st.markdown(f"##### {METRIC_DESCRIPTIONS[metric]['name']}") | |
| # Собираем данные для графика | |
| plot_data = [] | |
| for model_name in selected_models: | |
| if model_name in all_metrics: | |
| for target_name, metrics in all_metrics[model_name].items(): | |
| if metric in metrics and metrics[metric] is not None: | |
| try: | |
| value = float(metrics[metric]) | |
| plot_data.append({ | |
| 'Модель': model_name, | |
| 'Целевая переменная': TARGET_DESCRIPTIONS[target_name]['name'], | |
| 'Значение': value, | |
| 'Метрика': METRIC_DESCRIPTIONS[metric]['name'] | |
| }) | |
| except (TypeError, ValueError): | |
| continue | |
| if plot_data: | |
| plot_df = pd.DataFrame(plot_data) | |
| fig = px.bar( | |
| plot_df, | |
| x='Модель', | |
| y='Значение', | |
| color='Целевая переменная', | |
| barmode='group', | |
| title=f"Сравнение {METRIC_DESCRIPTIONS[metric]['name']}", | |
| labels={'Значение': METRIC_DESCRIPTIONS[metric]['name']} | |
| ) | |
| # Добавляем линию идеального значения если применимо | |
| perfect_value = METRIC_DESCRIPTIONS[metric].get('perfect_value', 0) | |
| if METRIC_DESCRIPTIONS[metric].get('better_lower', True): | |
| # Для метрик где лучше меньшее значение | |
| fig.add_hline(y=perfect_value, line_dash="dash", line_color="red") | |
| else: | |
| # Для метрик где лучше большее значение | |
| fig.add_hline(y=perfect_value, line_dash="dash", line_color="green") | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.info(f"ℹ️ Нет данных для метрики {METRIC_DESCRIPTIONS[metric]['name']}") | |
| with metric_tab3: | |
| st.markdown("#### 📋 Детальная таблица метрик") | |
| # Создаем подробную таблицу со всеми метриками | |
| detailed_data = [] | |
| for model_name in selected_models: | |
| if model_name in all_metrics: | |
| for target_name, metrics in all_metrics[model_name].items(): | |
| row = { | |
| 'Модель': model_name, | |
| 'Целевая переменная': TARGET_DESCRIPTIONS[target_name]['name'], | |
| 'Количество образцов': int(metrics.get('n_samples', 0)) | |
| } | |
| # Добавляем все вычисленные метрики | |
| for metric_name in ['mae', 'mse', 'rmse', 'r2', 'mape', 'correlation']: | |
| if metric_name in metrics and metrics[metric_name] is not None: | |
| try: | |
| value = float(metrics[metric_name]) | |
| row[METRIC_DESCRIPTIONS[metric_name]['name']] = value | |
| except (TypeError, ValueError): | |
| row[METRIC_DESCRIPTIONS[metric_name]['name']] = 'N/A' | |
| detailed_data.append(row) | |
| if detailed_data: | |
| detailed_df = pd.DataFrame(detailed_data) | |
| # Форматируем числовые колонки | |
| format_dict = {} | |
| for metric_name in ['mae', 'mse', 'rmse', 'mape']: | |
| display_name = METRIC_DESCRIPTIONS[metric_name]['name'] | |
| if display_name in detailed_df.columns: | |
| # Создаем форматированную версию | |
| detailed_df[display_name] = detailed_df[display_name].apply( | |
| lambda x: f"{float(x):.4f}" if x != 'N/A' and pd.notna(x) else 'N/A' | |
| ) | |
| for metric_name in ['r2', 'correlation']: | |
| display_name = METRIC_DESCRIPTIONS[metric_name]['name'] | |
| if display_name in detailed_df.columns: | |
| detailed_df[display_name] = detailed_df[display_name].apply( | |
| lambda x: f"{float(x):.3f}" if x != 'N/A' and pd.notna(x) else 'N/A' | |
| ) | |
| st.dataframe(detailed_df, width='stretch') | |
| # Скачивание таблицы метрик | |
| csv_metrics = detailed_df.to_csv(index=False, encoding='utf-8-sig') | |
| st.download_button( | |
| label="📥 Скачать таблицу метрик (CSV)", | |
| data=csv_metrics, | |
| file_name=f"pectin_metrics_comparison_{pd.Timestamp.now().strftime('%Y%m%d_%H%M')}.csv", | |
| mime="text/csv" | |
| ) | |
| else: | |
| st.info("ℹ️ Нет данных для создания детальной таблицы метрик") | |
| except Exception as e: | |
| st.error(f"❌ Ошибка создания дашборда метрик: {str(e)}") | |
| st.markdown('<div class="error-box">', unsafe_allow_html=True) | |
| st.error("**Детали ошибки:**") | |
| st.code(traceback.format_exc()) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| def display_metrics_dashboard(all_metrics, selected_models): | |
| """Создает дашборд с метриками качества""" | |
| try: | |
| st.markdown("### 📊 Дашборд метрик качества") | |
| if not all_metrics or not any(all_metrics.values()): | |
| st.info(""" | |
| ℹ️ **Для отображения метрик качества необходимы оригинальные значения в загруженном файле** | |
| Убедитесь, что ваш файл содержит колонки с настоящими значениями: | |
| - `pectin_yield` (ПВ, %) | |
| - `galacturonic_acid` (ГК, %) | |
| - `molecular_weight` (Mw, Д) | |
| - `esterification_degree` (СЭ, %) | |
| """) | |
| return | |
| # Создаем вкладки для разных представлений метрик | |
| metric_tab1, metric_tab2, metric_tab3 = st.tabs(["🎯 Карточки метрик", "📈 Сравнительные графики", "📋 Детальная таблица"]) | |
| with metric_tab1: | |
| st.markdown("#### 🎯 Основные метрики качества") | |
| # Показываем карточки с метриками для каждой модели и целевой переменной | |
| for model_name in selected_models: | |
| if model_name in all_metrics and all_metrics[model_name]: | |
| st.markdown(f"##### 🧮 Модель: {model_name}") | |
| model_metrics = all_metrics[model_name] | |
| cols = st.columns(4) | |
| for idx, (target_name, metrics) in enumerate(model_metrics.items()): | |
| with cols[idx % 4]: | |
| # Показываем основные метрики в карточках | |
| for metric_name in ['r2', 'mae', 'rmse', 'mape']: | |
| if metric_name in metrics and not np.isnan(metrics[metric_name]): | |
| card_html = create_metric_card( | |
| metric_name, | |
| metrics[metric_name], | |
| target_name | |
| ) | |
| st.markdown(card_html, unsafe_allow_html=True) | |
| with metric_tab2: | |
| st.markdown("#### 📈 Сравнение метрик между моделями") | |
| # Создаем сравнительные графики для основных метрик | |
| comparison_metrics = ['r2', 'mae', 'rmse', 'mape'] | |
| for metric in comparison_metrics: | |
| st.markdown(f"##### {METRIC_DESCRIPTIONS[metric]['name']}") | |
| # Собираем данные для графика | |
| plot_data = [] | |
| for model_name in selected_models: | |
| if model_name in all_metrics: | |
| for target_name, metrics in all_metrics[model_name].items(): | |
| if metric in metrics and not np.isnan(metrics[metric]): | |
| plot_data.append({ | |
| 'Модель': model_name, | |
| 'Целевая переменная': TARGET_DESCRIPTIONS[target_name]['name'], | |
| 'Значение': metrics[metric], | |
| 'Метрика': METRIC_DESCRIPTIONS[metric]['name'] | |
| }) | |
| if plot_data: | |
| plot_df = pd.DataFrame(plot_data) | |
| fig = px.bar( | |
| plot_df, | |
| x='Модель', | |
| y='Значение', | |
| color='Целевая переменная', | |
| barmode='group', | |
| title=f"Сравнение {METRIC_DESCRIPTIONS[metric]['name']}", | |
| labels={'Значение': METRIC_DESCRIPTIONS[metric]['name']} | |
| ) | |
| # Добавляем линию идеального значения если применимо | |
| perfect_value = METRIC_DESCRIPTIONS[metric].get('perfect_value', 0) | |
| if METRIC_DESCRIPTIONS[metric].get('better_lower', True): | |
| # Для метрик где лучше меньшее значение | |
| fig.add_hline(y=perfect_value, line_dash="dash", line_color="red") | |
| else: | |
| # Для метрик где лучше большее значение | |
| fig.add_hline(y=perfect_value, line_dash="dash", line_color="green") | |
| st.plotly_chart(fig, use_container_width=True) | |
| with metric_tab3: | |
| st.markdown("#### 📋 Детальная таблица метрик") | |
| # Создаем подробную таблицу со всеми метриками | |
| detailed_data = [] | |
| for model_name in selected_models: | |
| if model_name in all_metrics: | |
| for target_name, metrics in all_metrics[model_name].items(): | |
| row = { | |
| 'Модель': model_name, | |
| 'Целевая переменная': TARGET_DESCRIPTIONS[target_name]['name'], | |
| 'Количество образцов': metrics.get('n_samples', 0) | |
| } | |
| # Добавляем все вычисленные метрики | |
| for metric_name in ['mae', 'mse', 'rmse', 'r2', 'mape', 'correlation']: | |
| if metric_name in metrics: | |
| row[METRIC_DESCRIPTIONS[metric_name]['name']] = metrics[metric_name] | |
| detailed_data.append(row) | |
| if detailed_data: | |
| detailed_df = pd.DataFrame(detailed_data) | |
| # Форматируем числовые колонки | |
| format_dict = {} | |
| for metric_name in ['mae', 'mse', 'rmse', 'mape']: | |
| display_name = METRIC_DESCRIPTIONS[metric_name]['name'] | |
| if display_name in detailed_df.columns: | |
| format_dict[display_name] = "{:.4f}" | |
| for metric_name in ['r2', 'correlation']: | |
| display_name = METRIC_DESCRIPTIONS[metric_name]['name'] | |
| if display_name in detailed_df.columns: | |
| format_dict[display_name] = "{:.3f}" | |
| st.dataframe(detailed_df.style.format(format_dict), width='stretch') | |
| # Скачивание таблицы метрик | |
| csv_metrics = detailed_df.to_csv(index=False, encoding='utf-8-sig') | |
| st.download_button( | |
| label="📥 Скачать таблицу метрик (CSV)", | |
| data=csv_metrics, | |
| file_name=f"pectin_metrics_comparison_{pd.Timestamp.now().strftime('%Y%m%d_%H%M')}.csv", | |
| mime="text/csv" | |
| ) | |
| except Exception as e: | |
| st.error(f"❌ Ошибка создания дашборда метрик: {e}") | |
| # Main app layout | |
| st.markdown('<h1 class="main-header">🧪 Pectin Production Predictor</h1>', unsafe_allow_html=True) | |
| # Sidebar | |
| with st.sidebar: | |
| st.markdown("### ⚙️ Настройки модели") | |
| selected_model = st.selectbox( | |
| "Выберите модель:", | |
| options=list(AVAILABLE_MODELS.keys()), | |
| format_func=lambda x: f"{x} - {AVAILABLE_MODELS[x]['description'].split(' - ')[0]}" | |
| ) | |
| model_info = AVAILABLE_MODELS[selected_model] | |
| st.info(f"**Описание:** {model_info['description']}") | |
| # Load model | |
| with st.spinner(f"Загружаем {selected_model}..."): | |
| model_artifacts = load_pectin_model(selected_model) | |
| if model_artifacts["status"] == "success": | |
| st.success("✅ Модель готова к использованию") | |
| # Display model info | |
| metadata = model_artifacts["metadata"] | |
| st.markdown("### 📊 Информация о модели") | |
| st.write(f"**Признаки:** {len(metadata.get('feature_columns', []))}") | |
| st.write(f"**Целевые переменные:** {len(metadata.get('target_columns', []))}") | |
| st.write(f"**Размер словаря:** {len(model_artifacts['label_encoder'].classes_)}") | |
| elif model_artifacts["status"] == "demo": | |
| st.warning("🎭 Работаем в демо-режиме") | |
| st.info("Для использования реальных моделей проверьте:") | |
| st.info("1. Доступ к интернету") | |
| st.info("2. Наличие репозитория на Hugging Face") | |
| st.info("3. Правильность REPO_ID в настройках") | |
| else: | |
| st.error("❌ Не удалось загрузить модель") | |
| st.markdown("### 🔧 Решение проблем") | |
| st.info(""" | |
| 1. **Проверьте интернет-соединение** | |
| 2. **Убедитесь что репозиторий существует**: https://huggingface.co/arabovs-ai-lab/PectinProductionModels | |
| 3. **Используйте локальные модели** - поместите файлы в папку models/ | |
| 4. **Измените REPO_ID** на ваш репозиторий | |
| """) | |
| if st.button("🔄 Попробовать снова"): | |
| st.cache_resource.clear() | |
| st.rerun() | |
| # Main content tabs | |
| tab1, tab2, tab3, tab4 = st.tabs(["🎯 Одиночное предсказание", "📁 Пакетная обработка", "📊 Сравнение моделей", "🔄 Мультимодельная обработка"]) | |
| with tab1: | |
| st.markdown('<div class="section-header">🎯 Одиночное предсказание</div>', unsafe_allow_html=True) | |
| col1, col2 = st.columns([1, 1]) | |
| with col1: | |
| st.markdown("### 📝 Параметры процесса") | |
| with st.form("single_prediction_form"): | |
| sample = st.selectbox("Тип сырья:", options=SAMPLE_TYPES, help="Выберите тип исходного сырья") | |
| time_min = st.number_input("Время экстракции (мин):", min_value=0.0, max_value=300.0, value=5.0, step=1.0) | |
| temperature_c = st.number_input("Температура (°C):", min_value=0.0, max_value=200.0, value=120.0, step=1.0) | |
| pressure_atm = st.number_input("Давление (атм):", min_value=0.0, max_value=10.0, value=1.0, step=0.1) | |
| ph = st.number_input("pH:", min_value=0.0, max_value=14.0, value=2.5, step=0.1) | |
| submitted = st.form_submit_button("🎯 Выполнить предсказание") | |
| with col2: | |
| st.markdown("### 📊 Результаты предсказания") | |
| if submitted: | |
| input_data = { | |
| 'sample': sample, | |
| 'time_min': time_min, | |
| 'temperature_c': temperature_c, | |
| 'pressure_atm': pressure_atm, | |
| 'ph': ph | |
| } | |
| with st.spinner("🔮 Выполняем предсказание..."): | |
| prediction = predict_single(input_data, model_artifacts) | |
| if prediction: | |
| st.markdown('<div class="prediction-box">', unsafe_allow_html=True) | |
| if model_artifacts.get("status") == "demo": | |
| st.warning("🎭 Демо-режим: используются тестовые данные") | |
| else: | |
| st.success("✅ Предсказание успешно выполнено!") | |
| # Display predictions as metrics | |
| target_descriptions = { | |
| 'pectin_yield': 'Выход пектина (%)', | |
| 'galacturonic_acid': 'ГК (%)', | |
| 'molecular_weight': 'Мол. масса (Da)', | |
| 'esterification_degree': 'СЭ (%)' | |
| } | |
| cols = st.columns(2) | |
| for idx, (target, value) in enumerate(prediction.items()): | |
| with cols[idx % 2]: | |
| desc = target_descriptions.get(target, target) | |
| st.metric( | |
| label=desc, | |
| value=f"{value:.2f}" if target != 'molecular_weight' else f"{value:.0f}" | |
| ) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Show input parameters | |
| with st.expander("📋 Параметры ввода"): | |
| st.json(input_data) | |
| with tab2: | |
| st.markdown('<div class="section-header">📁 Пакетная обработка файлов</div>', unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="info-box"> | |
| <h4>🎯 Поддерживаемые форматы</h4> | |
| <ul> | |
| <li><strong>Excel (.xlsx)</strong> - автоматическое определение структуры</li> | |
| <li><strong>CSV/TXT</strong> - с разными разделителями и кодировками</li> | |
| </ul> | |
| <h4>📊 Обязательные поля</h4> | |
| <ul> | |
| <li><strong>sample</strong> (Образец пектина) - тип сырья</li> | |
| <li><strong>time_min</strong> (t, мин) - время экстракции</li> | |
| <li><strong>temperature_c</strong> (T, °C) - температура</li> | |
| <li><strong>pressure_atm</strong> (P, атм) - давление</li> | |
| <li><strong>ph</strong> (pH) - уровень кислотности</li> | |
| </ul> | |
| <h4>🔧 Особенности обработки</h4> | |
| <ul> | |
| <li>Автоматическое определение кодировки и разделителей</li> | |
| <li>Поддержка русских чисел с запятыми (2,08 → 2.08)</li> | |
| <li>Гибкая настройка пропуска строк</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| uploaded_file = st.file_uploader( | |
| "Загрузите файл с данными", | |
| type=['xlsx', 'csv', 'txt'], | |
| help="Поддерживаются Excel, CSV и текстовые файлы с данными экспериментов" | |
| ) | |
| if uploaded_file is not None: | |
| st.markdown("### ⚙️ Настройки обработки") | |
| col_set1, col_set2 = st.columns(2) | |
| with col_set1: | |
| skip_rows = st.number_input( | |
| "Пропустить строк в начале файла:", | |
| min_value=0, | |
| max_value=10, | |
| value=0, | |
| help="Используйте если в файле есть заголовки или пустые строки в начале" | |
| ) | |
| with col_set2: | |
| file_preview = st.checkbox("Показать предпросмотр файла", value=True) | |
| # Кнопка для проверки структуры файла | |
| if st.button("🔍 Проверить структуру файла", type="secondary"): | |
| is_valid, preview_df = validate_file_structure(uploaded_file, skip_rows) | |
| if is_valid: | |
| st.success("✅ Файл готов к обработке!") | |
| if file_preview and preview_df is not None: | |
| st.markdown("### 👀 Предпросмотр данных") | |
| st.dataframe(preview_df.head(10), width='stretch') | |
| if st.button("🚀 Обработать файл", type="primary"): | |
| with st.spinner("🔄 Обрабатываем файл..."): | |
| result_df = process_batch_file_single_model(uploaded_file, model_artifacts, selected_model, skip_rows) | |
| if result_df is not None: | |
| st.success(f"✅ Успешно обработано {len(result_df)} записей") | |
| if model_artifacts.get("status") == "demo": | |
| st.warning("🎭 Демо-режим: используются тестовые данные") | |
| # Показываем результаты | |
| st.markdown("### 📈 Результаты пакетной обработки") | |
| # Создаем красивый DataFrame для отображения | |
| display_columns = ['sample', 'time_min', 'temperature_c', 'pressure_atm', 'ph'] | |
| prediction_columns = [col for col in result_df.columns if col.startswith(f'{selected_model}_')] | |
| display_columns.extend(prediction_columns) | |
| # Выбираем только существующие колонки | |
| existing_columns = [col for col in display_columns if col in result_df.columns] | |
| display_df = result_df[existing_columns] | |
| st.dataframe(display_df, width='stretch') | |
| # Скачивание результатов | |
| csv = result_df.to_csv(index=False, encoding='utf-8-sig') | |
| st.download_button( | |
| label="📥 Скачать полные результаты (CSV)", | |
| data=csv, | |
| file_name=f"pectin_predictions_{selected_model}_{pd.Timestamp.now().strftime('%Y%m%d_%H%M')}.csv", | |
| mime="text/csv", | |
| help="Скачать все данные с предсказаниями в CSV формате" | |
| ) | |
| # Показываем статистику | |
| st.markdown("### 📊 Статистика предсказаний") | |
| if prediction_columns: | |
| stats_data = [] | |
| for col in prediction_columns: | |
| pred_values = result_df[col].dropna() | |
| if len(pred_values) > 0: | |
| stats_data.append({ | |
| 'Параметр': col.replace(f'{selected_model}_', ''), | |
| 'Среднее': pred_values.mean(), | |
| 'Стандартное отклонение': pred_values.std(), | |
| 'Минимум': pred_values.min(), | |
| 'Максимум': pred_values.max(), | |
| 'Успешных предсказаний': len(pred_values) | |
| }) | |
| if stats_data: | |
| stats_df = pd.DataFrame(stats_data) | |
| st.dataframe(stats_df, width='stretch') | |
| with tab3: | |
| st.markdown('<div class="section-header">📊 Сравнение моделей</div>', unsafe_allow_html=True) | |
| st.info("Сравните предсказания разных моделей на одних и тех же данных") | |
| col1, col2 = st.columns([1, 1]) | |
| with col1: | |
| st.markdown("### 📝 Тестовые параметры") | |
| compare_sample = st.selectbox("Тип сырья для сравнения:", options=SAMPLE_TYPES, key="compare_sample") | |
| compare_time = st.slider("Время экстракции (мин):", 0.0, 300.0, 5.0, key="compare_time") | |
| compare_temp = st.slider("Температура (°C):", 0.0, 200.0, 120.0, key="compare_temp") | |
| compare_pressure = st.slider("Давление (атм):", 0.0, 10.0, 1.0, key="compare_pressure") | |
| compare_ph = st.slider("pH:", 0.0, 14.0, 2.5, key="compare_ph") | |
| with col2: | |
| st.markdown("### 🎯 Выбор моделей") | |
| models_to_compare = st.multiselect( | |
| "Выберите моделей для сравнения:", | |
| options=list(AVAILABLE_MODELS.keys()), | |
| default=["best_model", "gradient_boosting", "random_forest"] | |
| ) | |
| if st.button("🔍 Сравнить модели", type="primary"): | |
| if not models_to_compare: | |
| st.warning("⚠️ Выберите хотя бы одну модель для сравнения") | |
| else: | |
| input_data = { | |
| 'sample': compare_sample, | |
| 'time_min': compare_time, | |
| 'temperature_c': compare_temp, | |
| 'pressure_atm': compare_pressure, | |
| 'ph': compare_ph | |
| } | |
| comparison_results = [] | |
| with st.spinner("🔍 Выполняем сравнение моделей..."): | |
| for model_name in models_to_compare: | |
| with st.spinner(f"Загружаем {model_name}..."): | |
| model_artifacts_compare = load_pectin_model(model_name) | |
| if model_artifacts_compare["status"] in ["success", "demo"]: | |
| prediction = predict_single(input_data, model_artifacts_compare) | |
| if prediction: | |
| comparison_results.append({ | |
| 'model': model_name, | |
| **prediction | |
| }) | |
| if comparison_results: | |
| st.success("✅ Сравнение завершено!") | |
| if any("demo" in str(r) for r in comparison_results): | |
| st.warning("🎭 Некоторые модели работают в демо-режиме") | |
| # Create comparison dataframe | |
| compare_df = pd.DataFrame(comparison_results) | |
| # Display comparison table | |
| st.markdown("### 📋 Сравнение предсказаний") | |
| st.dataframe(compare_df.set_index('model'), width='stretch') | |
| # Visualization | |
| st.markdown("### 📊 Визуальное сравнение") | |
| # Melt dataframe for plotting | |
| plot_df = compare_df.melt(id_vars=['model'], var_name='parameter', value_name='value') | |
| # Create bar chart | |
| fig = px.bar( | |
| plot_df, | |
| x='model', | |
| y='value', | |
| color='parameter', | |
| barmode='group', | |
| title="Сравнение предсказаний по моделям", | |
| labels={'value': 'Значение', 'model': 'Модель', 'parameter': 'Параметр'} | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| # Show input parameters | |
| with st.expander("📋 Параметры сравнения"): | |
| st.json(input_data) | |
| with tab4: | |
| st.markdown('<div class="section-header">🔄 Мультимодельная обработка файлов</div>', unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="info-box"> | |
| <h4>🎯 Возможности мультимодельной обработки</h4> | |
| <ul> | |
| <li><strong>Применение нескольких моделей</strong> к одному файлу данных</li> | |
| <li><strong>Сравнение результатов</strong> разных алгоритмов</li> | |
| <li><strong>Визуальный анализ</strong> распределений предсказаний</li> | |
| <li><strong>Статистическое сравнение</strong> производительности моделей</li> | |
| <li><strong>Метрики качества</strong> если есть настоящие значения</li> | |
| </ul> | |
| <h4>📊 Выходные данные</h4> | |
| <ul> | |
| <li><strong>Объединенная таблица</strong> с предсказаниями всех моделей</li> | |
| <li><strong>Графики сравнения</strong> по целевым переменным</li> | |
| <li><strong>Статистические сводки</strong> по каждой модели</li> | |
| <li><strong>Детальный анализ</strong> отдельных записей</li> | |
| <li><strong>Метрики качества</strong> для оценки точности</li> | |
| </ul> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Выбор моделей для мультимодельной обработки | |
| st.markdown("### 🎯 Выбор моделей для обработки") | |
| multi_models = st.multiselect( | |
| "Выберите модели для применения:", | |
| options=list(AVAILABLE_MODELS.keys()), | |
| default=["best_model", "gradient_boosting", "random_forest"], | |
| help="Выберите одну или несколько моделей для обработки файла" | |
| ) | |
| uploaded_file_multi = st.file_uploader( | |
| "Загрузите файл с данными для мультимодельной обработки", | |
| type=['xlsx', 'csv', 'txt'], | |
| key="multi_uploader" | |
| ) | |
| if uploaded_file_multi is not None and multi_models: | |
| st.markdown("### ⚙️ Настройки обработки") | |
| col_set1, col_set2 = st.columns(2) | |
| with col_set1: | |
| skip_rows_multi = st.number_input( | |
| "Пропустить строк в начале файла:", | |
| min_value=0, | |
| max_value=10, | |
| value=0, | |
| key="multi_skip", | |
| help="Используйте если в файле есть заголовки или пустые строки в начале" | |
| ) | |
| with col_set2: | |
| enable_viz = st.checkbox("Включить расширенную визуализацию", value=True) | |
| enable_metrics = st.checkbox("Включить расчет метрик", value=True) | |
| if st.button("🚀 Запустить мультимодельную обработку", type="primary"): | |
| if not multi_models: | |
| st.warning("⚠️ Выберите хотя бы одну модель для обработки") | |
| else: | |
| with st.spinner("🔄 Обрабатываем файл несколькими моделями..."): | |
| result_df, all_predictions, model_results, all_metrics = process_batch_file_multiple_models( | |
| uploaded_file_multi, multi_models, skip_rows_multi | |
| ) | |
| if result_df is not None: | |
| st.success(f"✅ Мультимодельная обработка завершена! Обработано {len(result_df)} записей") | |
| # Показываем сводку по моделям | |
| st.markdown("### 📊 Сводка по моделям") | |
| summary_data = [] | |
| for model_name in multi_models: | |
| if model_name in model_results: | |
| summary_data.append({ | |
| 'Модель': model_name, | |
| 'Успешных предсказаний': model_results[model_name]['successful'], | |
| 'Всего записей': model_results[model_name]['total'], | |
| 'Процент успеха': f"{(model_results[model_name]['successful'] / model_results[model_name]['total'] * 100):.1f}%", | |
| 'Режим': 'Демо' if model_results[model_name]['artifacts'].get('status') == 'demo' else 'Реальный' | |
| }) | |
| if summary_data: | |
| summary_df = pd.DataFrame(summary_data) | |
| st.dataframe(summary_df, width='stretch') | |
| # Показываем результаты | |
| st.markdown("### 📈 Результаты обработки") | |
| # Создаем упрощенный DataFrame для отображения | |
| display_columns = ['sample', 'time_min', 'temperature_c', 'pressure_atm', 'ph'] | |
| # Добавляем по одному предсказанию от каждой модели для компактности | |
| for model_name in multi_models: | |
| if f'{model_name}_pectin_yield' in result_df.columns: | |
| display_columns.append(f'{model_name}_pectin_yield') | |
| display_df = result_df[display_columns].head(10) # Показываем только первые 10 строк | |
| st.dataframe(display_df, width='stretch') | |
| # Скачивание полных результатов | |
| csv = result_df.to_csv(index=False, encoding='utf-8-sig') | |
| st.download_button( | |
| label="📥 Скачать полные результаты (CSV)", | |
| data=csv, | |
| file_name=f"pectin_predictions_multimodel_{pd.Timestamp.now().strftime('%Y%m%d_%H%M')}.csv", | |
| mime="text/csv", | |
| help="Скачать все данные с предсказаниями всех моделей в CSV формате" | |
| ) | |
| # Отображаем метрики качества если включено и есть ground truth | |
| if enable_metrics: | |
| display_metrics_dashboard(all_metrics, multi_models) | |
| # Визуализация сравнения моделей | |
| if enable_viz and len(multi_models) > 1: | |
| target_columns = ['pectin_yield', 'galacturonic_acid', 'molecular_weight', 'esterification_degree'] | |
| create_comparison_visualizations(result_df, multi_models, target_columns, all_metrics) | |
| # Детальный анализ расхождений | |
| if len(multi_models) > 1: | |
| st.markdown("### 🔍 Анализ расхождений между моделями") | |
| # Вычисляем стандартные отклонения для каждой записи по всем моделям | |
| divergence_data = [] | |
| for target in target_columns: | |
| model_cols = [f'{model}_{target}' for model in multi_models if f'{model}_{target}' in result_df.columns] | |
| if len(model_cols) > 1: | |
| # Вычисляем стандартное отклонение по строкам | |
| result_df[f'std_{target}'] = result_df[model_cols].std(axis=1) | |
| # Находим записи с наибольшими расхождениями | |
| max_std_idx = result_df[f'std_{target}'].idxmax() | |
| max_std = result_df.loc[max_std_idx, f'std_{target}'] | |
| if not pd.isna(max_std) and max_std > 0: | |
| divergence_data.append({ | |
| 'Целевая переменная': TARGET_DESCRIPTIONS[target]['name'], | |
| 'Максимальное std': max_std, | |
| 'Запись': max_std_idx + 1, | |
| 'Параметры': f"{result_df.loc[max_std_idx, 'sample']}, t={result_df.loc[max_std_idx, 'time_min']}мин" | |
| }) | |
| if divergence_data: | |
| st.info("📊 Записи с наибольшими расхождениями между моделями:") | |
| divergence_df = pd.DataFrame(divergence_data) | |
| st.dataframe(divergence_df, width='stretch') | |
| else: | |
| st.info("ℹ️ Значительных расхождений между моделями не обнаружено") | |
| # Footer | |
| st.markdown("---") | |
| st.markdown( | |
| """ | |
| <div style='text-align: center; color: #6c757d;'> | |
| <p>🧪 <strong>Pectin Production Predictor</strong> | Модели машинного обучения для прогнозирования параметров производства пектина</p> | |
| <p>📚 Репозиторий моделей: <a href="https://huggingface.co/arabovs-ai-lab/PectinProductionModels" target="_blank">Hugging Face Hub</a></p> | |
| </div> | |
| """, | |
| unsafe_allow_html=True | |
| ) |