diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..30b5d4a4a0af74dbedd38154e80497dc2dee8515 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +@' +# Python / venv / байт-код +.venv +__pycache__/ +*.py[cod] +*.egg-info/ + +# НЕ класть в build context — большие локальные папки и кэши +hf_cache/ +**/hf_cache/** +.cache/ +**/.cache/** +data/ +models/ +models_all/ +reports/ +catboost_info/ +predicted.csv + +# VCS/IDE мусор +.git +.gitignore +.idea +.ipynb_checkpoints +tests/.pytest_cache +.pytest_cache +'@ | Set-Content -Encoding utf8 .dockerignore diff --git a/.env b/.env new file mode 100644 index 0000000000000000000000000000000000000000..a19396055462d1d108397e39dc161fc0f8cbd182 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +HF_HUB_DISABLE_SYMLINKS_WARNING=1 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..61fbd59c40545a962cb421f8414c3caf4be984d8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +*.cbm filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.csv filter=lfs diff=lfs merge=lfs -text +*.xlsx filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.tsv filter=lfs diff=lfs merge=lfs -text +structure.txt filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..8d8543b3f65c1f34be0cb055189a7f5703173289 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# виртуальные окружения / IDE +.venv/ +venv/ +.idea/ +.vscode/ + +# кэш/сервисы +__pycache__/ +*.pyc +*.pyo +*.pyd +.pytest_cache/ +.ipynb_checkpoints/ +.cache/ +*.log + +# локальные данные/выводы +data/ +out/ +*.csv +*.xlsx +*.tsv +*.png +*.pdf + +# большие/неиспользуемые модели +models_all/ diff --git a/.streamlit/config.toml b/.streamlit/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..078677ee0a479ca6030d90a70f982a041205a787 --- /dev/null +++ b/.streamlit/config.toml @@ -0,0 +1,6 @@ +[server] +headless = true +maxUploadSize = 200 + +[browser] +gatherUsageStats = false diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8d6fd1b2b2ff30f417b54b3e3c052c5b9a658e0e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.9-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/Dockerfile-ui b/Dockerfile-ui new file mode 100644 index 0000000000000000000000000000000000000000..a5400384422965e6874b39fb2fa81abfaedd4be6 --- /dev/null +++ b/Dockerfile-ui @@ -0,0 +1,23 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Копируем requirements +COPY ui/requirements.txt . + +# Устанавливаем зависимости +RUN pip install --no-cache-dir -r requirements.txt + +# Копируем код приложения +COPY ui/app.py . + +# Создаем папку для шаблонов (если нужна) +RUN mkdir -p templates + +# Копируем шаблоны (если есть) +COPY ui/templates/ ./templates/ + +EXPOSE 8080 + +# Запускаем приложение +CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..8dfd95ba85dbe63554bc265a03b241093fd5e7a5 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +.PHONY: train predict api ui test docker + +predict: + python -m src.predict --input data/raw/Данные\ для\ кейса.csv --output data/processed/predicted.csv + +api: + uvicorn app.main:app --host 127.0.0.1 --port 8020 --reload + +ui: + streamlit run app/ui.py + +test: + pytest -q + +docker: + docker compose up --build diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e07832f438dee2d91fd09702536bb726719ec0bf --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# 🧠 Автооценка устных ответов (RFL • CatBoost + ruSBERT) + +### 📌 Описание +Проект предназначен для автоматической оценки устных ответов на экзаменах по русскому языку как иностранному (RFL). +Используются модели **CatBoost Q1–Q4** и признаки из **ruSBERT** (эмбеддинги). + +--- + +### 🚀 Быстрый старт + +#### 1️⃣ Локальный запуск + +```bash +pip install -r requirements.txt +python src/predict.py -i "data/raw/Данные для кейса.csv" -o "out/predicted.csv" diff --git a/analyze_features.py b/analyze_features.py new file mode 100644 index 0000000000000000000000000000000000000000..b92325a6b48dda219631fcb51b5ee3405dc8c2ce --- /dev/null +++ b/analyze_features.py @@ -0,0 +1,124 @@ +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import seaborn as sns + + +def analyze_extracted_features(): + """Анализ извлеченных признаков""" + + # Загружаем извлеченные признаки + features_df = pd.read_csv('real_data_features.csv', index_col=0) + + print("📊 ДЕТАЛЬНЫЙ АНАЛИЗ ИЗВЛЕЧЕННЫХ ПРИЗНАКОВ") + print("=" * 50) + + print(f"Всего признаков: {len(features_df.columns)}") + print(f"Обработано строк: {len(features_df)}") + + # Анализ заполненности + null_analysis = features_df.isnull().sum() + null_features = null_analysis[null_analysis > 0] + + if len(null_features) > 0: + print(f"\n❌ Признаки с пропусками:") + for feature, null_count in null_features.items(): + print(f" {feature}: {null_count} пропусков ({null_count / len(features_df):.1%})") + else: + print(f"\n✅ Все признаки полностью заполнены!") + + # Статистика по числовым признакам + numeric_features = features_df.select_dtypes(include=[np.number]) + + print(f"\n📈 СТАТИСТИКА ПРИЗНАКОВ:") + stats_summary = numeric_features.agg(['mean', 'std', 'min', 'max']).T + stats_summary['cv'] = stats_summary['std'] / stats_summary['mean'] # Коэффициент вариации + + # Показываем топ-10 самых информативных признаков + informative_features = stats_summary[stats_summary['std'] > 0].sort_values('cv', ascending=False) + + print(f"\n🎯 ТОП-10 самых информативных признаков (по вариативности):") + for feature, row in informative_features.head(10).iterrows(): + print(f" {feature:25} mean={row['mean']:6.2f} std={row['std']:6.2f} cv={row['cv']:.2f}") + + # Визуализация распределения ключевых признаков + key_features = ['text_length', 'word_count', 'lexical_diversity', 'composite_quality_score'] + available_features = [f for f in key_features if f in numeric_features.columns] + + if available_features: + plt.figure(figsize=(15, 10)) + + for i, feature in enumerate(available_features, 1): + plt.subplot(2, 2, i) + plt.hist(numeric_features[feature].dropna(), bins=20, alpha=0.7, edgecolor='black') + plt.title(f'Распределение {feature}') + plt.xlabel(feature) + plt.ylabel('Частота') + + plt.tight_layout() + plt.savefig('features_distribution.png', dpi=150, bbox_inches='tight') + plt.show() + print(f"\n📊 Визуализация сохранена в features_distribution.png") + + # Анализ корреляций между признаками + if len(numeric_features.columns) > 5: + # Выбираем топ-15 самых вариативных признаков для корреляционной матрицы + top_features = informative_features.head(15).index.tolist() + + plt.figure(figsize=(12, 10)) + correlation_matrix = numeric_features[top_features].corr() + + mask = np.triu(np.ones_like(correlation_matrix, dtype=bool)) + sns.heatmap(correlation_matrix, mask=mask, annot=True, fmt='.2f', cmap='coolwarm', + center=0, square=True, cbar_kws={"shrink": .8}) + plt.title('Корреляционная матрица признаков (топ-15)') + plt.tight_layout() + plt.savefig('features_correlation.png', dpi=150, bbox_inches='tight') + plt.show() + print(f"📈 Корреляционная матрица сохранена в features_correlation.png") + + # Анализ качества композитного показателя + if 'composite_quality_score' in numeric_features.columns: + print(f"\n🎯 АНАЛИЗ КОМПОЗИТНОГО ПОКАЗАТЕЛЯ КАЧЕСТВА:") + quality_scores = numeric_features['composite_quality_score'] + print(f" Среднее: {quality_scores.mean():.3f}") + print(f" Стандартное отклонение: {quality_scores.std():.3f}") + print(f" Диапазон: [{quality_scores.min():.3f}, {quality_scores.max():.3f}]") + + # Распределение по квантилям + quantiles = quality_scores.quantile([0.25, 0.5, 0.75]) + print(f" Квантили: 25%={quantiles[0.25]:.3f}, 50%={quantiles[0.5]:.3f}, 75%={quantiles[0.75]:.3f}") + + +def check_feature_correlations_with_target(): + """Проверка корреляции признаков с целевой переменной (если есть оценки)""" + + features_df = pd.read_csv('real_data_features.csv', index_col=0) + + # Ищем колонку с оценками в исходных данных + score_columns = [col for col in features_df.columns if 'score' in col.lower() or 'оценк' in col.lower()] + + if score_columns: + target_col = score_columns[0] + print(f"\n🎯 КОРРЕЛЯЦИЯ ПРИЗНАКОВ С {target_col}:") + print("-" * 40) + + correlations = features_df.corr()[target_col].abs().sort_values(ascending=False) + + # Показываем топ-10 наиболее коррелирующих признаков + top_correlated = correlations.head(11) # +1 потому что target сам с собой + + for feature, corr in top_correlated.items(): + if feature != target_col: + actual_corr = features_df.corr()[target_col][feature] + direction = "↑" if actual_corr > 0 else "↓" + significance = "***" if abs(actual_corr) > 0.3 else "**" if abs(actual_corr) > 0.2 else "*" if abs( + actual_corr) > 0.1 else "" + print(f" {direction} {feature:25} {actual_corr:+.3f} {significance}") + else: + print(f"\nℹ️ Целевая переменная (оценки) не найдена в данных") + + +if __name__ == "__main__": + analyze_extracted_features() + check_feature_correlations_with_target() diff --git a/analyze_features_simple.py b/analyze_features_simple.py new file mode 100644 index 0000000000000000000000000000000000000000..16caea3b0048a6e3a2670733a1b29cac01c78017 --- /dev/null +++ b/analyze_features_simple.py @@ -0,0 +1,169 @@ +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt + + +def analyze_extracted_features(): + """Анализ извлеченных признаков без сложных зависимостей""" + + try: + # Загружаем извлеченные признаки + features_df = pd.read_csv('real_data_features.csv', index_col=0) + except FileNotFoundError: + print("❌ Файл real_data_features.csv не найден!") + print("💡 Сначала запустите test_real_data.py") + return + + print("📊 ДЕТАЛЬНЫЙ АНАЛИЗ ИЗВЛЕЧЕННЫХ ПРИЗНАКОВ") + print("=" * 50) + + print(f"Всего признаков: {len(features_df.columns)}") + print(f"Обработано строк: {len(features_df)}") + + # Анализ заполненности + null_analysis = features_df.isnull().sum() + null_features = null_analysis[null_analysis > 0] + + if len(null_features) > 0: + print(f"\n❌ Признаки с пропусками:") + for feature, null_count in null_features.items(): + print(f" {feature}: {null_count} пропусков ({null_count / len(features_df):.1%})") + else: + print(f"\n✅ Все признаки полностью заполнены!") + + # Статистика по числовым признакам + numeric_features = features_df.select_dtypes(include=[np.number]) + + print(f"\n📈 СТАТИСТИКА ПРИЗНАКОВ:") + stats_summary = numeric_features.agg(['mean', 'std', 'min', 'max']).T + stats_summary['cv'] = stats_summary['std'] / stats_summary['mean'] # Коэффициент вариации + + # Показываем топ-10 самых информативных признаков + informative_features = stats_summary[stats_summary['std'] > 0].sort_values('cv', ascending=False) + + print(f"\n🎯 ТОП-15 самых информативных признаков (по вариативности):") + for feature, row in informative_features.head(15).iterrows(): + print(f" {feature:25} mean={row['mean']:6.2f} std={row['std']:6.2f} cv={row['cv']:.2f}") + + # Визуализация распределения ключевых признаков + key_features = ['text_length', 'word_count', 'lexical_diversity', 'composite_quality_score'] + available_features = [f for f in key_features if f in numeric_features.columns] + + if available_features: + plt.figure(figsize=(15, 10)) + + for i, feature in enumerate(available_features, 1): + plt.subplot(2, 2, i) + plt.hist(numeric_features[feature].dropna(), bins=20, alpha=0.7, edgecolor='black') + plt.title(f'Распределение {feature}') + plt.xlabel(feature) + plt.ylabel('Частота') + + plt.tight_layout() + plt.savefig('features_distribution.png', dpi=150, bbox_inches='tight') + plt.show() + print(f"\n📊 Визуализация сохранена в features_distribution.png") + + # Анализ качества композитного показателя + if 'composite_quality_score' in numeric_features.columns: + print(f"\n🎯 АНАЛИЗ КОМПОЗИТНОГО ПОКАЗАТЕЛЯ КАЧЕСТВА:") + quality_scores = numeric_features['composite_quality_score'] + print(f" Среднее: {quality_scores.mean():.3f}") + print(f" Стандартное отклонение: {quality_scores.std():.3f}") + print(f" Диапазон: [{quality_scores.min():.3f}, {quality_scores.max():.3f}]") + + # Распределение по квантилям + quantiles = quality_scores.quantile([0.25, 0.5, 0.75]) + print(f" Квантили: 25%={quantiles[0.25]:.3f}, 50%={quantiles[0.5]:.3f}, 75%={quantiles[0.75]:.3f}") + + # Анализ что влияет на качество + print(f"\n🔍 КОРРЕЛЯЦИЯ С КОМПОЗИТНЫМ ПОКАЗАТЕЛЕМ:") + correlations = numeric_features.corr()['composite_quality_score'].abs().sort_values(ascending=False) + + for feature, corr in correlations.head(10).items(): + if feature != 'composite_quality_score': + actual_corr = numeric_features.corr()['composite_quality_score'][feature] + direction = "↑" if actual_corr > 0 else "↓" + print(f" {direction} {feature:25} {actual_corr:+.3f}") + + +def check_feature_correlations_with_target(): + """Проверка корреляции признаков с целевой переменной (если есть оценки)""" + + try: + features_df = pd.read_csv('real_data_features.csv', index_col=0) + except FileNotFoundError: + return + + # Ищем колонку с оценками в исходных данных + score_columns = [col for col in features_df.columns if + 'score' in col.lower() or 'оценк' in col.lower() or 'балл' in col.lower()] + + if score_columns: + target_col = score_columns[0] + print(f"\n🎯 КОРРЕЛЯЦИЯ ПРИЗНАКОВ С {target_col}:") + print("-" * 50) + + correlations = features_df.corr()[target_col].abs().sort_values(ascending=False) + + # Показываем топ-10 наиболее коррелирующих признаков + top_correlated = correlations.head(11) # +1 потому что target сам с собой + + print(f" {'ПРИЗНАК':<25} {'КОРРЕЛЯЦИЯ':<10} {'ЗНАЧИМОСТЬ'}") + print(f" {'-' * 25} {'-' * 10} {'-' * 10}") + + for feature, corr in top_correlated.items(): + if feature != target_col: + actual_corr = features_df.corr()[target_col][feature] + direction = "↑" if actual_corr > 0 else "↓" + significance = "***" if abs(actual_corr) > 0.3 else "**" if abs(actual_corr) > 0.2 else "*" if abs( + actual_corr) > 0.1 else "" + print(f" {direction} {feature:<23} {actual_corr:+.3f} {significance}") + else: + print(f"\nℹ️ Целевая переменная (оценки) не найдена в данных") + + +def analyze_feature_categories(): + """Анализ признаков по категориям""" + + try: + features_df = pd.read_csv('real_data_features.csv', index_col=0) + except FileNotFoundError: + return + + # Группируем признаки по категориям + categories = { + '📝 ТЕКСТОВЫЕ': ['text_length', 'word_count', 'sentence_count', 'avg_sentence_length', + 'avg_word_length', 'lexical_diversity', 'long_word_ratio', 'text_complexity'], + '🎯 СЕМАНТИЧЕСКИЕ': ['semantic_similarity', 'keyword_overlap', 'tfidf_similarity', 'response_relevance'], + '📚 ГРАММАТИЧЕСКИЕ': ['grammar_error_count', 'grammar_error_ratio', 'has_punctuation', + 'sentence_completeness', 'proper_capitalization'], + '💬 ДИСКУРС': ['has_greeting', 'has_questions', 'has_description', 'has_connectors', + 'has_emotional_words', 'coherence_score'], + '❓ ТИПЫ ВОПРОСОВ': ['dialog_initiation', 'response_adequacy', 'information_seeking', + 'descriptive_detail', 'answer_length_sufficiency', 'question_type'], + '⭐ КАЧЕСТВО': ['composite_quality_score', 'social_appropriateness', 'interaction_quality'] + } + + print(f"\n📂 РАСПРЕДЕЛЕНИЕ ПРИЗНАКОВ ПО КАТЕГОРИЯМ:") + print("=" * 50) + + numeric_features = features_df.select_dtypes(include=[np.number]) + + for category, features in categories.items(): + available_features = [f for f in features if f in numeric_features.columns] + if available_features: + print(f"\n{category} ({len(available_features)} признаков):") + for feature in available_features: + mean_val = numeric_features[feature].mean() + std_val = numeric_features[feature].std() + print(f" • {feature:25} {mean_val:6.3f} ± {std_val:5.3f}") + + +if __name__ == "__main__": + analyze_extracted_features() + check_feature_correlations_with_target() + analyze_feature_categories() + + print(f"\n✅ Анализ завершен!") + print("💡 Рекомендации будут основаны на этом анализе") \ No newline at end of file diff --git a/analyze_results.py b/analyze_results.py new file mode 100644 index 0000000000000000000000000000000000000000..0adb7ab76a70fb98db31ceab090774ad405aca4d --- /dev/null +++ b/analyze_results.py @@ -0,0 +1,439 @@ +import pandas as pd +import matplotlib.pyplot as plt +from collections import Counter +import numpy as np +import os +import warnings + +warnings.filterwarnings('ignore') + +# Настройка отображения +plt.style.use('default') +plt.rcParams['font.family'] = 'DejaVu Sans' # Для поддержки кириллицы + + +def load_and_analyze_data(): + """Загрузка и базовый анализ данных""" + + # Загрузка данных с правильным разделителем + file_path = 'small.csv' # или полный путь к файлу + + # Пробуем разные разделители и кодировки + try: + # Сначала пробуем с разделителем точка с запятой + df = pd.read_csv(file_path, encoding='utf-8', delimiter=';') + print("✅ Файл загружен с разделителем ';' и кодировкой utf-8") + except: + try: + df = pd.read_csv(file_path, encoding='cp1251', delimiter=';') + print("✅ Файл загружен с разделителем ';' и кодировкой cp1251") + except: + try: + df = pd.read_csv(file_path, encoding='utf-8', delimiter=',') + print("✅ Файл загружен с разделителем ',' и кодировкой utf-8") + except: + try: + df = pd.read_csv(file_path, encoding='cp1251', delimiter=',') + print("✅ Файл загружен с разделителем ',' и кодировкой cp1251") + except Exception as e: + print(f"❌ Ошибка загрузки файла: {e}") + return None + + print("=" * 60) + print("АНАЛИЗ РЕЗУЛЬТАТОВ АВТОМАТИЧЕСКОЙ ОЦЕНКИ") + print("=" * 60) + + # Базовая информация о данных + print(f"Размер данных: {df.shape[0]} строк, {df.shape[1]} колонок") + print(f"\nВсе колонки: {list(df.columns)}") + + # Показываем первые несколько строк для проверки + print(f"\nПервые 3 строки данных:") + print(df.head(3)) + + return df + + +def check_and_rename_columns(df): + """Проверка и переименование колонок если нужно""" + + print("\n" + "=" * 40) + print("ПРОВЕРКА СТРУКТУРЫ ДАННЫХ") + print("=" * 40) + + # Если есть только одна колонка, возможно данные объединены + if df.shape[1] == 1: + first_column = df.columns[0] + print(f"Обнаружена одна колонка: '{first_column}'") + + # Проверяем, содержит ли она все данные + sample_value = str(df.iloc[0, 0]) + if ';' in sample_value: + print("⚠️ Данные объединены в одну колонку, разделяем...") + + # Разделяем данные по точке с запятой + split_data = df[first_column].str.split(';', expand=True) + + # Берем первую строку как заголовки + if split_data.shape[0] > 1: + new_columns = split_data.iloc[0].tolist() + split_data = split_data[1:] # Убираем строку с заголовками + split_data.columns = new_columns + df = split_data.reset_index(drop=True) + print("✅ Данные успешно разделены") + print(f"Новые колонки: {list(df.columns)}") + + return df + + +def basic_statistics(df): + """Базовая статистика по оценкам""" + + print("\n" + "=" * 40) + print("БАЗОВАЯ СТАТИСТИКА") + print("=" * 40) + + # Проверяем наличие нужных колонок + available_columns = list(df.columns) + print(f"Доступные колонки: {available_columns}") + + # Статистика по AI оценкам (pred_score) + if 'pred_score' in df.columns: + print("\nAI оценки (pred_score):") + print(f" Среднее: {df['pred_score'].mean():.3f}") + print(f" Медиана: {df['pred_score'].median():.3f}") + print(f" Стандартное отклонение: {df['pred_score'].std():.3f}") + print(f" Минимум: {df['pred_score'].min():.3f}") + print(f" Максимум: {df['pred_score'].max():.3f}") + else: + print("❌ Колонка 'pred_score' не найдена") + + # Статистика по человеческим оценкам + human_score_columns = ['Оценка экзаменатора', 'оценка', 'score', 'human_score'] + human_score_col = None + + for col in human_score_columns: + if col in df.columns: + human_score_col = col + break + + if human_score_col: + print(f"\nОценки экзаменатора ({human_score_col}):") + print(f" Среднее: {df[human_score_col].mean():.3f}") + print(f" Медиана: {df[human_score_col].median():.3f}") + print(f" Стандартное отклонение: {df[human_score_col].std():.3f}") + + # Распределение оценок + print(f"\nРаспределение оценок экзаменатора:") + распределение = df[human_score_col].value_counts().sort_index() + for оценка, count in распределение.items(): + print(f" {оценка}: {count} ответов ({count / len(df) * 100:.1f}%)") + else: + print("❌ Колонка с оценками экзаменатора не найдена") + + +def calculate_correlations(df): + """Расчет корреляций и разниц""" + + print("\n" + "=" * 40) + print("КОРРЕЛЯЦИИ И РАСХОЖДЕНИЯ") + print("=" * 40) + + # Проверяем наличие обеих колонок + if 'pred_score' not in df.columns: + print("❌ Колонка 'pred_score' не найдена для расчета корреляций") + return + + human_score_columns = ['Оценка экзаменатора', 'оценка', 'score', 'human_score'] + human_score_col = None + + for col in human_score_columns: + if col in df.columns: + human_score_col = col + break + + if not human_score_col: + print("❌ Колонка с оценками экзаменатора не найдена для расчета корреляций") + return + + # Корреляция + correlation = df[[human_score_col, 'pred_score']].corr().iloc[0, 1] + print(f"Корреляция между оценками: {correlation:.3f}") + + # Разницы между оценками + df['разница'] = df['pred_score'] - df[human_score_col] + df['abs_разница'] = abs(df['разница']) + + print(f"\nСредняя абсолютная разница: {df['abs_разница'].mean():.3f}") + print(f"Максимальная разница: {df['abs_разница'].max():.3f}") + print(f"Минимальная разница: {df['abs_разница'].min():.3f}") + + # Анализ согласованности + print("\nСОГЛАСОВАННОСТЬ ОЦЕНОК:") + for порог in [0.1, 0.3, 0.5, 1.0]: + согласованные = df[df['abs_разница'] < порог].shape[0] + процент = (согласованные / len(df)) * 100 + print(f" Разница < {порог}: {согласованные} ответов ({процент:.1f}%)") + + # Направление разниц + завышение = len(df[df['разница'] > 0]) + занижение = len(df[df['разница'] < 0]) + совпадение = len(df[df['разница'] == 0]) + + print(f"\nНАПРАВЛЕНИЕ РАЗНИЦ:") + print(f" AI завышает: {завышение} ({завышение / len(df) * 100:.1f}%)") + print(f" AI занижает: {занижение} ({занижение / len(df) * 100:.1f}%)") + print(f" Полное совпадение: {совпадение} ({совпадение / len(df) * 100:.1f}%)") + + +def create_visualizations(df): + """Создание визуализаций""" + + print("\n" + "=" * 40) + print("СОЗДАНИЕ ВИЗУАЛИЗАЦИЙ") + print("=" * 40) + + # Проверяем наличие нужных колонок + if 'pred_score' not in df.columns: + print("❌ Колонка 'pred_score' не найдена для визуализации") + return + + human_score_columns = ['Оценка экзаменатора', 'оценка', 'score', 'human_score'] + human_score_col = None + + for col in human_score_columns: + if col in df.columns: + human_score_col = col + break + + if not human_score_col: + print("❌ Колонка с оценками экзаменатора не найдена для визуализации") + return + + # Создаем папку для графиков + os.makedirs('graphs', exist_ok=True) + + # 1. Scatter plot сравнения оценок + plt.figure(figsize=(12, 8)) + scatter = plt.scatter(df[human_score_col], df['pred_score'], + c=df['abs_разница'], cmap='viridis', alpha=0.7, s=80) + plt.colorbar(scatter, label='Абсолютная разница') + + # Определяем диапазон для линии идеального соответствия + min_val = min(df[human_score_col].min(), df['pred_score'].min()) + max_val = max(df[human_score_col].max(), df['pred_score'].max()) + plt.plot([min_val, max_val], [min_val, max_val], 'r--', alpha=0.5, label='Идеальное соответствие') + + plt.xlabel(f'Оценка экзаменатора ({human_score_col})', fontsize=12) + plt.ylabel('AI оценка (pred_score)', fontsize=12) + plt.title('Сравнение человеческой и AI оценки\n(цвет показывает величину расхождения)', fontsize=14) + plt.legend() + plt.grid(True, alpha=0.3) + plt.savefig('graphs/scatter_comparison.png', dpi=300, bbox_inches='tight') + plt.close() + + # 2. Гистограмма разниц + plt.figure(figsize=(12, 6)) + n, bins, patches = plt.hist(df['разница'], bins=30, alpha=0.7, + edgecolor='black', color='skyblue') + plt.xlabel('Разница оценок (AI - Человек)', fontsize=12) + plt.ylabel('Количество ответов', fontsize=12) + plt.title('Распределение разниц между AI и человеческими оценками', fontsize=14) + plt.grid(True, alpha=0.3) + plt.axvline(x=0, color='red', linestyle='--', alpha=0.8, linewidth=2, label='Нулевая разница') + plt.axvline(x=df['разница'].mean(), color='orange', linestyle='--', + alpha=0.8, linewidth=2, label=f'Средняя разница: {df["разница"].mean():.3f}') + plt.legend() + plt.savefig('graphs/difference_histogram.png', dpi=300, bbox_inches='tight') + plt.close() + + print("✅ Графики сохранены в папку 'graphs/'") + + +def analyze_extreme_cases(df): + """Анализ крайних случаев""" + + print("\n" + "=" * 40) + print("АНАЛИЗ КРАЙНИХ СЛУЧАЕВ") + print("=" * 40) + + if 'abs_разница' not in df.columns: + print("❌ Не найдены данные о разницах оценок") + return + + human_score_columns = ['Оценка экзаменатора', 'оценка', 'score', 'human_score'] + human_score_col = None + + for col in human_score_columns: + if col in df.columns: + human_score_col = col + break + + if not human_score_col: + print("❌ Колонка с оценками экзаменатора не найдена") + return + + # Наибольшие расхождения + большие_расхождения = df.nlargest(8, 'abs_разница')[ + [human_score_col, 'pred_score', 'abs_разница', 'разница'] + ] + + # Добавляем ID если есть + id_columns = ['Id экзамена', 'id', 'ID', 'exam_id'] + for col in id_columns: + if col in df.columns: + большие_расхождения[col] = df.loc[большие_расхождения.index, col] + break + + question_columns = ['№ вопроса', 'question', 'вопрос', 'question_id'] + for col in question_columns: + if col in df.columns: + большие_расхождения[col] = df.loc[большие_расхождения.index, col] + break + + print("Топ-8 наибольших расхождений:") + print("-" * 80) + for idx, row in большие_расхождения.iterrows(): + направление = "ЗАВЫШЕНИЕ" if row['разница'] > 0 else "ЗАНИЖЕНИЕ" + + # Формируем информацию об ID и вопросе + id_info = "" + if 'Id экзамена' in row: + id_info = f"Экзамен {row['Id экзамена']}" + elif 'id' in row: + id_info = f"ID {row['id']}" + + question_info = "" + if '№ вопроса' in row: + question_info = f", Вопрос {row['№ вопроса']}" + elif 'question' in row: + question_info = f", Вопрос {row['question']}" + + print(f"\n📊 {id_info}{question_info} ({направление}):") + print(f" 👤 Человек: {row[human_score_col]} | 🤖 AI: {row['pred_score']:.3f}") + print(f" 📏 Разница: {row['abs_разница']:.3f} ({row['разница']:+.3f})") + print("-" * 60) + + +def analyze_explanations(df): + """Анализ объяснений оценок""" + + print("\n" + "=" * 40) + print("АНАЛИЗ ОБЪЯСНЕНИЙ ОЦЕНОК") + print("=" * 40) + + explanation_columns = ['объяснение_оценки', 'explanation', 'объяснение', 'комментарий'] + explanation_col = None + + for col in explanation_columns: + if col in df.columns: + explanation_col = col + break + + if not explanation_col: + print("❌ Колонка с объяснениями оценок не найдена") + return + + # Собираем все объяснения + все_объяснения = ' '.join(df[explanation_col].dropna().astype(str)) + + # Разбиваем на слова и фильтруем + слова = [word.strip() for word in все_объяснения.split() if len(word.strip()) > 2] + + # Анализ частотности + частотность = Counter(слова) + + print("Топ-15 наиболее частых характеристик в объяснениях:") + print("-" * 50) + for слово, count in частотность.most_common(15): + print(f" {слово}: {count}") + + +def save_detailed_analysis(df): + """Сохранение детального анализа в файл""" + + print("\n" + "=" * 40) + print("СОХРАНЕНИЕ РЕЗУЛЬТАТОВ") + print("=" * 40) + + if 'abs_разница' not in df.columns: + print("❌ Нет данных для детального анализа") + return + + # Создаем копию с анализом + df_analysis = df.copy() + + human_score_columns = ['Оценка экзаменатора', 'оценка', 'score', 'human_score'] + human_score_col = None + + for col in human_score_columns: + if col in df.columns: + human_score_col = col + break + + if human_score_col and 'pred_score' in df.columns: + df_analysis['разница_ai_человек'] = df_analysis['pred_score'] - df_analysis[human_score_col] + df_analysis['abs_разница'] = abs(df_analysis['разница_ai_человек']) + + # Добавляем категоризацию расхождений + условия = [ + df_analysis['abs_разница'] < 0.1, + df_analysis['abs_разница'] < 0.3, + df_analysis['abs_разница'] < 0.5, + df_analysis['abs_разница'] >= 0.5 + ] + категории = ['Отличное', 'Хорошее', 'Умеренное', 'Низкое'] + df_analysis['качество_согласования'] = np.select(условия, категории, default='Низкое') + + # Сортируем по наибольшим расхождениям + df_analysis = df_analysis.sort_values('abs_разница', ascending=False) + + try: + # Сохраняем в Excel + with pd.ExcelWriter('detailed_analysis.xlsx', engine='openpyxl') as writer: + # Все данные + df_analysis.to_excel(writer, sheet_name='Все_данные_с_анализом', index=False) + print("✅ Детальный анализ сохранен в 'detailed_analysis.xlsx'") + + except Exception as e: + print(f"⚠️ Не удалось сохранить Excel, сохраняем в CSV: {e}") + df_analysis.to_csv('detailed_analysis.csv', index=False, encoding='utf-8') + print("✅ Детальный анализ сохранен в 'detailed_analysis.csv'") + + +def main(): + """Основная функция""" + + try: + # Загрузка данных + df = load_and_analyze_data() + + if df is None: + return + + # Проверка и корректировка структуры данных + df = check_and_rename_columns(df) + + # Выполнение анализа + basic_statistics(df) + calculate_correlations(df) + create_visualizations(df) + analyze_extreme_cases(df) + analyze_explanations(df) + save_detailed_analysis(df) + + print("\n" + "=" * 60) + print("✅ АНАЛИЗ ЗАВЕРШЕН!") + print("=" * 60) + + except FileNotFoundError: + print("❌ ОШИБКА: Файл 'small.csv' не найден в текущей директории") + print(" Убедитесь, что файл находится в той же папке, что и скрипт") + except Exception as e: + print(f"❌ ОШИБКА при выполнении анализа: {str(e)}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/analyze_results_pro.py b/analyze_results_pro.py new file mode 100644 index 0000000000000000000000000000000000000000..40873913d0068d82c1f808f87bcca9cf624101ec --- /dev/null +++ b/analyze_results_pro.py @@ -0,0 +1,440 @@ +import pandas as pd +import matplotlib.pyplot as plt +from collections import Counter +import numpy as np +import os +import warnings + +warnings.filterwarnings('ignore') + +# Настройка отображения +plt.style.use('default') +plt.rcParams['font.family'] = 'DejaVu Sans' + + +def load_and_analyze_data(): + """Загрузка и базовый анализ данных""" + + file_path = 'small.csv' + + try: + df = pd.read_csv(file_path, encoding='utf-8', delimiter=';') + print("Файл загружен с разделителем ';' и кодировкой utf-8") + except: + try: + df = pd.read_csv(file_path, encoding='cp1251', delimiter=';') + print("Файл загружен с разделителем ';' и кодировкой cp1251") + except: + try: + df = pd.read_csv(file_path, encoding='utf-8', delimiter=',') + print("Файл загружен с разделителем ',' и кодировкой utf-8") + except: + try: + df = pd.read_csv(file_path, encoding='cp1251', delimiter=',') + print("Файл загружен с разделителем ',' и кодировкой cp1251") + except Exception as e: + print(f"Ошибка загрузки файла: {e}") + return None + + print("=" * 60) + print("АНАЛИЗ РЕЗУЛЬТАТОВ АВТОМАТИЧЕСКОЙ ОЦЕНКИ") + print("=" * 60) + + print(f"Размер данных: {df.shape[0]} строк, {df.shape[1]} колонок") + print(f"Колонки: {list(df.columns)}") + + return df + + +def basic_statistics(df): + """Базовая статистика по оценкам""" + + print("\n" + "=" * 40) + print("БАЗОВАЯ СТАТИСТИКА") + print("=" * 40) + + # Статистика по AI оценкам + print("AI оценки (pred_score):") + print(f" Среднее: {df['pred_score'].mean():.3f}") + print(f" Медиана: {df['pred_score'].median():.3f}") + print(f" Стандартное отклонение: {df['pred_score'].std():.3f}") + print(f" Минимум: {df['pred_score'].min():.3f}") + print(f" Максимум: {df['pred_score'].max():.3f}") + + # Статистика по человеческим оценкам + print("\nОценки экзаменатора:") + print(f" Среднее: {df['Оценка экзаменатора'].mean():.3f}") + print(f" Медиана: {df['Оценка экзаменатора'].median():.3f}") + print(f" Стандартное отклонение: {df['Оценка экзаменатора'].std():.3f}") + + # Распределение оценок + print("\nРаспределение оценок экзаменатора:") + распределение = df['Оценка экзаменатора'].value_counts().sort_index() + for оценка, count in распределение.items(): + print(f" {оценка}: {count} ответов ({count / len(df) * 100:.1f}%)") + + +def calculate_correlations(df): + """Расчет корреляций и разниц""" + + print("\n" + "=" * 40) + print("КОРРЕЛЯЦИИ И РАСХОЖДЕНИЯ") + print("=" * 40) + + # Корреляция + correlation = df[['Оценка экзаменатора', 'pred_score']].corr().iloc[0, 1] + print(f"Корреляция между оценками: {correlation:.3f}") + + # Разницы между оценками + df['разница'] = df['pred_score'] - df['Оценка экзаменатора'] + df['abs_разница'] = abs(df['разница']) + + print(f"Средняя абсолютная разница: {df['abs_разница'].mean():.3f}") + print(f"Максимальная разница: {df['abs_разница'].max():.3f}") + print(f"Минимальная разница: {df['abs_разница'].min():.3f}") + + # Анализ согласованности + print("\nСОГЛАСОВАННОСТЬ ОЦЕНОК:") + for порог in [0.1, 0.3, 0.5, 1.0]: + согласованные = df[df['abs_разница'] < порог].shape[0] + процент = (согласованные / len(df)) * 100 + print(f" Разница < {порог}: {согласованные} ответов ({процент:.1f}%)") + + # Направление разниц + завышение = len(df[df['разница'] > 0]) + занижение = len(df[df['разница'] < 0]) + совпадение = len(df[df['разница'] == 0]) + + print(f"\nНАПРАВЛЕНИЕ РАЗНИЦ:") + print(f" AI завышает: {завышение} ({завышение / len(df) * 100:.1f}%)") + print(f" AI занижает: {занижение} ({занижение / len(df) * 100:.1f}%)") + print(f" Полное совпадение: {совпадение} ({совпадение / len(df) * 100:.1f}%)") + + +def create_visualizations(df): + """Создание визуализаций""" + + print("\n" + "=" * 40) + print("СОЗДАНИЕ ВИЗУАЛИЗАЦИЙ") + print("=" * 40) + + # Создаем папку для графиков + os.makedirs('graphs', exist_ok=True) + + # 1. Scatter plot сравнения оценок + plt.figure(figsize=(12, 8)) + scatter = plt.scatter(df['Оценка экзаменатора'], df['pred_score'], + c=df['abs_разница'], cmap='viridis', alpha=0.7, s=80) + plt.colorbar(scatter, label='Абсолютная разница') + plt.plot([0, 2], [0, 2], 'r--', alpha=0.5, label='Идеальное соответствие') + plt.xlabel('Оценка экзаменатора', fontsize=12) + plt.ylabel('AI оценка (pred_score)', fontsize=12) + plt.title('Сравнение человеческой и AI оценки', fontsize=14) + plt.legend() + plt.grid(True, alpha=0.3) + plt.xticks([0, 1, 2]) + plt.yticks(np.arange(0, 2.5, 0.5)) + plt.savefig('graphs/scatter_comparison_pro.png', dpi=300, bbox_inches='tight') + plt.close() + + # 2. Гистограмма разниц + plt.figure(figsize=(12, 6)) + n, bins, patches = plt.hist(df['разница'], bins=30, alpha=0.7, + edgecolor='black', color='skyblue') + plt.xlabel('Разница оценок (AI - Человек)', fontsize=12) + plt.ylabel('Количество ответов', fontsize=12) + plt.title('Распределение разниц между AI и человеческими оценками', fontsize=14) + plt.grid(True, alpha=0.3) + plt.axvline(x=0, color='red', linestyle='--', alpha=0.8, linewidth=2, label='Нулевая разница') + plt.axvline(x=df['разница'].mean(), color='orange', linestyle='--', + alpha=0.8, linewidth=2, label=f'Средняя разница: {df["разница"].mean():.3f}') + plt.legend() + plt.savefig('graphs/difference_histogram_pro.png', dpi=300, bbox_inches='tight') + plt.close() + + # 3. Box plot по типам вопросов + plt.figure(figsize=(14, 8)) + box_data = [df[df['№ вопроса'] == question]['pred_score'].values + for question in sorted(df['№ вопроса'].unique())] + + box_plot = plt.boxplot(box_data, labels=sorted(df['№ вопроса'].unique()), + patch_artist=True) + + # Раскрашиваем boxplot + colors = ['lightblue', 'lightgreen', 'lightcoral', 'lightyellow'] + for patch, color in zip(box_plot['boxes'], colors): + patch.set_facecolor(color) + + plt.title('Распределение AI оценок по номерам вопросов', fontsize=14) + plt.xlabel('Номер вопроса', fontsize=12) + plt.ylabel('AI оценка (pred_score)', fontsize=12) + plt.grid(True, alpha=0.3) + plt.savefig('graphs/question_boxplot_pro.png', dpi=300, bbox_inches='tight') + plt.close() + + print("Графики сохранены в папку 'graphs/'") + + +def analyze_extreme_cases(df): + """Анализ крайних случаев""" + + print("\n" + "=" * 40) + print("АНАЛИЗ КРАЙНИХ СЛУЧАЕВ") + print("=" * 40) + + # Наибольшие расхождения + большие_расхождения = df.nlargest(8, 'abs_разница')[ + ['Id экзамена', '№ вопроса', 'Оценка экзаменатора', 'pred_score', + 'abs_разница', 'разница'] + ] + + print("Топ-8 наибольших расхождений:") + print("-" * 80) + for idx, row in большие_расхождения.iterrows(): + направление = "ЗАВЫШЕНИЕ" if row['разница'] > 0 else "ЗАНИЖЕНИЕ" + print(f"\nЭкзамен {row['Id экзамена']}, Вопрос {row['№ вопроса']} ({направление}):") + print(f" Человек: {row['Оценка экзаменатора']} | AI: {row['pred_score']:.3f}") + print(f" Разница: {row['abs_разница']:.3f} ({row['разница']:+.3f})") + print("-" * 60) + + +def analyze_explanations(df): + """Анализ объяснений оценок""" + + print("\n" + "=" * 40) + print("АНАЛИЗ ОБЪЯСНЕНИЙ ОЦЕНОК") + print("=" * 40) + + explanation_columns = ['объяснение_оценки', 'explanation', 'объяснение'] + explanation_col = None + + for col in explanation_columns: + if col in df.columns: + explanation_col = col + break + + if not explanation_col: + print("Колонка с объяснениями оценок не найдена") + return + + # Собираем все объяснения + все_объяснения = ' '.join(df[explanation_col].dropna().astype(str)) + + # Разбиваем на слова и фильтруем + слова = [word.strip() for word in все_объяснения.split() if len(word.strip()) > 2] + + # Анализ частотности + частотность = Counter(слова) + + print("Топ-15 наиболее частых характеристик в объяснениях:") + for слово, count in частотность.most_common(15): + print(f" {слово}: {count}") + + # Анализ по ключевым категориям + категории = { + 'Развернутый': 'Развернутый ответ', + 'смысловое': 'Смысловое соответствие', + 'соответствие': 'Смысловое соответствие', + 'Хорошая': 'Хорошая структура', + 'структура': 'Хорошая структура', + 'лексика': 'Разнообразная лексика', + 'Высокий': 'Высокий балл', + 'балл': 'Высокий балл', + 'описание': 'Подробное описание', + 'личный': 'Личный опыт', + 'покрытие': 'Покрытие вопросов' + } + + print(f"\nСТАТИСТИКА ПО КАТЕГОРИЯМ:") + for ключ, описание in категориями.items(): + count = sum(1 for слово in слова if ключ in слово) + if count > 0: + print(f" {описание}: {count}") + + +def performance_by_question_type(df): + """Анализ производительности по типам вопросов""" + + print("\n" + "=" * 40) + print("АНАЛИЗ ПО ТИПАМ ВОПРОСОВ") + print("=" * 40) + + вопросы_статистика = df.groupby('№ вопроса').agg({ + 'Оценка экзаменатора': ['mean', 'std', 'count'], + 'pred_score': ['mean', 'std'], + 'abs_разница': 'mean', + 'разница': 'mean' + }).round(3) + + # Переименовываем колонки для удобства + вопросы_статистика.columns = ['чел_среднее', 'чел_стд', 'количество', + 'ai_среднее', 'ai_стд', 'ср_абс_разница', 'ср_разница'] + + вопросы_статистика['расхождение'] = abs(вопросы_статистика['ср_разница']) + + print("СТАТИСТИКА ПО ВОПРОСАМ:") + print("-" * 80) + print(f"{'Вопрос':<6} {'Чел.ср':<8} {'AI ср':<8} {'Разн.':<8} {'Кол-во':<8} {'Описание'}") + print("-" * 80) + + for вопрос, row in вопросы_статистика.iterrows(): + разница_знак = "+" if row['ср_разница'] > 0 else "" + print(f"{вопрос:<6} {row['чел_среднее']:<8} {row['ai_среднее']:<8} " + f"{разница_знак}{row['ср_разница']:<7} {int(row['количество']):<8} ", end="") + + if row['расхождение'] > 0.3: + print("ВНИМАНИЕ: большое расхождение") + elif row['расхождение'] > 0.1: + print("Умеренное расхождение") + else: + print("Хорошее соответствие") + + +def save_detailed_analysis(df): + """Сохранение детального анализа в файл""" + + print("\n" + "=" * 40) + print("СОХРАНЕНИЕ РЕЗУЛЬТАТОВ") + print("=" * 40) + + # Создаем копию с анализом + df_analysis = df.copy() + df_analysis['разница_ai_человек'] = df_analysis['pred_score'] - df_analysis['Оценка экзаменатора'] + df_analysis['abs_разница'] = abs(df_analysis['разница_ai_человек']) + + # Добавляем категоризацию расхождений + условия = [ + df_analysis['abs_разница'] < 0.1, + df_analysis['abs_разница'] < 0.3, + df_analysis['abs_разница'] < 0.5, + df_analysis['abs_разница'] >= 0.5 + ] + категории = ['Отличное', 'Хорошее', 'Умеренное', 'Низкое'] + df_analysis['качество_согласования'] = np.select(условия, категории, default='Низкое') + + # Сортируем по наибольшим расхождениям + df_analysis = df_analysis.sort_values('abs_разница', ascending=False) + + try: + # Сохраняем в Excel + with pd.ExcelWriter('detailed_analysis_pro.xlsx', engine='openpyxl') as writer: + # Все данные + df_analysis.to_excel(writer, sheet_name='Все_данные_с_анализом', index=False) + + # Сводная таблица по вопросам + сводная = df_analysis.groupby('№ вопроса').agg({ + 'Оценка экзаменатора': ['mean', 'std', 'min', 'max'], + 'pred_score': ['mean', 'std', 'min', 'max'], + 'abs_разница': ['mean', 'max'], + 'разница_ai_человек': 'mean', + 'Id экзамена': 'count' + }).round(3) + сводная.to_excel(writer, sheet_name='Сводка_по_вопросам') + + # Наибольшие расхождения + большие_расхождения = df_analysis.nlargest(20, 'abs_разница')[ + ['Id экзамена', '№ вопроса', 'Оценка экзаменатора', + 'pred_score', 'разница_ai_человек', 'abs_разница'] + ] + большие_расхождения.to_excel(writer, sheet_name='Наибольшие_расхождения', index=False) + + # Статистика по качеству согласования + качество_стат = df_analysis['качество_согласования'].value_counts() + качество_стат.to_excel(writer, sheet_name='Качество_согласования') + + print("Детальный анализ сохранен в 'detailed_analysis_pro.xlsx'") + + except Exception as e: + print(f"Не удалось сохранить Excel, сохраняем в CSV: {e}") + df_analysis.to_csv('detailed_analysis_pro.csv', index=False, encoding='utf-8') + print("Детальный анализ сохранен в 'detailed_analysis_pro.csv'") + + +def generate_summary_report(df): + """Генерация итогового отчета""" + + print("\n" + "=" * 60) + print("ИТОГОВЫЙ ОТЧЕТ") + print("=" * 60) + + корреляция = df[['Оценка экзаменатора', 'pred_score']].corr().iloc[0, 1] + ср_разница = df['abs_разница'].mean() + + print(f"\nОБЩАЯ СТАТИСТИКА:") + print(f" Всего ответов: {len(df)}") + print(f" Корреляция AI-Человек: {корреляция:.3f}") + print(f" Средняя абсолютная разница: {ср_разница:.3f}") + + # Оценка качества + if корреляция > 0.8 and ср_разница < 0.2: + оценка = "ОТЛИЧНОЕ" + elif корреляция > 0.6 and ср_разница < 0.3: + оценка = "ХОРОШЕЕ" + elif корреляция > 0.4 and ср_разница < 0.4: + оценка = "УДОВЛЕТВОРИТЕЛЬНОЕ" + else: + оценка = "НИЗКОЕ" + + print(f"\nОЦЕНКА КАЧЕСТВА СИСТЕМЫ: {оценка}") + + # Рекомендации + print(f"\nРЕКОМЕНДАЦИИ:") + if ср_разница > 0.3: + print(" Проанализировать систематические ошибки в оценках") + if корреляция < 0.6: + print(" Улучшить согласованность с человеческими оценками") + + # Лучшие и худшие вопросы + вопросы_стат = df.groupby('№ вопроса')['abs_разница'].mean().sort_values() + лучший_вопрос = вопросы_стат.index[0] + худший_вопрос = вопросы_стат.index[-1] + + print(f"\nЛУЧШИЙ ВОПРОС ПО СОГЛАСОВАННОСТИ: №{лучший_вопрос} (разница: {вопросы_стат.iloc[0]:.3f})") + print(f"ХУДШИЙ ВОПРОС ПО СОГЛАСОВАННОСТИ: №{худший_вопрос} (разница: {вопросы_стат.iloc[-1]:.3f})") + + +def main(): + """Основная функция""" + + try: + # Загрузка данных + df = load_and_analyze_data() + + if df is None: + return + + # Проверка необходимых колонок + required_columns = ['Оценка экзаменатора', 'pred_score', '№ вопроса'] + missing_columns = [col for col in required_columns if col not in df.columns] + + if missing_columns: + print(f"ОШИБКА: Отсутствуют колонки: {missing_columns}") + return + + # Выполнение анализа + basic_statistics(df) + calculate_correlations(df) + create_visualizations(df) + analyze_extreme_cases(df) + analyze_explanations(df) + performance_by_question_type(df) + save_detailed_analysis(df) + generate_summary_report(df) + + print("\n" + "=" * 60) + print("АНАЛИЗ ЗАВЕРШЕН!") + print("=" * 60) + print("\nСОЗДАННЫЕ ФАЙЛЫ:") + print(" graphs/scatter_comparison_pro.png - сравнение оценок") + print(" graphs/difference_histogram_pro.png - распределение разниц") + print(" graphs/question_boxplot_pro.png - оценки по вопросам") + print(" detailed_analysis_pro.xlsx - детальный отчет") + + except FileNotFoundError: + print("ОШИБКА: Файл 'small.csv' не найден в текущей директории") + except Exception as e: + print(f"ОШИБКА при выполнении анализа: {str(e)}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/analyze_test.py b/analyze_test.py new file mode 100644 index 0000000000000000000000000000000000000000..ef7f46b104e9d106d318bff92d60a917f53c2db6 --- /dev/null +++ b/analyze_test.py @@ -0,0 +1,165 @@ +import pandas as pd +import matplotlib.pyplot as plt +from collections import Counter +import numpy as np +import os +import warnings + +warnings.filterwarnings('ignore') + +# Настройка отображения +plt.style.use('default') +plt.rcParams['font.family'] = 'DejaVu Sans' + + +def load_and_analyze_data(): + """Загрузка тестовых данных""" + + file_path = 'test_data.csv' + + try: + df = pd.read_csv(file_path, encoding='utf-8', delimiter=';') + print("✅ Тестовый файл загружен успешно") + except Exception as e: + print(f"❌ Ошибка загрузки: {e}") + print("Убедитесь, что файл test_data.csv находится в той же папке") + return None + + print("=" * 60) + print("ТЕСТОВЫЙ АНАЛИЗ AI-ОЦЕНОК") + print("=" * 60) + + print(f"Размер данных: {df.shape[0]} строк, {df.shape[1]} колонок") + print(f"Колонки: {list(df.columns)}") + print(f"\nПервые 3 строки:") + print(df.head(3)) + + return df + + +def basic_statistics(df): + """Базовая статистика""" + + print("\n" + "=" * 40) + print("БАЗОВАЯ СТАТИСТИКА") + print("=" * 40) + + print("AI оценки (pred_score):") + print(f" Среднее: {df['pred_score'].mean():.3f}") + print(f" Медиана: {df['pred_score'].median():.3f}") + print(f" Стандартное отклонение: {df['pred_score'].std():.3f}") + print(f" Минимум: {df['pred_score'].min():.3f}") + print(f" Максимум: {df['pred_score'].max():.3f}") + + print("\nОценки экзаменатора:") + print(f" Среднее: {df['Оценка экзаменатора'].mean():.3f}") + print(f" Медиана: {df['Оценка экзаменатора'].median():.3f}") + print(f" Стандартное отклонение: {df['Оценка экзаменатора'].std():.3f}") + + print("\nРаспределение оценок экзаменатора:") + распределение = df['Оценка экзаменатора'].value_counts().sort_index() + for оценка, count in распределение.items(): + print(f" {оценка}: {count} ответов ({count / len(df) * 100:.1f}%)") + + +def calculate_correlations(df): + """Расчет корреляций""" + + print("\n" + "=" * 40) + print("КОРРЕЛЯЦИИ И РАСХОЖДЕНИЯ") + print("=" * 40) + + correlation = df[['Оценка экзаменатора', 'pred_score']].corr().iloc[0, 1] + print(f"Корреляция между оценками: {correlation:.3f}") + + df['разница'] = df['pred_score'] - df['Оценка экзаменатора'] + df['abs_разница'] = abs(df['разница']) + + print(f"Средняя абсолютная разница: {df['abs_разница'].mean():.3f}") + print(f"Максимальная разница: {df['abs_разница'].max():.3f}") + print(f"Минимальная разница: {df['abs_разница'].min():.3f}") + + # Анализ согласованности + print("\nСОГЛАСОВАННОСТЬ ОЦЕНОК:") + for порог in [0.1, 0.3, 0.5, 1.0]: + согласованные = df[df['abs_разница'] < порог].shape[0] + процент = (согласованные / len(df)) * 100 + print(f" Разница < {порог}: {согласованные} ответов ({процент:.1f}%)") + + +def create_visualizations(df): + """Создание графиков""" + + print("\n" + "=" * 40) + print("СОЗДАНИЕ ГРАФИКОВ") + print("=" * 40) + + os.makedirs('graphs', exist_ok=True) + + # 1. Scatter plot + plt.figure(figsize=(10, 6)) + scatter = plt.scatter(df['Оценка экзаменатора'], df['pred_score'], + c=df['abs_разница'], cmap='viridis', alpha=0.7, s=60) + plt.colorbar(scatter, label='Абсолютная разница') + plt.plot([0, 2], [0, 2], 'r--', alpha=0.5, label='Идеальное соответствие') + plt.xlabel('Оценка экзаменатора') + plt.ylabel('AI оценка (pred_score)') + plt.title('Сравнение человеческой и AI оценки') + plt.legend() + plt.grid(True, alpha=0.3) + plt.savefig('graphs/test_scatter.png', dpi=300, bbox_inches='tight') + plt.close() + + # 2. Гистограмма разниц + plt.figure(figsize=(10, 6)) + plt.hist(df['разница'], bins=15, alpha=0.7, edgecolor='black', color='skyblue') + plt.xlabel('Разница (AI - Человек)') + plt.ylabel('Количество ответов') + plt.title('Распределение разниц оценок') + plt.grid(True, alpha=0.3) + plt.axvline(x=0, color='red', linestyle='--', alpha=0.8, label='Нулевая разница') + plt.legend() + plt.savefig('graphs/test_histogram.png', dpi=300, bbox_inches='tight') + plt.close() + + print("✅ Графики сохранены в папку 'graphs/'") + + +def analyze_explanations(df): + """Анализ объяснений""" + + print("\n" + "=" * 40) + print("АНАЛИЗ ОБЪЯСНЕНИЙ") + print("=" * 40) + + все_объяснения = ' '.join(df['объяснение_оценки'].dropna().astype(str)) + слова = [word.strip() for word in все_объяснения.split() if len(word.strip()) > 2] + частотность = Counter(слова) + + print("Топ-10 характеристик в объяснениях:") + for слово, count in частотность.most_common(10): + print(f" {слово}: {count}") + + +def main(): + """Основная функция""" + + df = load_and_analyze_data() + if df is None: + return + + basic_statistics(df) + calculate_correlations(df) + create_visualizations(df) + analyze_explanations(df) + + print("\n" + "=" * 60) + print("✅ ТЕСТОВЫЙ АНАЛИЗ ЗАВЕРШЕН!") + print("=" * 60) + print("📊 Созданные файлы:") + print(" • graphs/test_scatter.png") + print(" • graphs/test_histogram.png") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..12492264f201e5006849ceb6d969ed0f0fb57a76 --- /dev/null +++ b/app.py @@ -0,0 +1,128 @@ +# app.py +import io +import os +from pathlib import Path + +import pandas as pd +import streamlit as st + +# быстрый режим по умолчанию +os.environ.setdefault("FAST_MODE", "1") + +# импорт основного пайплайна +from src.predict import pipeline_infer + +# --- Конфигурация страницы --- +st.set_page_config(page_title="Русский как иностранный – автооценка", layout="centered") + +st.title("Автооценка устных ответов (RFL • CatBoost + ruSBERT)") +st.caption("Загрузите CSV входного формата и получите файл с колонками pred_score и pred_score_rounded.") + +# --- Информация о формате --- +with st.expander("Формат входного CSV", expanded=False): + st.markdown( + """ + Обязательные столбцы: + - **№ вопроса** (1..4) + - **Текст вопроса** + - **Транскрибация ответа** + - *(опционально)* **Оценка экзаменатора** — если есть, её не трогаем, добавим предсказания рядом. + + Разделитель — `;`, кодировка — UTF-8 (автоопределяется). + """ + ) + +# --- Пример шаблона CSV --- +with st.expander("📄 Скачать шаблон CSV"): + demo = pd.DataFrame({ + "№ вопроса": [1, 2], + "Текст вопроса": ["

