TimeFlowPro / app.py
ArabovMK's picture
Update app.py
5a47572 verified
raw
history blame
106 kB
# 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
@st.cache_resource(show_spinner="🔄 Загрузка модели и вспомогательных файлов...")
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
)