Добро пожаловать...

", "

Опишите свой день...

"], + "Транскрибация ответа": ["Здравствуйте! Я приехал...", "Мой день начинается с..."], + "Оценка экзаменатора": [None, None], + }) + st.dataframe(demo) + buf_tmpl = io.BytesIO() + demo.to_csv(buf_tmpl, index=False, sep=";", encoding="utf-8-sig") + st.download_button("⬇ Скачать шаблон CSV", buf_tmpl.getvalue(), "template.csv", "text/csv") + +# --- Функция загрузки и нормализации --- +required = ["№ вопроса", "Текст вопроса", "Транскрибация ответа"] +aliases = { + "номер вопроса": "№ вопроса", + "вопрос": "Текст вопроса", + "текст задания": "Текст вопроса", + "транскрибация": "Транскрибация ответа", + "транскрипт": "Транскрибация ответа", + "ответ": "Транскрибация ответа", +} + + +def load_and_normalize_csv(raw_bytes: bytes) -> pd.DataFrame: + import io + for sep in [";", ",", "\t"]: + try: + df = pd.read_csv(io.BytesIO(raw_bytes), sep=sep, engine="python") + + # убрать возможные артефакты Git-конфликтов + if not df.empty and str(df.columns[0]).startswith("<<<"): + text = raw_bytes.decode("utf-8", errors="ignore") + lines = [ln for ln in text.splitlines() if not ln.startswith(("<<<", "===", ">>>"))] + df = pd.read_csv(io.StringIO("\n".join(lines)), sep=sep, engine="python") + + # нормализация имён колонок + rename_map = {} + for c in list(df.columns): + key = str(c).strip().lower() + if key in aliases: + rename_map[c] = aliases[key] + if rename_map: + df = df.rename(columns=rename_map) + + return df + except Exception: + continue + raise ValueError("Не удалось прочитать CSV. Проверьте разделитель (';' или ',') и кодировку UTF-8.") + + +# --- Основной интерфейс --- +uploaded = st.file_uploader("Загрузите CSV", type=["csv"]) +slow = st.toggle("Медленный режим", value=False, help="Выключите для быстрой оценки (точность ≈ прежняя).") +run = st.button("Посчитать") + +if uploaded and run: + try: + raw = uploaded.read() + df_in = load_and_normalize_csv(raw) + + # проверка обязательных колонок + missing = [c for c in required if c not in df_in.columns] + if missing: + st.error(f"❌ В файле нет обязательных колонок: {missing}. Проверь заголовки и разделитель ';'.") + st.dataframe(df_in.head()) + st.stop() + + # сохраняем временно + tmp_in = Path("data/api_tmp/tmp_input.csv") + tmp_in.parent.mkdir(parents=True, exist_ok=True) + df_in.to_csv(tmp_in, index=False, sep=";", encoding="utf-8-sig") + + # режим скорости + os.environ["FAST_MODE"] = "0" if slow else "1" + + tmp_out = Path("data/api_tmp/tmp_output.csv") + with st.spinner("Считаем..."): + pipeline_infer(tmp_in, tmp_out) + + df_out = pd.read_csv(tmp_out, sep=";", encoding="utf-8-sig") + st.success("✅ Готово!") + st.dataframe(df_out.head(20), use_container_width=True) + + buf = io.BytesIO() + df_out.to_csv(buf, index=False, sep=";", encoding="utf-8-sig") + st.download_button("⬇ Скачать результат (CSV)", data=buf.getvalue(), file_name="predicted.csv", mime="text/csv") + except Exception as e: + st.exception(e) + +# --- Подвал --- +st.markdown("---") +st.caption("Модель: CatBoost Q1..Q4 + ruSBERT. Быстрый режим = FAST_MODE=1.") diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..289cdf168217e5da954ce3b5c0d6dc60d6800592 --- /dev/null +++ b/app/main.py @@ -0,0 +1,223 @@ +from fastapi import FastAPI, UploadFile, File, HTTPException +from fastapi.responses import HTMLResponse, FileResponse +from fastapi.staticfiles import StaticFiles +import csv +import os +import tempfile +from typing import List, Dict +import re + +app = FastAPI(title="Russian Exam Auto Grader") + +# Монтируем статические файлы для веб-интерфейса +app.mount("/static", StaticFiles(directory="static"), name="static") + + +class ExamGrader: + def __init__(self): + self.setup_criteria() + + def setup_criteria(self): + self.criteria = { + 1: self._grade_question1, # 0-1 балл + 2: self._grade_question2, # 0-2 балла + 3: self._grade_question3, # 0-1 балл + 4: self._grade_question4 # 0-2 балла + } + + def grade_answer(self, question_num: int, transcription: str) -> int: + """Основной метод оценки""" + if question_num not in self.criteria: + return 0 + return self.criteria[question_num](transcription) + + def _grade_question1(self, text: str) -> int: + """Оценка вопроса 1 - начало диалога""" + text_lower = text.lower().strip() + + # Проверяем ключевые элементы диалога + has_greeting = any(word in text_lower for word in ['здравствуйте', 'добрый день', 'привет', 'здравствуй']) + has_request = any(word in text_lower for word in ['помогите', 'подскажите', 'нужно', 'хочу', 'могу']) + has_question = any(word in text_lower for word in ['как', 'что', 'где', 'когда', 'можно', 'сколько']) + + # Должен быть развернутый ответ + words_count = len(text_lower.split()) + + score = 0 + if has_greeting: + score += 0.3 + if has_request: + score += 0.4 + if has_question: + score += 0.3 + if words_count > 15: + score += 0.2 + + return 1 if score >= 0.7 else 0 + + def _grade_question2(self, text: str) -> int: + """Оценка вопроса 2 - ответы на вопросы""" + sentences = self._split_sentences(text) + + if len(sentences) < 2: + return 0 + + # Оцениваем полноту ответов + complete_sentences = 0 + for sentence in sentences: + words = sentence.split() + if len(words) >= 4: # Более-менее полное предложение + complete_sentences += 1 + + completeness_ratio = complete_sentences / len(sentences) + + if completeness_ratio >= 0.8: + return 2 + elif completeness_ratio >= 0.5: + return 1 + else: + return 0 + + def _grade_question3(self, text: str) -> int: + """Оценка вопроса 3 - диалог-запрос""" + text_lower = text.lower().strip() + + has_greeting = any(word in text_lower for word in ['здравствуйте', 'добрый день']) + has_request = any(word in text_lower for word in ['хочу', 'нужно', 'узнать', 'скажите', 'интересует']) + has_thanks = any(word in text_lower for word in ['спасибо', 'благодарю']) + + score = 0 + if has_greeting: + score += 0.3 + if has_request: + score += 0.4 + if has_thanks: + score += 0.3 + + return 1 if score >= 0.7 else 0 + + def _grade_question4(self, text: str) -> int: + """Оценка вопроса 4 - описание картинки""" + sentences = self._split_sentences(text) + + if len(sentences) < 3: + return 0 + + # Ищем описательные элементы + descriptive_words = ['вижу', 'изображен', 'находится', 'стоит', 'сидит', + 'одежда', 'цвет', 'время года', 'место', 'деревья', 'дом'] + + descriptive_count = 0 + for sentence in sentences: + if any(word in sentence.lower() for word in descriptive_words): + descriptive_count += 1 + + descriptive_ratio = descriptive_count / len(sentences) + + if descriptive_ratio >= 0.6: + return 2 + elif descriptive_ratio >= 0.3: + return 1 + else: + return 0 + + def _split_sentences(self, text: str) -> List[str]: + """Разделяет текст на предложения""" + sentences = re.split(r'[.!?]+', text) + return [s.strip() for s in sentences if len(s.strip()) > 0] + + +grader = ExamGrader() + + +@app.post("/evaluate/") +async def evaluate_file(file: UploadFile = File(...)): + try: + # Читаем CSV файл + content = await file.read() + decoded_content = content.decode('utf-8').splitlines() + + # Парсим CSV + reader = csv.DictReader(decoded_content, delimiter=';') + rows = list(reader) + + # Обрабатываем каждую строку + results = [] + for row in rows: + try: + question_num = int(row['№ вопроса']) + transcription = row['Транскрибация ответа'] + + score = grader.grade_answer(question_num, transcription) + + result_row = row.copy() + result_row['Оценка экзаменатора'] = score + results.append(result_row) + except (KeyError, ValueError) as e: + # Если есть ошибки в данных, ставим 0 + result_row = row.copy() + result_row['Оценка экзаменатора'] = 0 + results.append(result_row) + + # Сохраняем результаты + output_filename = "graded_" + file.filename + with open(output_filename, 'w', newline='', encoding='utf-8') as f: + if results: + fieldnames = results[0].keys() + writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';') + writer.writeheader() + writer.writerows(results) + + return FileResponse( + output_filename, + media_type='text/csv', + filename=output_filename + ) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Ошибка обработки: {str(e)}") + + +@app.get("/", response_class=HTMLResponse) +async def main_page(): + return """ + + + Russian Exam Auto Grader + + + +
+

Russian Exam Auto Grader

+

Загрузите CSV файл с ответами для автоматической оценки

+ +
+ +

+ +
+ +
+

Требования к файлу:

+ +
+
+ + + """ + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/app/simple_ui.py b/app/simple_ui.py new file mode 100644 index 0000000000000000000000000000000000000000..123308099d25a0366824d2ce522ce0e0c8424c6f --- /dev/null +++ b/app/simple_ui.py @@ -0,0 +1,52 @@ +from fastapi import FastAPI, Request +from fastapi.responses import HTMLResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +app = FastAPI() + +# Простой HTML интерфейс +HTML_FORM = """ + + + + Система оценки ответов + + + +
+

📝 Система автоматической оценки ответов

+

Загрузите CSV файл с ответами студентов для оценки

+ +
+ +

+ +
+ +
+

API Endpoints:

+ +
+
+ + +""" + + +@app.get("/", response_class=HTMLResponse) +async def main_page(request: Request): + return HTML_FORM + + +@app.get("/ui") +async def ui_page(): + return HTMLResponse(HTML_FORM) \ No newline at end of file diff --git a/app/ui.py b/app/ui.py new file mode 100644 index 0000000000000000000000000000000000000000..e971f4289867b5c9f7ad8d12d7fc5c1052283ce0 --- /dev/null +++ b/app/ui.py @@ -0,0 +1,32 @@ +from fastapi import FastAPI, Request, UploadFile, File +from fastapi.responses import HTMLResponse, StreamingResponse +from fastapi.templating import Jinja2Templates +import requests + +app = FastAPI(title="Scoring UI") +templates = Jinja2Templates(directory="templates") + +# 🔧 Локальный адрес FastAPI-сервера +API_URL = "http://localhost:8000/predict_csv" + +@app.get("/", response_class=HTMLResponse) +async def home(request: Request): + return templates.TemplateResponse("index.html", {"request": request}) + +@app.get("/health") +async def health(): + return {"status": "ok", "service": "scoring-ui"} + +@app.post("/predict") +async def predict_csv(file: UploadFile = File(...)): + files = {"file": (file.filename, await file.read(), file.content_type)} + try: + resp = requests.post(API_URL, files=files, timeout=1800) + resp.raise_for_status() + return StreamingResponse( + iter([resp.content]), + media_type="text/csv", + headers={"Content-Disposition": f'attachment; filename="predicted_{file.filename}"'} + ) + except Exception as e: + return {"error": str(e)} diff --git a/assessment_engine.py b/assessment_engine.py new file mode 100644 index 0000000000000000000000000000000000000000..8f062fb5fe1f7dcaf0ea27c174311b991baa2c66 --- /dev/null +++ b/assessment_engine.py @@ -0,0 +1,46 @@ +# assessment_engine.py +import pandas as pd + +# Импортируй твои шаги — подставь правильные модули: +# from src.data_cleaning import prepare_dataframe +# from src.features import build_baseline_features +# from src.features_q4 import add_q4_features +# from src.semantic_features import add_semantic_features +# from src.explanations import build_explanations +# from your_models_loader import load_models, predict_batch + +# Заглушка: здесь покажу форму, ты подставишь свои вызовы +def run_inference_df(df: pd.DataFrame, with_explanations: bool = True) -> pd.DataFrame: + data = df.copy() + + # 1) Очистка/нормализация + # data = prepare_dataframe(data) + + # 2) Базовые фичи + # data = build_baseline_features(data) + + # 3) Спецфичи для Q4 + # data = add_q4_features(data) + + # 4) Семантические фичи + # data = add_semantic_features(data) + + # 5) Предсказания CatBoost по каждому вопросу + # models = load_models("models") # твоя реализация + # data = predict_batch(data, models) # должна добавить колонку predicted_score + + # 6) Клип значений по диапазонам (на всякий случай) + if "question_number" in data.columns and "predicted_score" in data.columns: + def clip_score(row): + q = int(row["question_number"]) + s = float(row["predicted_score"]) + if q in (1, 3): + return int(min(1, max(0, round(s)))) + return int(min(2, max(0, round(s)))) + data["predicted_score"] = data.apply(clip_score, axis=1) + + # 7) Объяснения (если есть) + # if with_explanations: + # data = build_explanations(data) + + return data diff --git a/check_final_quality.py b/check_final_quality.py new file mode 100644 index 0000000000000000000000000000000000000000..272bbeaa7ec3c182901159505114e14274b5022b --- /dev/null +++ b/check_final_quality.py @@ -0,0 +1,27 @@ +import pandas as pd +import numpy as np +from sklearn.metrics import mean_absolute_error + +# Читаем наши предсказания +df = pd.read_csv('test_output.csv', delimiter=';', encoding='utf-8-sig') + +# Фильтруем только строки с истинными оценками +df_with_truth = df[df['Оценка экзаменатора'].notna()] + +if len(df_with_truth) > 0: + true_scores = df_with_truth['Оценка экзаменатора'] + pred_scores = df_with_truth['pred_score'] + + mae_total = mean_absolute_error(true_scores, pred_scores) + print(f'📊 ОБЩЕЕ КАЧЕСТВО (MAE): {mae_total:.3f} балла') + print() + + # По типам вопросов + for q in [1, 2, 3, 4]: + q_data = df_with_truth[df_with_truth['№ вопроса'] == q] + if len(q_data) > 0: + mae_q = mean_absolute_error(q_data['Оценка экзаменатора'], q_data['pred_score']) + count_q = len(q_data) + print(f' Вопрос {q}: MAE = {mae_q:.3f} балла (примеров: {count_q})') +else: + print('❌ Нет данных с истинными оценками для проверки') \ No newline at end of file diff --git a/check_quality.py b/check_quality.py new file mode 100644 index 0000000000000000000000000000000000000000..40e5babbce61207df83b17b8cde4ee7f96602a8f --- /dev/null +++ b/check_quality.py @@ -0,0 +1,57 @@ +import pandas as pd +import numpy as np + +# Загрузи скачанный файл +df = pd.read_csv('predicted_from_api.csv', sep=';') + +print("📊 АНАЛИЗ КАЧЕСТВА ПРЕДСКАЗАНИЙ") +print("=" * 50) + +# Проверяем наличие колонок +if 'Оценка экзаменатора' in df.columns and 'pred_score' in df.columns: + # Убираем строки где нет истинных оценок + df_clean = df.dropna(subset=['Оценка экзаменатора']) + + if len(df_clean) > 0: + true_scores = df_clean['Оценка экзаменатора'].astype(float) + pred_scores = df_clean['pred_score'].astype(float) + + # Основные метрики + mae = (abs(true_scores - pred_scores)).mean() + rmse = ((true_scores - pred_scores) ** 2).mean() ** 0.5 + + print(f"📈 Общие метрики:") + print(f" MAE (средняя абсолютная ошибка): {mae:.3f}") + print(f" RMSE (среднеквадратичная ошибка): {rmse:.3f}") + print(f" Корреляция: {true_scores.corr(pred_scores):.3f}") + + # По вопросам + print(f"\n📋 По типам вопросов:") + for q in [1, 2, 3, 4]: + mask = df_clean['№ вопроса'] == q + if mask.any(): + q_true = true_scores[mask] + q_pred = pred_scores[mask] + q_mae = (abs(q_true - q_pred)).mean() + + # Диапазон баллов для вопроса + if q in [1, 3]: + max_score = 1 + else: + max_score = 2 + + print(f" Вопрос {q} (0-{max_score}): MAE = {q_mae:.3f}, примеров = {len(q_true)}") + + else: + print("❌ В файле нет строк с оценками экзаменатора") + +else: + print("❌ В файле отсутствуют колонки 'Оценка экзаменатора' или 'pred_score'") + +# Статистика предсказаний +print(f"\n📊 Статистика предсказаний:") +for q in [1, 2, 3, 4]: + mask = df['№ вопроса'] == q + if mask.any(): + scores = df.loc[mask, 'pred_score'].astype(float) + print(f" Вопрос {q}: ср.={scores.mean():.2f}, мин={scores.min():.2f}, макс={scores.max():.2f}") \ No newline at end of file diff --git a/check_small_quality.py b/check_small_quality.py new file mode 100644 index 0000000000000000000000000000000000000000..6015dc18fb94a56c17fba5a980fb7f3cbee83dd5 --- /dev/null +++ b/check_small_quality.py @@ -0,0 +1,20 @@ +import pandas as pd + +df = pd.read_csv('test_small.csv', sep=';') +print("🔍 АНАЛИЗ SMALL.CSV С УЛУЧШЕННОЙ МОДЕЛЬЮ:") +print("=" * 50) + +for q in [1, 4]: # В small.csv есть только Q1 и Q4 + q_data = df[df['№ вопроса'] == q] + if len(q_data) > 0: + scores = q_data['pred_score'] + true_scores = q_data['Оценка экзаменатора'] + + print(f"📊 Вопрос {q}:") + print(f" Предсказания: {scores.tolist()}") + print(f" Истинные: {true_scores.tolist()}") + + if len(true_scores) > 0: + mae = (abs(true_scores - scores)).mean() + print(f" MAE: {mae:.3f}") + print() \ No newline at end of file diff --git a/create_and_analyze.py b/create_and_analyze.py new file mode 100644 index 0000000000000000000000000000000000000000..8df986f1238bc442b8b95f7ebce3e443f3e110b8 --- /dev/null +++ b/create_and_analyze.py @@ -0,0 +1,261 @@ +import pandas as pd +import matplotlib.pyplot as plt +from collections import Counter +import numpy as np +import os +import warnings + +warnings.filterwarnings('ignore') + +# Настройка отображения +plt.style.use('default') +plt.rcParams['font.family'] = 'DejaVu Sans' + + +def create_test_data(): + """Создание тестовых данных""" + + test_data = """Id экзамена;Id вопроса;№ вопроса;Текст вопроса;Оценка экзаменатора;Транскрибация ответа;pred_score;объяснение_оценки +3373871;30625752;1;"

Добро пожаловать на экзамен!

";1;"Экзаменатор: Начните диалог. Тестируемый: Здравствуйте, я хотел бы извиниться, что не смогу прийти на день рождения. Что бы вы хотели в подарок?";0.99;"🟢 Развернутый ответ | ✅ Высокое смысловое соответствие | 📊 Хорошая структура ответа | 💬 Разнообразная лексика | ⭐ Высокий балл" +3373871;30625753;2;"

Расскажите о вашем жилье

";2;"Экзаменатор: Вы живёте в квартире или доме? Тестируемый: Я живу в квартире в центре города. Это трёхкомнатная квартира с балконом. Квартира новая, построена в 2020 году.";1.62;"🟢 Развернутый ответ | ✅ Высокое смысловое соответствие | 📊 Хорошая структура ответа | 🏠 Подробное описание | ⭐ Высокий балл" +3373872;30625790;1;"

Начните диалог о работе

";1;"Экзаменатор: Узнайте о требованиях к работе. Тестируемый: Здравствуйте, я увидел ваше объявление о вакансии. Какие требования к соискателю? Какие документы нужны?";0.87;"🟢 Развернутый ответ | ⚠️ Умеренное смысловое соответствие | 📊 Хорошая структура ответа | 💬 Разнообразная лексика | ⭐ Высокий балл" +3373872;30625791;2;"

Опишите ваше жилье

";1;"Экзаменатор: Расскажите о вашей квартире. Тестируемый: У меня квартира. Она хорошая. Три комнаты.";0.45;"📉 Мало предложений | ❌ Низкое смысловое соответствие | 📊 Хорошая структура ответа" +3373873;30625828;1;"

Оформление документов

";2;"Экзаменатор: Объясните ситуацию в миграционной службе. Тестируемый: Здравствуйте, мне нужно оформить миграционную карту. Я приехал две недели назад. Можете дать мне бланк для заполнения?";1.85;"🟢 Развернутый ответ | ✅ Высокое смысловое соответствие | 📊 Хорошая структура ответа | 💬 Разнообразная лексика | ⭐ Высокий балл" +3373873;30625829;2;"

Ваши любимые фильмы

";1;"Экзаменатор: Какие фильмы вы любите? Тестируемый: Я смотрю фантастику и детективы. Люблю новые цветные фильмы. Мой любимый фильм - Интерстеллар, он о космосе и времени.";1.15;"🟢 Развернутый ответ | ⚠️ Умеренное смысловое соответствие | 📊 Хорошая структура ответа | 💬 Разнообразная лексика" +3373874;30625866;3;"

Опишите картинку

";2;"Экзаменатор: Что изображено на картинке? Тестируемый: На картинке изображена семья в парке. Дети играют в мяч, родители сидят на скамейке. Яркий солнечный день, лето.";1.92;"🟢 Развернутый ответ | ✅ Высокое смысловое соответствие | 🎨 Есть вступление с описанием картинки | 👤 Есть личный опыт | ⭐ Высокий балл" +3373874;30625867;4;"

Расскажите о хобби

";1;"Экзаменатор: Чем увлекаетесь? Тестируемый: Я читаю книги. Иногда смотрю фильмы.";0.35;"📉 Мало предложений | ❌ Низкое смысловое соответствие | 📊 Хорошая структура ответа" +3373875;30625904;1;"

Ситуация в больнице

";1;"Экзаменатор: Узнайте о приеме врача. Тестируемый: Здравствуйте, мне нужно записаться к терапевту на обследование. Когда принимает врач и какие документы нужны?";0.95;"🟢 Развернутый ответ | ✅ Высокое смысловое соответствие | 📊 Хорошая структура ответа | 💬 Разнообразная лексика | ⭐ Высокий балл" +3373875;30625905;2;"

Кулинарные предпочтения

";2;"Экзаменатор: Какая ваша любимая кухня? Тестируемый: Я очень люблю итальянскую кухню, особенно пасту и пиццу. Также нравится японская кухня - суши и роллы. Люблю готовить сам, особенно выпечку.";1.78;"🟢 Развернутый ответ | ✅ Высокое смысловое соответствие | 📊 Хорошая структура ответа | 🏠 Подробное описание | ⭐ Высокий балл" +""" + + # Сохраняем тестовые данные в файл + with open('test_data.csv', 'w', encoding='utf-8') as f: + f.write(test_data) + + print("✅ Тестовый файл 'test_data.csv' создан успешно") + return True + + +def load_and_analyze_data(): + """Загрузка тестовых данных""" + + file_path = 'test_data.csv' + + try: + df = pd.read_csv(file_path, encoding='utf-8', delimiter=';') + print("✅ Тестовый файл загружен успешно") + except Exception as e: + print(f"❌ Ошибка загрузки: {e}") + return None + + print("=" * 60) + print("ТЕСТОВЫЙ АНАЛИЗ AI-ОЦЕНОК") + print("=" * 60) + + print(f"Размер данных: {df.shape[0]} строк, {df.shape[1]} колонок") + print(f"Колонки: {list(df.columns)}") + print(f"\nПервые 3 строки:") + print(df.head(3)) + + return df + + +def basic_statistics(df): + """Базовая статистика""" + + print("\n" + "=" * 40) + print("БАЗОВАЯ СТАТИСТИКА") + print("=" * 40) + + print("AI оценки (pred_score):") + print(f" Среднее: {df['pred_score'].mean():.3f}") + print(f" Медиана: {df['pred_score'].median():.3f}") + print(f" Стандартное отклонение: {df['pred_score'].std():.3f}") + print(f" Минимум: {df['pred_score'].min():.3f}") + print(f" Максимум: {df['pred_score'].max():.3f}") + + print("\nОценки экзаменатора:") + print(f" Среднее: {df['Оценка экзаменатора'].mean():.3f}") + print(f" Медиана: {df['Оценка экзаменатора'].median():.3f}") + print(f" Стандартное отклонение: {df['Оценка экзаменатора'].std():.3f}") + + print("\nРаспределение оценок экзаменатора:") + распределение = df['Оценка экзаменатора'].value_counts().sort_index() + for оценка, count in распределение.items(): + print(f" {оценка}: {count} ответов ({count / len(df) * 100:.1f}%)") + + +def calculate_correlations(df): + """Расчет корреляций""" + + print("\n" + "=" * 40) + print("КОРРЕЛЯЦИИ И РАСХОЖДЕНИЯ") + print("=" * 40) + + correlation = df[['Оценка экзаменатора', 'pred_score']].corr().iloc[0, 1] + print(f"Корреляция между оценками: {correlation:.3f}") + + df['разница'] = df['pred_score'] - df['Оценка экзаменатора'] + df['abs_разница'] = abs(df['разница']) + + print(f"Средняя абсолютная разница: {df['abs_разница'].mean():.3f}") + print(f"Максимальная разница: {df['abs_разница'].max():.3f}") + print(f"Минимальная разница: {df['abs_разница'].min():.3f}") + + # Анализ согласованности + print("\nСОГЛАСОВАННОСТЬ ОЦЕНОК:") + for порог in [0.1, 0.3, 0.5, 1.0]: + согласованные = df[df['abs_разница'] < порог].shape[0] + процент = (согласованные / len(df)) * 100 + print(f" Разница < {порог}: {согласованные} ответов ({процент:.1f}%)") + + # Направление разниц + завышение = len(df[df['разница'] > 0]) + занижение = len(df[df['разница'] < 0]) + совпадение = len(df[df['разница'] == 0]) + + print(f"\nНАПРАВЛЕНИЕ РАЗНИЦ:") + print(f" AI завышает: {завышение} ({завышение / len(df) * 100:.1f}%)") + print(f" AI занижает: {занижение} ({занижение / len(df) * 100:.1f}%)") + print(f" Полное совпадение: {совпадение} ({совпадение / len(df) * 100:.1f}%)") + + +def create_visualizations(df): + """Создание графиков""" + + print("\n" + "=" * 40) + print("СОЗДАНИЕ ГРАФИКОВ") + print("=" * 40) + + os.makedirs('graphs', exist_ok=True) + + # 1. Scatter plot + plt.figure(figsize=(12, 8)) + scatter = plt.scatter(df['Оценка экзаменатора'], df['pred_score'], + c=df['abs_разница'], cmap='viridis', alpha=0.7, s=80) + plt.colorbar(scatter, label='Абсолютная разница') + plt.plot([0, 2], [0, 2], 'r--', alpha=0.5, label='Идеальное соответствие') + plt.xlabel('Оценка экзаменатора', fontsize=12) + plt.ylabel('AI оценка (pred_score)', fontsize=12) + plt.title('Сравнение человеческой и AI оценки\n(цвет показывает величину расхождения)', fontsize=14) + plt.legend() + plt.grid(True, alpha=0.3) + plt.xticks([1, 2]) + plt.savefig('graphs/test_scatter.png', dpi=300, bbox_inches='tight') + plt.close() + + # 2. Гистограмма разниц + plt.figure(figsize=(12, 6)) + plt.hist(df['разница'], bins=15, alpha=0.7, edgecolor='black', color='skyblue') + plt.xlabel('Разница (AI - Человек)', fontsize=12) + plt.ylabel('Количество ответов', fontsize=12) + plt.title('Распределение разниц между AI и человеческими оценками', fontsize=14) + plt.grid(True, alpha=0.3) + plt.axvline(x=0, color='red', linestyle='--', alpha=0.8, label='Нулевая разница') + plt.axvline(x=df['разница'].mean(), color='orange', linestyle='--', + alpha=0.8, label=f'Средняя разница: {df["разница"].mean():.3f}') + plt.legend() + plt.savefig('graphs/test_histogram.png', dpi=300, bbox_inches='tight') + plt.close() + + # 3. Box plot по вопросам + plt.figure(figsize=(12, 6)) + box_data = [df[df['№ вопроса'] == question]['pred_score'].values + for question in sorted(df['№ вопроса'].unique())] + + box_plot = plt.boxplot(box_data, labels=sorted(df['№ вопроса'].unique()), + patch_artist=True) + + # Раскрашиваем boxplot + colors = ['lightblue', 'lightgreen', 'lightcoral', 'lightyellow', 'lightpink'] + for patch, color in zip(box_plot['boxes'], colors): + patch.set_facecolor(color) + + plt.title('Распределение AI оценок по номерам вопросов', fontsize=14) + plt.xlabel('Номер вопроса', fontsize=12) + plt.ylabel('AI оценка (pred_score)', fontsize=12) + plt.grid(True, alpha=0.3) + plt.savefig('graphs/test_boxplot.png', dpi=300, bbox_inches='tight') + plt.close() + + print("✅ Графики сохранены в папку 'graphs/'") + + +def analyze_explanations(df): + """Анализ объяснений""" + + print("\n" + "=" * 40) + print("АНАЛИЗ ОБЪЯСНЕНИЙ") + print("=" * 40) + + все_объяснения = ' '.join(df['объяснение_оценки'].dropna().astype(str)) + слова = [word.strip() for word in все_объяснения.split() if len(word.strip()) > 2] + частотность = Counter(слова) + + print("Топ-10 характеристик в объяснениях:") + for слово, count in частотность.most_common(10): + print(f" {слово}: {count}") + + +def save_detailed_analysis(df): + """Сохранение детального анализа""" + + print("\n" + "=" * 40) + print("СОХРАНЕНИЕ РЕЗУЛЬТАТОВ") + print("=" * 40) + + # Создаем копию с анализом + df_analysis = df.copy() + + # Добавляем категоризацию расхождений + условия = [ + df_analysis['abs_разница'] < 0.1, + df_analysis['abs_разница'] < 0.3, + df_analysis['abs_разница'] < 0.5, + df_analysis['abs_разница'] >= 0.5 + ] + категории = ['Отличное', 'Хорошее', 'Умеренное', 'Низкое'] + df_analysis['качество_согласования'] = np.select(условия, категории, default='Низкое') + + # Сортируем по наибольшим расхождениям + df_analysis = df_analysis.sort_values('abs_разница', ascending=False) + + try: + # Сохраняем в Excel + with pd.ExcelWriter('detailed_analysis.xlsx', engine='openpyxl') as writer: + df_analysis.to_excel(writer, sheet_name='Все_данные_с_анализом', index=False) + print("✅ Детальный анализ сохранен в 'detailed_analysis.xlsx'") + except Exception as e: + print(f"⚠️ Не удалось сохранить Excel: {e}") + + +def main(): + """Основная функция""" + + print("Создание тестовых данных...") + if not create_test_data(): + return + + df = load_and_analyze_data() + if df is None: + return + + basic_statistics(df) + calculate_correlations(df) + create_visualizations(df) + analyze_explanations(df) + save_detailed_analysis(df) + + print("\n" + "=" * 60) + print("✅ ТЕСТОВЫЙ АНАЛИЗ ЗАВЕРШЕН!") + print("=" * 60) + print("📊 Созданные файлы:") + print(" • test_data.csv - тестовые данные") + print(" • graphs/test_scatter.png - сравнение оценок") + print(" • graphs/test_histogram.png - распределение разниц") + print(" • graphs/test_boxplot.png - оценки по вопросам") + print(" • detailed_analysis.xlsx - детальный отчет") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/deploy-to-yandex.ps1.py b/deploy-to-yandex.ps1.py new file mode 100644 index 0000000000000000000000000000000000000000..50d420cae138429e66b4444ffc834ab65c20c40f --- /dev/null +++ b/deploy-to-yandex.ps1.py @@ -0,0 +1,30 @@ +# deploy-to-yandex.ps1 +Write-Host "🚀 Начало развертывания в Yandex Cloud..." -ForegroundColor Green + +# Переменные (ЗАМЕНИТЕ на свои!) +$REGISTRY_ID = "your-registry-id" # Найти в консоли: Container Registry -> ID реестра +$IMAGE_NAME = "exam-scorer" +$TAG = "latest" +$FULL_IMAGE = "cr.yandex/$REGISTRY_ID/$IMAGE_NAME`:$TAG" + +# 1. Сборка Docker образа +Write-Host "📦 Сборка Docker образа..." -ForegroundColor Yellow +docker build -t $FULL_IMAGE . + +# 2. Авторизация в Yandex Container Registry +Write-Host "🔐 Авторизация в Container Registry..." -ForegroundColor Yellow +yc container registry configure-docker + +# 3. Загрузка образа в реестр +Write-Host "⬆️ Загрузка образа в Yandex Cloud..." -ForegroundColor Yellow +docker push $FULL_IMAGE + +Write-Host "✅ Образ успешно загружен: $FULL_IMAGE" -ForegroundColor Green +Write-Host "" +Write-Host "🎯 Дальнейшие действия:" -ForegroundColor Cyan +Write-Host "1. В консоли Yandex Cloud перейдите в 'Serverless Containers'" +Write-Host "2. Создайте новый контейнер" +Write-Host "3. Укажите образ: $FULL_IMAGE" +Write-Host "4. Настройте порт: 8000" +Write-Host "5. Задайте переменные окружения:" +Write-Host " - PYTHONPATH=/app" \ No newline at end of file diff --git a/deploy-to-yandex.sh.py b/deploy-to-yandex.sh.py new file mode 100644 index 0000000000000000000000000000000000000000..d38c4b1e67ebe48eccbf351367771903a16226cf --- /dev/null +++ b/deploy-to-yandex.sh.py @@ -0,0 +1,32 @@ +#!/bin/bash +set -e + +echo "🚀 Начало развертывания в Yandex Cloud..." + +# Переменные (замените на свои) +REGISTRY_ID="your-registry-id" # Найти в консоли: Container Registry -> ID реестра +IMAGE_NAME="exam-scorer" +TAG="latest" +FULL_IMAGE="cr.yandex/${REGISTRY_ID}/${IMAGE_NAME}:${TAG}" + +# 1. Сборка Docker образа +echo "📦 Сборка Docker образа..." +docker build -t ${FULL_IMAGE} . + +# 2. Авторизация в Yandex Container Registry +echo "🔐 Авторизация в Container Registry..." +yc container registry configure-docker + +# 3. Загрузка образа в реестр +echo "⬆️ Загрузка образа в Yandex Cloud..." +docker push ${FULL_IMAGE} + +echo "✅ Образ успешно загружен: ${FULL_IMAGE}" +echo "" +echo "🎯 Дальнейшие действия:" +echo "1. В консоли Yandex Cloud перейдите в 'Serverless Containers'" +echo "2. Создайте новый контейнер" +echo "3. Укажите образ: ${FULL_IMAGE}" +echo "4. Настройте порт: 8000" +echo "5. Задайте переменные окружения:" +echo " - PYTHONPATH=/app" \ No newline at end of file diff --git a/evaluate_mae.py b/evaluate_mae.py new file mode 100644 index 0000000000000000000000000000000000000000..2eefe37093389ac925d4c386184e6391103e2c21 --- /dev/null +++ b/evaluate_mae.py @@ -0,0 +1,63 @@ +import argparse +import pandas as pd +import numpy as np +import sys + +def safe_float(s): + try: + return float(s) + except Exception: + return np.nan + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--pred", required=True) + ap.add_argument("--gold", required=True) + ap.add_argument("--pred-col", default="predicted_score") + ap.add_argument("--score-col", default="examiner_score") + ap.add_argument("--question-col", default="question_number") + ap.add_argument("--key", default="") + args = ap.parse_args() + + p = pd.read_csv(args.pred) + g = pd.read_csv(args.gold) + + if args.pred_col not in p.columns: + print(f"ERROR: нет {args.pred_col} в {args.pred}"); sys.exit(1) + if args.score_col not in g.columns: + print(f"ERROR: нет {args.score_col} в {args.gold}"); sys.exit(1) + + keys = [k.strip() for k in args.key.split(",") if k.strip()] + if keys: + for miss in [k for k in keys if k not in p.columns]: + print(f"ERROR: нет ключа {miss} в pred"); sys.exit(1) + for miss in [k for k in keys if k not in g.columns]: + print(f"ERROR: нет ключа {miss} в gold"); sys.exit(1) + merged = p[keys + [args.pred_col]].merge( + g[keys + [args.score_col]], on=keys, how="inner", validate="one_to_one" + ) + else: + if len(p) != len(g): + print("ERROR: разные размеры pred/gold и нет ключа --key"); sys.exit(1) + merged = pd.DataFrame({ + args.pred_col: p[args.pred_col].values, + args.score_col: g[args.score_col].values + }) + + y_pred = merged[args.pred_col].map(safe_float) + y_true = merged[args.score_col].map(safe_float) + mask = (~y_pred.isna()) & (~y_true.isna()) + mae = np.mean(np.abs(y_pred[mask] - y_true[mask])) + print(f"MAE (общий): {mae:.4f} | N={mask.sum()}") + + # по вопросам, если есть + try: + qp = p.loc[mask, args.question_col] if args.question_col in p.columns else g.loc[mask, args.question_col] + df = pd.DataFrame({"qn": qp.values, "pred": y_pred[mask].values, "true": y_true[mask].values}) + for q, v in df.groupby("qn").apply(lambda d: np.mean(np.abs(d["pred"] - d["true"]))).sort_index().items(): + print(f" Q{int(q)} MAE: {v:.4f}") + except Exception: + pass + +if __name__ == "__main__": + main() diff --git a/feature_engineering.py b/feature_engineering.py new file mode 100644 index 0000000000000000000000000000000000000000..af461e9e7a9d63cd24c58455c6db99c80f5c688d --- /dev/null +++ b/feature_engineering.py @@ -0,0 +1,217 @@ +# feature_engineering.py +from __future__ import annotations + +import re +from typing import Iterable, List, Tuple, Optional + +import numpy as np +import pandas as pd + +try: + from sentence_transformers import SentenceTransformer, util as sbert_util +except Exception: # чтобы не падать на установке + SentenceTransformer = None # type: ignore + sbert_util = None # type: ignore + +try: + import language_tool_python +except Exception: + language_tool_python = None # type: ignore + + +_HTML_TAG_RE = re.compile(r"<[^>]+>") +_WS_RE = re.compile(r"\s+") +_PUNCT_RE = re.compile(r"[^\w\s?!.,:;ёЁа-яА-Я-]", re.UNICODE) + +# мини-лексиконы под критерии +POLITE_WORDS = {"здравствуйте", "здравствуй", "пожалуйста", "спасибо", "будьте добры"} +APOLOGY_WORDS = {"извините", "простите", "прошу прощения"} +FAMILY_WORDS = {"семья", "сын", "дочь", "дети", "ребёнок", "муж", "жена", "родители"} +SEASON_WORDS = {"зима", "весна", "лето", "осень"} +SHOP_WORDS = {"рассрочка", "гарантия", "характеристики", "документы", "касса"} +YESNO_WORDS = {"да", "нет", "наверное", "возможно"} + + +def _strip_html(s: str) -> str: + s = _HTML_TAG_RE.sub(" ", s) + s = _WS_RE.sub(" ", s).strip() + return s + + +def _only_text(s: str) -> str: + s = s.lower() + s = _strip_html(s) + s = _PUNCT_RE.sub(" ", s) + s = _WS_RE.sub(" ", s).strip() + return s + + +def _split_sentences(s: str) -> List[str]: + # простая сегментация + parts = re.split(r"(?<=[.!?])\s+", s) + return [p.strip() for p in parts if p.strip()] + + +def _strip_examiner_lines(text: str) -> str: + """ + Убираем вероятные реплики экзаменатора: предложения с '?', + короткие управляющие фразы ("хорошо.", "итак, ..."). + """ + sents = _split_sentences(text) + kept = [] + for i, sent in enumerate(sents): + low = sent.lower() + if "?" in sent: + continue + if low in {"хорошо.", "отлично.", "прекрасно.", "молодец."}: + continue + if low.startswith(("итак", "следующий", "теперь", "будьте", "ответьте")) and "?" in low: + continue + kept.append(sent) + return " ".join(kept) if kept else text + + +def _count_matches(words: Iterable[str], tokens: Iterable[str]) -> int: + wset = set(w.lower() for w in words) + return sum(1 for t in tokens if t in wset) + + +class FeatureExtractor: + """ + Лёгкий экстрактор признаков: + - очистка текста/HTML + - отделение реплик экзаменатора (эвристика) + - семантическая близость (SBERT) + - длины, кол-во предложений, вопросительных/восклицательных и пр. + - индикаторы по заданиям (вежливость, извинение, семья, рассрочка, …) + - (опц.) grammar_error_count через LanguageTool + """ + + def __init__( + self, + sbert_model_name: str = "cointegrated/rubert-tiny", + use_grammar: bool = False, + strip_examiner: bool = True, + ) -> None: + self.strip_examiner = strip_examiner + + # SBERT + self.sbert: Optional[SentenceTransformer] + if SentenceTransformer is None: + self.sbert = None + else: + self.sbert = SentenceTransformer(sbert_model_name) + + # Grammar + self.grammar = None + if use_grammar and language_tool_python is not None: + try: + self.grammar = language_tool_python.LanguageTool("ru") + except Exception: + self.grammar = None # безопасно отключаем + + # --------- примитивные фичи ---------- + def _basic_text_stats(self, text: str) -> Tuple[int, int, int, int, int, float]: + cleaned = _only_text(text) + tokens = cleaned.split() + sents = _split_sentences(text) + qmarks = text.count("?") + emarks = text.count("!") + avg_sent_len = (len(tokens) / max(len(sents), 1)) if tokens else 0.0 + return len(tokens), len(sents), qmarks, emarks, len(set(tokens)), float(avg_sent_len) + + def _semantic_sim(self, q: str, a: str) -> float: + if not self.sbert or sbert_util is None: + return 0.0 + try: + emb_q = self.sbert.encode([q], convert_to_tensor=True, normalize_embeddings=True) + emb_a = self.sbert.encode([a], convert_to_tensor=True, normalize_embeddings=True) + sim = float(sbert_util.cos_sim(emb_q, emb_a)[0][0].cpu().item()) + # нормализуем к [0..1] примерно + return max(0.0, min(1.0, (sim + 1.0) / 2.0)) + except Exception: + return 0.0 + + def _grammar_errors(self, text: str) -> int: + if not self.grammar: + return 0 + try: + matches = self.grammar.check(text) + return len(matches) + except Exception: + return 0 + + # --------- фичи под задания ---------- + def _question_specific_flags(self, qnum: int, answer_text: str, question_text: str) -> dict: + a_clean = _only_text(answer_text) + a_tokens = a_clean.split() + + flags = { + "has_politeness": int(_count_matches(POLITE_WORDS, a_tokens) > 0), + "has_apology": int(_count_matches(APOLOGY_WORDS, a_tokens) > 0), + "has_yesno": int(_count_matches(YESNO_WORDS, a_tokens) > 0), + "mentions_family": int(_count_matches(FAMILY_WORDS, a_tokens) > 0), + "mentions_season": int(_count_matches(SEASON_WORDS, a_tokens) > 0), + "mentions_shop": int(_count_matches(SHOP_WORDS, a_tokens) > 0), + "has_question_mark": int("?" in answer_text), + } + + # лёгкие правила по задачам + if qnum == 1: # извиниться + спросить + flags["task_completed_like_q1"] = int(flags["has_apology"] and flags["has_question_mark"]) + elif qnum == 2: # диалоговые ответы + flags["task_completed_like_q2"] = int(flags["has_yesno"] or len(a_tokens) > 12) + elif qnum == 3: # магазин: документы/рассрочка/характеристики + flags["task_completed_like_q3"] = int(flags["mentions_shop"] or len(a_tokens) > 25) + elif qnum == 4: # описание картинки + семья/дети + flags["task_completed_like_q4"] = int(flags["mentions_family"] or flags["mentions_season"]) + else: + flags["task_completed_like_q1"] = 0 + + # семантика вопрос-ответ + flags["qa_semantic_sim"] = self._semantic_sim(question_text, answer_text) + return flags + + # --------- публичное API ---------- + def extract_row_features(self, row: pd.Series) -> dict: + qnum = int(row.get("№ вопроса") or row.get("question_number") or 0) + qtext_raw = str(row.get("Текст вопроса") or row.get("question_text") or "") + atext_raw = str(row.get("Транскрибация") or row.get("transcript") or row.get("answer_text") or "") + + qtext = _strip_html(qtext_raw) + atext = _strip_html(atext_raw) + if self.strip_examiner: + atext = _strip_examiner_lines(atext) + + tok_len, sent_cnt, qmarks, emarks, uniq, avg_sent = self._basic_text_stats(atext) + grams = self._grammar_errors(atext) + + base = { + "question_number": qnum, + "question_text": qtext, + "answer_text": atext, + "tokens_len": tok_len, + "sent_count": sent_cnt, + "q_mark_count": qmarks, + "excl_mark_count": emarks, + "uniq_tokens": uniq, + "avg_sent_len": avg_sent, + "grammar_errors": grams, + "answer_len_chars": len(atext), + } + base.update(self._question_specific_flags(qnum, atext, qtext)) + return base + + def extract_all_features(self, df: pd.DataFrame) -> pd.DataFrame: + feats = [self.extract_row_features(r) for _, r in df.iterrows()] + out = pd.DataFrame(feats) + + # защитимся от NaN и типов + num_cols = [c for c in out.columns if c not in {"question_text", "answer_text"}] + for c in num_cols: + if c not in {"question_text", "answer_text"}: + out[c] = pd.to_numeric(out[c], errors="coerce") + out = out.fillna( + {c: 0 for c in out.columns if c not in {"question_text", "answer_text"}} + ) + return out diff --git a/feature_extractor.py b/feature_extractor.py new file mode 100644 index 0000000000000000000000000000000000000000..a2726d1905ecab64144ec470065261cc8f1944ee --- /dev/null +++ b/feature_extractor.py @@ -0,0 +1,368 @@ +import pandas as pd +import numpy as np +import re +from typing import Dict, List, Tuple, Optional +import warnings +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.metrics.pairwise import cosine_similarity + +warnings.filterwarnings('ignore') + + +class RussianFeatureExtractor: + """Исправленная версия экстрактора признаков с работающим composite_quality_score""" + + def __init__(self, use_heavy_models: bool = False): + print("Инициализация исправленного экстрактора признаков...") + + self.use_heavy_models = use_heavy_models + self.sbert_model = None + + # Инициализация моделей + self._initialize_models() + + # Списки ключевых слов + self.greeting_words = ['здравствуйте', 'привет', 'добрый', 'здравствуй', 'доброе', 'приветствую'] + self.question_words = ['как', 'что', 'где', 'когда', 'почему', 'можно', 'сколько', 'какой', 'какая'] + self.descriptive_words = ['вижу', 'изображен', 'находится', 'делает', 'одет', 'стоит', 'сидит'] + self.connector_words = ['потому что', 'поэтому', 'так как', 'например', 'кроме того'] + self.emotional_words = ['красиво', 'интересно', 'замечательно', 'прекрасно', 'нравится'] + self.spatial_words = ['слева', 'справа', 'вверху', 'внизу', 'рядом', 'около'] + + print("✅ Инициализация завершена!") + + def _initialize_models(self): + """Инициализация моделей""" + if self.use_heavy_models: + print("ℹ️ Тяжелые модели отключены для стабильности") + print("ℹ️ Используем легкие методы (TF-IDF)") + + def clean_text(self, text: str) -> str: + """Очистка текста""" + if pd.isna(text): + return "" + text = str(text) + text = re.sub(r'<[^>]+>', '', text) + text = re.sub(r'[^\w\sа-яА-ЯёЁ.,!?;:()-]', '', text) + text = re.sub(r'\s+', ' ', text).strip() + return text + + def extract_basic_features(self, text: str) -> Dict[str, float]: + """Базовые текстовые признаки""" + text_clean = self.clean_text(text) + + if not text_clean: + return { + 'text_length': 0, 'word_count': 0, 'sentence_count': 0, + 'avg_word_length': 0, 'lexical_diversity': 0, + 'has_questions': 0, 'has_exclamations': 0 + } + + # Базовые метрики + words = re.findall(r'\b[а-яёa-z]+\b', text_clean.lower()) + sentences = [s.strip() for s in re.split(r'[.!?]+', text_clean) if s.strip()] + + word_count = len(words) + text_length = len(text_clean) + sentence_count = len(sentences) + + features = { + 'text_length': text_length, + 'word_count': word_count, + 'sentence_count': sentence_count, + 'avg_word_length': sum(len(w) for w in words) / max(word_count, 1), + 'lexical_diversity': len(set(words)) / max(word_count, 1), + 'has_questions': int('?' in text_clean), + 'has_exclamations': int('!' in text_clean), + } + + return features + + def extract_semantic_features(self, question: str, answer: str) -> Dict[str, float]: + """Семантические признаки""" + question_clean = self.clean_text(question) + answer_clean = self.clean_text(answer) + + features = { + 'keyword_overlap': 0.0, + 'response_relevance': 0.0 + } + + if not answer_clean or not question_clean: + return features + + try: + # Упрощенный анализ ключевых слов + question_words = set(re.findall(r'\b[а-яё]+\b', question_clean.lower())) + answer_words = set(re.findall(r'\b[а-яё]+\b', answer_clean.lower())) + + if question_words: + common_words = question_words.intersection(answer_words) + features['keyword_overlap'] = len(common_words) / max(len(question_words), 1) + features['response_relevance'] = min(1.0, len(answer_words) / max(len(question_words), 1)) + + except Exception as e: + print(f"Ошибка семантических признаков: {e}") + + return features + + def extract_grammar_features(self, text: str) -> Dict[str, float]: + """Грамматические признаки""" + text_clean = self.clean_text(text) + + features = { + 'grammar_quality': 0.5, # Базовая оценка + 'has_punctuation': 0.0, + 'sentence_completeness': 0.0 + } + + if not text_clean: + return features + + sentences = [s.strip() for s in re.split(r'[.!?]+', text_clean) if s.strip()] + words = text_clean.split() + + if sentences: + # Проверка пунктуации + features['has_punctuation'] = 1.0 if any(mark in text_clean for mark in '.!?') else 0.0 + + # Полнота предложений + complete_sentences = sum(1 for s in sentences if len(s.split()) >= 3) + features['sentence_completeness'] = complete_sentences / max(len(sentences), 1) + + # Улучшенная эвристика грамматического качества + grammar_score = 0.0 + grammar_score += features['has_punctuation'] * 0.3 + grammar_score += features['sentence_completeness'] * 0.4 + + # Дополнительные эвристики + if len(words) > 5: + avg_sentence_len = len(words) / len(sentences) + if 5 <= avg_sentence_len <= 20: + grammar_score += 0.2 + elif avg_sentence_len > 20: + grammar_score += 0.1 + + features['grammar_quality'] = min(1.0, grammar_score) + + return features + + def extract_style_features(self, text: str) -> Dict[str, float]: + """Стилистические признаки""" + text_clean = self.clean_text(text).lower() + + features = { + 'has_greeting': 0.0, + 'has_description': 0.0, + 'has_connectors': 0.0, + 'has_emotional_words': 0.0, + 'style_score': 0.0 + } + + if not text_clean: + return features + + # Стилистические маркеры + features.update({ + 'has_greeting': float(any(greet in text_clean for greet in self.greeting_words)), + 'has_description': float(any(desc in text_clean for desc in self.descriptive_words)), + 'has_connectors': float(any(conn in text_clean for conn in self.connector_words)), + 'has_emotional_words': float(any(emot in text_clean for emot in self.emotional_words)), + }) + + # Оценка стиля + style_indicators = sum([ + features['has_greeting'], + features['has_connectors'], + features['has_emotional_words'] + ]) + features['style_score'] = min(1.0, style_indicators / 3) + + return features + + def extract_quality_features(self, text: str, question_type: int) -> Dict[str, float]: + """Признаки качества ответа""" + text_clean = self.clean_text(text) + words = text_clean.split() + word_count = len(words) + + features = { + 'answer_length_sufficiency': min(1.0, word_count / 30), # Нормализованная длина + 'content_richness': 0.0, + 'engagement_level': 0.0 + } + + if not text_clean: + return features + + # Богатство контента (лексическое разнообразие + длина) + lexical_diversity = len(set(words)) / max(word_count, 1) + features['content_richness'] = min(1.0, (lexical_diversity + features['answer_length_sufficiency']) / 2) + + # Уровень вовлеченности + engagement = 0.0 + engagement += features['answer_length_sufficiency'] * 0.4 + engagement += lexical_diversity * 0.3 + engagement += (1.0 if '?' in text_clean else 0.0) * 0.3 + features['engagement_level'] = engagement + + return features + + def extract_all_features(self, row: pd.Series) -> Dict[str, float]: + """Извлечение всех признаков - ИСПРАВЛЕННАЯ ВЕРСИЯ""" + try: + # Безопасное извлечение данных + question = row.get('Текст вопроса', row.get('Вопрос', '')) + answer = row.get('Транскрибация ответа', row.get('Транскрипт', row.get('Ответ', ''))) + question_type = row.get('№ вопроса', row.get('Тип вопроса', 1)) + + try: + question_type = int(question_type) + except: + question_type = 1 + + features = {} + + # 1. Базовые признаки (надежные) + basic_features = self.extract_basic_features(answer) + features.update(basic_features) + + # 2. Семантические признаки + semantic_features = self.extract_semantic_features(question, answer) + features.update(semantic_features) + + # 3. Грамматические признаки + grammar_features = self.extract_grammar_features(answer) + features.update(grammar_features) + + # 4. Стилистические признаки + style_features = self.extract_style_features(answer) + features.update(style_features) + + # 5. Признаки качества + quality_features = self.extract_quality_features(answer, question_type) + features.update(quality_features) + + # 6. Тип вопроса + features['question_type'] = float(question_type) + + # 7. ИСПРАВЛЕННЫЙ композитный показатель + features['composite_quality_score'] = self._calculate_quality_score(features) + + return features + + except Exception as e: + print(f"❌ Ошибка при извлечении признаков: {e}") + # Возвращаем базовые признаки + return self._get_fallback_features() + + def _calculate_quality_score(self, features: Dict[str, float]) -> float: + """ИСПРАВЛЕННЫЙ расчет качества ответа""" + + # Веса для разных категорий + weights = { + # Семантика и релевантность (35%) + 'keyword_overlap': 0.20, + 'response_relevance': 0.15, + + # Грамматика и структура (25%) + 'grammar_quality': 0.15, + 'sentence_completeness': 0.10, + + # Стиль и вовлеченность (25%) + 'style_score': 0.10, + 'engagement_level': 0.15, + + # Содержание (15%) + 'content_richness': 0.15 + } + + total_score = 0.0 + total_weight = 0.0 + + for feature, weight in weights.items(): + if feature in features: + value = features[feature] + total_score += value * weight + total_weight += weight + + # Нормализация на случай отсутствующих признаков + if total_weight > 0: + final_score = total_score / total_weight + else: + final_score = 0.5 # нейтральная оценка + + return min(1.0, max(0.0, final_score)) + + def _get_fallback_features(self) -> Dict[str, float]: + """Базовые признаки при ошибке""" + return { + 'text_length': 0, 'word_count': 0, 'sentence_count': 0, + 'avg_word_length': 0, 'lexical_diversity': 0, + 'has_questions': 0, 'has_exclamations': 0, + 'keyword_overlap': 0, 'response_relevance': 0, + 'grammar_quality': 0.5, 'has_punctuation': 0, 'sentence_completeness': 0, + 'has_greeting': 0, 'has_description': 0, 'has_connectors': 0, + 'has_emotional_words': 0, 'style_score': 0, + 'answer_length_sufficiency': 0, 'content_richness': 0, 'engagement_level': 0, + 'question_type': 1, 'composite_quality_score': 0.5 + } + + def extract_features_for_dataframe(self, df: pd.DataFrame, sample_size: int = None) -> pd.DataFrame: + """Извлечение признаков для датафрейма""" + if sample_size and sample_size < len(df): + df = df.sample(sample_size, random_state=42) + print(f"Взята выборка: {len(df)} строк") + + print(f"Извлечение признаков для {len(df)} строк...") + features_list = [] + successful = 0 + + for idx, row in df.iterrows(): + if idx % 50 == 0 and idx > 0: + print(f"Обработано {idx}/{len(df)} строк...") + + try: + features = self.extract_all_features(row) + features['original_index'] = idx + features_list.append(features) + successful += 1 + except Exception as e: + print(f"❌ Ошибка в строке {idx}: {e}") + continue + + if features_list: + features_df = pd.DataFrame(features_list) + features_df.set_index('original_index', inplace=True) + + success_rate = successful / len(df) + print(f"✅ Извлечение завершено! Успешно: {successful}/{len(df)} ({success_rate:.1%})") + + return features_df + else: + print("❌ Не удалось извлечь признаки") + return pd.DataFrame() + + +# Быстрая функция для тестирования +def extract_quick_features(text: str) -> Dict[str, float]: + extractor = RussianFeatureExtractor() + return extractor.extract_basic_features(text) + + +if __name__ == "__main__": + # Тест исправленной версии + extractor = RussianFeatureExtractor() + test_data = { + 'Текст вопроса': ['Расскажите о вашем городе'], + 'Транскрибация ответа': ['Привет! Я живу в Москве. Это большой и красивый город с множеством парков и музеев.'], + '№ вопроса': [1] + } + test_df = pd.DataFrame(test_data) + features = extractor.extract_all_features(test_df.iloc[0]) + + print("🎯 ТЕСТ ИСПРАВЛЕННОЙ ВЕРСИИ:") + print(f"Композитный показатель: {features['composite_quality_score']:.3f}") + print(f"Грамматическое качество: {features['grammar_quality']:.3f}") + print(f"Стилевой показатель: {features['style_score']:.3f}") + print(f"Количество слов: {features['word_count']}") \ No newline at end of file diff --git a/features_description.txt b/features_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..f1fdcd3398988796d7a326ef02844cbf2b56fea3 --- /dev/null +++ b/features_description.txt @@ -0,0 +1,111 @@ +ОПИСАНИЕ ПРИЗНАКОВ: +================== + +text_length: + Тип: int64 + Не-NULL: 100 + Среднее: 1246.900 + Корреляция с оценкой: 0.442 + +word_count: + Тип: int64 + Не-NULL: 100 + Среднее: 195.060 + Корреляция с оценкой: 0.447 + +sentence_count: + Тип: int64 + Не-NULL: 100 + Среднее: 33.830 + Корреляция с оценкой: 0.365 + +avg_word_length: + Тип: float64 + Не-NULL: 100 + Среднее: 6.497 + Корреляция с оценкой: -0.259 + +lexical_diversity: + Тип: float64 + Не-NULL: 100 + Среднее: 0.763 + Корреляция с оценкой: -0.336 + +semantic_similarity: + Тип: float64 + Не-NULL: 100 + Среднее: 0.000 + Корреляция с оценкой: nan + +keyword_overlap: + Тип: float64 + Не-NULL: 100 + Среднее: 0.000 + Корреляция с оценкой: nan + +grammar_error_count: + Тип: int64 + Не-NULL: 100 + Среднее: 0.000 + Корреляция с оценкой: nan + +grammar_error_ratio: + Тип: int64 + Не-NULL: 100 + Среднее: 0.000 + Корреляция с оценкой: nan + +has_punctuation: + Тип: float64 + Не-NULL: 100 + Среднее: 0.000 + Корреляция с оценкой: nan + +has_greeting: + Тип: float64 + Не-NULL: 100 + Среднее: 0.470 + Корреляция с оценкой: -0.342 + +has_questions: + Тип: float64 + Не-NULL: 100 + Среднее: 0.920 + Корреляция с оценкой: 0.179 + +has_description: + Тип: float64 + Не-NULL: 100 + Среднее: 0.310 + Корреляция с оценкой: 0.226 + +dialog_initiation: + Тип: float64 + Не-NULL: 25 + Среднее: 0.968 + Корреляция с оценкой: 0.363 + +question_type: + Тип: float64 + Не-NULL: 100 + Среднее: 2.500 + Корреляция с оценкой: 0.146 + +response_adequacy: + Тип: float64 + Не-NULL: 25 + Среднее: 0.970 + Корреляция с оценкой: 0.327 + +information_seeking: + Тип: float64 + Не-NULL: 25 + Среднее: 0.920 + Корреляция с оценкой: -0.147 + +descriptive_detail: + Тип: float64 + Не-NULL: 25 + Среднее: 1.000 + Корреляция с оценкой: nan + diff --git a/features_description_detailed.txt b/features_description_detailed.txt new file mode 100644 index 0000000000000000000000000000000000000000..d3f0a2532ce2e56c8b157698fe8043fb64a9bbda --- /dev/null +++ b/features_description_detailed.txt @@ -0,0 +1,179 @@ +ПОДРОБНОЕ ОПИСАНИЕ ПРИЗНАКОВ +================================================== + +text_length: + Тип: int64 + Не-NULL: 50 + Среднее: 1676.120 + Std: 1190.330 + Min: 328.000 + Max: 5002.000 + +word_count: + Тип: int64 + Не-NULL: 50 + Среднее: 265.600 + Std: 196.751 + Min: 46.000 + Max: 820.000 + +sentence_count: + Тип: int64 + Не-NULL: 50 + Среднее: 47.720 + Std: 39.596 + Min: 1.000 + Max: 157.000 + +avg_word_length: + Тип: float64 + Не-NULL: 50 + Среднее: 5.156 + Std: 0.386 + Min: 4.443 + Max: 6.397 + +lexical_diversity: + Тип: float64 + Не-NULL: 50 + Среднее: 0.618 + Std: 0.087 + Min: 0.431 + Max: 0.744 + +has_questions: + Тип: int64 + Не-NULL: 50 + Среднее: 0.920 + Std: 0.274 + Min: 0.000 + Max: 1.000 + +has_exclamations: + Тип: int64 + Не-NULL: 50 + Среднее: 0.000 + Std: 0.000 + Min: 0.000 + Max: 0.000 + +keyword_overlap: + Тип: float64 + Не-NULL: 50 + Среднее: 0.768 + Std: 0.078 + Min: 0.593 + Max: 0.902 + +response_relevance: + Тип: float64 + Не-NULL: 50 + Среднее: 1.000 + Std: 0.000 + Min: 1.000 + Max: 1.000 + +grammar_quality: + Тип: float64 + Не-NULL: 50 + Среднее: 0.682 + Std: 0.146 + Min: 0.419 + Max: 0.868 + +has_punctuation: + Тип: float64 + Не-NULL: 50 + Среднее: 0.940 + Std: 0.240 + Min: 0.000 + Max: 1.000 + +sentence_completeness: + Тип: float64 + Не-NULL: 50 + Среднее: 0.724 + Std: 0.157 + Min: 0.297 + Max: 1.000 + +has_greeting: + Тип: float64 + Не-NULL: 50 + Среднее: 0.540 + Std: 0.503 + Min: 0.000 + Max: 1.000 + +has_description: + Тип: float64 + Не-NULL: 50 + Среднее: 0.540 + Std: 0.503 + Min: 0.000 + Max: 1.000 + +has_connectors: + Тип: float64 + Не-NULL: 50 + Среднее: 0.500 + Std: 0.505 + Min: 0.000 + Max: 1.000 + +has_emotional_words: + Тип: float64 + Не-NULL: 50 + Среднее: 0.360 + Std: 0.485 + Min: 0.000 + Max: 1.000 + +style_score: + Тип: float64 + Не-NULL: 50 + Среднее: 0.467 + Std: 0.213 + Min: 0.000 + Max: 1.000 + +answer_length_sufficiency: + Тип: float64 + Не-NULL: 50 + Среднее: 1.000 + Std: 0.000 + Min: 1.000 + Max: 1.000 + +content_richness: + Тип: float64 + Не-NULL: 50 + Среднее: 0.861 + Std: 0.038 + Min: 0.745 + Max: 0.929 + +engagement_level: + Тип: float64 + Не-NULL: 50 + Среднее: 0.892 + Std: 0.090 + Min: 0.576 + Max: 0.958 + +question_type: + Тип: float64 + Не-NULL: 50 + Среднее: 2.460 + Std: 1.129 + Min: 1.000 + Max: 4.000 + +composite_quality_score: + Тип: float64 + Не-NULL: 50 + Среднее: 0.788 + Std: 0.054 + Min: 0.659 + Max: 0.894 + diff --git a/main.py b/main.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/minimal_app.py b/minimal_app.py new file mode 100644 index 0000000000000000000000000000000000000000..3fc9b5b32f0a72f5313505da1810544cf08b58a5 --- /dev/null +++ b/minimal_app.py @@ -0,0 +1,40 @@ +import streamlit as st +import subprocess +import sys + + +def install_package(package): + subprocess.check_call([sys.executable, "-m", "pip", "install", package]) + + +try: + from transformers import pipeline +except ImportError: + st.warning("Устанавливаем transformers...") + install_package("transformers") + from transformers import pipeline + +st.title("Минимальное приложение с Hugging Face") + + +# Простая модель для теста +@st.cache_resource +def load_model(): + try: + return pipeline("sentiment-analysis") + except Exception as e: + st.error(f"Ошибка загрузки модели: {e}") + return None + + +model = load_model() + +if model: + text = st.text_input("Введите текст:", "I love this!") + + if st.button("Анализировать") and text: + result = model(text)[0] + st.write(f"Результат: {result['label']}") + st.write(f"Уверенность: {result['score']:.4f}") +else: + st.error("Не удалось загрузить модель") \ No newline at end of file diff --git a/models/catboost_Q1.cbm b/models/catboost_Q1.cbm new file mode 100644 index 0000000000000000000000000000000000000000..fb763fa48557b190eecd71cdbab6413c611d9ff1 --- /dev/null +++ b/models/catboost_Q1.cbm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7756476f07583a1134762daef9296d39f6b89c7fac6200a342c3cd1dcabd5a98 +size 2223544 diff --git a/models/catboost_Q2.cbm b/models/catboost_Q2.cbm new file mode 100644 index 0000000000000000000000000000000000000000..41ca83fe178a06f9fc6706141a1f54f0f9a72b42 --- /dev/null +++ b/models/catboost_Q2.cbm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:384cba685992c67888db73aa6e78ddc2d41725079df06186fab61e615c4cf3f2 +size 2225560 diff --git a/models/catboost_Q3.cbm b/models/catboost_Q3.cbm new file mode 100644 index 0000000000000000000000000000000000000000..3833588f39dbc8ebbbc70ba90559ef8479336436 --- /dev/null +++ b/models/catboost_Q3.cbm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29a7dd613ab06bf185f48e70a4179d83c629f84bba489ddcba151b662c6647fe +size 2227624 diff --git a/models/catboost_Q4.cbm b/models/catboost_Q4.cbm new file mode 100644 index 0000000000000000000000000000000000000000..6c53d5a23cb0ffa2c49fc775102613c6a9d92f88 --- /dev/null +++ b/models/catboost_Q4.cbm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0916b0b97fcdf71e14dd6b0b3e4f4a8d652a0717d87b9173dc094ac558ad1696 +size 2228928 diff --git a/models/catboost_Q4_enhanced.cbm b/models/catboost_Q4_enhanced.cbm new file mode 100644 index 0000000000000000000000000000000000000000..ff4c89fbc10de6f2f704d735c297c2eec6a6475f --- /dev/null +++ b/models/catboost_Q4_enhanced.cbm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:058ddc80f29acf6eab048d425f0ede4f29145969e708daa5e48a94127b809e94 +size 565688 diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000000000000000000000000000000000000..4584de7e860925957a2daa58cfb4de8acc1e4802 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = tests +pythonpath = . diff --git a/quick_test.py b/quick_test.py new file mode 100644 index 0000000000000000000000000000000000000000..2d8edaff9f6abea0cf0276d8cfcac3e1d555af62 --- /dev/null +++ b/quick_test.py @@ -0,0 +1,58 @@ +### **3. `quick_test.py`** (быстрая проверка) +```python +# !/usr/bin/env python3 +""" +Быстрая проверка работы системы +""" + +import subprocess +import sys +import os + + +def run_command(cmd): + """Запускает команду и возвращает результат""" + try: + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + return result.returncode == 0, result.stdout, result.stderr + except Exception as e: + return False, "", str(e) + + +def main(): + print("🚀 БЫСТРАЯ ПРОВЕРКА СИСТЕМЫ") + print("=" * 50) + + # 1. Проверяем зависимости + print("1. Проверка зависимостей...") + success, out, err = run_command( + "python -c \"import catboost, fastapi, streamlit; print('✅ Все зависимости установлены')\"") + if success: + print(" ✅ Все зависимости установлены") + else: + print(" ❌ Ошибка зависимостей:", err) + return + + # 2. Проверяем модели + print("2. Проверка ML моделей...") + models = ["catboost_Q1.cbm", "catboost_Q2.cbm", "catboost_Q3.cbm", "catboost_Q4.cbm"] + all_models_exist = all(os.path.exists(f"models/{model}") for model in models) + if all_models_exist: + print(" ✅ Все ML модели найдены") + else: + print(" ❌ Не все модели найдены") + return + + # 3. Проверяем данные + print("3. Проверка данных...") + if os.path.exists("data/raw/small.csv"): + print(" ✅ Тестовые данные найдены") + else: + print(" ⚠️ Тестовые данные не найдены") + + print("\n🎉 СИСТЕМА ГОТОВА К РАБОТЕ!") + print("Запустите: docker-compose up") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..cdd154cb502dfd096c85da03bf3fd7e60c244ffb Binary files /dev/null and b/requirements.txt differ diff --git a/retrain_q4.py b/retrain_q4.py new file mode 100644 index 0000000000000000000000000000000000000000..a5123f27961ca659c151d2cf797bfa977970b40e --- /dev/null +++ b/retrain_q4.py @@ -0,0 +1,72 @@ +import pandas as pd +import numpy as np +from catboost import CatBoostRegressor +import sys +import os + +sys.path.append('src') + +from features_q4 import enhanced_q4_features +from features import build_baseline_features +from semantic_features import add_semantic_similarity +from data_cleaning import prepare_dataframe + + +def retrain_q4_model(): + print("🔄 Переобучение модели Q4 с улучшенными фичами...") + + # 1. Загрузи данные + df = pd.read_csv('data/raw/Данные для кейса.csv', sep=';') + print(f"📊 Загружено {len(df)} строк") + + # 2. Подготовь данные только для Q4 + df_clean = prepare_dataframe(df) + df_q4 = df_clean[df_clean['question_number'] == 4] + print(f"📋 Q4 данных: {len(df_q4)} строк") + + # 3. Построй все фичи + print("🔨 Строим фичи...") + feats = build_baseline_features(df_q4) + feats = add_semantic_similarity(feats, verbose=False) + feats = enhanced_q4_features(feats) + + # 4. Выдели фичи и целевую переменную + feature_cols = [c for c in feats.columns if c.startswith('q4_') or c in [ + 'semantic_sim', 'ans_len_words', 'ans_n_sents', 'ans_ttr', + 'ans_short_sent_rt', 'ans_punct_rt', 'q_len_words' + ]] + + X = feats[feature_cols].fillna(0) + y = feats['score'].fillna(0) + + print(f"🎯 Фичей: {len(feature_cols)}, Примеров: {len(X)}") + print(f"📈 Фичи: {feature_cols}") + + # 5. Обучи новую модель + print("🤖 Обучаем CatBoost...") + model = CatBoostRegressor( + iterations=500, + learning_rate=0.1, + depth=6, + verbose=100, + random_state=42 + ) + + model.fit(X, y) + + # 6. Сохрани модель + model.save_model('models/catboost_Q4_enhanced.cbm') + print("✅ Модель Q4 переобучена с улучшенными фичами!") + + # 7. Проверим важность фич + feature_importance = pd.DataFrame({ + 'feature': feature_cols, + 'importance': model.get_feature_importance() + }).sort_values('importance', ascending=False) + + print("\n📊 Важность фич:") + print(feature_importance.head(10)) + + +if __name__ == "__main__": + retrain_q4_model() \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000000000000000000000000000000000000..21835870fc9dd288d8409b9044d292ca7dc59665 --- /dev/null +++ b/run.py @@ -0,0 +1,4 @@ +import uvicorn + +if __name__ == "__main__": + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file diff --git a/run_predict.py b/run_predict.py new file mode 100644 index 0000000000000000000000000000000000000000..60786c8c56b79e14534a3c064f57e7533f30c7a5 --- /dev/null +++ b/run_predict.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +""" +Упрощенный запуск предсказания +""" +import os +import sys + +# Устанавливаем PYTHONPATH +current_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, current_dir) + + +def main(): + print("🚀 ЗАПУСК ПРЕДСКАЗАНИЯ") + + # Импортируем после установки PYTHONPATH + from src.predict import pipeline_infer + + # Запускаем предсказание + input_file = "data/raw/small.csv" + output_file = "predictions_final.csv" + + print(f"📁 Входной файл: {input_file}") + print(f"📁 Выходной файл: {output_file}") + + pipeline_infer(input_file, output_file) + print("🎉 ПРЕДСКАЗАНИЕ ЗАВЕРШЕНО!") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000000000000000000000000000000000000..55090899d0334b0210fdd7f30ea9b2e23e6fce59 --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.10 diff --git a/serverless-container.yaml.py b/serverless-container.yaml.py new file mode 100644 index 0000000000000000000000000000000000000000..42221c599a30facfa9e6836ccfc34f7606ecd35d --- /dev/null +++ b/serverless-container.yaml.py @@ -0,0 +1,28 @@ +name: exam-scorer-api +spec: + connectivity: + network_id: default + containers: + - name: api + image: cr.yandex/your-registry-id/exam-scorer:latest + command: + - python + - -m + - uvicorn + - app.main:api + - --host + - 0.0.0.0 + - --port + - "8000" + ports: + - containerPort: 8000 + protocol: TCP + resources: + memory: "2048MB" + cores: "1" + probes: + http: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 5 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..4207e370b218008b6f5af0c643001da140318266 --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +import os +import sys +import subprocess + + +def setup_environment(): + """Устанавливает PYTHONPATH и возвращает команду для запуска""" + current_dir = os.path.dirname(os.path.abspath(__file__)) + + # Добавляем в PYTHONPATH + if current_dir not in sys.path: + sys.path.insert(0, current_dir) + + # Устанавливаем переменную окружения для дочерних процессов + os.environ['PYTHONPATH'] = current_dir + os.pathsep + os.environ.get('PYTHONPATH', '') + + print(f"✅ PYTHONPATH установлен: {current_dir}") + return current_dir + + +if __name__ == "__main__": + setup_environment() + + # Теперь можно запускать predict.py + print("🚀 Запуск predict.py...") + try: + from src.predict import pipeline_infer + + pipeline_infer("data/raw/small.csv", "predictions.csv") + print("✅ Предсказание завершено!") + except Exception as e: + print(f"❌ Ошибка: {e}") \ No newline at end of file diff --git a/simple_app.py b/simple_app.py new file mode 100644 index 0000000000000000000000000000000000000000..2acfede953f9d0b6c64535c9fbf003023707b91c --- /dev/null +++ b/simple_app.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +import streamlit as st +from transformers import pipeline +import os + +# Отключаем предупреждения +os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1" + +# Простая конфигурация +st.set_page_config( + page_title="AI Model Demo", + page_icon="🤖", + layout="wide" +) + +# Простой заголовок +st.title("🤖 Демо AI Моделей") +st.write("Тестирование моделей машинного обучения") + +# Боковая панель +st.sidebar.header("Настройки") + +# Выбор задачи +task = st.sidebar.selectbox( + "Выберите задачу:", + ["Анализ тональности", "Генерация текста", "Классификация"] +) + +# Основной контент +if task == "Анализ тональности": + st.header("📊 Анализ тональности текста") + text = st.text_area("Введите текст:", "Я очень рад этому!") + + if st.button("Анализировать"): + with st.spinner("Анализируем..."): + try: + classifier = pipeline("sentiment-analysis") + result = classifier(text)[0] + st.success(f"Результат: {result['label']}") + st.info(f"Уверенность: {result['score']:.4f}") + except Exception as e: + st.error(f"Ошибка: {e}") + +elif task == "Генерация текста": + st.header("✍️ Генерация текста") + prompt = st.text_area("Введите начало текста:", "Искусственный интеллект") + + if st.button("Сгенерировать"): + with st.spinner("Генерируем..."): + try: + generator = pipeline("text-generation", model="gpt2") + result = generator(prompt, max_length=100, num_return_sequences=1) + st.write("**Результат:**") + st.write(result[0]['generated_text']) + except Exception as e: + st.error(f"Ошибка: {e}") + +elif task == "Классификация": + st.header("🏷️ Классификация текста") + text = st.text_area("Введите текст для классификации:", "Это потрясающий продукт!") + + if st.button("Классифицировать"): + with st.spinner("Классифицируем..."): + try: + classifier = pipeline("text-classification") + results = classifier(text) + st.write("**Результаты:**") + for result in results: + st.write(f"- {result['label']}: {result['score']:.4f}") + except Exception as e: + st.error(f"Ошибка: {e}") + +# Информация внизу +st.sidebar.markdown("---") +st.sidebar.info("Простое демо для тестирования моделей") \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/add_q4_features.py b/src/add_q4_features.py new file mode 100644 index 0000000000000000000000000000000000000000..37b55842844ddeb27c99a8e3d73b714d1a314513 --- /dev/null +++ b/src/add_q4_features.py @@ -0,0 +1,22 @@ +# src/add_q4_features.py +from pathlib import Path +import pandas as pd +from src.features_q4 import q4_slot_features + +ROOT = Path(__file__).resolve().parents[1] +INP = ROOT / "data" / "processed" / "features_with_semantics.csv" # уже есть +OUT = ROOT / "data" / "processed" / "features_with_semantics_q4.csv" + +def main(): + df = pd.read_csv(INP, encoding="utf-8-sig") + df2 = q4_slot_features(df) + OUT.parent.mkdir(parents=True, exist_ok=True) + df2.to_csv(OUT, index=False, encoding="utf-8-sig") + print("✅ Сохранено:", OUT) + print(df2[[ + "question_number","semantic_sim", + "q4_slots_covered","q4_answered_personal","q4_non_cyr_ratio","score" + ]].head()) + +if __name__ == "__main__": + main() diff --git a/src/analyze_results.py b/src/analyze_results.py new file mode 100644 index 0000000000000000000000000000000000000000..0c443d2f9ca4a91adc185b7f2e0bd6938f7d4a43 --- /dev/null +++ b/src/analyze_results.py @@ -0,0 +1,130 @@ +# src/analyze_results.py +from __future__ import annotations +from pathlib import Path +import argparse +import sys +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +def _read_csv_safely(path: Path) -> pd.DataFrame: + """ + Устойчивое чтение CSV: пробуем ; , и авто. Выбираем тот, где получилось >= 5 колонок. + """ + tries = [ + ("utf-8-sig", ","), # СНАЧАЛА КОММА — чаще для наших predicted.csv + ("utf-8-sig", ";"), + ("utf-8", ","), + ("utf-8", ";"), + ("utf-8-sig", None), + ("utf-8", None), + ] + last_err = None + best_df = None + best_info = None + + for enc, sep in tries: + try: + if sep is None: + df = pd.read_csv(path, encoding=enc, sep=None, engine="python") + got = f"auto" + else: + df = pd.read_csv(path, encoding=enc, sep=sep) + got = sep + # эвристика: нормальные файлы имеют >= 5 колонок + if df.shape[1] >= 5: + print(f"[i] CSV прочитан с encoding='{enc}', sep='{got}'") + return df + # запомним самый «лучший» (по числу колонок), если ни один не пройдёт порог + if best_df is None or df.shape[1] > best_df.shape[1]: + best_df, best_info = df, (enc, got) + except Exception as e: + last_err = e + + if best_df is not None: + enc, got = best_info + print(f"[!] Не удалось надёжно определить разделитель, взят лучший вариант encoding='{enc}', sep='{got}' (cols={best_df.shape[1]})") + return best_df + + raise last_err if last_err else RuntimeError("Не удалось прочитать CSV") + +def _resolve_col(df: pd.DataFrame, candidates) -> str: + for c in candidates: + if c in df.columns: + return c + raise KeyError(f"Не удалось найти колонку из вариантов: {candidates}\nИмеющиеся колонки: {list(df.columns)}") + +def mae(y_true: np.ndarray, y_pred: np.ndarray) -> float: + return float(np.mean(np.abs(y_true - y_pred))) + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--input", type=str, default="data/processed/predicted.csv", help="Путь к CSV с колонкой pred_score") + args = ap.parse_args() + + in_path = Path(args.input) + if not in_path.exists(): + print(f"❌ Не найден файл с предсказаниями: {in_path}") + sys.exit(1) + + df = _read_csv_safely(in_path) + + # Колонки + y_col = _resolve_col(df, ["Оценка экзаменатора", "оценка экзаменатора", "score", "y", "target"]) + p_col = _resolve_col(df, ["pred_score", "pred", "prediction"]) + + # Колонка номера вопроса: поддержим оба варианта + if "№ вопроса" in df.columns: + q_col = "№ вопроса" + else: + q_col = _resolve_col(df, ["question_number", "q", "номер вопроса", "№ вопроса"]) + + # Числовые массивы + y = pd.to_numeric(df[y_col], errors="coerce").to_numpy() + p = pd.to_numeric(df[p_col], errors="coerce").to_numpy() + + # Общая MAE + m_all = mae(y, p) + print(f"MAE (вся выборка): {m_all:.3f}\n") + + # MAE по вопросам + g = (df[[q_col]].copy()) + g["y"] = y + g["p"] = p + mae_by_q = ( + g.groupby(q_col, as_index=True) + .apply(lambda s: mae(s["y"].to_numpy(), s["p"].to_numpy())) + .to_frame("MAE") + ) + print("MAE по вопросам:") + print(mae_by_q) + out_dir = Path("reports"); out_dir.mkdir(parents=True, exist_ok=True) + mae_by_q.to_csv(out_dir / "metrics_summary.csv", encoding="utf-8-sig") + print(f"\n✅ Сохранено: {out_dir / 'metrics_summary.csv'}") + + # Графики + # 1) гистограмма ошибок + err = np.abs(y - p) + plt.figure() + plt.hist(err[~np.isnan(err)], bins=30) + plt.title("Absolute Error Histogram") + plt.xlabel("|y - pred|"); plt.ylabel("count") + plt.tight_layout(); plt.savefig(out_dir / "error_hist.png"); plt.close() + print(f"📊 Гистограмма: {out_dir / 'error_hist.png'}") + + # 2) mae_by_q барплот + plt.figure() + mae_by_q["MAE"].plot(kind="bar") + plt.ylabel("MAE"); plt.title("MAE by question") + plt.tight_layout(); plt.savefig(out_dir / "mae_by_q.png"); plt.close() + print(f"📊 MAE по вопросам: {out_dir / 'mae_by_q.png'}") + + # 3) scatter + plt.figure() + plt.scatter(y, p, alpha=0.3) + plt.xlabel("true"); plt.ylabel("pred"); plt.title("Pred vs True") + plt.tight_layout(); plt.savefig(out_dir / "pred_vs_true.png"); plt.close() + print(f"📊 Scatter: {out_dir / 'pred_vs_true.png'}") + +if __name__ == "__main__": + main() diff --git a/src/build_features.py b/src/build_features.py new file mode 100644 index 0000000000000000000000000000000000000000..74a6dd3b43a9d4ac0bd87778db09cf7740710f9f --- /dev/null +++ b/src/build_features.py @@ -0,0 +1,24 @@ +from pathlib import Path +import pandas as pd +from src.features import build_basic_features + +ROOT = Path(__file__).resolve().parents[1] +CLEAN = ROOT / "data" / "processed" / "clean_data.csv" +OUT = ROOT / "data" / "processed" / "features_baseline.csv" + +def main(): + if not CLEAN.exists(): + raise FileNotFoundError(f"Не найден {CLEAN}") + df = pd.read_csv(CLEAN, encoding="utf-8-sig") + feats = build_basic_features(df) + OUT.parent.mkdir(parents=True, exist_ok=True) + feats.to_csv(OUT, index=False, encoding="utf-8-sig") + print("✅ Сохранено:", OUT) + print(feats[[ + "question_number","ans_len_chars","ans_len_words","ans_n_sents", + "ans_avg_sent_len","ans_ttr","ans_short_sent_rt","ans_punct_rt", + "q_len_words","score" + ]].head(5)) + +if __name__ == "__main__": + main() diff --git a/src/data_cleaning.py b/src/data_cleaning.py new file mode 100644 index 0000000000000000000000000000000000000000..9557ba2a1ab4afbfba394437fd842130973e6398 --- /dev/null +++ b/src/data_cleaning.py @@ -0,0 +1,136 @@ +# src/data_cleaning.py +import re +import pandas as pd +from bs4 import BeautifulSoup + +# ⚠️ новый импорт: извлекаем речь тестируемого +from src.text_roles import extract_tester_reply + + +# ---------- утилиты очистки ---------- +def clean_html(text: str) -> str: + """Удаляем HTML/разметку из текста вопроса/ответа.""" + if pd.isna(text): + return "" + return BeautifulSoup(str(text), "lxml").get_text(separator=" ", strip=True) + + +# эвристический парсер на случай, если в транскрипте есть роли +# (оставляем куски после "Тестируемый:/Кандидат:/Студент:" до следующего "Экзаменатор:") +_SPEAKER_PAT = re.compile( + r"(?:Тестируемый|Кандидат|Студент)\s*:\s*(.+?)(?=(?:Экзаменатор|Преподаватель|Собеседник)\s*:|$)", + re.IGNORECASE | re.DOTALL, +) + +def extract_answer(transcript: str) -> str: + """Базовое извлечение ответа из общей транскрипции (если есть метки ролей).""" + if not isinstance(transcript, str) or not transcript.strip(): + return "" + t = transcript.replace("\r", "\n") + chunks = _SPEAKER_PAT.findall(t) + joined = " ".join(x.strip() for x in chunks) if chunks else t + return re.sub(r"\s+", " ", joined).strip() + + +# ---------- поиски колонок ---------- +_CANDIDATES = { + "question_number": [ + "номер вопроса", "порядковый номер", "порядковый номер вопроса", + "№ вопроса", "вопрос №", "номер", "question_number" + ], + "question_text": [ + "текст вопроса", "вопрос", "формулировка вопроса", + "question_text", "question" + ], + "transcript": [ + "транскрипция ответа", "транскрибация ответа", "транскрипт", + "диалог", "ответ (текст)", "аудио транскрипт", "текст ответа", + "transcript", "answer_text" + ], + "score": [ + "оценка", "оценка экзаменатора", "балл", "баллы", + "score", "target" + ], +} + +def _find_column(df: pd.DataFrame, keys: list[str]) -> str: + """Ищем колонку по списку рус/англ вариантов (точно или по подстроке).""" + # уже стандартизированный файл? — возвращаем ключ, если он есть + for k in keys: + if k in df.columns: + return k + + norm = {str(c).lower().strip(): c for c in df.columns} + for key in keys: + k = key.lower().strip() + if k in norm: + return norm[k] + for nk, orig in norm.items(): + if k in nk: # частичное совпадение + return orig + raise KeyError(f"Не удалось найти колонку из набора: {keys} в {list(df.columns)}") + + +# ---------- основная функция ---------- +def prepare_dataframe(df: pd.DataFrame) -> pd.DataFrame: + """ + Приводим датафрейм к стандартному виду: + columns = [question_number, question_text, answer_text, score] + + Умеет работать и с «сырым» CSV из задания, и с уже обработанным, + где колонки могли быть: question_number, question_text, answer_text, score. + """ + cols = set(df.columns) + + # кейс: файл уже в стандарте — просто мягко нормализуем + if {"question_number", "question_text", "answer_text", "score"}.issubset(cols): + out = df[["question_number", "question_text", "answer_text", "score"]].copy() + + # подчистим HTML и лишние пробелы + out["question_text"] = out["question_text"].apply(clean_html).str.replace(r"\s+", " ", regex=True).str.strip() + out["answer_text"] = ( + out["answer_text"] + .fillna("").astype(str) + .apply(clean_html) + .apply(extract_tester_reply) # ⚠️ извлекаем реплики тестируемого + .str.replace(r"\s+", " ", regex=True).str.strip() + ) + # приведение типа номера вопроса (если возможно) + with pd.option_context("mode.chained_assignment", None): + try: + out["question_number"] = pd.to_numeric(out["question_number"], errors="coerce").astype("Int64") + except Exception: + pass + return out + + # кейс: «сырой» файл из задания — ищем русские колонки + qnum_col = _find_column(df, _CANDIDATES["question_number"]) + qtxt_col = _find_column(df, _CANDIDATES["question_text"]) + tran_col = _find_column(df, _CANDIDATES["transcript"]) + score_col = _find_column(df, _CANDIDATES["score"]) + + out = pd.DataFrame() + out["question_number"] = df[qnum_col] + out["question_text"] = df[qtxt_col].apply(clean_html) + # 1) базовое извлечение по ролям (если есть метки) + # 2) затем более мягкая эвристика extract_tester_reply (из src.text_roles) + out["answer_text"] = ( + df[tran_col].apply(extract_answer) + .fillna("").astype(str) + .apply(clean_html) + .apply(extract_tester_reply) + ) + out["score"] = df[score_col] + + # финальная нормализация пробелов + out["question_text"] = out["question_text"].str.replace(r"\s+", " ", regex=True).str.strip() + out["answer_text"] = out["answer_text"].str.replace(r"\s+", " ", regex=True).str.strip() + + # аккуратно приводим номер вопроса к целочисленному типу, если возможно + with pd.option_context("mode.chained_assignment", None): + try: + out["question_number"] = pd.to_numeric(out["question_number"], errors="coerce").astype("Int64") + except Exception: + pass + + return out diff --git a/src/debug_cache.py b/src/debug_cache.py new file mode 100644 index 0000000000000000000000000000000000000000..aae7830efc0be7ec2f6d447f5ba2d60236ff689e --- /dev/null +++ b/src/debug_cache.py @@ -0,0 +1,11 @@ +# src/debug_cache.py +from sentence_transformers import SentenceTransformer +from src.semantic_cache import embed_with_cache +from pathlib import Path + +if __name__ == "__main__": + print("🔧 Smoke-test кэша") + model = SentenceTransformer("ai-forever/sbert_large_nlu_ru") + texts = ["привет", "как дела", "экзамен по русскому языку", "описание картинки в парке"] + vecs = embed_with_cache(texts, model, batch_size=4, verbose=True) + print("OK, vecs:", vecs.shape) diff --git a/src/evaluate_local.py b/src/evaluate_local.py new file mode 100644 index 0000000000000000000000000000000000000000..92fe44c5394f0ca89851b96406c67a5bbebc211e --- /dev/null +++ b/src/evaluate_local.py @@ -0,0 +1,42 @@ +# src/evaluate_local.py +from pathlib import Path +import pandas as pd +from sklearn.metrics import mean_absolute_error as MAE + +ROOT = Path(__file__).resolve().parents[1] +RAW = ROOT / "data" / "raw" / "Данные для кейса.csv" +PRED = ROOT / "data" / "processed" / "predicted.csv" + +def main(): + # читаем сырой (для реальной оценки берём «живую» колонку оценок) + raw = pd.read_csv(RAW, encoding="utf-8-sig", sep=";") + pred = pd.read_csv(PRED, encoding="utf-8-sig") + + # аккуратно сопоставим по двум id, если есть; иначе по порядку строк + cols = list(raw.columns) + if {"Id экзамена","Id вопроса"}.issubset(cols): + key = ["Id экзамена","Id вопроса"] + df = raw[key + ["Оценка экзаменатора"]].merge( + pred[key + ["pred_score"]], on=key, how="inner" + ) + else: + df = pd.DataFrame({ + "Оценка экзаменатора": raw["Оценка экзаменатора"], + "pred_score": pred["pred_score"], + "№ вопроса": raw["№ вопроса"] if "№ вопроса" in raw.columns else None + }) + + df = df.rename(columns={"№ вопроса":"question_number"}) + df = df.dropna(subset=["Оценка экзаменатора"]).copy() + df["err"] = (df["Оценка экзаменатора"] - df["pred_score"]).abs() + + overall = df["err"].mean() + print(f"MAE (вся выборка): {overall:.3f}") + + if "question_number" in df.columns and df["question_number"].notna().any(): + for q in [1,2,3,4]: + mae_q = df.loc[df["question_number"]==q, "err"].mean() + print(f" Q{q}: MAE={mae_q:.3f}") + +if __name__ == "__main__": + main() diff --git a/src/explanations.py b/src/explanations.py new file mode 100644 index 0000000000000000000000000000000000000000..e6c689c47b3a73ffb9f1e630e97c1bff68039e84 --- /dev/null +++ b/src/explanations.py @@ -0,0 +1,73 @@ +from __future__ import annotations +import pandas as pd +import numpy as np +from typing import List + + +def add_score_explanations(feats: pd.DataFrame, pred_scores: np.ndarray) -> pd.DataFrame: + """ + Добавляет объяснения для предсказанных оценок на основе фичей + """ + out = feats.copy() + explanations = [] + + for idx, row in feats.iterrows(): + q_num = row['question_number'] + pred_score = pred_scores[idx] + explanation_parts = [] + + # Базовые метрики + if row.get('ans_len_words', 0) < 10: + explanation_parts.append("🔴 Короткий ответ") + elif row.get('ans_len_words', 0) > 50: + explanation_parts.append("🟢 Развернутый ответ") + else: + explanation_parts.append("🟡 Средняя длина ответа") + + # Семантическое сходство + semantic_sim = row.get('semantic_sim', 0) + if semantic_sim > 0.7: + explanation_parts.append("✅ Высокое смысловое соответствие") + elif semantic_sim > 0.4: + explanation_parts.append("⚠️ Умеренное смысловое соответствие") + else: + explanation_parts.append("❌ Низкое смысловое соответствие") + + # Структура ответа + if row.get('ans_n_sents', 0) >= 3: + explanation_parts.append("📊 Хорошая структура ответа") + else: + explanation_parts.append("📉 Мало предложений") + + # Специфичные для вопросов объяснения + if q_num == 4: + # Для вопроса 4 - описание картинки + if row.get('q4_has_intro', 0) == 1: + explanation_parts.append("🎨 Есть вступление с описанием картинки") + if row.get('q4_has_personal', 0) == 1: + explanation_parts.append("👤 Есть личный опыт") + if row.get('q4_coverage_ratio', 0) > 0.6: + explanation_parts.append("📋 Хорошее покрытие подвопросов") + + elif q_num in [1, 3]: + # Для диалоговых вопросов + if row.get('ans_ttr', 0) > 0.6: + explanation_parts.append("💬 Разнообразная лексика") + + elif q_num == 2: + # Для вопросов про жилье + if row.get('ans_len_words', 0) > 30: + explanation_parts.append("🏠 Подробное описание") + + # Оценка качества + if pred_score >= (2.0 if q_num in [2, 4] else 1.0) * 0.8: + explanation_parts.append("⭐ Высокий балл") + elif pred_score >= (2.0 if q_num in [2, 4] else 1.0) * 0.5: + explanation_parts.append("⚡ Средний балл") + else: + explanation_parts.append("💤 Низкий балл") + + explanations.append(" | ".join(explanation_parts)) + + out['score_explanation'] = explanations + return out \ No newline at end of file diff --git a/src/features.py b/src/features.py new file mode 100644 index 0000000000000000000000000000000000000000..5f7dc624837e0277591f7130ff2117b5e43b54a0 --- /dev/null +++ b/src/features.py @@ -0,0 +1,107 @@ +# src/features.py +from __future__ import annotations +import re +from pathlib import Path +from typing import List + +import numpy as np +import pandas as pd + + +_WORD_RE = re.compile(r"[A-Za-zА-Яа-яЁё0-9]+", re.UNICODE) +_SENT_SPLIT_RE = re.compile(r"[.!?]+[\s\n]+") +_PUNCT_RE = re.compile(r"[^\w\s]", re.UNICODE) + + +def _tokenize_words(text: str) -> List[str]: + if not isinstance(text, str) or not text: + return [] + return _WORD_RE.findall(text) + + +def _split_sentences(text: str) -> List[str]: + if not isinstance(text, str) or not text.strip(): + return [] + parts = _SENT_SPLIT_RE.split(text.strip()) + return [p.strip() for p in parts if p.strip()] + + +def build_baseline_features(df: pd.DataFrame) -> pd.DataFrame: + """ + Принимает датафрейм со столбцами: + - question_number + - question_text + - answer_text + - score (может отсутствовать на инференсе, тогда создадим NaN) + + Возвращает df с базовыми признаками: + ans_len_chars, ans_len_words, ans_n_sents, ans_avg_sent_len, + ans_ttr, ans_short_sent_rt, ans_punct_rt, q_len_words, has_intro + """ + + out = df.copy() + + if "score" not in out.columns: + out["score"] = np.nan + + # гарантируем строки + out["question_text"] = out["question_text"].fillna("").astype(str) + out["answer_text"] = out["answer_text"].fillna("").astype(str) + + # длины/токены + ans_words = out["answer_text"].apply(_tokenize_words) + q_words = out["question_text"].apply(_tokenize_words) + ans_sents = out["answer_text"].apply(_split_sentences) + + out["ans_len_chars"] = out["answer_text"].str.len() + out["ans_len_words"] = ans_words.apply(len).astype(int) + + out["ans_n_sents"] = ans_sents.apply(len).astype(int) + out["ans_avg_sent_len"] = ( + out["ans_len_words"] / out["ans_n_sents"].replace({0: np.nan}) + ).fillna(0).astype(float) + + # Type-Token Ratio + def _ttr(ws: List[str]) -> float: + return 0.0 if not ws else len(set(map(str.lower, ws))) / float(len(ws)) + + out["ans_ttr"] = ans_words.apply(_ttr).astype(float) + + # доля коротких предложений (<= 5 слов) + def _short_rate(sents: List[str]) -> float: + if not sents: + return 0.0 + cnt = 0 + for s in sents: + if len(_tokenize_words(s)) <= 5: + cnt += 1 + return cnt / float(len(sents)) + + out["ans_short_sent_rt"] = ans_sents.apply(_short_rate).astype(float) + + # доля пунктуации в ответе + def _punct_ratio(text: str) -> float: + if not text: + return 0.0 + punct = len(_PUNCT_RE.findall(text)) + return punct / float(len(text)) + + out["ans_punct_rt"] = out["answer_text"].apply(_punct_ratio).astype(float) + + # длина вопроса в словах + out["q_len_words"] = q_words.apply(len).astype(int) + + # наличие вводной части — простая эвристика + out["has_intro"] = out["answer_text"].str.contains( + r"\b(во-первых|например|сначала|итак|сперва|прежде всего)\b", + case=False, na=False + ).astype(float) + + # порядок колонок + cols = [ + "question_number", "question_text", "answer_text", "score", + "ans_len_chars", "ans_len_words", "ans_n_sents", "ans_avg_sent_len", + "ans_ttr", "ans_short_sent_rt", "ans_punct_rt", "q_len_words", "has_intro", + ] + cols = [c for c in cols if c in out.columns] + return out[cols] diff --git a/src/features_q4.py b/src/features_q4.py new file mode 100644 index 0000000000000000000000000000000000000000..f637f1c9bcebe06794427a5545a13f5036c99a7f --- /dev/null +++ b/src/features_q4.py @@ -0,0 +1,143 @@ +from __future__ import annotations +import re +import pandas as pd +from typing import List + + +def enhanced_q4_features(df: pd.DataFrame) -> pd.DataFrame: + """ + Улучшенные фичи для вопроса 4 - ВСЯ ЛОГИКА В ОДНОЙ ФУНКЦИИ + """ + out = df.copy() + + if "question_number" not in out.columns: + raise ValueError("В датафрейме нет колонки 'question_number'") + + for col in ["question_text", "answer_text"]: + if col not in out.columns: + out[col] = "" + + mask = out["question_number"] == 4 + q = out.loc[mask, "question_text"].fillna("").astype(str) + a = out.loc[mask, "answer_text"].fillna("").astype(str) + + # --- БАЗОВЫЕ ФИЧИ --- + PLACE_WORDS = r"(?:кухн|парк|сквер|берег|река|дом|улиц|квартир|комнат|набережн)" + SEASON_WORDS = r"(?:лето|зим|весн|осен|снег|жарко|холодно|листопад|сосулк)" + PEOPLE_WORDS = r"(?:мама|папа|дедушк|бабушк|женщин|мужчин|ребен|дет|сем|дочка|сын|парень|девушк)" + ACTION_WORDS = r"(?:игра|моет|готов|накрыва|бежит|катает|кормит|сидит|спит|несет|перепрыг|гуляет)" + DETAIL_WORDS = r"(?:одет|рост|волос|глаз|характер|возраст|пальто|рубашк|кроссовк|плать|кофт|ботинк)" + PIC_INTRO = r"(?:на картинке|на рисунке|я вижу|изображен)" + + CHILDREN_Q = r"(?:сколько детей|детям|о них|как.*играете.*дет(?:ями|ьми))" + FREE_TIME_Q = r"(?:свободн(?:ое|ым)\s+врем|как.*проводите.*время|выходн(?:ой|ые))" + + # Базовые детекции + has_place_time = a.str.contains(PLACE_WORDS, case=False, regex=True) | a.str.contains(SEASON_WORDS, case=False, + regex=True) + has_people = a.str.contains(PEOPLE_WORDS, case=False, regex=True) + has_actions = a.str.contains(ACTION_WORDS, case=False, regex=True) + has_detail = a.str.contains(DETAIL_WORDS, case=False, regex=True) + + expects_children = q.str.contains(CHILDREN_Q, case=False, regex=True) + expects_free = q.str.contains(FREE_TIME_Q, case=False, regex=True) + + answered_children = a.str.contains(r"(?:у меня.*дет(?:и|ей)|в моей семье.*дет|сын|дочк|мы.*играем)", case=False, + regex=True) + answered_free = a.str.contains( + r"(?:свободн.*врем|обычно.*(?:в выходн|по выходн)|люблю|занимаюсь|хожу|смотрю|рисую|спорт|танц)", case=False, + regex=True) + + non_cyr_ratio = a.apply(lambda t: (len(re.findall(r"[A-Za-z]", t)) / max(1, len(t)))) + non_cyr_ratio = pd.to_numeric(non_cyr_ratio, errors="coerce").fillna(0.0).astype(float) + + picture_first = a.str.contains(PIC_INTRO, case=False, regex=True) + dont_know = a.str.contains(r"(?:не знаю|не понимаю|трудно сказать|не могу описать)", case=False, regex=True) + + # --- УЛУЧШЕННЫЕ ФИЧИ --- + + # 1. Детекция всех подвопросов + subquestion_patterns = { + 'place_time': r'(место|время|сезон|лето|зима|весна|осень|кухня|парк|улица)', + 'people': r'(люди|человек|мужчина|женщина|ребенок|дети|семья|бабушка|дедушка)', + 'actions': r'(делает|стоит|сидит|играет|готовит|моет|читает|смотрит)', + 'person_detail': r'(одет|носит|платье|рубашка|брюки|волосы|глаза)', + 'family_children': r'(дет[еи]|семь[яеи]|сын|дочь|брат|сестра)', + 'playing': r'(игра[ею]|играем|гуля[ею]|занимаюсь)' + } + + for name, pattern in subquestion_patterns.items(): + out.loc[mask, f'q4_has_{name}'] = a.str.contains(pattern, case=False, regex=True).astype(int) + + # 2. Структура ответа + def analyze_structure(text: str) -> dict: + text_lower = text.lower() + + has_intro = any(marker in text_lower for marker in [ + 'на картинке', 'на рисунке', 'изображен', 'вижу', 'показан' + ]) + + has_personal = any(marker in text_lower for marker in [ + 'у меня', 'в моей', 'мои', 'я ', 'мы ', 'наш' + ]) + + sentences = re.split(r'[.!?]+', text) + num_sentences = len([s for s in sentences if len(s.strip()) > 10]) + + return { + 'has_intro': has_intro, + 'has_personal': has_personal, + 'num_sentences': num_sentences + } + + structure_features = a.apply(analyze_structure).apply(pd.Series) + out.loc[mask, 'q4_has_intro'] = structure_features['has_intro'].astype(int) + out.loc[mask, 'q4_has_personal'] = structure_features['has_personal'].astype(int) + out.loc[mask, 'q4_num_sentences'] = structure_features['num_sentences'] + + # 3. Полнота ответа + subq_columns = [f'q4_has_{name}' for name in subquestion_patterns.keys()] + out.loc[mask, 'q4_coverage_ratio'] = out.loc[mask, subq_columns].sum(axis=1) / len(subq_columns) + + # 4. Базовые слоты (оригинальные фичи) + slots = (has_place_time.astype(int) + has_people.astype(int) + has_actions.astype(int) + has_detail.astype(int)) + slots_covered = (slots / 4.0).clip(0, 1) + + personal_ok = ( + (expects_children & answered_children) | + (expects_free & answered_free) | + (~expects_children & ~expects_free) + ) + + # Заполняем базовые фичи + float_cols = ["q4_slots_covered", "q4_non_cyr_ratio"] + int_cols = [ + "q4_has_place_time", "q4_has_people", "q4_has_actions", "q4_has_detail", + "q4_expects_children", "q4_expects_free", "q4_answered_personal", + "q4_picture_first", "q4_dont_know" + ] + + for name in float_cols: + out[name] = 0.0 + for name in int_cols: + out[name] = 0 + + out.loc[mask, "q4_slots_covered"] = slots_covered.astype(float).to_numpy() + out.loc[mask, "q4_has_place_time"] = has_place_time.astype(int).to_numpy() + out.loc[mask, "q4_has_people"] = has_people.astype(int).to_numpy() + out.loc[mask, "q4_has_actions"] = has_actions.astype(int).to_numpy() + out.loc[mask, "q4_has_detail"] = has_detail.astype(int).to_numpy() + out.loc[mask, "q4_expects_children"] = expects_children.astype(int).to_numpy() + out.loc[mask, "q4_expects_free"] = expects_free.astype(int).to_numpy() + out.loc[mask, "q4_answered_personal"] = personal_ok.astype(int).to_numpy() + out.loc[mask, "q4_non_cyr_ratio"] = non_cyr_ratio.astype(float).to_numpy() + out.loc[mask, "q4_picture_first"] = picture_first.astype(int).to_numpy() + out.loc[mask, "q4_dont_know"] = dont_know.astype(int).to_numpy() + + return out + + +# Совместимость с существующим кодом +def add_q4_features(df: pd.DataFrame) -> pd.DataFrame: + """Совместимое имя функции для predict.py""" + return enhanced_q4_features(df) \ No newline at end of file diff --git a/src/make_training_table.py b/src/make_training_table.py new file mode 100644 index 0000000000000000000000000000000000000000..1efea9b3be6cc87f0d63ec5000002215b230b536 --- /dev/null +++ b/src/make_training_table.py @@ -0,0 +1,69 @@ +# src/make_training_table.py +from __future__ import annotations +from pathlib import Path +import pandas as pd + +from src.data_cleaning import prepare_dataframe +from src.features import build_baseline_features # базовые признаки (len, ttr, punctuation, etc.) +from src.semantic_features import add_semantic_similarity # semantic_sim (ruSBERT + cache) +from src.features_q4 import add_q4_features # rule-based фичи для Q4 + +RAW = Path("data/raw/Данные для кейса.csv") +CLEAN = Path("data/processed/clean_data.csv") +OUT = Path("data/processed/features_with_semantics_q4.csv") + +def read_input() -> pd.DataFrame: + # стараемся воспроизвести ту же «устойчивую» загрузку, что и в predict + tries = [ + ("utf-8-sig", ";"), + ("utf-8", ";"), + ("utf-8-sig", ","), + ("utf-8", ","), + ("utf-8-sig", None), + ("utf-8", None), + ] + last_err = None + for enc, sep in tries: + try: + if sep is None: + df = pd.read_csv(RAW, encoding=enc, sep=None, engine="python") + else: + df = pd.read_csv(RAW, encoding=enc, sep=sep) + print(f"[i] CSV прочитан с encoding='{enc}', sep='{sep or 'auto'}'") + return df + except Exception as e: + last_err = e + raise last_err + +def main(): + # 1) читаем сырой CSV → приводим к стандартной схеме + if CLEAN.exists(): + print(f"[i] Использую уже подготовленный clean: {CLEAN}") + df_clean = pd.read_csv(CLEAN, encoding="utf-8-sig") + else: + df_raw = read_input() + df_clean = prepare_dataframe(df_raw) + CLEAN.parent.mkdir(parents=True, exist_ok=True) + df_clean.to_csv(CLEAN, index=False, encoding="utf-8-sig") + print(f"✅ Сохранён clean: {CLEAN}") + + # 2) базовые признаки + feats = build_baseline_features(df_clean) + + # 3) семантическая близость (кэш ruSBERT) + print("🔹 Вычисляем semantic_sim (ruSBERT + cache)...") + feats = add_semantic_similarity(feats, batch_size=64) + + # 4) rule-based признаки Q4 + print("🔹 Добавляю rule-based признаки Q4...") + feats = add_q4_features(feats) + + # 5) сохраняем обучающую таблицу + OUT.parent.mkdir(parents=True, exist_ok=True) + feats.to_csv(OUT, index=False, encoding="utf-8-sig") + print(f"✅ Готово: {OUT}") + print("Превью:") + print(feats.head()) + +if __name__ == "__main__": + main() diff --git a/src/merge_features.py b/src/merge_features.py new file mode 100644 index 0000000000000000000000000000000000000000..391eaf9994a97d8826801f10bd961ccb23782193 --- /dev/null +++ b/src/merge_features.py @@ -0,0 +1,29 @@ +# src/merge_features.py +from pathlib import Path +import pandas as pd + +from src.semantic_features import add_semantic_similarity +from src.features_q4 import q4_slot_features # <- берём функцию с признаками для Q4 + +ROOT = Path(__file__).resolve().parents[1] +FEAT_PATH = ROOT / "data" / "processed" / "features_baseline.csv" +OUT_PATH = ROOT / "data" / "processed" / "features_with_semantics_q4.csv" + +def main(): + print(f"🔹 Читаю базовые фичи: {FEAT_PATH}", flush=True) + df = pd.read_csv(FEAT_PATH, encoding="utf-8-sig") + + print("🔹 Добавляю семантическую близость (ruSBERT)...", flush=True) + df = add_semantic_similarity(df, batch_size=64) # будет использовать кэш + + print("🔹 Добавляю rule-based признаки для Q4...", flush=True) + df = q4_slot_features(df) + + print(f"🔹 Сохраняю итог: {OUT_PATH}", flush=True) + df.to_csv(OUT_PATH, index=False, encoding="utf-8-sig") + + print("✅ Готово. Превью:", flush=True) + print(df[['question_number','semantic_sim','q4_slots_covered','q4_answered_personal','score']].head(), flush=True) + +if __name__ == "__main__": + main() diff --git a/src/on_topic.py b/src/on_topic.py new file mode 100644 index 0000000000000000000000000000000000000000..be9b5326aa33845085317ca50a3d7da401e23fd3 --- /dev/null +++ b/src/on_topic.py @@ -0,0 +1,34 @@ +# src/on_topic.py +from __future__ import annotations +from pathlib import Path +import pandas as pd +import numpy as np +from sklearn.linear_model import LogisticRegression +from sklearn.model_selection import train_test_split +from sklearn.metrics import roc_auc_score +import joblib + +ROOT = Path(__file__).resolve().parents[1] +DATA = ROOT / "data" / "processed" / "features_with_semantics_q4.csv" +MODEL_PATH = ROOT / "models" / "on_topic.pkl" + +FEATURES = ["semantic_sim","ans_len_words","ans_ttr","ans_avg_sent_len"] + +def main(): + df = pd.read_csv(DATA, encoding="utf-8-sig") + # бинарная цель: >0 считается «по теме» + y = (df["score"] > 0).astype(int).values + X = df[FEATURES].fillna(0).values + + Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y) + clf = LogisticRegression(max_iter=200, n_jobs=None) + clf.fit(Xtr, ytr) + p = clf.predict_proba(Xte)[:,1] + print(f"AUC on holdout: {roc_auc_score(yte, p):.3f}") + + MODEL_PATH.parent.mkdir(parents=True, exist_ok=True) + joblib.dump({"model": clf, "features": FEATURES}, MODEL_PATH) + print(f"✅ on_topic model saved: {MODEL_PATH}") + +if __name__ == "__main__": + main() diff --git a/src/predict.py b/src/predict.py new file mode 100644 index 0000000000000000000000000000000000000000..d758629beb4c27f8aac96f0f7810b1af7c367784 --- /dev/null +++ b/src/predict.py @@ -0,0 +1,235 @@ +# src/predict.py +from __future__ import annotations + +from pathlib import Path +import argparse +import os +import sys +import tempfile +from typing import Dict, List + +import numpy as np +import pandas as pd +import joblib +from catboost import CatBoostRegressor + +# --- импорты проекта --- +HERE = Path(__file__).parent +ROOT = HERE.parent +if str(ROOT) not in sys.path: + sys.path.append(str(ROOT)) + +try: + # если feature_engineering.py лежит в корне проекта + from feature_engineering import FeatureExtractor +except ModuleNotFoundError: + # если файл лежит в src/ + from src.feature_engineering import FeatureExtractor # type: ignore + +# --- пути --- +MODELS_DIR = ROOT / "models" # catboost_Q1.cbm ... catboost_Q4.cbm +ON_TOPIC_PATH = MODELS_DIR / "on_topic.pkl" # опционально + +# --- служебные колонки (не подавать в модель) --- +NON_NUMERIC_KEEP = {"question_number", "question_text", "answer_text"} +TARGET_COLS = {"score", "Оценка экзаменатора"} + + +# ========================= +# Утилиты +# ========================= +def _read_csv_safely(path: Path) -> pd.DataFrame: + """Надёжное чтение CSV: пробуем разные кодировки/разделители.""" + tries = [ + ("utf-8-sig", ";"), + ("utf-8", ";"), + ("utf-8-sig", ","), + ("utf-8", ","), + ("utf-8-sig", None), + ("utf-8", None), + ] + last_err = None + for enc, sep in tries: + try: + if sep is None: + df = pd.read_csv(path, encoding=enc, sep=None, engine="python") + used_sep = "auto" + else: + df = pd.read_csv(path, encoding=enc, sep=sep) + used_sep = sep + print(f"[i] CSV прочитан с encoding='{enc}', sep='{used_sep}'") + return df + except Exception as e: + last_err = e + raise last_err # type: ignore[misc] + + +def _clip_by_q(qnum: int, preds: np.ndarray) -> np.ndarray: + """Клип по допустимому диапазону оценок для каждого вопроса.""" + if qnum in (1, 3): + lo, hi = 0.0, 1.0 + elif qnum in (2, 4): + lo, hi = 0.0, 2.0 + else: + lo, hi = 0.0, 2.0 + return np.clip(preds, lo, hi) + + +def _load_model(qnum: int) -> CatBoostRegressor: + """Загрузка CatBoost-модели для указанного вопроса.""" + model_path = MODELS_DIR / f"catboost_Q{qnum}.cbm" + if not model_path.exists(): + raise FileNotFoundError(f"Не найден файл модели: {model_path}") + model = CatBoostRegressor() + model.load_model(str(model_path)) + return model + + +def _align_to_model_features(model: CatBoostRegressor, X: pd.DataFrame) -> pd.DataFrame: + """Выравниваем матрицу признаков под порядок/набор, с которым обучалась модель.""" + names = list(getattr(model, "feature_names_", [])) + if not names: + return X + Z = pd.DataFrame(index=X.index, dtype=float) + for col in names: + Z[col] = X[col] if col in X.columns else 0.0 + return Z + + +def _maybe_add_on_topic(df_feats: pd.DataFrame) -> pd.DataFrame: + """ + Если есть on_topic.pkl (pack = {'model': clf, 'features': [...]}) + — добавляем вероятность 'on_topic_prob'. Иначе 0.0. + """ + out = df_feats.copy() + if not ON_TOPIC_PATH.exists(): + out["on_topic_prob"] = 0.0 + return out + + try: + pack = joblib.load(ON_TOPIC_PATH) + clf = pack["model"] + need_feats: List[str] = pack.get("features", []) + for f in need_feats: + if f not in out.columns: + out[f] = 0.0 + X_on = out[need_feats].fillna(0).values + out["on_topic_prob"] = clf.predict_proba(X_on)[:, 1].astype("float32") + except Exception as e: + print(f"[!] Не удалось применить on_topic.pkl: {e}") + out["on_topic_prob"] = 0.0 + return out + + +def _select_numeric_features(feats: pd.DataFrame) -> pd.DataFrame: + """Оставляем только числовые признаки, исключая служебные/текстовые колонки.""" + cols = [] + for c in feats.columns: + if c in NON_NUMERIC_KEEP or c in TARGET_COLS: + continue + if pd.api.types.is_numeric_dtype(feats[c]): + cols.append(c) + X = feats[cols].copy() + return X.fillna(0.0) + + +# ========================= +# Основной конвейер +# ========================= +def pipeline_infer(input_csv: Path, output_csv: Path) -> None: + """ + 1) читаем входной CSV + 2) строим признаки (FeatureExtractor) + 3) (опц.) добавляем on_topic_prob + 4) предсказываем по 4 моделям CatBoost + 5) сохраняем исходный CSV + pred_score + pred_score_rounded + """ + # 1) входной CSV + df_raw = _read_csv_safely(input_csv) + + # 2) извлечение признаков (быстрый режим по умолчанию) + fast_mode = os.environ.get("FAST_MODE", "1") == "1" + # лёгкая русская модель эмбеддингов — быстрее на CPU + sbert_name = "cointegrated/rubert-tiny" if fast_mode else "ai-forever/sbert_large_nlu_ru" + use_grammar = False if fast_mode else True + + fe = FeatureExtractor( + sbert_model_name=sbert_name, + use_grammar=use_grammar, # на HF лучше False + strip_examiner=True + ) + feats = fe.extract_all_features(df_raw) + + # 3) on_topic (если есть) + feats = _maybe_add_on_topic(feats) + + # 4) предсказания + preds = np.zeros(len(feats), dtype=float) + models_cache: Dict[int, CatBoostRegressor] = {} + X_all = _select_numeric_features(feats) + + for q in (1, 2, 3, 4): + mask = feats["question_number"] == q + if not mask.any(): + continue + if q not in models_cache: + models_cache[q] = _load_model(q) + model = models_cache[q] + Xq = _align_to_model_features(model, X_all.loc[mask]) + pq = model.predict(Xq) + pq = np.asarray(pq, dtype=float).reshape(-1) + preds[mask.values] = _clip_by_q(q, pq) + + # --- новое надёжное округление (без .loc по индексам) --- + qnums = feats["question_number"].astype(int).to_numpy() + rounded = np.rint(preds).astype(np.float32) + mask13 = (qnums == 1) | (qnums == 3) + mask24 = (qnums == 2) | (qnums == 4) + rounded[mask13] = np.clip(rounded[mask13], 0, 1) + rounded[mask24] = np.clip(rounded[mask24], 0, 2) + rounded = rounded.astype(int) + + # 5) сборка результата + out = df_raw.copy() + if "Оценка экзаменатора" not in out.columns: + out["Оценка экзаменатора"] = np.nan + out["pred_score"] = preds + out["pred_score_rounded"] = rounded + + # безопасная запись + output_csv.parent.mkdir(parents=True, exist_ok=True) + tmp_out = output_csv.with_suffix(".tmp.csv") + out.to_csv(tmp_out, index=False, encoding="utf-8-sig", sep=";") + os.replace(tmp_out, output_csv) + print(f"[✓] Готово: {output_csv}") + + +def predict_dataframe(df: pd.DataFrame) -> pd.DataFrame: + """Инференс для DataFrame (без файлов).""" + tmp_dir = Path(tempfile.mkdtemp(prefix="predict_df_")) + tmp_in = tmp_dir / "input.csv" + tmp_out = tmp_dir / "output.csv" + df.to_csv(tmp_in, index=False, encoding="utf-8-sig", sep=";") + pipeline_infer(tmp_in, tmp_out) + return pd.read_csv(tmp_out, encoding="utf-8-sig", sep=";") + + +# ========================= +# CLI +# ========================= +def _build_argparser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(description="Auto-grader inference pipeline") + p.add_argument("-i", "--input", type=str, required=True, help="Путь к входному CSV") + p.add_argument("-o", "--output", type=str, required=True, help="Путь к выходному CSV") + return p + + +def main(): + args = _build_argparser().parse_args() + input_csv = Path(args.input).resolve() + output_csv = Path(args.output).resolve() + pipeline_infer(input_csv, output_csv) + + +if __name__ == "__main__": + main() diff --git a/src/prepare_data.py b/src/prepare_data.py new file mode 100644 index 0000000000000000000000000000000000000000..2dc137f54183ad32aaf74377c5f104cc075775e9 --- /dev/null +++ b/src/prepare_data.py @@ -0,0 +1,53 @@ +# src/prepare_data.py +from pathlib import Path +import pandas as pd +import re + +from src.data_cleaning import prepare_dataframe + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +INPUT_PATH = PROJECT_ROOT / "data" / "raw" / "Данные для кейса.csv" +OUTPUT_PATH = PROJECT_ROOT / "data" / "processed" / "clean_data.csv" + +ENCODINGS = ["utf-8-sig", "utf-8", "cp1251", "windows-1251", "koi8-r", "iso-8859-5"] +SEPARATORS = [",", ";", "\t", "|"] + +def has_cyrillic(s: str) -> bool: + return bool(re.search(r"[А-Яа-яЁё]", s)) + +def smart_read_csv(path: Path) -> tuple[pd.DataFrame, str, str]: + best = None + for enc in ENCODINGS: + for sep in SEPARATORS: + try: + df = pd.read_csv(path, encoding=enc, sep=sep) + cols_joined = " ".join(map(str, df.columns)) + if df.shape[1] > 1 and has_cyrillic(cols_joined): + print(f"[i] Выбраны encoding='{enc}', sep='{sep}'") + return df, enc, sep + # запомним хоть какой-то валидный вариант на случай без кириллицы + if best is None and df.shape[1] > 1: + best = (df, enc, sep) + except Exception: + pass + if best: + print("[!] Кириллица в заголовках не обнаружена, берём первый валидный вариант.") + return best + raise RuntimeError("Не удалось прочитать CSV: перепроверьте файл и кодировку.") + +def main(): + if not INPUT_PATH.exists(): + raise FileNotFoundError(f"Не найден файл: {INPUT_PATH}") + + df, enc, sep = smart_read_csv(INPUT_PATH) + print(f"[i] колонки: {list(df.columns)}") + print(f"[i] размер: {df.shape}") + + clean_df = prepare_dataframe(df) + OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + clean_df.to_csv(OUTPUT_PATH, index=False, encoding="utf-8-sig") + print(f"✅ Готово: {OUTPUT_PATH}") + print(clean_df.head(3)) + +if __name__ == "__main__": + main() diff --git a/src/semantic_cache.py b/src/semantic_cache.py new file mode 100644 index 0000000000000000000000000000000000000000..556aaf11198f07c61a6a3d58ad706049b85c00b8 --- /dev/null +++ b/src/semantic_cache.py @@ -0,0 +1,115 @@ +# src/semantic_cache.py +from __future__ import annotations +import sqlite3 +import hashlib +from pathlib import Path +from typing import List, Dict, Tuple +import numpy as np + +# Абсолютный путь к БД: <корень проекта>/data/cache/embeddings.sqlite +ROOT = Path(__file__).resolve().parents[1] +DB_PATH = ROOT / "data" / "cache" / "embeddings.sqlite" + +def _norm_text(t: str) -> str: + return " ".join((t or "").strip().split()) + +def _hash_text(t: str) -> str: + return hashlib.blake2s(_norm_text(t).encode("utf-8")).hexdigest() + +def _ensure_db() -> None: + DB_PATH.parent.mkdir(parents=True, exist_ok=True) + with sqlite3.connect(DB_PATH) as con: + # Немного настроек для Windows, чтобы запись была надёжнее + con.execute("PRAGMA journal_mode = WAL;") + con.execute("PRAGMA synchronous = NORMAL;") + con.execute(""" + CREATE TABLE IF NOT EXISTS emb ( + h TEXT PRIMARY KEY, + text TEXT NOT NULL, + dim INTEGER NOT NULL, + vec BLOB NOT NULL + ) + """) + con.commit() + +def fetch_from_cache(texts: List[str]) -> Dict[str, np.ndarray]: + _ensure_db() + hashes = [_hash_text(t) for t in texts] + out: Dict[str, np.ndarray] = {} + if not hashes: + return out + with sqlite3.connect(DB_PATH) as con: + q = "SELECT h, dim, vec FROM emb WHERE h IN ({})".format(",".join("?" * len(hashes))) + for h, dim, blob in con.execute(q, hashes): + arr = np.frombuffer(blob, dtype=np.float32).reshape(dim) + out[h] = arr + return out + +def write_to_cache(items: List[Tuple[str, str, np.ndarray]]) -> None: + if not items: + return + _ensure_db() + with sqlite3.connect(DB_PATH) as con: + con.executemany( + "INSERT OR REPLACE INTO emb(h, text, dim, vec) VALUES (?,?,?,?)", + [(h, _norm_text(t), v.size, v.astype(np.float32).tobytes()) for h, t, v in items] + ) + con.commit() + +def embed_with_cache(texts: List[str], model, batch_size: int = 16, verbose: bool = True) -> np.ndarray: + """ + Возвращает эмбеддинги для texts. Сначала достаёт из кэша, + для недостающих — считает моделью и кладёт в кэш. + """ + _ensure_db() + hashes = [_hash_text(t) for t in texts] + cached = fetch_from_cache(texts) # hash -> vec + + out: List[np.ndarray | None] = [None] * len(texts) + missing_idx = [i for i, h in enumerate(hashes) if h not in cached] + + if verbose: + print(f"[cache] DB: {DB_PATH}") + print(f"[cache] всего текстов: {len(texts)} | из кэша найдено: {len(texts) - len(missing_idx)} | посчитать: {len(missing_idx)}") + + # Из кэша + for i, h in enumerate(hashes): + if h in cached: + out[i] = cached[h] + + # Досчитываем моделью + if missing_idx: + to_compute = [texts[i] for i in missing_idx] + vecs = [] + if verbose: + print(f"[cache] считаем моделью в батчах по {batch_size}...") + + for b in range(0, len(to_compute), batch_size): + chunk = to_compute[b:b + batch_size] + if verbose: + print(f"[cache] batch {b//batch_size + 1}/{(len(to_compute)+batch_size-1)//batch_size} | {len(chunk)} примеров") + vecs_chunk = model.encode( + chunk, + convert_to_numpy=True, + normalize_embeddings=True, + show_progress_bar=True # включаем прогрессбар + ) + vecs.append(vecs_chunk) + vecs = np.vstack(vecs) + + # Запись в out и в кэш + items_for_cache: List[Tuple[str, str, np.ndarray]] = [] + for j, idx in enumerate(missing_idx): + v = vecs[j] + out[idx] = v + items_for_cache.append((hashes[idx], texts[idx], v)) + + write_to_cache(items_for_cache) + if verbose: + print(f"[cache] записано в кэш: {len(items_for_cache)} векторов") + + # Упаковываем результат + result = np.vstack(out).astype(np.float32) + if verbose: + print(f"[cache] готово: shape={result.shape}, dtype={result.dtype}") + return result diff --git a/src/semantic_features.py b/src/semantic_features.py new file mode 100644 index 0000000000000000000000000000000000000000..be32a059536c3dcb370f0cf5bb4157facff2cee4 --- /dev/null +++ b/src/semantic_features.py @@ -0,0 +1,58 @@ +# src/semantic_features.py +from __future__ import annotations +from functools import lru_cache +import numpy as np +import pandas as pd +from sentence_transformers import SentenceTransformer + +from src.semantic_cache import embed_with_cache + +_MODEL_NAME = "ai-forever/sbert_large_nlu_ru" + + +@lru_cache(maxsize=1) +def _load_model() -> SentenceTransformer: + """Ленивая загрузка модели (кэшируется в процессе).""" + return SentenceTransformer(_MODEL_NAME) + + +def add_semantic_similarity( + df: pd.DataFrame, + batch_size: int = 16, # меньше батч по умолчанию, чтобы точно писался кэш + verbose: bool = True, +) -> pd.DataFrame: + """ + Добавляет колонку 'semantic_sim' — косинусное сходство вопроса и ответа. + Эмбеддинги извлекаются через embed_with_cache (нормализованы), + поэтому cos_sim == dot(a, q). + """ + out = df.copy() + + # гарантируем наличие столбцов + for col in ("question_text", "answer_text"): + if col not in out.columns: + out[col] = "" + + if len(out) == 0: + out["semantic_sim"] = np.array([], dtype=np.float32) + return out + + model = _load_model() + + q_texts = out["question_text"].fillna("").astype(str).tolist() + a_texts = out["answer_text"].fillna("").astype(str).tolist() + + if verbose: + print("🔹 Проверяем кэш (вопросы)...") + q_emb = embed_with_cache(q_texts, model, batch_size=batch_size, verbose=verbose) # (N, D) float32 + + if verbose: + print("🔹 Проверяем кэш (ответы)...") + a_emb = embed_with_cache(a_texts, model, batch_size=batch_size, verbose=verbose) # (N, D) float32 + + # косинус = скалярное произведение (векторы уже нормированы в embed_with_cache) + sims = (a_emb * q_emb).sum(axis=1).astype(np.float32) + np.clip(sims, -1.0, 1.0, out=sims) + + out["semantic_sim"] = sims + return out diff --git a/src/text_roles.py b/src/text_roles.py new file mode 100644 index 0000000000000000000000000000000000000000..1d657fcd8ac23d2f275d9170f30a28a8e9f94afc --- /dev/null +++ b/src/text_roles.py @@ -0,0 +1,45 @@ +# src/text_roles.py +import re + +TESTER_MARKERS = [ + r"^(?:тестируемый|кандидат|студент)\s*[:\-]\s*", + r"^(?:я|ну|значит|смотрите)\b", # мягкая эвристика на начало своей реплики +] +EXAMINER_MARKERS = [ + r"^(?:экзаменатор|собеседник|преподаватель|интервьюер)\s*[:\-]\s*", + r"^(?:вопрос|подскажите|расскажите|опишите)\b", # часто задают вопрос +] + +def extract_tester_reply(transcript: str) -> str: + """ + Пытаемся оставить только реплики тестируемого из общей транскрибации. + Простая эвристика: разбиваем по строкам / точкам с запятой / переносам, + фильтруем явные метки 'Экзаменатор:' и оставляем нейтральные предложения. + """ + if not isinstance(transcript, str) or not transcript.strip(): + return "" + + # разбивка на строки/квази-реплики + parts = re.split(r"(?:\r?\n|[.;]{1}\s+)", transcript) + cleaned = [] + for p in parts: + t = p.strip() + if not t: + continue + + # отбрасываем явные реплики экзаменатора + if any(re.match(pat, t, flags=re.IGNORECASE) for pat in EXAMINER_MARKERS): + continue + + # если явно помечен тестируемый — оставляем без метки + for pat in TESTER_MARKERS: + t = re.sub(pat, "", t, flags=re.IGNORECASE).strip() + + # отбрасываем чисто служебные фразы + if re.fullmatch(r"(хорошо|угу|да|нет|ладно|понятно)", t, flags=re.IGNORECASE): + continue + + cleaned.append(t) + + # если после фильтра пусто — вернём исходник (лучше не потерять текст) + return " ".join(cleaned) if cleaned else transcript.strip() diff --git a/src/train_baseline.py b/src/train_baseline.py new file mode 100644 index 0000000000000000000000000000000000000000..28f0d73b27830ed8be18030c7d79088a7a1dd746 --- /dev/null +++ b/src/train_baseline.py @@ -0,0 +1,47 @@ +# src/train_baseline.py +from pathlib import Path +import pandas as pd +from catboost import CatBoostRegressor +from sklearn.model_selection import KFold +from sklearn.metrics import mean_absolute_error +import numpy as np + +ROOT = Path(__file__).resolve().parents[1] +DATA_PATH = ROOT / "data" / "processed" / "features_with_semantics.csv" +MODEL_PATH = ROOT / "models" / "catboost_baseline.cbm" + +def main(): + df = pd.read_csv(DATA_PATH, encoding="utf-8-sig") + + # Разделяем признаки и таргет + target = df["score"] + features = df.drop(columns=["score", "question_text", "answer_text"]) + + # Кросс-валидация + kf = KFold(n_splits=5, shuffle=True, random_state=42) + maes = [] + + for fold, (train_idx, val_idx) in enumerate(kf.split(features)): + X_train, X_val = features.iloc[train_idx], features.iloc[val_idx] + y_train, y_val = target.iloc[train_idx], target.iloc[val_idx] + + model = CatBoostRegressor( + iterations=200, + depth=6, + learning_rate=0.05, + loss_function="MAE", + verbose=False, + random_state=42 + ) + model.fit(X_train, y_train) + preds = model.predict(X_val) + mae = mean_absolute_error(y_val, preds) + maes.append(mae) + print(f"Fold {fold+1}: MAE = {mae:.3f}") + + print(f"🔹 Среднее MAE: {np.mean(maes):.3f}") + model.save_model(MODEL_PATH) + print(f"✅ Модель сохранена: {MODEL_PATH}") + +if __name__ == "__main__": + main() diff --git a/src/train_q4_only.py b/src/train_q4_only.py new file mode 100644 index 0000000000000000000000000000000000000000..e6bac0c9f30a361e4fe23c95de0910879a1572c0 --- /dev/null +++ b/src/train_q4_only.py @@ -0,0 +1,70 @@ +# src/features_q4.py +import re +import pandas as pd + +# мини-словарики (можно расширять) +PLACE_WORDS = r"(кухн|парк|сквер|берег|река|дом|улиц|квартир|комнат|набережн)" +SEASON_WORDS = r"(лето|зим|весн|осен|снег|жарко|холодно|листопад|сосулк)" +PEOPLE_WORDS = r"(мама|папа|дедушк|бабушк|женщин|мужчин|ребен|дет|сем|дочка|сын|парень|девушк)" +ACTION_WORDS = r"(игра|моет|готов|накрыва|бежит|катает|кормит|сидит|спит|несет|перепрыг|гуляет)" +DETAIL_WORDS = r"(одет|рост|волос|глаз|характер|возраст|пальто|рубашк|кроссовк|плать|кофт|ботинк)" +PIC_INTRO = r"(на картинке|на рисунке|я вижу|изображен)" + +CHILDREN_Q = r"(сколько детей|детям|о них|как.*играете.*дет(ями|ьми))" +FREE_TIME_Q = r"(свободн(ое|ым)\s+врем|как.*проводите.*время|выходн(ой|ые))" + +def q4_slot_features(df: pd.DataFrame) -> pd.DataFrame: + out = df.copy() + if "question_number" not in out.columns: + raise ValueError("В датафрейме нет колонки 'question_number'") + for col in ["question_text", "answer_text"]: + if col not in out.columns: + out[col] = "" # на всякий случай + + mask = out["question_number"] == 4 + q = out.loc[mask, "question_text"].fillna("").astype(str) + a = out.loc[mask, "answer_text"].fillna("").astype(str) + + expects_children = q.str.contains(CHILDREN_Q, case=False, regex=True) + expects_free = q.str.contains(FREE_TIME_Q, case=False, regex=True) + + has_place_time = a.str.contains(PLACE_WORDS, case=False, regex=True) | a.str.contains(SEASON_WORDS, case=False, regex=True) + has_people = a.str.contains(PEOPLE_WORDS, case=False, regex=True) + has_actions = a.str_contains(ACTION_WORDS, case=False, regex=True) if hasattr(a, "str_contains") else a.str.contains(ACTION_WORDS, case=False, regex=True) + has_detail = a.str.contains(DETAIL_WORDS, case=False, regex=True) + + answered_children = a.str.contains(r"(у меня.*дет(и|ей)|в моей семье.*дет|сын|дочк|мы.*играем)", case=False, regex=True) + answered_free = a.str.contains(r"(свободн.*врем|обычно.*(в выходн|по выходн)|люблю|занимаюсь|хожу|смотрю|рисую|спорт|танц)", case=False, regex=True) + + non_cyr_ratio = a.apply(lambda t: (len(re.findall(r"[A-Za-z]", t)) / max(1, len(t)))) + picture_first = a.str.contains(PIC_INTRO, case=False, regex=True) + dont_know = a.str.contains(r"(не знаю|не понимаю|трудно сказать|не могу описать)", case=False, regex=True) + + slots = (has_place_time.astype(int) + has_people.astype(int) + has_actions.astype(int) + has_detail.astype(int)) + slots_covered = (slots / 4.0).clip(0, 1) + + personal_ok = ( + (expects_children & answered_children) | + (expects_free & answered_free) | + (~expects_children & ~expects_free) + ) + + # создаём колонки с нулями; для Q4 — заполняем значениями + cols = { + "q4_slots_covered": slots_covered, + "q4_has_place_time": has_place_time.astype(int), + "q4_has_people": has_people.astype(int), + "q4_has_actions": has_actions.astype(int), + "q4_has_detail": has_detail.astype(int), + "q4_expects_children": expects_children.astype(int), + "q4_expects_free": expects_free.astype(int), + "q4_answered_personal": personal_ok.astype(int), + "q4_non_cyr_ratio": non_cyr_ratio, + "q4_picture_first": picture_first.astype(int), + "q4_dont_know": dont_know.astype(int), + } + for name, series in cols.items(): + out[name] = 0 + out.loc[mask, name] = series.values + + return out diff --git a/src/train_qmodels.py b/src/train_qmodels.py new file mode 100644 index 0000000000000000000000000000000000000000..870660626829e21e2730be55743cb8f46b1e8527 --- /dev/null +++ b/src/train_qmodels.py @@ -0,0 +1,107 @@ +# src/train_qmodels.py +from __future__ import annotations + +from pathlib import Path +import numpy as np +import pandas as pd +from catboost import CatBoostRegressor, Pool, cv + +RANDOM_STATE = 42 +ROOT = Path(__file__).resolve().parents[1] +DATA = ROOT / "data" / "processed" +MODELS = ROOT / "models" + +# ВАЖНО: учимся именно на файле с семантикой и фичами Q4 +FEATURES_CSV = DATA / "features_with_semantics_q4.csv" + +EXCLUDE_COLS = { + "question_number", "question_text", "answer_text", "score" +} + +# Границы оценок по каждому вопросу (для клиппинга и sanity-check) +BOUNDS = { + 1: (0.0, 1.0), + 2: (0.0, 2.0), + 3: (0.0, 1.0), + 4: (0.0, 2.0), +} + +def load_data() -> pd.DataFrame: + df = pd.read_csv(FEATURES_CSV, encoding="utf-8-sig") + # sanity: приведём типы + df["question_number"] = df["question_number"].astype(int) + df["score"] = df["score"].astype(float) + return df + +def pick_feature_columns(df: pd.DataFrame) -> list[str]: + # берём все числовые, кроме служебных + candidates = [c for c in df.columns if c not in EXCLUDE_COLS] + num_cols = [c for c in candidates if pd.api.types.is_numeric_dtype(df[c])] + return num_cols + +def train_one_q(df: pd.DataFrame, q: int, feature_cols: list[str]) -> float: + mask = df["question_number"] == q + d = df.loc[mask].copy() + X = d[feature_cols] + y = d["score"].values + + params = dict( + loss_function="MAE", + eval_metric="MAE", + random_seed=RANDOM_STATE, + learning_rate=0.08, + depth=6, + l2_leaf_reg=6.0, + iterations=2000, + od_type="Iter", + od_wait=200, + verbose=False + ) + + train_pool = Pool(X, y) + cv_res = cv( + params=params, + pool=train_pool, + fold_count=5, + partition_random_seed=RANDOM_STATE, + shuffle=True, + verbose=False + ) + mae_cv = float(cv_res["test-MAE-mean"].iloc[-1]) + + print(f"\n=== Обучаем модель для Q{q} (N={len(d)}) ===") + for i in range(5): + # Просто показываем сводный CV — фолды уже в cv_res усреднены + pass + print(f"Q{q} | CV MAE: {mae_cv:.3f}") + + # Финальная дообученная на всех данных модель + model = CatBoostRegressor(**params) + model.fit(train_pool, verbose=False) + + MODELS.mkdir(parents=True, exist_ok=True) + model_path = MODELS / f"catboost_Q{q}.cbm" + model.save_model(str(model_path)) + return mae_cv + +def main(): + df = load_data() + feature_cols = pick_feature_columns(df) + + # быстрый sanity-check: ключевые новые фичи должны быть в списке + must_have = {"semantic_sim", "q4_slots_covered"} + missing = [c for c in must_have if c not in feature_cols] + if missing: + print(f"⚠️ Внимание: не найдено фич {missing} среди обучающих столбцов!") + + maes = {} + for q in (1, 2, 3, 4): + maes[q] = train_one_q(df, q, feature_cols) + + print("\n------ ИТОГО ------") + for q in (1, 2, 3, 4): + print(f"Q{q}: MAE={maes[q]:.3f}") + print(f"Среднее MAE по всем вопросам: {np.mean(list(maes.values())):.3f}") + +if __name__ == "__main__": + main() diff --git a/src/visualize_errors.py b/src/visualize_errors.py new file mode 100644 index 0000000000000000000000000000000000000000..f9c7aedec1ae6ef3b08aed260ff4e679c7110657 --- /dev/null +++ b/src/visualize_errors.py @@ -0,0 +1,40 @@ +import pandas as pd +import matplotlib.pyplot as plt +from pathlib import Path +import re + +# Пути +pred_path = Path("data/processed/predicted.csv") + +# Читаем CSV +df = pd.read_csv(pred_path, encoding="utf-8-sig") + + +# Удаляем HTML и NBSP, если есть +def clean_text(text): + if isinstance(text, str): + text = re.sub(r"<[^>]*>", "", text) # убираем HTML-теги + text = text.replace(" ", " ") # заменяем HTML NBSP + text = text.replace(" ", " ") # убираем неразрывные пробелы + return text + + +df = df.applymap(clean_text) + +# Проверяем нужные колонки +if "Оценка экзаменатора" in df.columns and "pred_score" in df.columns: + df["abs_error"] = (df["pred_score"] - df["Оценка экзаменатора"]).abs() + + print(f"Средняя ошибка (MAE): {df['abs_error'].mean():.3f}") + print(f"Максимальная ошибка: {df['abs_error'].max():.2f}") + + # Строим гистограмму ошибок + plt.figure(figsize=(8, 5)) + plt.hist(df["abs_error"], bins=10, color='skyblue', edgecolor='black') + plt.title("Распределение ошибок предсказаний (|pred - human|)") + plt.xlabel("Абсолютная ошибка") + plt.ylabel("Количество ответов") + plt.grid(alpha=0.3) + plt.show() +else: + print("⚠️ Нет нужных колонок ('Оценка экзаменатора' и 'pred_score') в predicted.csv") diff --git a/structure.txt b/structure.txt new file mode 100644 index 0000000000000000000000000000000000000000..f41691b3141a179864a7867b8e70de2e105de335 --- /dev/null +++ b/structure.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62b2f68c5778f8ff5ecc69d23bb0aaa18aa9193d598a50fcc9ae5e1863fd1675 +size 6907736 diff --git a/test_data.csv.py b/test_data.csv.py new file mode 100644 index 0000000000000000000000000000000000000000..9e1b10cc4dda2e964ed9aa32b1d445b668f70887 --- /dev/null +++ b/test_data.csv.py @@ -0,0 +1,7 @@ +Id экзамена;Id вопроса;№ вопроса;Текст вопроса;Оценка экзаменатора;Транскрибация ответа;pred_score;объяснение_оценки +3373871;30625752;1;"

Добро пожаловать на экзамен!

";1;"Экзаменатор: Начните диалог. Тестируемый: Здравствуйте, я хотел бы извиниться, что не смогу прийти на день рождения. Что бы вы хотели в подарок?";0.99;"🟢 Развернутый ответ | ✅ Высокое смысловое соответствие | 📊 Хорошая структура ответа | 💬 Разнообразная лексика | ⭐ Высокий балл" +3373871;30625753;2;"

Расскажите о вашем жилье

";2;"Экзаменатор: Вы живёте в квартире или доме? Тестируемый: Я живу в квартире в центре города. Это трёхкомнатная квартира с балконом. Квартира новая, построена в 2020 году.";1.62;"🟢 Развернутый ответ | ✅ Высокое смысловое соответствие | 📊 Хорошая структура ответа | 🏠 Подробное описание | ⭐ Высокий балл" +3373872;30625790;1;"

Начните диалог о работе

";1;"Экзаменатор: Узнайте о требованиях к работе. Тестируемый: Здравствуйте, я увидел ваше объявление о вакансии. Какие требования к соискателю? Какие документы нужны?";0.87;"🟢 Развернутый ответ | ⚠️ Умеренное смысловое соответствие | 📊 Хорошая структура ответа | 💬 Разнообразная лексика | ⭐ Высокий балл" +3373872;30625791;2;"

Опишите ваше жилье

";1;"Экзаменатор: Расскажите о вашей квартире. Тестируемый: У меня квартира. Она хорошая. Три комнаты.";0.45;"📉 Мало предложений | ❌ Низкое смысловое соответствие | 📊 Хорошая структура ответа" +3373873;30625828;1;"

Оформление документов

";2;"Экзаменатор: Объясните ситуацию в миграционной службе. Тестируемый: Здравствуйте, мне нужно оформить миграционную карту. Я приехал две недели назад. Можете дать мне бланк для заполнения?";1.85;"🟢 Развернутый ответ | ✅ Высокое смысловое соответствие | 📊 Хорошая структура ответа | 💬 Разнообразная лексика | ⭐ Высокий балл" +3373873;30625829;2;"

Ваши любимые фильмы

";1;"Экзаменатор: Какие фильмы вы любите? Тестируемый: Я смотрю фантастику и детективы. Люблю новые цветные фильмы. Мой любимый фильм - Интерстеллар, он о космосе и времени.";1.15;"🟢 Развернутый ответ | ⚠️ Умеренное смысловое соответствие | 📊 Хорошая структура ответа | 💬 Разнообразная лексика" \ No newline at end of file diff --git a/test_features.py b/test_features.py new file mode 100644 index 0000000000000000000000000000000000000000..107c6f2bd37536fa0d0a842da39b7ecc373efcc8 --- /dev/null +++ b/test_features.py @@ -0,0 +1,303 @@ +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import seaborn as sns +from feature_extractor import RussianFeatureExtractor +import os +import sys +import subprocess +import traceback +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.metrics.pairwise import cosine_similarity +import re + + +def check_environment(): + """Проверка окружения и зависимостей""" + print("=== ПРОВЕРКА ОКРУЖЕНИЯ ===") + + # Проверка пакетов + packages = ['pandas', 'numpy', 'matplotlib', 'seaborn', 'scikit-learn', 'torch'] + for package in packages: + try: + __import__(package) + print(f"✅ {package}") + except ImportError as e: + print(f"❌ {package}: {e}") + + # Проверка Java + try: + subprocess.run(['java', '-version'], capture_output=True, check=True) + print("✅ Java установлена") + except: + print("❌ Java не установлена - грамматический анализ не будет работать") + + +def load_and_analyze_dataset(): + """Загрузка и анализ структуры данных""" + print("\n=== АНАЛИЗ ДАННЫХ ===") + + try: + # Пробуем разные варианты загрузки + for filename in ['small.csv', 'dataset.csv', 'train.csv']: + if os.path.exists(filename): + print(f"Найден файл: {filename}") + + # Пробуем разные разделители + for delimiter in [';', ',', '\t']: + try: + df = pd.read_csv(filename, encoding='utf-8', delimiter=delimiter) + if len(df.columns) > 1: # Успешная загрузка + print(f"✅ Успешно загружен с разделителем '{delimiter}'") + break + except: + continue + else: + print("❌ Не удалось определить разделитель") + return None + + print(f"Размер данных: {df.shape[0]} строк, {df.shape[1]} колонок") + print(f"Колонки: {df.columns.tolist()}") + + # Анализ содержания + print("\n--- СТРУКТУРА ДАННЫХ ---") + for col in df.columns: + print(f"{col}: {df[col].dtype}, пропусков: {df[col].isnull().sum()}") + + if df[col].dtype == 'object': + sample = df[col].iloc[0] if not df[col].isnull().all() else "N/A" + print(f" Пример: {str(sample)[:100]}...") + + # Поиск ключевых колонок + question_cols = [col for col in df.columns if 'вопрос' in col.lower()] + transcript_cols = [col for col in df.columns if 'транскрипт' in col.lower()] + score_cols = [col for col in df.columns if 'оценк' in col.lower() or 'балл' in col.lower()] + + print(f"\n--- ВЫЯВЛЕННЫЕ КОЛОНКИ ---") + print(f"Вопросы: {question_cols}") + print(f"Транскрипты: {transcript_cols}") + print(f"Оценки: {score_cols}") + + return df + + print("❌ Не найден файл с данными") + return None + + except Exception as e: + print(f"❌ Ошибка загрузки данных: {e}") + return None + + +def test_alternative_features(texts): + """Тест альтернативных методов извлечения признаков""" + print("\n=== ТЕСТ АЛЬТЕРНАТИВНЫХ ПРИЗНАКОВ ===") + + features_list = [] + + for i, text in enumerate(texts): + if pd.isna(text): + features_list.append({}) + continue + + text_str = str(text) + features = {} + + # Базовые текстовые метрики + features['text_length'] = len(text_str) + + words = re.findall(r'\b[а-яёa-z]+\b', text_str.lower()) + features['word_count'] = len(words) + + sentences = re.split(r'[.!?]+', text_str) + features['sentence_count'] = len([s for s in sentences if len(s.strip()) > 10]) + + features['avg_word_length'] = np.mean([len(w) for w in words]) if words else 0 + features['lexical_diversity'] = len(set(words)) / len(words) if words else 0 + + # Стилистические особенности + features['has_questions'] = int('?' in text_str) + features['has_exclamations'] = int('!' in text_str) + features['has_ellipsis'] = int('...' in text_str) + + # Сложность текста + long_words = [w for w in words if len(w) > 6] + features['long_word_ratio'] = len(long_words) / len(words) if words else 0 + + features_list.append(features) + + if i < 3: # Показать пример для первых 3 текстов + print(f"Пример {i + 1}: {text_str[:80]}...") + for k, v in features.items(): + print(f" {k}: {v:.3f}") + + return pd.DataFrame(features_list) + + +def enhanced_feature_extraction(df): + """Улучшенное извлечение признаков с резервными методами""" + print("\n=== ЗАПУСК УЛУЧШЕННОГО ИЗВЛЕЧЕНИЯ ПРИЗНАКОВ ===") + + # Определяем колонку с транскриптами + transcript_cols = [col for col in df.columns if 'транскрипт' in col.lower()] + if not transcript_cols: + print("❌ Не найдена колонка с транскриптами") + return pd.DataFrame() + + transcript_col = transcript_cols[0] + texts = df[transcript_col].fillna('') + + print(f"Обработка {len(texts)} транскриптов...") + + try: + # Пробуем основной экстрактор + print("🔄 Попытка использовать RussianFeatureExtractor...") + extractor = RussianFeatureExtractor() + features_df = extractor.extract_features_for_dataframe(df) + + if not features_df.empty: + print("✅ RussianFeatureExtractor успешно отработал") + return features_df + else: + print("❌ RussianFeatureExtractor вернул пустой DataFrame") + + except Exception as e: + print(f"❌ Ошибка в RussianFeatureExtractor: {e}") + print("🔄 Переход на резервный метод...") + + # Резервный метод - базовые признаки + print("Использование резервного метода извлечения признаков...") + features_df = test_alternative_features(texts) + + return features_df + + +def analyze_correlations_with_scores(features_df, original_df): + """Анализ корреляций с реальными оценками""" + print("\n=== АНАЛИЗ КОРРЕЛЯЦИЙ ===") + + # Находим колонку с оценками + score_cols = [col for col in original_df.columns if 'оценк' in col.lower() or 'балл' in col.lower()] + if not score_cols: + print("❌ Не найдены колонки с оценками") + return + + score_col = score_cols[0] + + # Объединяем признаки с оценками + analysis_df = features_df.copy() + analysis_df['real_score'] = original_df[score_col].values + + # Удаляем строки с пропусками + analysis_clean = analysis_df.dropna() + + if len(analysis_clean) < 2: + print("❌ Недостаточно данных для анализа корреляций") + return + + # Анализ корреляций + correlations = analysis_clean.corr()['real_score'].sort_values(key=abs, ascending=False) + + print("\nТоп-15 наиболее коррелирующих признаков:") + print("-" * 50) + + for feature, corr in correlations.items(): + if feature != 'real_score': + direction = "+" if corr > 0 else "-" + significance = "***" if abs(corr) > 0.3 else "**" if abs(corr) > 0.2 else "*" if abs(corr) > 0.1 else "" + print(f" {direction} {feature}: {corr:+.3f} {significance}") + + # Визуализация топ-признаков + top_features = correlations.head(6).index.tolist() + if 'real_score' in top_features: + top_features.remove('real_score') + + if top_features: + plt.figure(figsize=(12, 8)) + + for i, feature in enumerate(top_features[:5]): + plt.subplot(2, 3, i + 1) + plt.scatter(analysis_clean[feature], analysis_clean['real_score'], alpha=0.6) + plt.xlabel(feature) + plt.ylabel('Real Score') + plt.title(f'r = {correlations[feature]:.3f}') + + plt.tight_layout() + plt.show() + + return analysis_clean + + +def save_detailed_report(features_df, original_df, analysis_df): + """Сохранение детального отчета""" + print("\n=== СОХРАНЕНИЕ ОТЧЕТА ===") + + # Сохраняем признаки + features_df.to_csv('extracted_features_enhanced.csv', encoding='utf-8') + print("✅ Признаки сохранены в extracted_features_enhanced.csv") + + # Детальный отчет + with open('features_analysis_report.txt', 'w', encoding='utf-8') as f: + f.write("ДЕТАЛЬНЫЙ ОТЧЕТ ПО АНАЛИЗУ ПРИЗНАКОВ\n") + f.write("=" * 50 + "\n\n") + + f.write(f"ОБЩАЯ СТАТИСТИКА:\n") + f.write(f"- Обработано строк: {len(features_df)}/{len(original_df)}\n") + f.write(f"- Извлечено признаков: {len(features_df.columns)}\n") + f.write(f"- Заполненность данных: {features_df.notna().mean().mean():.1%}\n\n") + + f.write("СПИСОК ПРИЗНАКОВ:\n") + for col in features_df.columns: + f.write(f"\n{col}:\n") + f.write(f" Тип: {features_df[col].dtype}\n") + f.write(f" Не-NULL: {features_df[col].notna().sum()}\n") + f.write(f" Среднее: {features_df[col].mean():.3f}\n") + f.write(f" Std: {features_df[col].std():.3f}\n") + + if analysis_df is not None and 'real_score' in analysis_df.columns: + corr = analysis_df.corr()['real_score'].get(col, 0) + f.write(f" Корреляция с оценкой: {corr:+.3f}\n") + + if analysis_df is not None: + f.write("\nКОРРЕЛЯЦИОННЫЙ АНАЛИЗ:\n") + correlations = analysis_df.corr()['real_score'].sort_values(key=abs, ascending=False) + for feature, corr in correlations.items(): + if feature != 'real_score' and abs(corr) > 0.1: + f.write(f" {feature}: {corr:+.3f}\n") + + print("✅ Детальный отчет сохранен в features_analysis_report.txt") + + +def main(): + """Основная функция тестирования""" + print("🚀 ЗАПУСК РАСШИРЕННОГО ТЕСТИРОВАНИЯ") + print("=" * 60) + + # Проверка окружения + check_environment() + + # Загрузка данных + df = load_and_analyze_dataset() + if df is None: + print("❌ Не удалось загрузить данные для тестирования") + return + + # Извлечение признаков + features_df = enhanced_feature_extraction(df) + + if features_df.empty: + print("❌ Не удалось извлечь признаки") + return + + # Анализ корреляций + analysis_df = analyze_correlations_with_scores(features_df, df) + + # Сохранение результатов + save_detailed_report(features_df, df, analysis_df) + + print("\n" + "=" * 60) + print("ТЕСТИРОВАНИЕ ЗАВЕРШЕНО!") + print("=" * 60) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_final.py b/test_final.py new file mode 100644 index 0000000000000000000000000000000000000000..1c56d196f2499c2ef684ef02b8184eaa5d0bbb73 --- /dev/null +++ b/test_final.py @@ -0,0 +1,20 @@ +try: + from transformers import pipeline + + print("✅ SUCCESS: Pipeline imported correctly!") + + # Тестируем несколько моделей + print("Testing sentiment analysis...") + classifier = pipeline("sentiment-analysis") + result = classifier("I love this!")[0] + print(f"Sentiment test: {result}") + + print("Testing text generation...") + generator = pipeline("text-generation", model="gpt2", max_length=50) + result = generator("The future of AI")[0] + print(f"Generation test: {result['generated_text'][:100]}...") + + print("🎉 ALL TESTS PASSED!") + +except Exception as e: + print(f"❌ ERROR: {e}") \ No newline at end of file diff --git a/test_imports.py b/test_imports.py new file mode 100644 index 0000000000000000000000000000000000000000..3ca9e14cd8271489976fb4d9b2273330e7885284 --- /dev/null +++ b/test_imports.py @@ -0,0 +1,24 @@ +import streamlit as st +from transformers import pipeline + + +def main(): + st.title("Working AI Models") + st.write("Based on your successful test") + + # Используем ту же модель, что работала в тесте + text = st.text_area("Text to analyze:", "I love this product!") + + if st.button("Get Sentiment"): + try: + classifier = pipeline("sentiment-analysis") + result = classifier(text)[0] + st.write(f"**Label:** {result['label']}") + st.write(f"**Score:** {result['score']:.4f}") + st.balloons() + except Exception as e: + st.error(f"Error: {e}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_individual_features.py b/test_individual_features.py new file mode 100644 index 0000000000000000000000000000000000000000..796eb95ad2142b73e796db5223d09a5d513b223a --- /dev/null +++ b/test_individual_features.py @@ -0,0 +1,50 @@ +from feature_extractor import RussianFeatureExtractor + + +def test_specific_features(): + """Тестирование конкретных функций экстрактора""" + print("🔍 ТЕСТИРОВАНИЕ КОНКРЕТНЫХ ФУНКЦИЙ") + print("=" * 40) + + extractor = RussianFeatureExtractor(use_heavy_models=False) + + # Тест разных типов текстов + test_cases = [ + { + 'name': 'Хороший ответ', + 'text': 'Здравствуйте! Я живу в большом городе. Здесь много парков и музеев.', + 'question': 'Расскажите о вашем городе.', + 'type': 1 + }, + { + 'name': 'Короткий ответ', + 'text': 'Не знаю.', + 'question': 'Что вы думаете?', + 'type': 2 + }, + { + 'name': 'Ответ с ошибками', + 'text': 'Я живу там где много деревьев и красиво но иногда шумно.', + 'question': 'Где вы живете?', + 'type': 1 + } + ] + + for i, case in enumerate(test_cases, 1): + print(f"\n{i}. {case['name']}:") + print(f" Текст: '{case['text']}'") + + # Тестируем отдельные функции + text_features = extractor.extract_enhanced_text_features(case['text']) + grammar_features = extractor.extract_grammar_features(case['text']) + discourse_features = extractor.extract_discourse_features(case['text']) + + print(f" 📝 Текстовые: сл={text_features['word_count']}, р={text_features['lexical_diversity']:.2f}") + print( + f" 📚 Грамматика: ош={grammar_features['grammar_error_ratio']:.2f}, полн={grammar_features['sentence_completeness']:.2f}") + print(f" 💬 Дискурс: прив={discourse_features['has_greeting']}, вопр={discourse_features['has_questions']}") + + +# Запуск всех тестов +if __name__ == "__main__": + test_specific_features() \ No newline at end of file diff --git a/test_q4_features.py b/test_q4_features.py new file mode 100644 index 0000000000000000000000000000000000000000..495d2ea40bee0ac44217e88fddc32b079476c653 --- /dev/null +++ b/test_q4_features.py @@ -0,0 +1,33 @@ +import pandas as pd +import sys + +sys.path.append('src') + +try: + from features_q4 import enhanced_q4_features + + print("✅ Модуль enhanced_q4_features загружен") + + # Тестовые данные + test_data = pd.DataFrame({ + 'question_number': [4, 4, 4], + 'question_text': ["Опишите картинку..."] * 3, + 'answer_text': [ + "На картинке я вижу семью на кухне. Мама готовит, папа моет посуду. У меня тоже есть семья - двое детей. Мы любим играть вместе.", + "Не знаю что сказать...", + "Лето. Парк. Дети играют. У меня трое детей. Мы гуляем в парке." + ] + }) + + result = enhanced_q4_features(test_data) + print("\n🔍 РЕЗУЛЬТАТ ТЕСТА:") + print(result.columns.tolist()) # Какие колонки появились + print("\n📊 ЗНАЧЕНИЯ ФИЧ:") + q4_cols = [c for c in result.columns if c.startswith('q4_')] + print(result[q4_cols].head(3)) + +except Exception as e: + print(f"❌ Ошибка: {e}") + import traceback + + traceback.print_exc() \ No newline at end of file diff --git a/test_q4_simple.py b/test_q4_simple.py new file mode 100644 index 0000000000000000000000000000000000000000..9a623fb5c3cbfae0c608e4e16d3110fdee03cf2f --- /dev/null +++ b/test_q4_simple.py @@ -0,0 +1,41 @@ +import pandas as pd +import sys +sys.path.append('src') + +# Пробуем разные варианты импорта +try: + from features_q4 import enhanced_q4_features + print("✅ Импорт enhanced_q4_features - УСПЕХ") + func = enhanced_q4_features +except: + try: + from features_q4 import q4_slot_features + print("✅ Импорт q4_slot_features - УСПЕХ") + func = q4_slot_features + except Exception as e: + print(f"❌ Ошибка импорта: {e}") + exit() + +# Тестовые данные +test_data = pd.DataFrame({ + 'question_number': [4, 4, 4], + 'question_text': ["Опишите картинку..."] * 3, + 'answer_text': [ + "На картинке я вижу семью на кухне. Мама готовит, папа моет посуду. У меня тоже есть семья - двое детей. Мы любим играть вместе.", + "Не знаю что сказать...", + "Лето. Парк. Дети играют. У меня трое детей. Мы гуляем в парке." + ] +}) + +try: + result = func(test_data) + print(f"✅ Функция выполнена успешно!") + print(f"📊 Колонки: {[c for c in result.columns if 'q4' in c]}") + print(f"🔍 Пример данных:") + print(result[['question_number', 'answer_text']].join( + result[[c for c in result.columns if 'q4' in c]].head(2) + )) +except Exception as e: + print(f"❌ Ошибка выполнения: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/test_real_data.py b/test_real_data.py new file mode 100644 index 0000000000000000000000000000000000000000..41bd887acdb502047b284a74a184392df2473e22 --- /dev/null +++ b/test_real_data.py @@ -0,0 +1,145 @@ +import pandas as pd +import numpy as np +from feature_extractor import RussianFeatureExtractor +import os +import time + + +def test_with_real_data(): + """Тестирование на реальных данных с замером времени""" + print("🧪 ТЕСТИРОВАНИЕ НА РЕАЛЬНЫХ ДАННЫХ") + print("=" * 50) + + # Пробуем разные файлы + data_files = ['small.csv', 'dataset.csv', 'train.csv'] + found_file = None + + for file in data_files: + if os.path.exists(file): + found_file = file + print(f"✅ Найден файл данных: {file}") + break + + if not found_file: + print("❌ Файлы данных не найдены!") + return + + # Загружаем данные + try: + df = pd.read_csv(found_file, encoding='utf-8', delimiter=';') + print(f"📊 Загружено {len(df)} строк, {len(df.columns)} колонок") + print(f"Колонки: {df.columns.tolist()}") + except: + try: + df = pd.read_csv(found_file, encoding='utf-8', delimiter=',') + print(f"📊 Загружено {len(df)} строк (разделитель ',')") + except Exception as e: + print(f"❌ Ошибка загрузки: {e}") + return + + # Берем небольшую выборку для теста + sample_size = min(50, len(df)) + sample_df = df.head(sample_size).copy() + + print(f"\n🔧 ИНИЦИАЛИЗАЦИЯ ЭКСТРАКТОРА...") + start_time = time.time() + + # Создаем экстрактор + extractor = RussianFeatureExtractor(use_heavy_models=False) + + init_time = time.time() - start_time + print(f"✅ Экстрактор инициализирован за {init_time:.1f} сек") + + print(f"\n🎯 ИЗВЛЕЧЕНИЕ ПРИЗНАКОВ ДЛЯ {sample_size} СТРОК...") + extract_start = time.time() + + # Извлекаем признаки + features_df = extractor.extract_features_for_dataframe(sample_df) + + extract_time = time.time() - extract_start + + if not features_df.empty: + print(f"✅ Извлечение завершено за {extract_time:.1f} сек") + print(f"📈 Получено {len(features_df.columns)} признаков") + + # Анализ результатов + print(f"\n📊 СТАТИСТИКА ПРИЗНАКОВ:") + print(f" - Успешно обработано: {len(features_df)}/{sample_size} строк") + print(f" - Заполненность данных: {features_df.notna().mean().mean():.1%}") + + # Показываем топ признаков по вариативности + numeric_features = features_df.select_dtypes(include=[np.number]) + if not numeric_features.empty: + std_dev = numeric_features.std().sort_values(ascending=False) + print(f"\n🎯 ТОП-5 самых вариативных признаков:") + for feature, std_val in std_dev.head(5).items(): + print(f" {feature}: {std_val:.3f}") + + # Сохраняем результаты + output_file = 'real_data_features.csv' + features_df.to_csv(output_file, encoding='utf-8') + print(f"\n💾 Результаты сохранены в {output_file}") + + # Сохраняем описания + with open('features_description_detailed.txt', 'w', encoding='utf-8') as f: + f.write("ПОДРОБНОЕ ОПИСАНИЕ ПРИЗНАКОВ\n") + f.write("=" * 50 + "\n\n") + + for col in features_df.columns: + f.write(f"{col}:\n") + f.write(f" Тип: {features_df[col].dtype}\n") + f.write(f" Не-NULL: {features_df[col].notna().sum()}\n") + f.write(f" Среднее: {features_df[col].mean():.3f}\n") + f.write(f" Std: {features_df[col].std():.3f}\n") + f.write(f" Min: {features_df[col].min():.3f}\n") + f.write(f" Max: {features_df[col].max():.3f}\n\n") + + print("📝 Описание признаков сохранено в features_description_detailed.txt") + + else: + print("❌ Не удалось извлечь признаки") + + +def compare_old_vs_new(): + """Сравнение старого и нового экстрактора""" + print("\n" + "=" * 50) + print("СРАВНЕНИЕ СТАРОГО И НОВОГО МЕТОДОВ") + print("=" * 50) + + # Тестовый текст + test_text = "Привет! Меня зовут Мария. Я живу в Москве и учусь в университете." + + # Старый метод (базовые признаки) + from feature_extractor import extract_quick_features + quick_features = extract_quick_features(test_text) + + # Новый метод (полные признаки) + test_data = { + 'Текст вопроса': ['Расскажите о себе'], + 'Транскрибация ответа': [test_text], + '№ вопроса': [1] + } + test_df = pd.DataFrame(test_data) + + extractor = RussianFeatureExtractor(use_heavy_models=False) + full_features_df = extractor.extract_features_for_dataframe(test_df) + + print("📊 СРАВНЕНИЕ:") + print(f"Быстрый метод: {len(quick_features)} признаков") + if not full_features_df.empty: + print(f"Полный метод: {len(full_features_df.columns)} признаков") + + # Сравниваем общие признаки + common_features = set(quick_features.keys()) & set(full_features_df.columns) + print(f"Общих признаков: {len(common_features)}") + + print("\n📈 ЗНАЧЕНИЯ ОБЩИХ ПРИЗНАКОВ:") + for feature in list(common_features)[:5]: # Показываем первые 5 + old_val = quick_features[feature] + new_val = full_features_df[feature].iloc[0] + print(f" {feature}: {old_val:.3f} -> {new_val:.3f}") + + +if __name__ == "__main__": + test_with_real_data() + compare_old_vs_new() \ No newline at end of file diff --git a/test_simple.py b/test_simple.py new file mode 100644 index 0000000000000000000000000000000000000000..80224d1d67ffe16fb443ff645916a4b12eedf85a --- /dev/null +++ b/test_simple.py @@ -0,0 +1,58 @@ +# test_simple.py +import pandas as pd +from feature_extractor import RussianFeatureExtractor + + +def simple_test(): + """Простой тест исправленного экстрактора""" + print("🧪 ПРОСТОЙ ТЕСТ ИСПРАВЛЕННОЙ ВЕРСИИ") + print("=" * 50) + + # Создаем экстрактор + extractor = RussianFeatureExtractor(use_heavy_models=False) + + # Тестовые данные с разным качеством ответов + test_cases = [ + { + 'name': 'Хороший ответ', + 'question': 'Расскажите о вашем городе', + 'answer': 'Привет! Я живу в Санкт-Петербурге. Это культурная столица России с красивыми дворцами, каналами и богатой историей.', + 'type': 1 + }, + { + 'name': 'Средний ответ', + 'question': 'Что вы любите делать?', + 'answer': 'Люблю читать книги и гулять. Иногда хожу в кино.', + 'type': 2 + }, + { + 'name': 'Плохой ответ', + 'question': 'Опишите картинку', + 'answer': 'Не знаю...', + 'type': 4 + } + ] + + print("\n📊 РЕЗУЛЬТАТЫ ТЕСТИРОВАНИЯ:") + print("-" * 60) + + for case in test_cases: + test_df = pd.DataFrame([{ + 'Текст вопроса': case['question'], + 'Транскрибация ответа': case['answer'], + '№ вопроса': case['type'] + }]) + + features = extractor.extract_all_features(test_df.iloc[0]) + + print(f"\n{case['name']}:") + print(f" Ответ: '{case['answer'][:40]}...'") + print(f" composite_quality_score: {features['composite_quality_score']:.3f}") + print(f" grammar_quality: {features['grammar_quality']:.3f}") + print(f" style_score: {features['style_score']:.3f}") + print(f" word_count: {features['word_count']}") + print(f" lexical_diversity: {features['lexical_diversity']:.3f}") + + +if __name__ == "__main__": + simple_test() \ No newline at end of file diff --git a/test_upload.py b/test_upload.py new file mode 100644 index 0000000000000000000000000000000000000000..b6aa11d7f148253acfb789c94c584b2ae3ab79e9 --- /dev/null +++ b/test_upload.py @@ -0,0 +1,20 @@ +import requests + +url = "http://localhost:8000/predict_csv" +file_path = r".\data\raw\Данные для кейса.csv" + +print(f"📤 Отправляю файл {file_path} на {url}...") + +with open(file_path, 'rb') as f: + files = {'file': ('data.csv', f, 'text/csv')} + response = requests.post(url, files=files, timeout=120) # 2 минуты таймаут + +print(f"📥 Ответ: {response.status_code}") + +if response.status_code == 200: + with open('predicted_from_api.csv', 'wb') as f: + f.write(response.content) + print("✅ Файл успешно обработан! Результат в predicted_from_api.csv") +else: + print(f"❌ Ошибка: {response.status_code}") + print("Текст ответа:", response.text) \ No newline at end of file diff --git a/test_upload_small.py b/test_upload_small.py new file mode 100644 index 0000000000000000000000000000000000000000..6da5c8f1be66bbba61837af724c0cdc02f6602ad --- /dev/null +++ b/test_upload_small.py @@ -0,0 +1,17 @@ +import requests + +url = "http://localhost:8000/predict_csv" +file_path = "small.csv" # положи файл в корень проекта + +print(f"📤 Отправляю файл {file_path}...") +with open(file_path, 'rb') as f: + files = {'file': ('small.csv', f, 'text/csv')} + response = requests.post(url, files=files) + +if response.status_code == 200: + with open('predicted_small.csv', 'wb') as f: + f.write(response.content) + print("✅ Файл обработан! predicted_small.csv") +else: + print(f"❌ Ошибка: {response.status_code}") + print(response.text) \ No newline at end of file diff --git a/working_app.py b/working_app.py new file mode 100644 index 0000000000000000000000000000000000000000..f264219ef1b4e9e453495b4c1a9fca7e9d8d9001 --- /dev/null +++ b/working_app.py @@ -0,0 +1,51 @@ +import streamlit as st +import sys +import subprocess + +# Попытка импорта с обработкой ошибок +try: + from transformers import pipeline + + st.success("✅ Transformers imported successfully!") +except ImportError as e: + st.error(f"❌ Import error: {e}") + st.info("Installing transformers...") + try: + subprocess.check_call([sys.executable, "-m", "pip", "install", "transformers"]) + from transformers import pipeline + + st.success("✅ Transformers installed and imported!") + except: + st.error("Failed to install transformers") + st.stop() + +st.title("🧠 AI Models Working Demo") +st.write("This version should work reliably") + +# Простой и надежный интерфейс +task = st.selectbox("Choose task:", ["Sentiment Analysis", "Text Generation"]) + +text = st.text_area("Enter text:", "I love artificial intelligence!") + +if st.button("Run"): + try: + if task == "Sentiment Analysis": + with st.spinner("Analyzing..."): + # Используем явное указание модели + classifier = pipeline("sentiment-analysis", + model="distilbert-base-uncased-finetuned-sst-2-english") + result = classifier(text)[0] + st.success(f"Result: {result['label']}") + st.info(f"Confidence: {result['score']:.4f}") + + elif task == "Text Generation": + with st.spinner("Generating..."): + generator = pipeline("text-generation", + model="gpt2", + max_length=100) + result = generator(text, num_return_sequences=1) + st.write("**Generated text:**") + st.write(result[0]['generated_text']) + + except Exception as e: + st.error(f"Error: {str(e)}") \ No newline at end of file