KarmanovaLidiia commited on
Commit
bcb314a
·
0 Parent(s):

Initial clean commit for HF Space (models via Git LFS)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.dockerignore ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @'
2
+ # Python / venv / байт-код
3
+ .venv
4
+ __pycache__/
5
+ *.py[cod]
6
+ *.egg-info/
7
+
8
+ # НЕ класть в build context — большие локальные папки и кэши
9
+ hf_cache/
10
+ **/hf_cache/**
11
+ .cache/
12
+ **/.cache/**
13
+ data/
14
+ models/
15
+ models_all/
16
+ reports/
17
+ catboost_info/
18
+ predicted.csv
19
+
20
+ # VCS/IDE мусор
21
+ .git
22
+ .gitignore
23
+ .idea
24
+ .ipynb_checkpoints
25
+ tests/.pytest_cache
26
+ .pytest_cache
27
+ '@ | Set-Content -Encoding utf8 .dockerignore
.env ADDED
@@ -0,0 +1 @@
 
 
1
+ HF_HUB_DISABLE_SYMLINKS_WARNING=1
.gitattributes ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ *.cbm filter=lfs diff=lfs merge=lfs -text
2
+ *.pkl filter=lfs diff=lfs merge=lfs -text
3
+ *.csv filter=lfs diff=lfs merge=lfs -text
4
+ *.xlsx filter=lfs diff=lfs merge=lfs -text
5
+ *.png filter=lfs diff=lfs merge=lfs -text
6
+ *.tsv filter=lfs diff=lfs merge=lfs -text
7
+ structure.txt filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # виртуальные окружения / IDE
2
+ .venv/
3
+ venv/
4
+ .idea/
5
+ .vscode/
6
+
7
+ # кэш/сервисы
8
+ __pycache__/
9
+ *.pyc
10
+ *.pyo
11
+ *.pyd
12
+ .pytest_cache/
13
+ .ipynb_checkpoints/
14
+ .cache/
15
+ *.log
16
+
17
+ # локальные данные/выводы
18
+ data/
19
+ out/
20
+ *.csv
21
+ *.xlsx
22
+ *.tsv
23
+ *.png
24
+ *.pdf
25
+
26
+ # большие/неиспользуемые модели
27
+ models_all/
.streamlit/config.toml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ [server]
2
+ headless = true
3
+ maxUploadSize = 200
4
+
5
+ [browser]
6
+ gatherUsageStats = false
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 8000
11
+
12
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Dockerfile-ui ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Копируем requirements
6
+ COPY ui/requirements.txt .
7
+
8
+ # Устанавливаем зависимости
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ # Копируем код приложения
12
+ COPY ui/app.py .
13
+
14
+ # Создаем папку для шаблонов (если нужна)
15
+ RUN mkdir -p templates
16
+
17
+ # Копируем шаблоны (если есть)
18
+ COPY ui/templates/ ./templates/
19
+
20
+ EXPOSE 8080
21
+
22
+ # Запускаем приложение
23
+ CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
Makefile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: train predict api ui test docker
2
+
3
+ predict:
4
+ python -m src.predict --input data/raw/Данные\ для\ кейса.csv --output data/processed/predicted.csv
5
+
6
+ api:
7
+ uvicorn app.main:app --host 127.0.0.1 --port 8020 --reload
8
+
9
+ ui:
10
+ streamlit run app/ui.py
11
+
12
+ test:
13
+ pytest -q
14
+
15
+ docker:
16
+ docker compose up --build
README.md ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🧠 Автооценка устных ответов (RFL • CatBoost + ruSBERT)
2
+
3
+ ### 📌 Описание
4
+ Проект предназначен для автоматической оценки устных ответов на экзаменах по русскому языку как иностранному (RFL).
5
+ Используются модели **CatBoost Q1–Q4** и признаки из **ruSBERT** (эмбеддинги).
6
+
7
+ ---
8
+
9
+ ### 🚀 Быстрый старт
10
+
11
+ #### 1️⃣ Локальный запуск
12
+
13
+ ```bash
14
+ pip install -r requirements.txt
15
+ python src/predict.py -i "data/raw/Данные для кейса.csv" -o "out/predicted.csv"
analyze_features.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ import seaborn as sns
5
+
6
+
7
+ def analyze_extracted_features():
8
+ """Анализ извлеченных признаков"""
9
+
10
+ # Загружаем извлеченные признаки
11
+ features_df = pd.read_csv('real_data_features.csv', index_col=0)
12
+
13
+ print("📊 ДЕТАЛЬНЫЙ АНАЛИЗ ИЗВЛЕЧЕННЫХ ПРИЗНАКОВ")
14
+ print("=" * 50)
15
+
16
+ print(f"Всего признаков: {len(features_df.columns)}")
17
+ print(f"Обработано строк: {len(features_df)}")
18
+
19
+ # Анализ заполненности
20
+ null_analysis = features_df.isnull().sum()
21
+ null_features = null_analysis[null_analysis > 0]
22
+
23
+ if len(null_features) > 0:
24
+ print(f"\n❌ Признаки с пропусками:")
25
+ for feature, null_count in null_features.items():
26
+ print(f" {feature}: {null_count} пропусков ({null_count / len(features_df):.1%})")
27
+ else:
28
+ print(f"\n✅ Все признаки полностью заполнены!")
29
+
30
+ # Статистика по числовым признакам
31
+ numeric_features = features_df.select_dtypes(include=[np.number])
32
+
33
+ print(f"\n📈 СТАТИСТИКА ПРИЗНАКОВ:")
34
+ stats_summary = numeric_features.agg(['mean', 'std', 'min', 'max']).T
35
+ stats_summary['cv'] = stats_summary['std'] / stats_summary['mean'] # Коэффициент вариации
36
+
37
+ # Показываем топ-10 самых информативных признаков
38
+ informative_features = stats_summary[stats_summary['std'] > 0].sort_values('cv', ascending=False)
39
+
40
+ print(f"\n🎯 ТОП-10 самых информативных признаков (по вариативности):")
41
+ for feature, row in informative_features.head(10).iterrows():
42
+ print(f" {feature:25} mean={row['mean']:6.2f} std={row['std']:6.2f} cv={row['cv']:.2f}")
43
+
44
+ # Визуализация распределения ключевых признаков
45
+ key_features = ['text_length', 'word_count', 'lexical_diversity', 'composite_quality_score']
46
+ available_features = [f for f in key_features if f in numeric_features.columns]
47
+
48
+ if available_features:
49
+ plt.figure(figsize=(15, 10))
50
+
51
+ for i, feature in enumerate(available_features, 1):
52
+ plt.subplot(2, 2, i)
53
+ plt.hist(numeric_features[feature].dropna(), bins=20, alpha=0.7, edgecolor='black')
54
+ plt.title(f'Распределение {feature}')
55
+ plt.xlabel(feature)
56
+ plt.ylabel('Частота')
57
+
58
+ plt.tight_layout()
59
+ plt.savefig('features_distribution.png', dpi=150, bbox_inches='tight')
60
+ plt.show()
61
+ print(f"\n📊 Визуализация сохранена в features_distribution.png")
62
+
63
+ # Анализ корреляций между признаками
64
+ if len(numeric_features.columns) > 5:
65
+ # Выбираем топ-15 самых вариативных признаков для корреляционной матрицы
66
+ top_features = informative_features.head(15).index.tolist()
67
+
68
+ plt.figure(figsize=(12, 10))
69
+ correlation_matrix = numeric_features[top_features].corr()
70
+
71
+ mask = np.triu(np.ones_like(correlation_matrix, dtype=bool))
72
+ sns.heatmap(correlation_matrix, mask=mask, annot=True, fmt='.2f', cmap='coolwarm',
73
+ center=0, square=True, cbar_kws={"shrink": .8})
74
+ plt.title('Корреляционная матрица признаков (топ-15)')
75
+ plt.tight_layout()
76
+ plt.savefig('features_correlation.png', dpi=150, bbox_inches='tight')
77
+ plt.show()
78
+ print(f"📈 Корреляционная матрица сохранена в features_correlation.png")
79
+
80
+ # Анализ качества композитного показателя
81
+ if 'composite_quality_score' in numeric_features.columns:
82
+ print(f"\n🎯 АНАЛИЗ КОМПОЗИТНОГО ПОКАЗАТЕЛЯ КАЧЕСТВА:")
83
+ quality_scores = numeric_features['composite_quality_score']
84
+ print(f" Среднее: {quality_scores.mean():.3f}")
85
+ print(f" Стандартное отклонение: {quality_scores.std():.3f}")
86
+ print(f" Диапазон: [{quality_scores.min():.3f}, {quality_scores.max():.3f}]")
87
+
88
+ # Распределение по квантилям
89
+ quantiles = quality_scores.quantile([0.25, 0.5, 0.75])
90
+ print(f" Квантили: 25%={quantiles[0.25]:.3f}, 50%={quantiles[0.5]:.3f}, 75%={quantiles[0.75]:.3f}")
91
+
92
+
93
+ def check_feature_correlations_with_target():
94
+ """Проверка корреляции признаков с целевой переменной (если есть оценки)"""
95
+
96
+ features_df = pd.read_csv('real_data_features.csv', index_col=0)
97
+
98
+ # Ищем колонку с оценками в исходных данных
99
+ score_columns = [col for col in features_df.columns if 'score' in col.lower() or 'оценк' in col.lower()]
100
+
101
+ if score_columns:
102
+ target_col = score_columns[0]
103
+ print(f"\n🎯 КОРРЕЛЯЦИЯ ПРИЗНАКОВ С {target_col}:")
104
+ print("-" * 40)
105
+
106
+ correlations = features_df.corr()[target_col].abs().sort_values(ascending=False)
107
+
108
+ # Показываем топ-10 наиболее коррелирующих признаков
109
+ top_correlated = correlations.head(11) # +1 потому что target сам с собой
110
+
111
+ for feature, corr in top_correlated.items():
112
+ if feature != target_col:
113
+ actual_corr = features_df.corr()[target_col][feature]
114
+ direction = "↑" if actual_corr > 0 else "↓"
115
+ significance = "***" if abs(actual_corr) > 0.3 else "**" if abs(actual_corr) > 0.2 else "*" if abs(
116
+ actual_corr) > 0.1 else ""
117
+ print(f" {direction} {feature:25} {actual_corr:+.3f} {significance}")
118
+ else:
119
+ print(f"\nℹ️ Целевая переменная (оценки) не найдена в данных")
120
+
121
+
122
+ if __name__ == "__main__":
123
+ analyze_extracted_features()
124
+ check_feature_correlations_with_target()
analyze_features_simple.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+
5
+
6
+ def analyze_extracted_features():
7
+ """Анализ извлеченных признаков без сложных зависимостей"""
8
+
9
+ try:
10
+ # Загружаем извлеченные признаки
11
+ features_df = pd.read_csv('real_data_features.csv', index_col=0)
12
+ except FileNotFoundError:
13
+ print("❌ Файл real_data_features.csv не найден!")
14
+ print("💡 Сначала запустите test_real_data.py")
15
+ return
16
+
17
+ print("📊 ДЕТАЛЬНЫЙ АНАЛИЗ ИЗВЛЕЧЕННЫХ ПРИЗНАКОВ")
18
+ print("=" * 50)
19
+
20
+ print(f"Всего признаков: {len(features_df.columns)}")
21
+ print(f"Обработано строк: {len(features_df)}")
22
+
23
+ # Анализ заполненности
24
+ null_analysis = features_df.isnull().sum()
25
+ null_features = null_analysis[null_analysis > 0]
26
+
27
+ if len(null_features) > 0:
28
+ print(f"\n❌ Признаки с пропусками:")
29
+ for feature, null_count in null_features.items():
30
+ print(f" {feature}: {null_count} пропусков ({null_count / len(features_df):.1%})")
31
+ else:
32
+ print(f"\n✅ Все признаки полностью заполнены!")
33
+
34
+ # Статистика по числовым признакам
35
+ numeric_features = features_df.select_dtypes(include=[np.number])
36
+
37
+ print(f"\n📈 СТАТИСТИКА ПРИЗНАКОВ:")
38
+ stats_summary = numeric_features.agg(['mean', 'std', 'min', 'max']).T
39
+ stats_summary['cv'] = stats_summary['std'] / stats_summary['mean'] # Коэффициент вариации
40
+
41
+ # Показываем топ-10 самых информативных признаков
42
+ informative_features = stats_summary[stats_summary['std'] > 0].sort_values('cv', ascending=False)
43
+
44
+ print(f"\n🎯 ТОП-15 самых информативных признаков (по вариативности):")
45
+ for feature, row in informative_features.head(15).iterrows():
46
+ print(f" {feature:25} mean={row['mean']:6.2f} std={row['std']:6.2f} cv={row['cv']:.2f}")
47
+
48
+ # Визуализация распределения ключевых признаков
49
+ key_features = ['text_length', 'word_count', 'lexical_diversity', 'composite_quality_score']
50
+ available_features = [f for f in key_features if f in numeric_features.columns]
51
+
52
+ if available_features:
53
+ plt.figure(figsize=(15, 10))
54
+
55
+ for i, feature in enumerate(available_features, 1):
56
+ plt.subplot(2, 2, i)
57
+ plt.hist(numeric_features[feature].dropna(), bins=20, alpha=0.7, edgecolor='black')
58
+ plt.title(f'Распределение {feature}')
59
+ plt.xlabel(feature)
60
+ plt.ylabel('Частота')
61
+
62
+ plt.tight_layout()
63
+ plt.savefig('features_distribution.png', dpi=150, bbox_inches='tight')
64
+ plt.show()
65
+ print(f"\n📊 Визуализация сохранена в features_distribution.png")
66
+
67
+ # Анализ качества композитного показателя
68
+ if 'composite_quality_score' in numeric_features.columns:
69
+ print(f"\n🎯 АНАЛИЗ КОМПОЗИТНОГО ПОКАЗАТЕЛЯ КАЧЕСТВА:")
70
+ quality_scores = numeric_features['composite_quality_score']
71
+ print(f" Среднее: {quality_scores.mean():.3f}")
72
+ print(f" Стандартное отклонение: {quality_scores.std():.3f}")
73
+ print(f" Диапазон: [{quality_scores.min():.3f}, {quality_scores.max():.3f}]")
74
+
75
+ # Распределение по квантилям
76
+ quantiles = quality_scores.quantile([0.25, 0.5, 0.75])
77
+ print(f" Квантили: 25%={quantiles[0.25]:.3f}, 50%={quantiles[0.5]:.3f}, 75%={quantiles[0.75]:.3f}")
78
+
79
+ # Анализ что влияет на качество
80
+ print(f"\n🔍 КОРРЕЛЯЦИЯ С КОМПОЗИТНЫМ ПОКАЗАТЕЛЕМ:")
81
+ correlations = numeric_features.corr()['composite_quality_score'].abs().sort_values(ascending=False)
82
+
83
+ for feature, corr in correlations.head(10).items():
84
+ if feature != 'composite_quality_score':
85
+ actual_corr = numeric_features.corr()['composite_quality_score'][feature]
86
+ direction = "↑" if actual_corr > 0 else "↓"
87
+ print(f" {direction} {feature:25} {actual_corr:+.3f}")
88
+
89
+
90
+ def check_feature_correlations_with_target():
91
+ """Проверка корреляции признаков с целевой переменной (если есть оценки)"""
92
+
93
+ try:
94
+ features_df = pd.read_csv('real_data_features.csv', index_col=0)
95
+ except FileNotFoundError:
96
+ return
97
+
98
+ # Ищем колонку с оценками в исходных данных
99
+ score_columns = [col for col in features_df.columns if
100
+ 'score' in col.lower() or 'оценк' in col.lower() or 'балл' in col.lower()]
101
+
102
+ if score_columns:
103
+ target_col = score_columns[0]
104
+ print(f"\n🎯 КОРРЕЛЯЦИЯ ПРИЗНАКОВ С {target_col}:")
105
+ print("-" * 50)
106
+
107
+ correlations = features_df.corr()[target_col].abs().sort_values(ascending=False)
108
+
109
+ # Показываем топ-10 наиболее коррелирующих признаков
110
+ top_correlated = correlations.head(11) # +1 потому что target сам с собой
111
+
112
+ print(f" {'ПРИЗНАК':<25} {'КОРРЕЛЯЦИЯ':<10} {'ЗНАЧИМОСТЬ'}")
113
+ print(f" {'-' * 25} {'-' * 10} {'-' * 10}")
114
+
115
+ for feature, corr in top_correlated.items():
116
+ if feature != target_col:
117
+ actual_corr = features_df.corr()[target_col][feature]
118
+ direction = "↑" if actual_corr > 0 else "↓"
119
+ significance = "***" if abs(actual_corr) > 0.3 else "**" if abs(actual_corr) > 0.2 else "*" if abs(
120
+ actual_corr) > 0.1 else ""
121
+ print(f" {direction} {feature:<23} {actual_corr:+.3f} {significance}")
122
+ else:
123
+ print(f"\nℹ️ Целевая переменная (оценки) не найдена в данных")
124
+
125
+
126
+ def analyze_feature_categories():
127
+ """Анализ признаков по категориям"""
128
+
129
+ try:
130
+ features_df = pd.read_csv('real_data_features.csv', index_col=0)
131
+ except FileNotFoundError:
132
+ return
133
+
134
+ # Группируем признаки по категориям
135
+ categories = {
136
+ '📝 ТЕКСТОВЫЕ': ['text_length', 'word_count', 'sentence_count', 'avg_sentence_length',
137
+ 'avg_word_length', 'lexical_diversity', 'long_word_ratio', 'text_complexity'],
138
+ '🎯 СЕМАНТИЧЕСКИЕ': ['semantic_similarity', 'keyword_overlap', 'tfidf_similarity', 'response_relevance'],
139
+ '📚 ГРАММАТИЧЕСКИЕ': ['grammar_error_count', 'grammar_error_ratio', 'has_punctuation',
140
+ 'sentence_completeness', 'proper_capitalization'],
141
+ '💬 ДИСКУРС': ['has_greeting', 'has_questions', 'has_description', 'has_connectors',
142
+ 'has_emotional_words', 'coherence_score'],
143
+ '❓ ТИПЫ ВОПРОСОВ': ['dialog_initiation', 'response_adequacy', 'information_seeking',
144
+ 'descriptive_detail', 'answer_length_sufficiency', 'question_type'],
145
+ '⭐ КАЧЕСТВО': ['composite_quality_score', 'social_appropriateness', 'interaction_quality']
146
+ }
147
+
148
+ print(f"\n📂 РАСПРЕДЕЛЕНИЕ ПРИЗНАКОВ ПО КАТЕГОРИЯМ:")
149
+ print("=" * 50)
150
+
151
+ numeric_features = features_df.select_dtypes(include=[np.number])
152
+
153
+ for category, features in categories.items():
154
+ available_features = [f for f in features if f in numeric_features.columns]
155
+ if available_features:
156
+ print(f"\n{category} ({len(available_features)} признаков):")
157
+ for feature in available_features:
158
+ mean_val = numeric_features[feature].mean()
159
+ std_val = numeric_features[feature].std()
160
+ print(f" • {feature:25} {mean_val:6.3f} ± {std_val:5.3f}")
161
+
162
+
163
+ if __name__ == "__main__":
164
+ analyze_extracted_features()
165
+ check_feature_correlations_with_target()
166
+ analyze_feature_categories()
167
+
168
+ print(f"\n✅ Анализ завершен!")
169
+ print("💡 Рекомендации будут основаны на этом анализе")
analyze_results.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import matplotlib.pyplot as plt
3
+ from collections import Counter
4
+ import numpy as np
5
+ import os
6
+ import warnings
7
+
8
+ warnings.filterwarnings('ignore')
9
+
10
+ # Настройка отображения
11
+ plt.style.use('default')
12
+ plt.rcParams['font.family'] = 'DejaVu Sans' # Для поддержки кириллицы
13
+
14
+
15
+ def load_and_analyze_data():
16
+ """Загрузка и базовый анализ данных"""
17
+
18
+ # Загрузка данных с правильным разделителем
19
+ file_path = 'small.csv' # или полный путь к файлу
20
+
21
+ # Пробуем разные разделители и кодировки
22
+ try:
23
+ # Сначала пробуем с разделителем точка с запятой
24
+ df = pd.read_csv(file_path, encoding='utf-8', delimiter=';')
25
+ print("✅ Файл загружен с разделителем ';' и кодировкой utf-8")
26
+ except:
27
+ try:
28
+ df = pd.read_csv(file_path, encoding='cp1251', delimiter=';')
29
+ print("✅ Файл загружен с разделителем ';' и кодировкой cp1251")
30
+ except:
31
+ try:
32
+ df = pd.read_csv(file_path, encoding='utf-8', delimiter=',')
33
+ print("✅ Файл загружен с разделителем ',' и кодировкой utf-8")
34
+ except:
35
+ try:
36
+ df = pd.read_csv(file_path, encoding='cp1251', delimiter=',')
37
+ print("✅ Файл загружен с разделителем ',' и кодировкой cp1251")
38
+ except Exception as e:
39
+ print(f"❌ Ошибка загрузки файла: {e}")
40
+ return None
41
+
42
+ print("=" * 60)
43
+ print("АНАЛИЗ РЕЗУЛЬТАТОВ АВТОМАТИЧЕСКОЙ ОЦЕНКИ")
44
+ print("=" * 60)
45
+
46
+ # Базовая информация о данных
47
+ print(f"Размер данных: {df.shape[0]} строк, {df.shape[1]} колонок")
48
+ print(f"\nВсе колонки: {list(df.columns)}")
49
+
50
+ # Показываем первые несколько строк для проверки
51
+ print(f"\nПервые 3 строки данных:")
52
+ print(df.head(3))
53
+
54
+ return df
55
+
56
+
57
+ def check_and_rename_columns(df):
58
+ """Проверка и переименование колонок если нужно"""
59
+
60
+ print("\n" + "=" * 40)
61
+ print("ПРОВЕРКА СТРУКТУРЫ ДАННЫХ")
62
+ print("=" * 40)
63
+
64
+ # Если есть только одна колонка, возможно данные объединены
65
+ if df.shape[1] == 1:
66
+ first_column = df.columns[0]
67
+ print(f"Обнаружена одна колонка: '{first_column}'")
68
+
69
+ # Проверяем, содержит ли она все данные
70
+ sample_value = str(df.iloc[0, 0])
71
+ if ';' in sample_value:
72
+ print("⚠️ Данные объединены в одну колонку, разделяем...")
73
+
74
+ # Разделяем данные по точке с запятой
75
+ split_data = df[first_column].str.split(';', expand=True)
76
+
77
+ # Берем первую строку как заголовки
78
+ if split_data.shape[0] > 1:
79
+ new_columns = split_data.iloc[0].tolist()
80
+ split_data = split_data[1:] # Убираем строку с заголовками
81
+ split_data.columns = new_columns
82
+ df = split_data.reset_index(drop=True)
83
+ print("✅ Данные успешно разделены")
84
+ print(f"Новые колонки: {list(df.columns)}")
85
+
86
+ return df
87
+
88
+
89
+ def basic_statistics(df):
90
+ """Базовая статистика по оценкам"""
91
+
92
+ print("\n" + "=" * 40)
93
+ print("БАЗОВАЯ СТАТИСТИКА")
94
+ print("=" * 40)
95
+
96
+ # Проверяем наличие нужных колонок
97
+ available_columns = list(df.columns)
98
+ print(f"Доступные колонки: {available_columns}")
99
+
100
+ # Статистика по AI оценкам (pred_score)
101
+ if 'pred_score' in df.columns:
102
+ print("\nAI оценки (pred_score):")
103
+ print(f" Среднее: {df['pred_score'].mean():.3f}")
104
+ print(f" Медиана: {df['pred_score'].median():.3f}")
105
+ print(f" Стандартное отклонение: {df['pred_score'].std():.3f}")
106
+ print(f" Минимум: {df['pred_score'].min():.3f}")
107
+ print(f" Максимум: {df['pred_score'].max():.3f}")
108
+ else:
109
+ print("❌ Колонка 'pred_score' не найдена")
110
+
111
+ # Статистика по человеческим оценкам
112
+ human_score_columns = ['Оценка экзаменатора', 'оценка', 'score', 'human_score']
113
+ human_score_col = None
114
+
115
+ for col in human_score_columns:
116
+ if col in df.columns:
117
+ human_score_col = col
118
+ break
119
+
120
+ if human_score_col:
121
+ print(f"\nОценки экзаменатора ({human_score_col}):")
122
+ print(f" Среднее: {df[human_score_col].mean():.3f}")
123
+ print(f" Медиана: {df[human_score_col].median():.3f}")
124
+ print(f" Стандартное отклонение: {df[human_score_col].std():.3f}")
125
+
126
+ # Распределение оценок
127
+ print(f"\nРаспределение оценок экзаменатора:")
128
+ распределение = df[human_score_col].value_counts().sort_index()
129
+ for оценка, count in распределение.items():
130
+ print(f" {оценка}: {count} ответов ({count / len(df) * 100:.1f}%)")
131
+ else:
132
+ print("❌ Колонка с оценками экзаменатора не найдена")
133
+
134
+
135
+ def calculate_correlations(df):
136
+ """Расчет корреляций и разниц"""
137
+
138
+ print("\n" + "=" * 40)
139
+ print("КОРРЕЛЯЦИИ И РАСХОЖДЕНИЯ")
140
+ print("=" * 40)
141
+
142
+ # Проверяем наличие обеих колонок
143
+ if 'pred_score' not in df.columns:
144
+ print("❌ Колонка 'pred_score' не найдена для расчета корреляций")
145
+ return
146
+
147
+ human_score_columns = ['Оценка экзаменатора', 'оценка', 'score', 'human_score']
148
+ human_score_col = None
149
+
150
+ for col in human_score_columns:
151
+ if col in df.columns:
152
+ human_score_col = col
153
+ break
154
+
155
+ if not human_score_col:
156
+ print("❌ Колонка с оценками экзаменатора не найдена для расчета корреляций")
157
+ return
158
+
159
+ # Корреляция
160
+ correlation = df[[human_score_col, 'pred_score']].corr().iloc[0, 1]
161
+ print(f"Корреляция между оценками: {correlation:.3f}")
162
+
163
+ # Разницы между оценками
164
+ df['разница'] = df['pred_score'] - df[human_score_col]
165
+ df['abs_разница'] = abs(df['разница'])
166
+
167
+ print(f"\nСредняя абсолютная разница: {df['abs_разница'].mean():.3f}")
168
+ print(f"Максимальная разница: {df['abs_разница'].max():.3f}")
169
+ print(f"Минимальная разница: {df['abs_разница'].min():.3f}")
170
+
171
+ # Анализ согласованности
172
+ print("\nСОГЛАСОВАННОСТЬ ОЦЕНОК:")
173
+ for порог in [0.1, 0.3, 0.5, 1.0]:
174
+ согласованные = df[df['abs_разница'] < порог].shape[0]
175
+ процент = (согласованные / len(df)) * 100
176
+ print(f" Разница < {порог}: {согласованные} ответов ({процент:.1f}%)")
177
+
178
+ # Направление разниц
179
+ завышение = len(df[df['разница'] > 0])
180
+ занижение = len(df[df['разница'] < 0])
181
+ совпадение = len(df[df['разница'] == 0])
182
+
183
+ print(f"\nНАПРАВЛЕНИЕ РАЗНИЦ:")
184
+ print(f" AI завышает: {завышение} ({завышение / len(df) * 100:.1f}%)")
185
+ print(f" AI занижает: {занижение} ({занижение / len(df) * 100:.1f}%)")
186
+ print(f" Полное совпадение: {совпадение} ({совпадение / len(df) * 100:.1f}%)")
187
+
188
+
189
+ def create_visualizations(df):
190
+ """Создание визуализаций"""
191
+
192
+ print("\n" + "=" * 40)
193
+ print("СОЗДАНИЕ ВИЗУАЛИЗАЦИЙ")
194
+ print("=" * 40)
195
+
196
+ # Проверяем наличие нужных колонок
197
+ if 'pred_score' not in df.columns:
198
+ print("❌ Колонка 'pred_score' не найдена для визуализации")
199
+ return
200
+
201
+ human_score_columns = ['Оценка экзаменатора', 'оценка', 'score', 'human_score']
202
+ human_score_col = None
203
+
204
+ for col in human_score_columns:
205
+ if col in df.columns:
206
+ human_score_col = col
207
+ break
208
+
209
+ if not human_score_col:
210
+ print("❌ Колонка с оценками экзаменатора не найдена для визуализации")
211
+ return
212
+
213
+ # Создаем папку для графиков
214
+ os.makedirs('graphs', exist_ok=True)
215
+
216
+ # 1. Scatter plot сравнения оценок
217
+ plt.figure(figsize=(12, 8))
218
+ scatter = plt.scatter(df[human_score_col], df['pred_score'],
219
+ c=df['abs_разница'], cmap='viridis', alpha=0.7, s=80)
220
+ plt.colorbar(scatter, label='Абсолютная разница')
221
+
222
+ # Определяем диапазон для линии идеального соответствия
223
+ min_val = min(df[human_score_col].min(), df['pred_score'].min())
224
+ max_val = max(df[human_score_col].max(), df['pred_score'].max())
225
+ plt.plot([min_val, max_val], [min_val, max_val], 'r--', alpha=0.5, label='Идеальное соответствие')
226
+
227
+ plt.xlabel(f'Оценка экзаменатора ({human_score_col})', fontsize=12)
228
+ plt.ylabel('AI оценка (pred_score)', fontsize=12)
229
+ plt.title('Сравнение человеческой и AI оценки\n(цвет показывает величину расхождения)', fontsize=14)
230
+ plt.legend()
231
+ plt.grid(True, alpha=0.3)
232
+ plt.savefig('graphs/scatter_comparison.png', dpi=300, bbox_inches='tight')
233
+ plt.close()
234
+
235
+ # 2. Гистограмма разниц
236
+ plt.figure(figsize=(12, 6))
237
+ n, bins, patches = plt.hist(df['разница'], bins=30, alpha=0.7,
238
+ edgecolor='black', color='skyblue')
239
+ plt.xlabel('Разница оценок (AI - Человек)', fontsize=12)
240
+ plt.ylabel('Количество ответов', fontsize=12)
241
+ plt.title('Распределение разниц между AI и человеческими оценками', fontsize=14)
242
+ plt.grid(True, alpha=0.3)
243
+ plt.axvline(x=0, color='red', linestyle='--', alpha=0.8, linewidth=2, label='Нулевая разница')
244
+ plt.axvline(x=df['разница'].mean(), color='orange', linestyle='--',
245
+ alpha=0.8, linewidth=2, label=f'Средняя разница: {df["разница"].mean():.3f}')
246
+ plt.legend()
247
+ plt.savefig('graphs/difference_histogram.png', dpi=300, bbox_inches='tight')
248
+ plt.close()
249
+
250
+ print("✅ Графики сохранены в папку 'graphs/'")
251
+
252
+
253
+ def analyze_extreme_cases(df):
254
+ """Анализ крайних случаев"""
255
+
256
+ print("\n" + "=" * 40)
257
+ print("АНАЛИЗ КРАЙНИХ СЛУЧАЕВ")
258
+ print("=" * 40)
259
+
260
+ if 'abs_разница' not in df.columns:
261
+ print("❌ Не найдены данные о разницах оценок")
262
+ return
263
+
264
+ human_score_columns = ['Оценка экзаменатора', 'оценка', 'score', 'human_score']
265
+ human_score_col = None
266
+
267
+ for col in human_score_columns:
268
+ if col in df.columns:
269
+ human_score_col = col
270
+ break
271
+
272
+ if not human_score_col:
273
+ print("❌ Колонка с оценками экзаменатора не найдена")
274
+ return
275
+
276
+ # Наибольшие расхождения
277
+ большие_расхождения = df.nlargest(8, 'abs_разница')[
278
+ [human_score_col, 'pred_score', 'abs_разница', 'разница']
279
+ ]
280
+
281
+ # Добавляем ID если есть
282
+ id_columns = ['Id экзамена', 'id', 'ID', 'exam_id']
283
+ for col in id_columns:
284
+ if col in df.columns:
285
+ большие_расхождения[col] = df.loc[большие_расхождения.index, col]
286
+ break
287
+
288
+ question_columns = ['№ вопроса', 'question', 'вопрос', 'question_id']
289
+ for col in question_columns:
290
+ if col in df.columns:
291
+ большие_расхождения[col] = df.loc[большие_расхождения.index, col]
292
+ break
293
+
294
+ print("Топ-8 наибольших расхождений:")
295
+ print("-" * 80)
296
+ for idx, row in большие_расхождения.iterrows():
297
+ направление = "ЗАВЫШЕНИЕ" if row['разница'] > 0 else "ЗАНИЖЕНИЕ"
298
+
299
+ # Формируем информацию об ID и вопросе
300
+ id_info = ""
301
+ if 'Id экзамена' in row:
302
+ id_info = f"Экзамен {row['Id экзамена']}"
303
+ elif 'id' in row:
304
+ id_info = f"ID {row['id']}"
305
+
306
+ question_info = ""
307
+ if '№ вопроса' in row:
308
+ question_info = f", Вопрос {row['№ вопроса']}"
309
+ elif 'question' in row:
310
+ question_info = f", Вопрос {row['question']}"
311
+
312
+ print(f"\n📊 {id_info}{question_info} ({направление}):")
313
+ print(f" 👤 Человек: {row[human_score_col]} | 🤖 AI: {row['pred_score']:.3f}")
314
+ print(f" 📏 Разница: {row['abs_разница']:.3f} ({row['разница']:+.3f})")
315
+ print("-" * 60)
316
+
317
+
318
+ def analyze_explanations(df):
319
+ """Анализ объяснений оценок"""
320
+
321
+ print("\n" + "=" * 40)
322
+ print("АНАЛИЗ ОБЪЯСНЕНИЙ ОЦЕНОК")
323
+ print("=" * 40)
324
+
325
+ explanation_columns = ['объяснение_оценки', 'explanation', 'объяснение', 'комментарий']
326
+ explanation_col = None
327
+
328
+ for col in explanation_columns:
329
+ if col in df.columns:
330
+ explanation_col = col
331
+ break
332
+
333
+ if not explanation_col:
334
+ print("❌ Колонка с объяснениями оценок не найдена")
335
+ return
336
+
337
+ # Собираем все объяснения
338
+ все_объяснения = ' '.join(df[explanation_col].dropna().astype(str))
339
+
340
+ # Разбиваем на слова и фил��труем
341
+ слова = [word.strip() for word in все_объяснения.split() if len(word.strip()) > 2]
342
+
343
+ # Анализ частотности
344
+ частотность = Counter(слова)
345
+
346
+ print("Топ-15 наиболее частых характеристик в объяснениях:")
347
+ print("-" * 50)
348
+ for слово, count in частотность.most_common(15):
349
+ print(f" {слово}: {count}")
350
+
351
+
352
+ def save_detailed_analysis(df):
353
+ """Сохранение детального анализа в файл"""
354
+
355
+ print("\n" + "=" * 40)
356
+ print("СОХРАНЕНИЕ РЕЗУЛЬТАТОВ")
357
+ print("=" * 40)
358
+
359
+ if 'abs_разница' not in df.columns:
360
+ print("❌ Нет данных для детального анализа")
361
+ return
362
+
363
+ # Создаем копию с анализом
364
+ df_analysis = df.copy()
365
+
366
+ human_score_columns = ['Оценка экзаменатора', 'оценка', 'score', 'human_score']
367
+ human_score_col = None
368
+
369
+ for col in human_score_columns:
370
+ if col in df.columns:
371
+ human_score_col = col
372
+ break
373
+
374
+ if human_score_col and 'pred_score' in df.columns:
375
+ df_analysis['разница_ai_человек'] = df_analysis['pred_score'] - df_analysis[human_score_col]
376
+ df_analysis['abs_разница'] = abs(df_analysis['разница_ai_человек'])
377
+
378
+ # Добавляем категоризацию расхождений
379
+ условия = [
380
+ df_analysis['abs_разница'] < 0.1,
381
+ df_analysis['abs_разница'] < 0.3,
382
+ df_analysis['abs_разница'] < 0.5,
383
+ df_analysis['abs_разница'] >= 0.5
384
+ ]
385
+ категории = ['Отличное', 'Хорошее', 'Умеренное', 'Низкое']
386
+ df_analysis['качество_согласования'] = np.select(условия, категории, default='Низкое')
387
+
388
+ # Сортируем по наибольшим расхождениям
389
+ df_analysis = df_analysis.sort_values('abs_разница', ascending=False)
390
+
391
+ try:
392
+ # Сохраняем в Excel
393
+ with pd.ExcelWriter('detailed_analysis.xlsx', engine='openpyxl') as writer:
394
+ # Все данные
395
+ df_analysis.to_excel(writer, sheet_name='Все_данные_с_анализом', index=False)
396
+ print("✅ Детальный анализ сохранен в 'detailed_analysis.xlsx'")
397
+
398
+ except Exception as e:
399
+ print(f"⚠️ Не удалось сохранить Excel, сохраняем в CSV: {e}")
400
+ df_analysis.to_csv('detailed_analysis.csv', index=False, encoding='utf-8')
401
+ print("✅ Детальный анализ сохранен в 'detailed_analysis.csv'")
402
+
403
+
404
+ def main():
405
+ """Основная функция"""
406
+
407
+ try:
408
+ # Загрузка данных
409
+ df = load_and_analyze_data()
410
+
411
+ if df is None:
412
+ return
413
+
414
+ # Проверка и корректировка структуры данных
415
+ df = check_and_rename_columns(df)
416
+
417
+ # Выполнение анализа
418
+ basic_statistics(df)
419
+ calculate_correlations(df)
420
+ create_visualizations(df)
421
+ analyze_extreme_cases(df)
422
+ analyze_explanations(df)
423
+ save_detailed_analysis(df)
424
+
425
+ print("\n" + "=" * 60)
426
+ print("✅ АНАЛИЗ ЗАВЕРШЕН!")
427
+ print("=" * 60)
428
+
429
+ except FileNotFoundError:
430
+ print("❌ ОШИБКА: Файл 'small.csv' не найден в текущей директории")
431
+ print(" Убедитесь, что файл находится в той же папке, что и скрипт")
432
+ except Exception as e:
433
+ print(f"❌ ОШИБКА при выполнении анализа: {str(e)}")
434
+ import traceback
435
+ traceback.print_exc()
436
+
437
+
438
+ if __name__ == "__main__":
439
+ main()
analyze_results_pro.py ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import matplotlib.pyplot as plt
3
+ from collections import Counter
4
+ import numpy as np
5
+ import os
6
+ import warnings
7
+
8
+ warnings.filterwarnings('ignore')
9
+
10
+ # Настройка отображения
11
+ plt.style.use('default')
12
+ plt.rcParams['font.family'] = 'DejaVu Sans'
13
+
14
+
15
+ def load_and_analyze_data():
16
+ """Загрузка и базовый анализ данных"""
17
+
18
+ file_path = 'small.csv'
19
+
20
+ try:
21
+ df = pd.read_csv(file_path, encoding='utf-8', delimiter=';')
22
+ print("Файл загружен с разделителем ';' и кодировкой utf-8")
23
+ except:
24
+ try:
25
+ df = pd.read_csv(file_path, encoding='cp1251', delimiter=';')
26
+ print("Файл загружен с разделителем ';' и кодировкой cp1251")
27
+ except:
28
+ try:
29
+ df = pd.read_csv(file_path, encoding='utf-8', delimiter=',')
30
+ print("Файл загружен с разделителем ',' и кодировкой utf-8")
31
+ except:
32
+ try:
33
+ df = pd.read_csv(file_path, encoding='cp1251', delimiter=',')
34
+ print("Файл загружен с разделителем ',' и кодировкой cp1251")
35
+ except Exception as e:
36
+ print(f"Ошибка загрузки файла: {e}")
37
+ return None
38
+
39
+ print("=" * 60)
40
+ print("АНАЛИЗ РЕЗУЛЬТАТОВ АВТОМАТИЧЕСКОЙ ОЦЕНКИ")
41
+ print("=" * 60)
42
+
43
+ print(f"Размер данных: {df.shape[0]} строк, {df.shape[1]} колонок")
44
+ print(f"Колонки: {list(df.columns)}")
45
+
46
+ return df
47
+
48
+
49
+ def basic_statistics(df):
50
+ """Базовая статистика по оценкам"""
51
+
52
+ print("\n" + "=" * 40)
53
+ print("БАЗОВАЯ СТАТИСТИКА")
54
+ print("=" * 40)
55
+
56
+ # Статистика по AI оценкам
57
+ print("AI оценки (pred_score):")
58
+ print(f" Среднее: {df['pred_score'].mean():.3f}")
59
+ print(f" Медиана: {df['pred_score'].median():.3f}")
60
+ print(f" Стандартное отклонение: {df['pred_score'].std():.3f}")
61
+ print(f" Минимум: {df['pred_score'].min():.3f}")
62
+ print(f" Максимум: {df['pred_score'].max():.3f}")
63
+
64
+ # Статистика по человеческим оценкам
65
+ print("\nОценки экзаменатора:")
66
+ print(f" Среднее: {df['Оценка экзаменатора'].mean():.3f}")
67
+ print(f" Медиана: {df['Оценка экзаменатора'].median():.3f}")
68
+ print(f" Стандартное отклонение: {df['Оценка экзаменатора'].std():.3f}")
69
+
70
+ # Распределение оценок
71
+ print("\nРаспределение оценок экзаменатора:")
72
+ распределение = df['Оценка экзаменатора'].value_counts().sort_index()
73
+ for оценка, count in распределение.items():
74
+ print(f" {оценка}: {count} ответов ({count / len(df) * 100:.1f}%)")
75
+
76
+
77
+ def calculate_correlations(df):
78
+ """Расчет корреляций и разниц"""
79
+
80
+ print("\n" + "=" * 40)
81
+ print("КОРРЕЛЯЦИИ И РАСХОЖДЕНИЯ")
82
+ print("=" * 40)
83
+
84
+ # Корреляция
85
+ correlation = df[['Оценка экзаменатора', 'pred_score']].corr().iloc[0, 1]
86
+ print(f"Корреляция между оценками: {correlation:.3f}")
87
+
88
+ # Разницы между оценками
89
+ df['разница'] = df['pred_score'] - df['Оценка экзаменатора']
90
+ df['abs_разница'] = abs(df['разница'])
91
+
92
+ print(f"Средняя абсолютная разница: {df['abs_разница'].mean():.3f}")
93
+ print(f"Максимальная разница: {df['abs_разница'].max():.3f}")
94
+ print(f"Минимальная разница: {df['abs_разница'].min():.3f}")
95
+
96
+ # Анализ согласованности
97
+ print("\nСОГЛАСОВАННОСТЬ ОЦЕНОК:")
98
+ for порог in [0.1, 0.3, 0.5, 1.0]:
99
+ согласованные = df[df['abs_разница'] < порог].shape[0]
100
+ процент = (согласованные / len(df)) * 100
101
+ print(f" Разница < {порог}: {согласованные} ответов ({процент:.1f}%)")
102
+
103
+ # Направление разниц
104
+ завышение = len(df[df['разница'] > 0])
105
+ занижение = len(df[df['разница'] < 0])
106
+ совпадение = len(df[df['разница'] == 0])
107
+
108
+ print(f"\nНАПРАВЛЕНИЕ РАЗНИЦ:")
109
+ print(f" AI завышает: {завышение} ({завышение / len(df) * 100:.1f}%)")
110
+ print(f" AI занижает: {занижение} ({занижение / len(df) * 100:.1f}%)")
111
+ print(f" Полное совпадение: {совпадение} ({совпадение / len(df) * 100:.1f}%)")
112
+
113
+
114
+ def create_visualizations(df):
115
+ """Создание визуализаций"""
116
+
117
+ print("\n" + "=" * 40)
118
+ print("СОЗДАНИЕ ВИЗУАЛИЗАЦИЙ")
119
+ print("=" * 40)
120
+
121
+ # Создаем папку для графиков
122
+ os.makedirs('graphs', exist_ok=True)
123
+
124
+ # 1. Scatter plot сравнения оценок
125
+ plt.figure(figsize=(12, 8))
126
+ scatter = plt.scatter(df['Оценка экзаменатора'], df['pred_score'],
127
+ c=df['abs_разница'], cmap='viridis', alpha=0.7, s=80)
128
+ plt.colorbar(scatter, label='Абсолютная разница')
129
+ plt.plot([0, 2], [0, 2], 'r--', alpha=0.5, label='Идеальное соответствие')
130
+ plt.xlabel('Оценка экзаменатора', fontsize=12)
131
+ plt.ylabel('AI оценка (pred_score)', fontsize=12)
132
+ plt.title('Сравнение человеческой и AI оценки', fontsize=14)
133
+ plt.legend()
134
+ plt.grid(True, alpha=0.3)
135
+ plt.xticks([0, 1, 2])
136
+ plt.yticks(np.arange(0, 2.5, 0.5))
137
+ plt.savefig('graphs/scatter_comparison_pro.png', dpi=300, bbox_inches='tight')
138
+ plt.close()
139
+
140
+ # 2. Гистограмма разниц
141
+ plt.figure(figsize=(12, 6))
142
+ n, bins, patches = plt.hist(df['разница'], bins=30, alpha=0.7,
143
+ edgecolor='black', color='skyblue')
144
+ plt.xlabel('Разница оценок (AI - Человек)', fontsize=12)
145
+ plt.ylabel('Количество ответов', fontsize=12)
146
+ plt.title('Распределение разниц между AI и человеческими оценками', fontsize=14)
147
+ plt.grid(True, alpha=0.3)
148
+ plt.axvline(x=0, color='red', linestyle='--', alpha=0.8, linewidth=2, label='Нулевая разница')
149
+ plt.axvline(x=df['разница'].mean(), color='orange', linestyle='--',
150
+ alpha=0.8, linewidth=2, label=f'Средняя разница: {df["разница"].mean():.3f}')
151
+ plt.legend()
152
+ plt.savefig('graphs/difference_histogram_pro.png', dpi=300, bbox_inches='tight')
153
+ plt.close()
154
+
155
+ # 3. Box plot по типам вопросов
156
+ plt.figure(figsize=(14, 8))
157
+ box_data = [df[df['№ вопроса'] == question]['pred_score'].values
158
+ for question in sorted(df['№ вопроса'].unique())]
159
+
160
+ box_plot = plt.boxplot(box_data, labels=sorted(df['№ вопроса'].unique()),
161
+ patch_artist=True)
162
+
163
+ # Раскрашиваем boxplot
164
+ colors = ['lightblue', 'lightgreen', 'lightcoral', 'lightyellow']
165
+ for patch, color in zip(box_plot['boxes'], colors):
166
+ patch.set_facecolor(color)
167
+
168
+ plt.title('Распределение AI оценок по номерам вопросов', fontsize=14)
169
+ plt.xlabel('Номер вопроса', fontsize=12)
170
+ plt.ylabel('AI оценка (pred_score)', fontsize=12)
171
+ plt.grid(True, alpha=0.3)
172
+ plt.savefig('graphs/question_boxplot_pro.png', dpi=300, bbox_inches='tight')
173
+ plt.close()
174
+
175
+ print("Графики сохранены в папку 'graphs/'")
176
+
177
+
178
+ def analyze_extreme_cases(df):
179
+ """Анализ крайних случаев"""
180
+
181
+ print("\n" + "=" * 40)
182
+ print("АНАЛИЗ КРАЙНИХ СЛУЧАЕВ")
183
+ print("=" * 40)
184
+
185
+ # Наибольшие расхождения
186
+ большие_расхождения = df.nlargest(8, 'abs_разница')[
187
+ ['Id экзамена', '№ вопроса', 'Оценка экзаменатора', 'pred_score',
188
+ 'abs_разница', 'разница']
189
+ ]
190
+
191
+ print("Топ-8 наибольших расхождений:")
192
+ print("-" * 80)
193
+ for idx, row in большие_расхождения.iterrows():
194
+ направление = "ЗАВЫШЕНИЕ" if row['разница'] > 0 else "ЗАНИЖЕНИЕ"
195
+ print(f"\nЭкзамен {row['Id экзамена']}, Вопрос {row['№ вопроса']} ({направление}):")
196
+ print(f" Человек: {row['Оценка экзаменатора']} | AI: {row['pred_score']:.3f}")
197
+ print(f" Разница: {row['abs_разница']:.3f} ({row['разница']:+.3f})")
198
+ print("-" * 60)
199
+
200
+
201
+ def analyze_explanations(df):
202
+ """Анализ объяснений оценок"""
203
+
204
+ print("\n" + "=" * 40)
205
+ print("АНАЛИЗ ОБЪЯСНЕНИЙ ОЦЕНОК")
206
+ print("=" * 40)
207
+
208
+ explanation_columns = ['объяснение_оценки', 'explanation', 'объяснение']
209
+ explanation_col = None
210
+
211
+ for col in explanation_columns:
212
+ if col in df.columns:
213
+ explanation_col = col
214
+ break
215
+
216
+ if not explanation_col:
217
+ print("Колонка с объяснениями оценок не найдена")
218
+ return
219
+
220
+ # Собираем все объяснения
221
+ все_объяснения = ' '.join(df[explanation_col].dropna().astype(str))
222
+
223
+ # Разбиваем на слова и фильтруем
224
+ слова = [word.strip() for word in все_объяснения.split() if len(word.strip()) > 2]
225
+
226
+ # Анализ частотности
227
+ частотность = Counter(слова)
228
+
229
+ print("Топ-15 наиболее частых характеристик в объяснениях:")
230
+ for слово, count in частотность.most_common(15):
231
+ print(f" {слово}: {count}")
232
+
233
+ # Анализ по ключевым категориям
234
+ категории = {
235
+ 'Развернутый': 'Развернутый ответ',
236
+ 'смысловое': 'Смысловое соответствие',
237
+ 'соответствие': 'Смысловое соответствие',
238
+ 'Хорошая': 'Хорошая структура',
239
+ 'структура': 'Хорошая структура',
240
+ 'лексика': 'Разнообразная лексика',
241
+ 'Высокий': 'Высокий балл',
242
+ 'балл': 'Высокий балл',
243
+ 'описание': 'Подробное описание',
244
+ 'личный': 'Личный опыт',
245
+ 'покрытие': 'Покрытие вопросов'
246
+ }
247
+
248
+ print(f"\nСТАТИСТИКА ПО КАТЕГОРИЯМ:")
249
+ for ключ, описание in категориями.items():
250
+ count = sum(1 for слово in слова if ключ in слово)
251
+ if count > 0:
252
+ print(f" {описание}: {count}")
253
+
254
+
255
+ def performance_by_question_type(df):
256
+ """Анализ производительности по типам вопросов"""
257
+
258
+ print("\n" + "=" * 40)
259
+ print("АНАЛИЗ ПО ТИПАМ ВОПРОСОВ")
260
+ print("=" * 40)
261
+
262
+ вопросы_статистика = df.groupby('№ вопроса').agg({
263
+ 'Оценка экзаменатора': ['mean', 'std', 'count'],
264
+ 'pred_score': ['mean', 'std'],
265
+ 'abs_разница': 'mean',
266
+ 'разница': 'mean'
267
+ }).round(3)
268
+
269
+ # Переименовываем колонки для удобства
270
+ вопросы_статистика.columns = ['чел_среднее', 'чел_стд', 'количество',
271
+ 'ai_среднее', 'ai_стд', 'ср_абс_разница', 'ср_разница']
272
+
273
+ вопросы_статистика['расхождение'] = abs(вопросы_статистика['ср_разница'])
274
+
275
+ print("СТАТИСТИКА ПО ВОПРОСАМ:")
276
+ print("-" * 80)
277
+ print(f"{'Вопрос':<6} {'Чел.ср':<8} {'AI ср':<8} {'Разн.':<8} {'Кол-во':<8} {'Описание'}")
278
+ print("-" * 80)
279
+
280
+ for вопрос, row in вопросы_статистика.iterrows():
281
+ разница_знак = "+" if row['ср_разница'] > 0 else ""
282
+ print(f"{вопрос:<6} {row['чел_среднее']:<8} {row['ai_среднее']:<8} "
283
+ f"{разница_знак}{row['ср_разница']:<7} {int(row['количество']):<8} ", end="")
284
+
285
+ if row['расхождение'] > 0.3:
286
+ print("ВНИМАНИЕ: большое расхождение")
287
+ elif row['расхождение'] > 0.1:
288
+ print("Умеренное расхождение")
289
+ else:
290
+ print("Хорошее соответствие")
291
+
292
+
293
+ def save_detailed_analysis(df):
294
+ """Сохранение детального анализа в файл"""
295
+
296
+ print("\n" + "=" * 40)
297
+ print("СОХРАНЕНИЕ РЕЗУЛЬТАТОВ")
298
+ print("=" * 40)
299
+
300
+ # Создаем копию с анализом
301
+ df_analysis = df.copy()
302
+ df_analysis['разница_ai_человек'] = df_analysis['pred_score'] - df_analysis['Оценка экзаменатора']
303
+ df_analysis['abs_разница'] = abs(df_analysis['разница_ai_человек'])
304
+
305
+ # Добавляем категоризацию расхождений
306
+ условия = [
307
+ df_analysis['abs_разница'] < 0.1,
308
+ df_analysis['abs_разница'] < 0.3,
309
+ df_analysis['abs_разница'] < 0.5,
310
+ df_analysis['abs_разница'] >= 0.5
311
+ ]
312
+ категории = ['Отличное', 'Хорошее', 'Умеренное', 'Низкое']
313
+ df_analysis['качество_согласования'] = np.select(условия, категории, default='Низкое')
314
+
315
+ # Сортируем по наибольшим расхождениям
316
+ df_analysis = df_analysis.sort_values('abs_разница', ascending=False)
317
+
318
+ try:
319
+ # Сохраняем в Excel
320
+ with pd.ExcelWriter('detailed_analysis_pro.xlsx', engine='openpyxl') as writer:
321
+ # Все данные
322
+ df_analysis.to_excel(writer, sheet_name='Все_данные_с_анализом', index=False)
323
+
324
+ # Сводная та��лица по вопросам
325
+ сводная = df_analysis.groupby('№ вопроса').agg({
326
+ 'Оценка экзаменатора': ['mean', 'std', 'min', 'max'],
327
+ 'pred_score': ['mean', 'std', 'min', 'max'],
328
+ 'abs_разница': ['mean', 'max'],
329
+ 'разница_ai_человек': 'mean',
330
+ 'Id экзамена': 'count'
331
+ }).round(3)
332
+ сводная.to_excel(writer, sheet_name='Сводка_по_вопросам')
333
+
334
+ # Наибольшие расхождения
335
+ большие_расхождения = df_analysis.nlargest(20, 'abs_разница')[
336
+ ['Id экзамена', '№ вопроса', 'Оценка экзаменатора',
337
+ 'pred_score', 'разница_ai_человек', 'abs_разница']
338
+ ]
339
+ большие_расхождения.to_excel(writer, sheet_name='Наибольшие_расхождения', index=False)
340
+
341
+ # Статистика по качеству согласования
342
+ качество_стат = df_analysis['качество_согласования'].value_counts()
343
+ качество_стат.to_excel(writer, sheet_name='Качество_согласования')
344
+
345
+ print("Детальный анализ сохранен в 'detailed_analysis_pro.xlsx'")
346
+
347
+ except Exception as e:
348
+ print(f"Не удалось сохранить Excel, сохраняем в CSV: {e}")
349
+ df_analysis.to_csv('detailed_analysis_pro.csv', index=False, encoding='utf-8')
350
+ print("Детальный анализ сохранен в 'detailed_analysis_pro.csv'")
351
+
352
+
353
+ def generate_summary_report(df):
354
+ """Генерация итогового отчета"""
355
+
356
+ print("\n" + "=" * 60)
357
+ print("ИТОГОВЫЙ ОТЧЕТ")
358
+ print("=" * 60)
359
+
360
+ корреляция = df[['Оценка экзаменатора', 'pred_score']].corr().iloc[0, 1]
361
+ ср_разница = df['abs_разница'].mean()
362
+
363
+ print(f"\nОБЩАЯ СТАТИСТИКА:")
364
+ print(f" Всего ответов: {len(df)}")
365
+ print(f" Корреляция AI-Человек: {корреляция:.3f}")
366
+ print(f" Средняя абсолютная разница: {ср_разница:.3f}")
367
+
368
+ # Оценка качества
369
+ if корреляция > 0.8 and ср_разница < 0.2:
370
+ оценка = "ОТЛИЧНОЕ"
371
+ elif корреляция > 0.6 and ср_разница < 0.3:
372
+ оценка = "ХОРОШЕЕ"
373
+ elif корреляция > 0.4 and ср_разница < 0.4:
374
+ оценка = "УДОВЛЕТВОРИТЕЛЬНОЕ"
375
+ else:
376
+ оценка = "НИЗКОЕ"
377
+
378
+ print(f"\nОЦЕНКА КАЧЕСТВА СИСТЕМЫ: {оценка}")
379
+
380
+ # Рекомендации
381
+ print(f"\nРЕКОМЕНДАЦИИ:")
382
+ if ср_разница > 0.3:
383
+ print(" Проанализировать систематические ошибки в оценках")
384
+ if корреляция < 0.6:
385
+ print(" Улучшить согласованность с человеческими оценками")
386
+
387
+ # Лучшие и худшие вопросы
388
+ вопросы_стат = df.groupby('№ вопроса')['abs_разница'].mean().sort_values()
389
+ лучший_вопрос = вопросы_стат.index[0]
390
+ худший_вопрос = вопросы_стат.index[-1]
391
+
392
+ print(f"\nЛУЧШИЙ ВОПРОС ПО СОГЛАСОВАННОСТИ: №{лучший_вопрос} (разница: {вопросы_стат.iloc[0]:.3f})")
393
+ print(f"ХУДШИЙ ВОПРОС ПО СОГЛАСОВАННОСТИ: №{худший_вопрос} (разница: {вопросы_стат.iloc[-1]:.3f})")
394
+
395
+
396
+ def main():
397
+ """Основная функция"""
398
+
399
+ try:
400
+ # Загрузка данных
401
+ df = load_and_analyze_data()
402
+
403
+ if df is None:
404
+ return
405
+
406
+ # Проверка необходимых колонок
407
+ required_columns = ['Оценка экзаменатора', 'pred_score', '№ вопроса']
408
+ missing_columns = [col for col in required_columns if col not in df.columns]
409
+
410
+ if missing_columns:
411
+ print(f"ОШИБКА: Отсутствуют колонки: {missing_columns}")
412
+ return
413
+
414
+ # Выполнение анализа
415
+ basic_statistics(df)
416
+ calculate_correlations(df)
417
+ create_visualizations(df)
418
+ analyze_extreme_cases(df)
419
+ analyze_explanations(df)
420
+ performance_by_question_type(df)
421
+ save_detailed_analysis(df)
422
+ generate_summary_report(df)
423
+
424
+ print("\n" + "=" * 60)
425
+ print("АНАЛИЗ ЗАВЕРШЕН!")
426
+ print("=" * 60)
427
+ print("\nСОЗДАННЫЕ ФАЙЛЫ:")
428
+ print(" graphs/scatter_comparison_pro.png - сравнение оц��нок")
429
+ print(" graphs/difference_histogram_pro.png - распределение разниц")
430
+ print(" graphs/question_boxplot_pro.png - оценки по вопросам")
431
+ print(" detailed_analysis_pro.xlsx - детальный отчет")
432
+
433
+ except FileNotFoundError:
434
+ print("ОШИБКА: Файл 'small.csv' не найден в текущей директории")
435
+ except Exception as e:
436
+ print(f"ОШИБКА при выполнении анализа: {str(e)}")
437
+
438
+
439
+ if __name__ == "__main__":
440
+ main()
analyze_test.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import matplotlib.pyplot as plt
3
+ from collections import Counter
4
+ import numpy as np
5
+ import os
6
+ import warnings
7
+
8
+ warnings.filterwarnings('ignore')
9
+
10
+ # Настройка отображения
11
+ plt.style.use('default')
12
+ plt.rcParams['font.family'] = 'DejaVu Sans'
13
+
14
+
15
+ def load_and_analyze_data():
16
+ """Загрузка тестовых данных"""
17
+
18
+ file_path = 'test_data.csv'
19
+
20
+ try:
21
+ df = pd.read_csv(file_path, encoding='utf-8', delimiter=';')
22
+ print("✅ Тестовый файл загружен успешно")
23
+ except Exception as e:
24
+ print(f"❌ Ошибка загрузки: {e}")
25
+ print("Убедитесь, что файл test_data.csv находится в той же папке")
26
+ return None
27
+
28
+ print("=" * 60)
29
+ print("ТЕСТОВЫЙ АНАЛИЗ AI-ОЦЕНОК")
30
+ print("=" * 60)
31
+
32
+ print(f"Размер данных: {df.shape[0]} строк, {df.shape[1]} колонок")
33
+ print(f"Колонки: {list(df.columns)}")
34
+ print(f"\nПервые 3 строки:")
35
+ print(df.head(3))
36
+
37
+ return df
38
+
39
+
40
+ def basic_statistics(df):
41
+ """Базовая статистика"""
42
+
43
+ print("\n" + "=" * 40)
44
+ print("БАЗОВАЯ СТАТИСТИКА")
45
+ print("=" * 40)
46
+
47
+ print("AI оценки (pred_score):")
48
+ print(f" Среднее: {df['pred_score'].mean():.3f}")
49
+ print(f" Медиана: {df['pred_score'].median():.3f}")
50
+ print(f" Стандартное отклонение: {df['pred_score'].std():.3f}")
51
+ print(f" Минимум: {df['pred_score'].min():.3f}")
52
+ print(f" Максимум: {df['pred_score'].max():.3f}")
53
+
54
+ print("\nОценки экзаменатора:")
55
+ print(f" Среднее: {df['Оценка экзаменатора'].mean():.3f}")
56
+ print(f" Медиана: {df['Оценка экзаменатора'].median():.3f}")
57
+ print(f" Стандартное отклонение: {df['Оценка экзаменатора'].std():.3f}")
58
+
59
+ print("\nРаспределение оценок экзаменатора:")
60
+ распределение = df['Оценка экзаменатора'].value_counts().sort_index()
61
+ for оценка, count in распределение.items():
62
+ print(f" {оценка}: {count} ответов ({count / len(df) * 100:.1f}%)")
63
+
64
+
65
+ def calculate_correlations(df):
66
+ """Расчет корреляций"""
67
+
68
+ print("\n" + "=" * 40)
69
+ print("КОРРЕЛЯЦИИ И РАСХОЖДЕНИЯ")
70
+ print("=" * 40)
71
+
72
+ correlation = df[['Оценка экзаменатора', 'pred_score']].corr().iloc[0, 1]
73
+ print(f"Корреляция между оценками: {correlation:.3f}")
74
+
75
+ df['разница'] = df['pred_score'] - df['Оценка экзаменатора']
76
+ df['abs_разница'] = abs(df['разница'])
77
+
78
+ print(f"Средняя абсолютная разница: {df['abs_разница'].mean():.3f}")
79
+ print(f"Максимальная разница: {df['abs_разница'].max():.3f}")
80
+ print(f"Минимальная разница: {df['abs_разница'].min():.3f}")
81
+
82
+ # Анализ согласованности
83
+ print("\nСОГЛАСОВАННОСТЬ ОЦЕНОК:")
84
+ for порог in [0.1, 0.3, 0.5, 1.0]:
85
+ согласованные = df[df['abs_разница'] < порог].shape[0]
86
+ процент = (согласованные / len(df)) * 100
87
+ print(f" Разница < {порог}: {согласованные} ответов ({процент:.1f}%)")
88
+
89
+
90
+ def create_visualizations(df):
91
+ """Создание графиков"""
92
+
93
+ print("\n" + "=" * 40)
94
+ print("СОЗДАНИЕ ГРАФИКОВ")
95
+ print("=" * 40)
96
+
97
+ os.makedirs('graphs', exist_ok=True)
98
+
99
+ # 1. Scatter plot
100
+ plt.figure(figsize=(10, 6))
101
+ scatter = plt.scatter(df['Оценка экзаменатора'], df['pred_score'],
102
+ c=df['abs_разница'], cmap='viridis', alpha=0.7, s=60)
103
+ plt.colorbar(scatter, label='Абсолютная разница')
104
+ plt.plot([0, 2], [0, 2], 'r--', alpha=0.5, label='Идеальное соответствие')
105
+ plt.xlabel('Оценка экзаменатора')
106
+ plt.ylabel('AI оценка (pred_score)')
107
+ plt.title('Сравнение человеческой и AI оценки')
108
+ plt.legend()
109
+ plt.grid(True, alpha=0.3)
110
+ plt.savefig('graphs/test_scatter.png', dpi=300, bbox_inches='tight')
111
+ plt.close()
112
+
113
+ # 2. Гистограмма разниц
114
+ plt.figure(figsize=(10, 6))
115
+ plt.hist(df['разница'], bins=15, alpha=0.7, edgecolor='black', color='skyblue')
116
+ plt.xlabel('Разница (AI - Человек)')
117
+ plt.ylabel('Количество ответов')
118
+ plt.title('Распределение разниц оценок')
119
+ plt.grid(True, alpha=0.3)
120
+ plt.axvline(x=0, color='red', linestyle='--', alpha=0.8, label='Нулевая разница')
121
+ plt.legend()
122
+ plt.savefig('graphs/test_histogram.png', dpi=300, bbox_inches='tight')
123
+ plt.close()
124
+
125
+ print("✅ Графики сохранены в папку 'graphs/'")
126
+
127
+
128
+ def analyze_explanations(df):
129
+ """Анализ объяснений"""
130
+
131
+ print("\n" + "=" * 40)
132
+ print("АНАЛИЗ ОБЪЯСНЕНИЙ")
133
+ print("=" * 40)
134
+
135
+ все_объяснения = ' '.join(df['объяснение_оценки'].dropna().astype(str))
136
+ слова = [word.strip() for word in все_объяснения.split() if len(word.strip()) > 2]
137
+ частотность = Counter(слова)
138
+
139
+ print("Топ-10 характеристик в объяснениях:")
140
+ for слово, count in частотность.most_common(10):
141
+ print(f" {слово}: {count}")
142
+
143
+
144
+ def main():
145
+ """Основная функция"""
146
+
147
+ df = load_and_analyze_data()
148
+ if df is None:
149
+ return
150
+
151
+ basic_statistics(df)
152
+ calculate_correlations(df)
153
+ create_visualizations(df)
154
+ analyze_explanations(df)
155
+
156
+ print("\n" + "=" * 60)
157
+ print("✅ ТЕСТОВЫЙ АНАЛИЗ ЗАВЕРШЕН!")
158
+ print("=" * 60)
159
+ print("📊 Созданные файлы:")
160
+ print(" • graphs/test_scatter.png")
161
+ print(" • graphs/test_histogram.png")
162
+
163
+
164
+ if __name__ == "__main__":
165
+ main()
app.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ import io
3
+ import os
4
+ from pathlib import Path
5
+
6
+ import pandas as pd
7
+ import streamlit as st
8
+
9
+ # быстрый режим по умолчанию
10
+ os.environ.setdefault("FAST_MODE", "1")
11
+
12
+ # импорт основного пайплайна
13
+ from src.predict import pipeline_infer
14
+
15
+ # --- Конфигурация страницы ---
16
+ st.set_page_config(page_title="Русский как иностранный – автооценка", layout="centered")
17
+
18
+ st.title("Автооценка устных ответов (RFL • CatBoost + ruSBERT)")
19
+ st.caption("Загрузите CSV входного формата и получите файл с колонками pred_score и pred_score_rounded.")
20
+
21
+ # --- Информация о формате ---
22
+ with st.expander("Формат входного CSV", expanded=False):
23
+ st.markdown(
24
+ """
25
+ Обязательные столбцы:
26
+ - **№ вопроса** (1..4)
27
+ - **Текст вопроса**
28
+ - **Транскрибация ответа**
29
+ - *(опционально)* **Оценка экзаменатора** — если есть, её не трогаем, добавим предсказания рядом.
30
+
31
+ Разделитель — `;`, кодировка — UTF-8 (автоопределяется).
32
+ """
33
+ )
34
+
35
+ # --- Пример шаблона CSV ---
36
+ with st.expander("📄 Скачать шаблон CSV"):
37
+ demo = pd.DataFrame({
38
+ "№ вопроса": [1, 2],
39
+ "Текст вопроса": ["<p>Добро пожаловать...</p>", "<p>Опишите свой день...</p>"],
40
+ "Транскрибация ответа": ["Здравствуйте! Я приехал...", "Мой день начинается с..."],
41
+ "Оценка экзаменатора": [None, None],
42
+ })
43
+ st.dataframe(demo)
44
+ buf_tmpl = io.BytesIO()
45
+ demo.to_csv(buf_tmpl, index=False, sep=";", encoding="utf-8-sig")
46
+ st.download_button("⬇ Скачать шаблон CSV", buf_tmpl.getvalue(), "template.csv", "text/csv")
47
+
48
+ # --- Функция загрузки и нормализации ---
49
+ required = ["№ вопроса", "Текст вопроса", "Транскрибация ответа"]
50
+ aliases = {
51
+ "номер вопроса": "№ вопроса",
52
+ "вопрос": "Текст вопроса",
53
+ "текст задания": "Текст вопроса",
54
+ "транскрибация": "Транскрибация ответа",
55
+ "транскрипт": "Транскрибация ответа",
56
+ "ответ": "Транскрибация ответа",
57
+ }
58
+
59
+
60
+ def load_and_normalize_csv(raw_bytes: bytes) -> pd.DataFrame:
61
+ import io
62
+ for sep in [";", ",", "\t"]:
63
+ try:
64
+ df = pd.read_csv(io.BytesIO(raw_bytes), sep=sep, engine="python")
65
+
66
+ # убрать возможные артефакты Git-конфликтов
67
+ if not df.empty and str(df.columns[0]).startswith("<<<"):
68
+ text = raw_bytes.decode("utf-8", errors="ignore")
69
+ lines = [ln for ln in text.splitlines() if not ln.startswith(("<<<", "===", ">>>"))]
70
+ df = pd.read_csv(io.StringIO("\n".join(lines)), sep=sep, engine="python")
71
+
72
+ # нормализация имён колонок
73
+ rename_map = {}
74
+ for c in list(df.columns):
75
+ key = str(c).strip().lower()
76
+ if key in aliases:
77
+ rename_map[c] = aliases[key]
78
+ if rename_map:
79
+ df = df.rename(columns=rename_map)
80
+
81
+ return df
82
+ except Exception:
83
+ continue
84
+ raise ValueError("Не удалось прочитать CSV. Проверьте разделитель (';' или ',') и кодировку UTF-8.")
85
+
86
+
87
+ # --- Основной интерфейс ---
88
+ uploaded = st.file_uploader("Загрузите CSV", type=["csv"])
89
+ slow = st.toggle("Медленный режим", value=False, help="Выключите для быстрой оценки (точность ≈ прежняя).")
90
+ run = st.button("Посчитать")
91
+
92
+ if uploaded and run:
93
+ try:
94
+ raw = uploaded.read()
95
+ df_in = load_and_normalize_csv(raw)
96
+
97
+ # проверка обязательных колонок
98
+ missing = [c for c in required if c not in df_in.columns]
99
+ if missing:
100
+ st.error(f"❌ В файле нет обязательных колонок: {missing}. Проверь заголовки и разделитель ';'.")
101
+ st.dataframe(df_in.head())
102
+ st.stop()
103
+
104
+ # сохраняем временно
105
+ tmp_in = Path("data/api_tmp/tmp_input.csv")
106
+ tmp_in.parent.mkdir(parents=True, exist_ok=True)
107
+ df_in.to_csv(tmp_in, index=False, sep=";", encoding="utf-8-sig")
108
+
109
+ # режим скорости
110
+ os.environ["FAST_MODE"] = "0" if slow else "1"
111
+
112
+ tmp_out = Path("data/api_tmp/tmp_output.csv")
113
+ with st.spinner("Считаем..."):
114
+ pipeline_infer(tmp_in, tmp_out)
115
+
116
+ df_out = pd.read_csv(tmp_out, sep=";", encoding="utf-8-sig")
117
+ st.success("✅ Готово!")
118
+ st.dataframe(df_out.head(20), use_container_width=True)
119
+
120
+ buf = io.BytesIO()
121
+ df_out.to_csv(buf, index=False, sep=";", encoding="utf-8-sig")
122
+ st.download_button("⬇ Скачать результат (CSV)", data=buf.getvalue(), file_name="predicted.csv", mime="text/csv")
123
+ except Exception as e:
124
+ st.exception(e)
125
+
126
+ # --- Подвал ---
127
+ st.markdown("---")
128
+ st.caption("Модель: CatBoost Q1..Q4 + ruSBERT. Быстрый режим = FAST_MODE=1.")
app/__init__.py ADDED
File without changes
app/main.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException
2
+ from fastapi.responses import HTMLResponse, FileResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ import csv
5
+ import os
6
+ import tempfile
7
+ from typing import List, Dict
8
+ import re
9
+
10
+ app = FastAPI(title="Russian Exam Auto Grader")
11
+
12
+ # Монтируем статические файлы для веб-интерфейса
13
+ app.mount("/static", StaticFiles(directory="static"), name="static")
14
+
15
+
16
+ class ExamGrader:
17
+ def __init__(self):
18
+ self.setup_criteria()
19
+
20
+ def setup_criteria(self):
21
+ self.criteria = {
22
+ 1: self._grade_question1, # 0-1 балл
23
+ 2: self._grade_question2, # 0-2 балла
24
+ 3: self._grade_question3, # 0-1 балл
25
+ 4: self._grade_question4 # 0-2 балла
26
+ }
27
+
28
+ def grade_answer(self, question_num: int, transcription: str) -> int:
29
+ """Основной метод оценки"""
30
+ if question_num not in self.criteria:
31
+ return 0
32
+ return self.criteria[question_num](transcription)
33
+
34
+ def _grade_question1(self, text: str) -> int:
35
+ """Оценка вопроса 1 - начало диалога"""
36
+ text_lower = text.lower().strip()
37
+
38
+ # Проверяем ключевые элементы диалога
39
+ has_greeting = any(word in text_lower for word in ['здравствуйте', 'добрый день', 'привет', 'здравствуй'])
40
+ has_request = any(word in text_lower for word in ['помогите', 'подскажите', 'нужно', 'хочу', 'могу'])
41
+ has_question = any(word in text_lower for word in ['как', 'что', 'где', 'когда', 'можно', 'сколько'])
42
+
43
+ # Должен быть развернутый ответ
44
+ words_count = len(text_lower.split())
45
+
46
+ score = 0
47
+ if has_greeting:
48
+ score += 0.3
49
+ if has_request:
50
+ score += 0.4
51
+ if has_question:
52
+ score += 0.3
53
+ if words_count > 15:
54
+ score += 0.2
55
+
56
+ return 1 if score >= 0.7 else 0
57
+
58
+ def _grade_question2(self, text: str) -> int:
59
+ """Оценка вопроса 2 - ответы на вопросы"""
60
+ sentences = self._split_sentences(text)
61
+
62
+ if len(sentences) < 2:
63
+ return 0
64
+
65
+ # Оцениваем полноту ответов
66
+ complete_sentences = 0
67
+ for sentence in sentences:
68
+ words = sentence.split()
69
+ if len(words) >= 4: # Более-менее полное предложение
70
+ complete_sentences += 1
71
+
72
+ completeness_ratio = complete_sentences / len(sentences)
73
+
74
+ if completeness_ratio >= 0.8:
75
+ return 2
76
+ elif completeness_ratio >= 0.5:
77
+ return 1
78
+ else:
79
+ return 0
80
+
81
+ def _grade_question3(self, text: str) -> int:
82
+ """Оценка вопроса 3 - диалог-запрос"""
83
+ text_lower = text.lower().strip()
84
+
85
+ has_greeting = any(word in text_lower for word in ['здравствуйте', 'добрый день'])
86
+ has_request = any(word in text_lower for word in ['хочу', 'нужно', 'узнать', 'скажите', 'интересует'])
87
+ has_thanks = any(word in text_lower for word in ['спасибо', 'благодарю'])
88
+
89
+ score = 0
90
+ if has_greeting:
91
+ score += 0.3
92
+ if has_request:
93
+ score += 0.4
94
+ if has_thanks:
95
+ score += 0.3
96
+
97
+ return 1 if score >= 0.7 else 0
98
+
99
+ def _grade_question4(self, text: str) -> int:
100
+ """Оценка вопроса 4 - описание картинки"""
101
+ sentences = self._split_sentences(text)
102
+
103
+ if len(sentences) < 3:
104
+ return 0
105
+
106
+ # Ищем описательные элементы
107
+ descriptive_words = ['вижу', 'изображен', 'находится', 'стоит', 'сидит',
108
+ 'одежда', 'цвет', 'время года', 'место', 'деревья', 'дом']
109
+
110
+ descriptive_count = 0
111
+ for sentence in sentences:
112
+ if any(word in sentence.lower() for word in descriptive_words):
113
+ descriptive_count += 1
114
+
115
+ descriptive_ratio = descriptive_count / len(sentences)
116
+
117
+ if descriptive_ratio >= 0.6:
118
+ return 2
119
+ elif descriptive_ratio >= 0.3:
120
+ return 1
121
+ else:
122
+ return 0
123
+
124
+ def _split_sentences(self, text: str) -> List[str]:
125
+ """Разделяет текст на предложения"""
126
+ sentences = re.split(r'[.!?]+', text)
127
+ return [s.strip() for s in sentences if len(s.strip()) > 0]
128
+
129
+
130
+ grader = ExamGrader()
131
+
132
+
133
+ @app.post("/evaluate/")
134
+ async def evaluate_file(file: UploadFile = File(...)):
135
+ try:
136
+ # Читаем CSV файл
137
+ content = await file.read()
138
+ decoded_content = content.decode('utf-8').splitlines()
139
+
140
+ # Парсим CSV
141
+ reader = csv.DictReader(decoded_content, delimiter=';')
142
+ rows = list(reader)
143
+
144
+ # Обрабатываем каждую строку
145
+ results = []
146
+ for row in rows:
147
+ try:
148
+ question_num = int(row['№ вопроса'])
149
+ transcription = row['Транскрибация ответа']
150
+
151
+ score = grader.grade_answer(question_num, transcription)
152
+
153
+ result_row = row.copy()
154
+ result_row['Оценка экзаменатора'] = score
155
+ results.append(result_row)
156
+ except (KeyError, ValueError) as e:
157
+ # Если есть ошибки в данных, ставим 0
158
+ result_row = row.copy()
159
+ result_row['Оценка экзаменатора'] = 0
160
+ results.append(result_row)
161
+
162
+ # Сохраняем результаты
163
+ output_filename = "graded_" + file.filename
164
+ with open(output_filename, 'w', newline='', encoding='utf-8') as f:
165
+ if results:
166
+ fieldnames = results[0].keys()
167
+ writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=';')
168
+ writer.writeheader()
169
+ writer.writerows(results)
170
+
171
+ return FileResponse(
172
+ output_filename,
173
+ media_type='text/csv',
174
+ filename=output_filename
175
+ )
176
+
177
+ except Exception as e:
178
+ raise HTTPException(status_code=500, detail=f"Ошибка обработки: {str(e)}")
179
+
180
+
181
+ @app.get("/", response_class=HTMLResponse)
182
+ async def main_page():
183
+ return """
184
+ <html>
185
+ <head>
186
+ <title>Russian Exam Auto Grader</title>
187
+ <style>
188
+ body { font-family: Arial, sans-serif; margin: 40px; }
189
+ .container { max-width: 600px; margin: 0 auto; }
190
+ .upload-form { border: 2px dashed #ccc; padding: 40px; text-align: center; }
191
+ .btn { background: #007bff; color: white; padding: 10px 20px; border: none; cursor: pointer; }
192
+ .btn:hover { background: #0056b3; }
193
+ </style>
194
+ </head>
195
+ <body>
196
+ <div class="container">
197
+ <h1>Russian Exam Auto Grader</h1>
198
+ <p>Загрузите CSV файл с ответами для автоматической оценки</p>
199
+
200
+ <form class="upload-form" action="/evaluate/" method="post" enctype="multipart/form-data">
201
+ <input type="file" name="file" accept=".csv" required>
202
+ <br><br>
203
+ <button type="submit" class="btn">Оценить ответы</button>
204
+ </form>
205
+
206
+ <div style="margin-top: 30px;">
207
+ <h3>Требования к файлу:</h3>
208
+ <ul>
209
+ <li>Формат: CSV с разделителем ";"</li>
210
+ <li>Колонки: № вопроса, Транскрибация ответа</li>
211
+ <li>Кодировка: UTF-8</li>
212
+ </ul>
213
+ </div>
214
+ </div>
215
+ </body>
216
+ </html>
217
+ """
218
+
219
+
220
+ if __name__ == "__main__":
221
+ import uvicorn
222
+
223
+ uvicorn.run(app, host="0.0.0.0", port=8000)
app/simple_ui.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request
2
+ from fastapi.responses import HTMLResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.templating import Jinja2Templates
5
+
6
+ app = FastAPI()
7
+
8
+ # Простой HTML интерфейс
9
+ HTML_FORM = """
10
+ <!DOCTYPE html>
11
+ <html>
12
+ <head>
13
+ <title>Система оценки ответов</title>
14
+ <style>
15
+ body { font-family: Arial, sans-serif; margin: 40px; }
16
+ .container { max-width: 600px; margin: 0 auto; }
17
+ .upload-form { border: 2px dashed #ccc; padding: 20px; text-align: center; }
18
+ .btn { background: #007cba; color: white; padding: 10px 20px; border: none; cursor: pointer; }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <div class="container">
23
+ <h1>📝 Система автоматической оценки ответов</h1>
24
+ <p>Загрузите CSV файл с ответами студентов для оценки</p>
25
+
26
+ <form class="upload-form" action="/predict_csv" method="post" enctype="multipart/form-data">
27
+ <input type="file" name="file" accept=".csv" required>
28
+ <br><br>
29
+ <button type="submit" class="btn">Оценить ответы</button>
30
+ </form>
31
+
32
+ <div style="margin-top: 30px;">
33
+ <h3>API Endpoints:</h3>
34
+ <ul>
35
+ <li><a href="/health">Health Check</a></li>
36
+ <li><a href="/docs">API Documentation</a></li>
37
+ </ul>
38
+ </div>
39
+ </div>
40
+ </body>
41
+ </html>
42
+ """
43
+
44
+
45
+ @app.get("/", response_class=HTMLResponse)
46
+ async def main_page(request: Request):
47
+ return HTML_FORM
48
+
49
+
50
+ @app.get("/ui")
51
+ async def ui_page():
52
+ return HTMLResponse(HTML_FORM)
app/ui.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, UploadFile, File
2
+ from fastapi.responses import HTMLResponse, StreamingResponse
3
+ from fastapi.templating import Jinja2Templates
4
+ import requests
5
+
6
+ app = FastAPI(title="Scoring UI")
7
+ templates = Jinja2Templates(directory="templates")
8
+
9
+ # 🔧 Локальный адрес FastAPI-сервера
10
+ API_URL = "http://localhost:8000/predict_csv"
11
+
12
+ @app.get("/", response_class=HTMLResponse)
13
+ async def home(request: Request):
14
+ return templates.TemplateResponse("index.html", {"request": request})
15
+
16
+ @app.get("/health")
17
+ async def health():
18
+ return {"status": "ok", "service": "scoring-ui"}
19
+
20
+ @app.post("/predict")
21
+ async def predict_csv(file: UploadFile = File(...)):
22
+ files = {"file": (file.filename, await file.read(), file.content_type)}
23
+ try:
24
+ resp = requests.post(API_URL, files=files, timeout=1800)
25
+ resp.raise_for_status()
26
+ return StreamingResponse(
27
+ iter([resp.content]),
28
+ media_type="text/csv",
29
+ headers={"Content-Disposition": f'attachment; filename="predicted_{file.filename}"'}
30
+ )
31
+ except Exception as e:
32
+ return {"error": str(e)}
assessment_engine.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # assessment_engine.py
2
+ import pandas as pd
3
+
4
+ # Импортируй твои шаги — подставь правильные модули:
5
+ # from src.data_cleaning import prepare_dataframe
6
+ # from src.features import build_baseline_features
7
+ # from src.features_q4 import add_q4_features
8
+ # from src.semantic_features import add_semantic_features
9
+ # from src.explanations import build_explanations
10
+ # from your_models_loader import load_models, predict_batch
11
+
12
+ # Заглушка: здесь покажу форму, ты подставишь свои вызовы
13
+ def run_inference_df(df: pd.DataFrame, with_explanations: bool = True) -> pd.DataFrame:
14
+ data = df.copy()
15
+
16
+ # 1) Очистка/нормализация
17
+ # data = prepare_dataframe(data)
18
+
19
+ # 2) Базовые фичи
20
+ # data = build_baseline_features(data)
21
+
22
+ # 3) Спецфичи для Q4
23
+ # data = add_q4_features(data)
24
+
25
+ # 4) Семантические фичи
26
+ # data = add_semantic_features(data)
27
+
28
+ # 5) Предсказания CatBoost по каждому вопросу
29
+ # models = load_models("models") # твоя реализация
30
+ # data = predict_batch(data, models) # должна добавить колонку predicted_score
31
+
32
+ # 6) Клип значений по диапазонам (на всякий случай)
33
+ if "question_number" in data.columns and "predicted_score" in data.columns:
34
+ def clip_score(row):
35
+ q = int(row["question_number"])
36
+ s = float(row["predicted_score"])
37
+ if q in (1, 3):
38
+ return int(min(1, max(0, round(s))))
39
+ return int(min(2, max(0, round(s))))
40
+ data["predicted_score"] = data.apply(clip_score, axis=1)
41
+
42
+ # 7) Объяснения (если есть)
43
+ # if with_explanations:
44
+ # data = build_explanations(data)
45
+
46
+ return data
check_final_quality.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ from sklearn.metrics import mean_absolute_error
4
+
5
+ # Читаем наши предсказания
6
+ df = pd.read_csv('test_output.csv', delimiter=';', encoding='utf-8-sig')
7
+
8
+ # Фильтруем только строки с истинными оценками
9
+ df_with_truth = df[df['Оценка экзаменатора'].notna()]
10
+
11
+ if len(df_with_truth) > 0:
12
+ true_scores = df_with_truth['Оценка экзаменатора']
13
+ pred_scores = df_with_truth['pred_score']
14
+
15
+ mae_total = mean_absolute_error(true_scores, pred_scores)
16
+ print(f'📊 ОБЩЕЕ КАЧЕСТВО (MAE): {mae_total:.3f} балла')
17
+ print()
18
+
19
+ # По типам вопросов
20
+ for q in [1, 2, 3, 4]:
21
+ q_data = df_with_truth[df_with_truth['№ вопроса'] == q]
22
+ if len(q_data) > 0:
23
+ mae_q = mean_absolute_error(q_data['Оценка экзаменатора'], q_data['pred_score'])
24
+ count_q = len(q_data)
25
+ print(f' Вопрос {q}: MAE = {mae_q:.3f} балла (примеров: {count_q})')
26
+ else:
27
+ print('❌ Нет данных с истинными оценками для проверки')
check_quality.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+
4
+ # Загрузи скачанный файл
5
+ df = pd.read_csv('predicted_from_api.csv', sep=';')
6
+
7
+ print("📊 АНАЛИЗ КАЧЕСТВА ПРЕДСКАЗАНИЙ")
8
+ print("=" * 50)
9
+
10
+ # Проверяем наличие колонок
11
+ if 'Оценка экзаменатора' in df.columns and 'pred_score' in df.columns:
12
+ # Убираем строки где нет истинных оценок
13
+ df_clean = df.dropna(subset=['Оценка экзаменатора'])
14
+
15
+ if len(df_clean) > 0:
16
+ true_scores = df_clean['Оценка экзаменатора'].astype(float)
17
+ pred_scores = df_clean['pred_score'].astype(float)
18
+
19
+ # Основные метрики
20
+ mae = (abs(true_scores - pred_scores)).mean()
21
+ rmse = ((true_scores - pred_scores) ** 2).mean() ** 0.5
22
+
23
+ print(f"📈 Общие метрики:")
24
+ print(f" MAE (средняя абсолютная ошибка): {mae:.3f}")
25
+ print(f" RMSE (среднеквадратичная ошибка): {rmse:.3f}")
26
+ print(f" Корреляция: {true_scores.corr(pred_scores):.3f}")
27
+
28
+ # По вопросам
29
+ print(f"\n📋 По типам вопросов:")
30
+ for q in [1, 2, 3, 4]:
31
+ mask = df_clean['№ вопроса'] == q
32
+ if mask.any():
33
+ q_true = true_scores[mask]
34
+ q_pred = pred_scores[mask]
35
+ q_mae = (abs(q_true - q_pred)).mean()
36
+
37
+ # Диапазон баллов для вопроса
38
+ if q in [1, 3]:
39
+ max_score = 1
40
+ else:
41
+ max_score = 2
42
+
43
+ print(f" Вопрос {q} (0-{max_score}): MAE = {q_mae:.3f}, примеров = {len(q_true)}")
44
+
45
+ else:
46
+ print("❌ В файле нет строк с оценками экзаменатора")
47
+
48
+ else:
49
+ print("❌ В файле отсутствуют колонки 'Оценка экзаменатора' или 'pred_score'")
50
+
51
+ # Статистика предсказаний
52
+ print(f"\n📊 Статистика предсказаний:")
53
+ for q in [1, 2, 3, 4]:
54
+ mask = df['№ вопроса'] == q
55
+ if mask.any():
56
+ scores = df.loc[mask, 'pred_score'].astype(float)
57
+ print(f" Вопрос {q}: ср.={scores.mean():.2f}, мин={scores.min():.2f}, макс={scores.max():.2f}")
check_small_quality.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+
3
+ df = pd.read_csv('test_small.csv', sep=';')
4
+ print("🔍 АНАЛИЗ SMALL.CSV С УЛУЧШЕННОЙ МОДЕЛЬЮ:")
5
+ print("=" * 50)
6
+
7
+ for q in [1, 4]: # В small.csv есть только Q1 и Q4
8
+ q_data = df[df['№ вопроса'] == q]
9
+ if len(q_data) > 0:
10
+ scores = q_data['pred_score']
11
+ true_scores = q_data['Оценка экзаменатора']
12
+
13
+ print(f"📊 Вопрос {q}:")
14
+ print(f" Предсказания: {scores.tolist()}")
15
+ print(f" Истинные: {true_scores.tolist()}")
16
+
17
+ if len(true_scores) > 0:
18
+ mae = (abs(true_scores - scores)).mean()
19
+ print(f" MAE: {mae:.3f}")
20
+ print()
create_and_analyze.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import matplotlib.pyplot as plt
3
+ from collections import Counter
4
+ import numpy as np
5
+ import os
6
+ import warnings
7
+
8
+ warnings.filterwarnings('ignore')
9
+
10
+ # Настройка отображения
11
+ plt.style.use('default')
12
+ plt.rcParams['font.family'] = 'DejaVu Sans'
13
+
14
+
15
+ def create_test_data():
16
+ """Создание тестовых данных"""
17
+
18
+ test_data = """Id экзамена;Id вопроса;№ вопроса;Текст вопроса;Оценка экзаменатора;Транскрибация ответа;pred_score;объяснение_оценки
19
+ 3373871;30625752;1;"<p>Добро пожаловать на экзамен!</p>";1;"Экзаменатор: Начните диалог. Тестируемый: Здравствуйте, я хотел бы извиниться, что не смогу прийти на день рождения. Что бы вы хотели в подарок?";0.99;"🟢 Развернутый ответ | ✅ Высокое смысловое соответствие | 📊 Хорошая структура ответа | 💬 Разнообразная лексика | ⭐ Высокий балл"
20
+ 3373871;30625753;2;"<p>Расскажите о вашем жилье</p>";2;"Экзаменатор: Вы живёте в квартире или доме? Тестируемый: Я живу в квартире в центре города. Это трёхкомнатная квартира с балконом. Квартира новая, построена в 2020 году.";1.62;"🟢 Развернутый ответ | ✅ Высокое смысловое соответствие | 📊 Хорошая структура ответа | 🏠 Подробное описание | ⭐ Высокий балл"
21
+ 3373872;30625790;1;"<p>Начните диалог о работе</p>";1;"Экзаменатор: Узнайте о требованиях к работе. Тестируемый: Здравствуйте, я увидел ваше объявление о вакансии. Какие требования к соискателю? Какие документы нужны?";0.87;"🟢 Развернутый ответ | ⚠️ Умеренное смысловое соответствие | 📊 Хорошая структура ответа | 💬 Разнообразная лексика | ⭐ Высокий балл"
22
+ 3373872;30625791;2;"<p>Опишите ваше жилье</p>";1;"Экзаменатор: Расскажите о вашей квартире. Тестируемый: У меня квартира. Она хорошая. Три комнаты.";0.45;"📉 Мало предложений | ❌ Низкое смысловое соответствие | 📊 Хорошая структура ответа"
23
+ 3373873;30625828;1;"<p>Оформление документов</p>";2;"Экзаменатор: Объясните ситуацию в миграционной службе. Тестируемый: Здравствуйте, мне нужно оформить миграционную карту. Я приехал две недели назад. Можете дать мне бланк для заполнения?";1.85;"🟢 Развернутый ответ | ✅ Высокое смысловое соответствие | 📊 Хорошая структура ответа | 💬 Разнообразная лексика | ⭐ Высокий балл"
24
+ 3373873;30625829;2;"<p>Ваши любимые фильмы</p>";1;"Экзаменатор: Какие фильмы вы любите? Тестируемый: Я смотрю фантастику и детективы. Люблю новые цветные фильмы. Мой любимый фильм - Интерстеллар, он о космосе и времени.";1.15;"🟢 Развернутый ответ | ⚠️ Умеренное смысловое соответствие | 📊 Хорошая структура ответа | 💬 Разнообразная лексика"
25
+ 3373874;30625866;3;"<p>Опишите картинку</p>";2;"Экзаменатор: Что изображено на картинке? Тестируемый: На картинке изображена семья в парке. Дети играют в мяч, родители сидят на скамейке. Яркий солнечный день, лето.";1.92;"🟢 Развернутый ответ | ✅ Высокое смысловое соответствие | 🎨 Есть вступление с описанием картинки | 👤 Есть личный опыт | ⭐ Высокий балл"
26
+ 3373874;30625867;4;"<p>Расскажите о хобби</p>";1;"Экзаменатор: Чем увлекаетесь? Тестируемый: Я читаю книги. Иногда смотрю фильмы.";0.35;"📉 Мало предложений | ❌ Низкое смысловое соответствие | 📊 Хорошая структура ответа"
27
+ 3373875;30625904;1;"<p>Ситуация в больнице</p>";1;"Экзаменатор: Узнайте о приеме врача. Тестируемый: Здравствуйте, мне нужно записаться к терапевту на обследование. Когда принимает врач и какие документы нужны?";0.95;"🟢 Развернутый ответ | ✅ Высокое смысловое соответствие | 📊 Хорошая структура ответа | 💬 Разнообразная лексика | ⭐ Высокий балл"
28
+ 3373875;30625905;2;"<p>Кулинарные предпочтения</p>";2;"Экзаменатор: Какая ваша любимая кухня? Тестируемый: Я очень люблю итальянскую кухню, особенно пасту и пиццу. Также нравится японская кухня - суши и роллы. Люблю готовить сам, особенно выпечку.";1.78;"🟢 Развернутый ответ | ✅ Высокое смысловое соответствие | 📊 Хорошая структура ответа | 🏠 Подробное описание | ⭐ Высокий балл"
29
+ """
30
+
31
+ # Сохраняем тестовые данные в файл
32
+ with open('test_data.csv', 'w', encoding='utf-8') as f:
33
+ f.write(test_data)
34
+
35
+ print("✅ Тестовый файл 'test_data.csv' создан успешно")
36
+ return True
37
+
38
+
39
+ def load_and_analyze_data():
40
+ """Загрузка тестовых данных"""
41
+
42
+ file_path = 'test_data.csv'
43
+
44
+ try:
45
+ df = pd.read_csv(file_path, encoding='utf-8', delimiter=';')
46
+ print("✅ Тестовый файл загружен успешно")
47
+ except Exception as e:
48
+ print(f"❌ Ошибка загрузки: {e}")
49
+ return None
50
+
51
+ print("=" * 60)
52
+ print("ТЕСТОВЫЙ АНАЛИЗ AI-ОЦЕНОК")
53
+ print("=" * 60)
54
+
55
+ print(f"Размер данных: {df.shape[0]} строк, {df.shape[1]} колонок")
56
+ print(f"Колонки: {list(df.columns)}")
57
+ print(f"\nПервые 3 строки:")
58
+ print(df.head(3))
59
+
60
+ return df
61
+
62
+
63
+ def basic_statistics(df):
64
+ """Базовая статистика"""
65
+
66
+ print("\n" + "=" * 40)
67
+ print("БАЗОВАЯ СТАТИСТИКА")
68
+ print("=" * 40)
69
+
70
+ print("AI оценки (pred_score):")
71
+ print(f" Среднее: {df['pred_score'].mean():.3f}")
72
+ print(f" Медиана: {df['pred_score'].median():.3f}")
73
+ print(f" Стандартное отклонение: {df['pred_score'].std():.3f}")
74
+ print(f" Минимум: {df['pred_score'].min():.3f}")
75
+ print(f" Максимум: {df['pred_score'].max():.3f}")
76
+
77
+ print("\nОценки экзаменатора:")
78
+ print(f" Среднее: {df['Оценка экзаменатора'].mean():.3f}")
79
+ print(f" Медиана: {df['Оценка экзаменатора'].median():.3f}")
80
+ print(f" Стандартное отклонение: {df['Оценка экзаменатора'].std():.3f}")
81
+
82
+ print("\nРаспределение оценок экзаменатора:")
83
+ распределение = df['Оценка экзаменатора'].value_counts().sort_index()
84
+ for оценка, count in распределение.items():
85
+ print(f" {оценка}: {count} ответов ({count / len(df) * 100:.1f}%)")
86
+
87
+
88
+ def calculate_correlations(df):
89
+ """Расчет корреляций"""
90
+
91
+ print("\n" + "=" * 40)
92
+ print("КОРРЕЛЯЦИИ И РАСХОЖДЕНИЯ")
93
+ print("=" * 40)
94
+
95
+ correlation = df[['Оценка экзаменатора', 'pred_score']].corr().iloc[0, 1]
96
+ print(f"Корреляция между оценками: {correlation:.3f}")
97
+
98
+ df['разница'] = df['pred_score'] - df['Оценка экзаменатора']
99
+ df['abs_разница'] = abs(df['разница'])
100
+
101
+ print(f"Средняя абсолютная разница: {df['abs_разница'].mean():.3f}")
102
+ print(f"Максимальная разница: {df['abs_разница'].max():.3f}")
103
+ print(f"Минимальная разница: {df['abs_разница'].min():.3f}")
104
+
105
+ # Анализ согласованности
106
+ print("\nСОГЛАСОВАННОСТЬ ОЦЕНОК:")
107
+ for порог in [0.1, 0.3, 0.5, 1.0]:
108
+ согласованные = df[df['abs_разница'] < порог].shape[0]
109
+ процент = (согласованные / len(df)) * 100
110
+ print(f" Разница < {порог}: {согласованные} ответов ({процент:.1f}%)")
111
+
112
+ # Направление разниц
113
+ завышение = len(df[df['разница'] > 0])
114
+ занижение = len(df[df['разница'] < 0])
115
+ совпадение = len(df[df['разница'] == 0])
116
+
117
+ print(f"\nНАПРАВЛЕНИЕ РАЗНИЦ:")
118
+ print(f" AI завышает: {завышение} ({завышение / len(df) * 100:.1f}%)")
119
+ print(f" AI занижает: {занижение} ({занижение / len(df) * 100:.1f}%)")
120
+ print(f" Полное совпадение: {совпадение} ({совпадение / len(df) * 100:.1f}%)")
121
+
122
+
123
+ def create_visualizations(df):
124
+ """Создание графиков"""
125
+
126
+ print("\n" + "=" * 40)
127
+ print("СОЗДАНИЕ ГРАФИКОВ")
128
+ print("=" * 40)
129
+
130
+ os.makedirs('graphs', exist_ok=True)
131
+
132
+ # 1. Scatter plot
133
+ plt.figure(figsize=(12, 8))
134
+ scatter = plt.scatter(df['Оценка экзаменатора'], df['pred_score'],
135
+ c=df['abs_разница'], cmap='viridis', alpha=0.7, s=80)
136
+ plt.colorbar(scatter, label='Абсолютная разница')
137
+ plt.plot([0, 2], [0, 2], 'r--', alpha=0.5, label='Идеальное соответствие')
138
+ plt.xlabel('Оценка экзаменатора', fontsize=12)
139
+ plt.ylabel('AI оценка (pred_score)', fontsize=12)
140
+ plt.title('Сравнение человеческой и AI оценки\n(цвет показывает величину расхождения)', fontsize=14)
141
+ plt.legend()
142
+ plt.grid(True, alpha=0.3)
143
+ plt.xticks([1, 2])
144
+ plt.savefig('graphs/test_scatter.png', dpi=300, bbox_inches='tight')
145
+ plt.close()
146
+
147
+ # 2. Гистограмма разниц
148
+ plt.figure(figsize=(12, 6))
149
+ plt.hist(df['разница'], bins=15, alpha=0.7, edgecolor='black', color='skyblue')
150
+ plt.xlabel('Разница (AI - Человек)', fontsize=12)
151
+ plt.ylabel('Количество ответов', fontsize=12)
152
+ plt.title('Распределение разниц между AI и человеческими оценками', fontsize=14)
153
+ plt.grid(True, alpha=0.3)
154
+ plt.axvline(x=0, color='red', linestyle='--', alpha=0.8, label='Нулевая разница')
155
+ plt.axvline(x=df['разница'].mean(), color='orange', linestyle='--',
156
+ alpha=0.8, label=f'Средняя разница: {df["разница"].mean():.3f}')
157
+ plt.legend()
158
+ plt.savefig('graphs/test_histogram.png', dpi=300, bbox_inches='tight')
159
+ plt.close()
160
+
161
+ # 3. Box plot по вопросам
162
+ plt.figure(figsize=(12, 6))
163
+ box_data = [df[df['№ вопроса'] == question]['pred_score'].values
164
+ for question in sorted(df['№ вопроса'].unique())]
165
+
166
+ box_plot = plt.boxplot(box_data, labels=sorted(df['№ вопроса'].unique()),
167
+ patch_artist=True)
168
+
169
+ # Раскрашиваем boxplot
170
+ colors = ['lightblue', 'lightgreen', 'lightcoral', 'lightyellow', 'lightpink']
171
+ for patch, color in zip(box_plot['boxes'], colors):
172
+ patch.set_facecolor(color)
173
+
174
+ plt.title('Распределение AI оценок по номерам вопросов', fontsize=14)
175
+ plt.xlabel('Номер вопроса', fontsize=12)
176
+ plt.ylabel('AI оценка (pred_score)', fontsize=12)
177
+ plt.grid(True, alpha=0.3)
178
+ plt.savefig('graphs/test_boxplot.png', dpi=300, bbox_inches='tight')
179
+ plt.close()
180
+
181
+ print("✅ Графики сохранены в папку 'graphs/'")
182
+
183
+
184
+ def analyze_explanations(df):
185
+ """Анализ объяснений"""
186
+
187
+ print("\n" + "=" * 40)
188
+ print("АНАЛИЗ ОБЪЯСНЕНИЙ")
189
+ print("=" * 40)
190
+
191
+ все_объяснения = ' '.join(df['объяснение_оценки'].dropna().astype(str))
192
+ слова = [word.strip() for word in все_объяснения.split() if len(word.strip()) > 2]
193
+ частотность = Counter(слова)
194
+
195
+ print("Топ-10 характеристик в объяснениях:")
196
+ for слово, count in частотность.most_common(10):
197
+ print(f" {слово}: {count}")
198
+
199
+
200
+ def save_detailed_analysis(df):
201
+ """Сохранение детального анализа"""
202
+
203
+ print("\n" + "=" * 40)
204
+ print("СОХРАНЕНИЕ РЕЗУЛЬТАТОВ")
205
+ print("=" * 40)
206
+
207
+ # Создаем копию с анализом
208
+ df_analysis = df.copy()
209
+
210
+ # Добавляем категоризацию расхождений
211
+ условия = [
212
+ df_analysis['abs_разница'] < 0.1,
213
+ df_analysis['abs_разница'] < 0.3,
214
+ df_analysis['abs_разница'] < 0.5,
215
+ df_analysis['abs_разница'] >= 0.5
216
+ ]
217
+ категории = ['Отличное', 'Хорошее', 'Умеренное', 'Низкое']
218
+ df_analysis['качество_согласования'] = np.select(условия, категории, default='Низкое')
219
+
220
+ # Сортируем по наибольшим расхождениям
221
+ df_analysis = df_analysis.sort_values('abs_разница', ascending=False)
222
+
223
+ try:
224
+ # Сохраняем в Excel
225
+ with pd.ExcelWriter('detailed_analysis.xlsx', engine='openpyxl') as writer:
226
+ df_analysis.to_excel(writer, sheet_name='Все_данные_с_анализом', index=False)
227
+ print("✅ Детальный анализ сохранен в 'detailed_analysis.xlsx'")
228
+ except Exception as e:
229
+ print(f"⚠️ Не удалось сохранить Excel: {e}")
230
+
231
+
232
+ def main():
233
+ """Основная функция"""
234
+
235
+ print("Создание тестовых данных...")
236
+ if not create_test_data():
237
+ return
238
+
239
+ df = load_and_analyze_data()
240
+ if df is None:
241
+ return
242
+
243
+ basic_statistics(df)
244
+ calculate_correlations(df)
245
+ create_visualizations(df)
246
+ analyze_explanations(df)
247
+ save_detailed_analysis(df)
248
+
249
+ print("\n" + "=" * 60)
250
+ print("✅ ТЕСТОВЫЙ АНАЛИЗ ЗАВЕРШЕН!")
251
+ print("=" * 60)
252
+ print("📊 Созданные файлы:")
253
+ print(" • test_data.csv - тестовые данные")
254
+ print(" • graphs/test_scatter.png - сравнение оценок")
255
+ print(" • graphs/test_histogram.png - распределение разниц")
256
+ print(" • graphs/test_boxplot.png - оценки по вопросам")
257
+ print(" • detailed_analysis.xlsx - детальный отчет")
258
+
259
+
260
+ if __name__ == "__main__":
261
+ main()
deploy-to-yandex.ps1.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # deploy-to-yandex.ps1
2
+ Write-Host "🚀 Начало развертывания в Yandex Cloud..." -ForegroundColor Green
3
+
4
+ # Переменные (ЗАМЕНИТЕ на свои!)
5
+ $REGISTRY_ID = "your-registry-id" # Найти в консоли: Container Registry -> ID реестра
6
+ $IMAGE_NAME = "exam-scorer"
7
+ $TAG = "latest"
8
+ $FULL_IMAGE = "cr.yandex/$REGISTRY_ID/$IMAGE_NAME`:$TAG"
9
+
10
+ # 1. Сборка Docker образа
11
+ Write-Host "📦 Сборка Docker образа..." -ForegroundColor Yellow
12
+ docker build -t $FULL_IMAGE .
13
+
14
+ # 2. Авторизация в Yandex Container Registry
15
+ Write-Host "🔐 Авторизация в Container Registry..." -ForegroundColor Yellow
16
+ yc container registry configure-docker
17
+
18
+ # 3. Загрузка образа в реестр
19
+ Write-Host "⬆️ Загрузка образа в Yandex Cloud..." -ForegroundColor Yellow
20
+ docker push $FULL_IMAGE
21
+
22
+ Write-Host "✅ Образ успешно загружен: $FULL_IMAGE" -ForegroundColor Green
23
+ Write-Host ""
24
+ Write-Host "🎯 Дальнейшие действия:" -ForegroundColor Cyan
25
+ Write-Host "1. В консоли Yandex Cloud перейдите в 'Serverless Containers'"
26
+ Write-Host "2. Создайте новый контейнер"
27
+ Write-Host "3. Укажите образ: $FULL_IMAGE"
28
+ Write-Host "4. Настройте порт: 8000"
29
+ Write-Host "5. Задайте переменные окружения:"
30
+ Write-Host " - PYTHONPATH=/app"
deploy-to-yandex.sh.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "🚀 Начало развертывания в Yandex Cloud..."
5
+
6
+ # Переменные (замените на свои)
7
+ REGISTRY_ID="your-registry-id" # Найти в консоли: Container Registry -> ID реестра
8
+ IMAGE_NAME="exam-scorer"
9
+ TAG="latest"
10
+ FULL_IMAGE="cr.yandex/${REGISTRY_ID}/${IMAGE_NAME}:${TAG}"
11
+
12
+ # 1. Сборка Docker образа
13
+ echo "📦 Сборка Docker образа..."
14
+ docker build -t ${FULL_IMAGE} .
15
+
16
+ # 2. Авторизация в Yandex Container Registry
17
+ echo "🔐 Авторизация в Container Registry..."
18
+ yc container registry configure-docker
19
+
20
+ # 3. Загрузка образа в реестр
21
+ echo "⬆️ Загрузка образа в Yandex Cloud..."
22
+ docker push ${FULL_IMAGE}
23
+
24
+ echo "✅ Образ успешно загружен: ${FULL_IMAGE}"
25
+ echo ""
26
+ echo "🎯 Дальнейшие действия:"
27
+ echo "1. В консоли Yandex Cloud перейдите в 'Serverless Containers'"
28
+ echo "2. Создайте новый контейнер"
29
+ echo "3. Укажите образ: ${FULL_IMAGE}"
30
+ echo "4. Настройте порт: 8000"
31
+ echo "5. Задайте переменные окружения:"
32
+ echo " - PYTHONPATH=/app"
evaluate_mae.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import pandas as pd
3
+ import numpy as np
4
+ import sys
5
+
6
+ def safe_float(s):
7
+ try:
8
+ return float(s)
9
+ except Exception:
10
+ return np.nan
11
+
12
+ def main():
13
+ ap = argparse.ArgumentParser()
14
+ ap.add_argument("--pred", required=True)
15
+ ap.add_argument("--gold", required=True)
16
+ ap.add_argument("--pred-col", default="predicted_score")
17
+ ap.add_argument("--score-col", default="examiner_score")
18
+ ap.add_argument("--question-col", default="question_number")
19
+ ap.add_argument("--key", default="")
20
+ args = ap.parse_args()
21
+
22
+ p = pd.read_csv(args.pred)
23
+ g = pd.read_csv(args.gold)
24
+
25
+ if args.pred_col not in p.columns:
26
+ print(f"ERROR: нет {args.pred_col} в {args.pred}"); sys.exit(1)
27
+ if args.score_col not in g.columns:
28
+ print(f"ERROR: нет {args.score_col} в {args.gold}"); sys.exit(1)
29
+
30
+ keys = [k.strip() for k in args.key.split(",") if k.strip()]
31
+ if keys:
32
+ for miss in [k for k in keys if k not in p.columns]:
33
+ print(f"ERROR: нет ключа {miss} в pred"); sys.exit(1)
34
+ for miss in [k for k in keys if k not in g.columns]:
35
+ print(f"ERROR: нет ключа {miss} в gold"); sys.exit(1)
36
+ merged = p[keys + [args.pred_col]].merge(
37
+ g[keys + [args.score_col]], on=keys, how="inner", validate="one_to_one"
38
+ )
39
+ else:
40
+ if len(p) != len(g):
41
+ print("ERROR: разные размеры pred/gold и нет ключа --key"); sys.exit(1)
42
+ merged = pd.DataFrame({
43
+ args.pred_col: p[args.pred_col].values,
44
+ args.score_col: g[args.score_col].values
45
+ })
46
+
47
+ y_pred = merged[args.pred_col].map(safe_float)
48
+ y_true = merged[args.score_col].map(safe_float)
49
+ mask = (~y_pred.isna()) & (~y_true.isna())
50
+ mae = np.mean(np.abs(y_pred[mask] - y_true[mask]))
51
+ print(f"MAE (общий): {mae:.4f} | N={mask.sum()}")
52
+
53
+ # по вопросам, если есть
54
+ try:
55
+ qp = p.loc[mask, args.question_col] if args.question_col in p.columns else g.loc[mask, args.question_col]
56
+ df = pd.DataFrame({"qn": qp.values, "pred": y_pred[mask].values, "true": y_true[mask].values})
57
+ for q, v in df.groupby("qn").apply(lambda d: np.mean(np.abs(d["pred"] - d["true"]))).sort_index().items():
58
+ print(f" Q{int(q)} MAE: {v:.4f}")
59
+ except Exception:
60
+ pass
61
+
62
+ if __name__ == "__main__":
63
+ main()
feature_engineering.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # feature_engineering.py
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from typing import Iterable, List, Tuple, Optional
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+
10
+ try:
11
+ from sentence_transformers import SentenceTransformer, util as sbert_util
12
+ except Exception: # чтобы не падать на установке
13
+ SentenceTransformer = None # type: ignore
14
+ sbert_util = None # type: ignore
15
+
16
+ try:
17
+ import language_tool_python
18
+ except Exception:
19
+ language_tool_python = None # type: ignore
20
+
21
+
22
+ _HTML_TAG_RE = re.compile(r"<[^>]+>")
23
+ _WS_RE = re.compile(r"\s+")
24
+ _PUNCT_RE = re.compile(r"[^\w\s?!.,:;ёЁа-яА-Я-]", re.UNICODE)
25
+
26
+ # мини-лексиконы под критерии
27
+ POLITE_WORDS = {"здравствуйте", "здравствуй", "пожалуйста", "спасибо", "будьте добры"}
28
+ APOLOGY_WORDS = {"извините", "простите", "прошу прощения"}
29
+ FAMILY_WORDS = {"семья", "сын", "дочь", "дети", "ребёнок", "муж", "жена", "родители"}
30
+ SEASON_WORDS = {"зима", "весна", "лето", "осень"}
31
+ SHOP_WORDS = {"рассрочка", "гарантия", "характеристики", "документы", "касса"}
32
+ YESNO_WORDS = {"да", "нет", "наверное", "возможно"}
33
+
34
+
35
+ def _strip_html(s: str) -> str:
36
+ s = _HTML_TAG_RE.sub(" ", s)
37
+ s = _WS_RE.sub(" ", s).strip()
38
+ return s
39
+
40
+
41
+ def _only_text(s: str) -> str:
42
+ s = s.lower()
43
+ s = _strip_html(s)
44
+ s = _PUNCT_RE.sub(" ", s)
45
+ s = _WS_RE.sub(" ", s).strip()
46
+ return s
47
+
48
+
49
+ def _split_sentences(s: str) -> List[str]:
50
+ # простая сегментация
51
+ parts = re.split(r"(?<=[.!?])\s+", s)
52
+ return [p.strip() for p in parts if p.strip()]
53
+
54
+
55
+ def _strip_examiner_lines(text: str) -> str:
56
+ """
57
+ Убираем вероятные реплики экзаменатора: предложения с '?',
58
+ короткие управляющие фразы ("хорошо.", "итак, ...").
59
+ """
60
+ sents = _split_sentences(text)
61
+ kept = []
62
+ for i, sent in enumerate(sents):
63
+ low = sent.lower()
64
+ if "?" in sent:
65
+ continue
66
+ if low in {"хорошо.", "отлично.", "прекрасно.", "молодец."}:
67
+ continue
68
+ if low.startswith(("итак", "следующий", "теперь", "будьте", "ответьте")) and "?" in low:
69
+ continue
70
+ kept.append(sent)
71
+ return " ".join(kept) if kept else text
72
+
73
+
74
+ def _count_matches(words: Iterable[str], tokens: Iterable[str]) -> int:
75
+ wset = set(w.lower() for w in words)
76
+ return sum(1 for t in tokens if t in wset)
77
+
78
+
79
+ class FeatureExtractor:
80
+ """
81
+ Лёгкий экстрактор признаков:
82
+ - очистка текста/HTML
83
+ - отделение реплик экзаменатора (эвристика)
84
+ - семантическая близость (SBERT)
85
+ - длины, кол-во предложений, вопросительных/восклицательных и пр.
86
+ - индикаторы по заданиям (вежливость, извинение, семья, рассрочка, …)
87
+ - (опц.) grammar_error_count через LanguageTool
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ sbert_model_name: str = "cointegrated/rubert-tiny",
93
+ use_grammar: bool = False,
94
+ strip_examiner: bool = True,
95
+ ) -> None:
96
+ self.strip_examiner = strip_examiner
97
+
98
+ # SBERT
99
+ self.sbert: Optional[SentenceTransformer]
100
+ if SentenceTransformer is None:
101
+ self.sbert = None
102
+ else:
103
+ self.sbert = SentenceTransformer(sbert_model_name)
104
+
105
+ # Grammar
106
+ self.grammar = None
107
+ if use_grammar and language_tool_python is not None:
108
+ try:
109
+ self.grammar = language_tool_python.LanguageTool("ru")
110
+ except Exception:
111
+ self.grammar = None # безопасно отключаем
112
+
113
+ # --------- примитивные фичи ----------
114
+ def _basic_text_stats(self, text: str) -> Tuple[int, int, int, int, int, float]:
115
+ cleaned = _only_text(text)
116
+ tokens = cleaned.split()
117
+ sents = _split_sentences(text)
118
+ qmarks = text.count("?")
119
+ emarks = text.count("!")
120
+ avg_sent_len = (len(tokens) / max(len(sents), 1)) if tokens else 0.0
121
+ return len(tokens), len(sents), qmarks, emarks, len(set(tokens)), float(avg_sent_len)
122
+
123
+ def _semantic_sim(self, q: str, a: str) -> float:
124
+ if not self.sbert or sbert_util is None:
125
+ return 0.0
126
+ try:
127
+ emb_q = self.sbert.encode([q], convert_to_tensor=True, normalize_embeddings=True)
128
+ emb_a = self.sbert.encode([a], convert_to_tensor=True, normalize_embeddings=True)
129
+ sim = float(sbert_util.cos_sim(emb_q, emb_a)[0][0].cpu().item())
130
+ # нормализуем к [0..1] прим��рно
131
+ return max(0.0, min(1.0, (sim + 1.0) / 2.0))
132
+ except Exception:
133
+ return 0.0
134
+
135
+ def _grammar_errors(self, text: str) -> int:
136
+ if not self.grammar:
137
+ return 0
138
+ try:
139
+ matches = self.grammar.check(text)
140
+ return len(matches)
141
+ except Exception:
142
+ return 0
143
+
144
+ # --------- фичи под задания ----------
145
+ def _question_specific_flags(self, qnum: int, answer_text: str, question_text: str) -> dict:
146
+ a_clean = _only_text(answer_text)
147
+ a_tokens = a_clean.split()
148
+
149
+ flags = {
150
+ "has_politeness": int(_count_matches(POLITE_WORDS, a_tokens) > 0),
151
+ "has_apology": int(_count_matches(APOLOGY_WORDS, a_tokens) > 0),
152
+ "has_yesno": int(_count_matches(YESNO_WORDS, a_tokens) > 0),
153
+ "mentions_family": int(_count_matches(FAMILY_WORDS, a_tokens) > 0),
154
+ "mentions_season": int(_count_matches(SEASON_WORDS, a_tokens) > 0),
155
+ "mentions_shop": int(_count_matches(SHOP_WORDS, a_tokens) > 0),
156
+ "has_question_mark": int("?" in answer_text),
157
+ }
158
+
159
+ # лёгкие правила по задачам
160
+ if qnum == 1: # извиниться + спросить
161
+ flags["task_completed_like_q1"] = int(flags["has_apology"] and flags["has_question_mark"])
162
+ elif qnum == 2: # диалоговые ответы
163
+ flags["task_completed_like_q2"] = int(flags["has_yesno"] or len(a_tokens) > 12)
164
+ elif qnum == 3: # магазин: документы/рассрочка/характеристики
165
+ flags["task_completed_like_q3"] = int(flags["mentions_shop"] or len(a_tokens) > 25)
166
+ elif qnum == 4: # описание картинки + семья/дети
167
+ flags["task_completed_like_q4"] = int(flags["mentions_family"] or flags["mentions_season"])
168
+ else:
169
+ flags["task_completed_like_q1"] = 0
170
+
171
+ # семантика вопрос-ответ
172
+ flags["qa_semantic_sim"] = self._semantic_sim(question_text, answer_text)
173
+ return flags
174
+
175
+ # --------- публичное API ----------
176
+ def extract_row_features(self, row: pd.Series) -> dict:
177
+ qnum = int(row.get("№ вопроса") or row.get("question_number") or 0)
178
+ qtext_raw = str(row.get("Текст вопроса") or row.get("question_text") or "")
179
+ atext_raw = str(row.get("Транскрибация") or row.get("transcript") or row.get("answer_text") or "")
180
+
181
+ qtext = _strip_html(qtext_raw)
182
+ atext = _strip_html(atext_raw)
183
+ if self.strip_examiner:
184
+ atext = _strip_examiner_lines(atext)
185
+
186
+ tok_len, sent_cnt, qmarks, emarks, uniq, avg_sent = self._basic_text_stats(atext)
187
+ grams = self._grammar_errors(atext)
188
+
189
+ base = {
190
+ "question_number": qnum,
191
+ "question_text": qtext,
192
+ "answer_text": atext,
193
+ "tokens_len": tok_len,
194
+ "sent_count": sent_cnt,
195
+ "q_mark_count": qmarks,
196
+ "excl_mark_count": emarks,
197
+ "uniq_tokens": uniq,
198
+ "avg_sent_len": avg_sent,
199
+ "grammar_errors": grams,
200
+ "answer_len_chars": len(atext),
201
+ }
202
+ base.update(self._question_specific_flags(qnum, atext, qtext))
203
+ return base
204
+
205
+ def extract_all_features(self, df: pd.DataFrame) -> pd.DataFrame:
206
+ feats = [self.extract_row_features(r) for _, r in df.iterrows()]
207
+ out = pd.DataFrame(feats)
208
+
209
+ # защитимся от NaN и типов
210
+ num_cols = [c for c in out.columns if c not in {"question_text", "answer_text"}]
211
+ for c in num_cols:
212
+ if c not in {"question_text", "answer_text"}:
213
+ out[c] = pd.to_numeric(out[c], errors="coerce")
214
+ out = out.fillna(
215
+ {c: 0 for c in out.columns if c not in {"question_text", "answer_text"}}
216
+ )
217
+ return out
feature_extractor.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import re
4
+ from typing import Dict, List, Tuple, Optional
5
+ import warnings
6
+ from sklearn.feature_extraction.text import TfidfVectorizer
7
+ from sklearn.metrics.pairwise import cosine_similarity
8
+
9
+ warnings.filterwarnings('ignore')
10
+
11
+
12
+ class RussianFeatureExtractor:
13
+ """Исправленная версия экстрактора признаков с работающим composite_quality_score"""
14
+
15
+ def __init__(self, use_heavy_models: bool = False):
16
+ print("Инициализация исправленного экстрактора признаков...")
17
+
18
+ self.use_heavy_models = use_heavy_models
19
+ self.sbert_model = None
20
+
21
+ # Инициализация моделей
22
+ self._initialize_models()
23
+
24
+ # Списки ключевых слов
25
+ self.greeting_words = ['здравствуйте', 'привет', 'добрый', 'здравствуй', 'доброе', 'приветствую']
26
+ self.question_words = ['как', 'что', 'где', 'когда', 'почему', 'можно', 'сколько', 'какой', 'какая']
27
+ self.descriptive_words = ['вижу', 'изображен', 'находится', 'делает', 'одет', 'стоит', 'сидит']
28
+ self.connector_words = ['потому что', 'поэтому', 'так как', 'например', 'кроме того']
29
+ self.emotional_words = ['красиво', 'интересно', 'замечательно', 'прекрасно', 'нравится']
30
+ self.spatial_words = ['слева', 'справа', 'вверху', 'внизу', 'рядом', 'около']
31
+
32
+ print("✅ Инициализация завершена!")
33
+
34
+ def _initialize_models(self):
35
+ """Инициализация моделей"""
36
+ if self.use_heavy_models:
37
+ print("ℹ️ Тяжелые модели отключены для стабильности")
38
+ print("ℹ️ Используем легкие методы (TF-IDF)")
39
+
40
+ def clean_text(self, text: str) -> str:
41
+ """Очистка текста"""
42
+ if pd.isna(text):
43
+ return ""
44
+ text = str(text)
45
+ text = re.sub(r'<[^>]+>', '', text)
46
+ text = re.sub(r'[^\w\sа-яА-ЯёЁ.,!?;:()-]', '', text)
47
+ text = re.sub(r'\s+', ' ', text).strip()
48
+ return text
49
+
50
+ def extract_basic_features(self, text: str) -> Dict[str, float]:
51
+ """Базовые текстовые признаки"""
52
+ text_clean = self.clean_text(text)
53
+
54
+ if not text_clean:
55
+ return {
56
+ 'text_length': 0, 'word_count': 0, 'sentence_count': 0,
57
+ 'avg_word_length': 0, 'lexical_diversity': 0,
58
+ 'has_questions': 0, 'has_exclamations': 0
59
+ }
60
+
61
+ # Базовые метрики
62
+ words = re.findall(r'\b[а-яёa-z]+\b', text_clean.lower())
63
+ sentences = [s.strip() for s in re.split(r'[.!?]+', text_clean) if s.strip()]
64
+
65
+ word_count = len(words)
66
+ text_length = len(text_clean)
67
+ sentence_count = len(sentences)
68
+
69
+ features = {
70
+ 'text_length': text_length,
71
+ 'word_count': word_count,
72
+ 'sentence_count': sentence_count,
73
+ 'avg_word_length': sum(len(w) for w in words) / max(word_count, 1),
74
+ 'lexical_diversity': len(set(words)) / max(word_count, 1),
75
+ 'has_questions': int('?' in text_clean),
76
+ 'has_exclamations': int('!' in text_clean),
77
+ }
78
+
79
+ return features
80
+
81
+ def extract_semantic_features(self, question: str, answer: str) -> Dict[str, float]:
82
+ """Семантические признаки"""
83
+ question_clean = self.clean_text(question)
84
+ answer_clean = self.clean_text(answer)
85
+
86
+ features = {
87
+ 'keyword_overlap': 0.0,
88
+ 'response_relevance': 0.0
89
+ }
90
+
91
+ if not answer_clean or not question_clean:
92
+ return features
93
+
94
+ try:
95
+ # Упрощенный анализ ключевых слов
96
+ question_words = set(re.findall(r'\b[а-яё]+\b', question_clean.lower()))
97
+ answer_words = set(re.findall(r'\b[а-яё]+\b', answer_clean.lower()))
98
+
99
+ if question_words:
100
+ common_words = question_words.intersection(answer_words)
101
+ features['keyword_overlap'] = len(common_words) / max(len(question_words), 1)
102
+ features['response_relevance'] = min(1.0, len(answer_words) / max(len(question_words), 1))
103
+
104
+ except Exception as e:
105
+ print(f"Ошибка семантических признаков: {e}")
106
+
107
+ return features
108
+
109
+ def extract_grammar_features(self, text: str) -> Dict[str, float]:
110
+ """Грамматические признаки"""
111
+ text_clean = self.clean_text(text)
112
+
113
+ features = {
114
+ 'grammar_quality': 0.5, # Базовая оценка
115
+ 'has_punctuation': 0.0,
116
+ 'sentence_completeness': 0.0
117
+ }
118
+
119
+ if not text_clean:
120
+ return features
121
+
122
+ sentences = [s.strip() for s in re.split(r'[.!?]+', text_clean) if s.strip()]
123
+ words = text_clean.split()
124
+
125
+ if sentences:
126
+ # Проверка пунктуации
127
+ features['has_punctuation'] = 1.0 if any(mark in text_clean for mark in '.!?') else 0.0
128
+
129
+ # Полнота предложений
130
+ complete_sentences = sum(1 for s in sentences if len(s.split()) >= 3)
131
+ features['sentence_completeness'] = complete_sentences / max(len(sentences), 1)
132
+
133
+ # Улучшенная эвристика грамматического качества
134
+ grammar_score = 0.0
135
+ grammar_score += features['has_punctuation'] * 0.3
136
+ grammar_score += features['sentence_completeness'] * 0.4
137
+
138
+ # Дополнительные эвристики
139
+ if len(words) > 5:
140
+ avg_sentence_len = len(words) / len(sentences)
141
+ if 5 <= avg_sentence_len <= 20:
142
+ grammar_score += 0.2
143
+ elif avg_sentence_len > 20:
144
+ grammar_score += 0.1
145
+
146
+ features['grammar_quality'] = min(1.0, grammar_score)
147
+
148
+ return features
149
+
150
+ def extract_style_features(self, text: str) -> Dict[str, float]:
151
+ """Стилистические признаки"""
152
+ text_clean = self.clean_text(text).lower()
153
+
154
+ features = {
155
+ 'has_greeting': 0.0,
156
+ 'has_description': 0.0,
157
+ 'has_connectors': 0.0,
158
+ 'has_emotional_words': 0.0,
159
+ 'style_score': 0.0
160
+ }
161
+
162
+ if not text_clean:
163
+ return features
164
+
165
+ # Стилистические маркеры
166
+ features.update({
167
+ 'has_greeting': float(any(greet in text_clean for greet in self.greeting_words)),
168
+ 'has_description': float(any(desc in text_clean for desc in self.descriptive_words)),
169
+ 'has_connectors': float(any(conn in text_clean for conn in self.connector_words)),
170
+ 'has_emotional_words': float(any(emot in text_clean for emot in self.emotional_words)),
171
+ })
172
+
173
+ # Оценка стиля
174
+ style_indicators = sum([
175
+ features['has_greeting'],
176
+ features['has_connectors'],
177
+ features['has_emotional_words']
178
+ ])
179
+ features['style_score'] = min(1.0, style_indicators / 3)
180
+
181
+ return features
182
+
183
+ def extract_quality_features(self, text: str, question_type: int) -> Dict[str, float]:
184
+ """Признаки качества ответа"""
185
+ text_clean = self.clean_text(text)
186
+ words = text_clean.split()
187
+ word_count = len(words)
188
+
189
+ features = {
190
+ 'answer_length_sufficiency': min(1.0, word_count / 30), # Нормализованная длина
191
+ 'content_richness': 0.0,
192
+ 'engagement_level': 0.0
193
+ }
194
+
195
+ if not text_clean:
196
+ return features
197
+
198
+ # Богатство контента (лексическое разнообразие + длина)
199
+ lexical_diversity = len(set(words)) / max(word_count, 1)
200
+ features['content_richness'] = min(1.0, (lexical_diversity + features['answer_length_sufficiency']) / 2)
201
+
202
+ # Уровень вовлеченности
203
+ engagement = 0.0
204
+ engagement += features['answer_length_sufficiency'] * 0.4
205
+ engagement += lexical_diversity * 0.3
206
+ engagement += (1.0 if '?' in text_clean else 0.0) * 0.3
207
+ features['engagement_level'] = engagement
208
+
209
+ return features
210
+
211
+ def extract_all_features(self, row: pd.Series) -> Dict[str, float]:
212
+ """Извлечение всех признаков - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
213
+ try:
214
+ # Безопасное извлечение данных
215
+ question = row.get('Текст вопроса', row.get('Вопрос', ''))
216
+ answer = row.get('Транскрибация ответа', row.get('Транскрипт', row.get('Ответ', '')))
217
+ question_type = row.get('№ вопроса', row.get('Тип вопроса', 1))
218
+
219
+ try:
220
+ question_type = int(question_type)
221
+ except:
222
+ question_type = 1
223
+
224
+ features = {}
225
+
226
+ # 1. Базовые признаки (надежные)
227
+ basic_features = self.extract_basic_features(answer)
228
+ features.update(basic_features)
229
+
230
+ # 2. Семантические признаки
231
+ semantic_features = self.extract_semantic_features(question, answer)
232
+ features.update(semantic_features)
233
+
234
+ # 3. Грамматические признаки
235
+ grammar_features = self.extract_grammar_features(answer)
236
+ features.update(grammar_features)
237
+
238
+ # 4. Стилистические признаки
239
+ style_features = self.extract_style_features(answer)
240
+ features.update(style_features)
241
+
242
+ # 5. Признаки качества
243
+ quality_features = self.extract_quality_features(answer, question_type)
244
+ features.update(quality_features)
245
+
246
+ # 6. Тип вопроса
247
+ features['question_type'] = float(question_type)
248
+
249
+ # 7. ИСПРАВЛЕННЫЙ композитный показатель
250
+ features['composite_quality_score'] = self._calculate_quality_score(features)
251
+
252
+ return features
253
+
254
+ except Exception as e:
255
+ print(f"❌ Ошибка при извлечении признаков: {e}")
256
+ # Возвращаем базовые признаки
257
+ return self._get_fallback_features()
258
+
259
+ def _calculate_quality_score(self, features: Dict[str, float]) -> float:
260
+ """ИСПРАВЛЕННЫЙ расчет качества ответа"""
261
+
262
+ # Веса для разных категорий
263
+ weights = {
264
+ # Семантика и релевантность (35%)
265
+ 'keyword_overlap': 0.20,
266
+ 'response_relevance': 0.15,
267
+
268
+ # Грамматика и структура (25%)
269
+ 'grammar_quality': 0.15,
270
+ 'sentence_completeness': 0.10,
271
+
272
+ # Стиль и вовлеченность (25%)
273
+ 'style_score': 0.10,
274
+ 'engagement_level': 0.15,
275
+
276
+ # Содержание (15%)
277
+ 'content_richness': 0.15
278
+ }
279
+
280
+ total_score = 0.0
281
+ total_weight = 0.0
282
+
283
+ for feature, weight in weights.items():
284
+ if feature in features:
285
+ value = features[feature]
286
+ total_score += value * weight
287
+ total_weight += weight
288
+
289
+ # Нормализация на случай отсутствующих признаков
290
+ if total_weight > 0:
291
+ final_score = total_score / total_weight
292
+ else:
293
+ final_score = 0.5 # нейтральная оценка
294
+
295
+ return min(1.0, max(0.0, final_score))
296
+
297
+ def _get_fallback_features(self) -> Dict[str, float]:
298
+ """Базовые признаки при ошибке"""
299
+ return {
300
+ 'text_length': 0, 'word_count': 0, 'sentence_count': 0,
301
+ 'avg_word_length': 0, 'lexical_diversity': 0,
302
+ 'has_questions': 0, 'has_exclamations': 0,
303
+ 'keyword_overlap': 0, 'response_relevance': 0,
304
+ 'grammar_quality': 0.5, 'has_punctuation': 0, 'sentence_completeness': 0,
305
+ 'has_greeting': 0, 'has_description': 0, 'has_connectors': 0,
306
+ 'has_emotional_words': 0, 'style_score': 0,
307
+ 'answer_length_sufficiency': 0, 'content_richness': 0, 'engagement_level': 0,
308
+ 'question_type': 1, 'composite_quality_score': 0.5
309
+ }
310
+
311
+ def extract_features_for_dataframe(self, df: pd.DataFrame, sample_size: int = None) -> pd.DataFrame:
312
+ """Извлечение признаков для датафрейма"""
313
+ if sample_size and sample_size < len(df):
314
+ df = df.sample(sample_size, random_state=42)
315
+ print(f"Взята выборка: {len(df)} строк")
316
+
317
+ print(f"Извлечение признаков для {len(df)} строк...")
318
+ features_list = []
319
+ successful = 0
320
+
321
+ for idx, row in df.iterrows():
322
+ if idx % 50 == 0 and idx > 0:
323
+ print(f"Обработано {idx}/{len(df)} строк...")
324
+
325
+ try:
326
+ features = self.extract_all_features(row)
327
+ features['original_index'] = idx
328
+ features_list.append(features)
329
+ successful += 1
330
+ except Exception as e:
331
+ print(f"❌ Ошибка в строке {idx}: {e}")
332
+ continue
333
+
334
+ if features_list:
335
+ features_df = pd.DataFrame(features_list)
336
+ features_df.set_index('original_index', inplace=True)
337
+
338
+ success_rate = successful / len(df)
339
+ print(f"✅ Извлечение завершено! Успешно: {successful}/{len(df)} ({success_rate:.1%})")
340
+
341
+ return features_df
342
+ else:
343
+ print("❌ Не удалось извлечь признаки")
344
+ return pd.DataFrame()
345
+
346
+
347
+ # Быстрая функция для тестирования
348
+ def extract_quick_features(text: str) -> Dict[str, float]:
349
+ extractor = RussianFeatureExtractor()
350
+ return extractor.extract_basic_features(text)
351
+
352
+
353
+ if __name__ == "__main__":
354
+ # Тест исправленной версии
355
+ extractor = RussianFeatureExtractor()
356
+ test_data = {
357
+ 'Текст вопроса': ['Расскажите о вашем городе'],
358
+ 'Транскрибация ответа': ['Привет! Я живу в Москве. Это большой и красивый город с множеством парков и музеев.'],
359
+ '№ вопроса': [1]
360
+ }
361
+ test_df = pd.DataFrame(test_data)
362
+ features = extractor.extract_all_features(test_df.iloc[0])
363
+
364
+ print("🎯 ТЕСТ ИСПРАВЛЕННОЙ ВЕРСИИ:")
365
+ print(f"Композитный показатель: {features['composite_quality_score']:.3f}")
366
+ print(f"Грамматическое качество: {features['grammar_quality']:.3f}")
367
+ print(f"Стилевой показатель: {features['style_score']:.3f}")
368
+ print(f"Количество слов: {features['word_count']}")
features_description.txt ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ОПИСАНИЕ ПРИЗНАКОВ:
2
+ ==================
3
+
4
+ text_length:
5
+ Тип: int64
6
+ Не-NULL: 100
7
+ Среднее: 1246.900
8
+ Корреляция с оценкой: 0.442
9
+
10
+ word_count:
11
+ Тип: int64
12
+ Не-NULL: 100
13
+ Среднее: 195.060
14
+ Корреляция с оценкой: 0.447
15
+
16
+ sentence_count:
17
+ Тип: int64
18
+ Не-NULL: 100
19
+ Среднее: 33.830
20
+ Корреляция с оценкой: 0.365
21
+
22
+ avg_word_length:
23
+ Тип: float64
24
+ Не-NULL: 100
25
+ Среднее: 6.497
26
+ Корреляция с оценкой: -0.259
27
+
28
+ lexical_diversity:
29
+ Тип: float64
30
+ Не-NULL: 100
31
+ Среднее: 0.763
32
+ Корреляция с оценкой: -0.336
33
+
34
+ semantic_similarity:
35
+ Тип: float64
36
+ Не-NULL: 100
37
+ Среднее: 0.000
38
+ Корреляция с оценкой: nan
39
+
40
+ keyword_overlap:
41
+ Тип: float64
42
+ Не-NULL: 100
43
+ Среднее: 0.000
44
+ Корреляция с оценкой: nan
45
+
46
+ grammar_error_count:
47
+ Тип: int64
48
+ Не-NULL: 100
49
+ Среднее: 0.000
50
+ Корреляция с оценкой: nan
51
+
52
+ grammar_error_ratio:
53
+ Тип: int64
54
+ Не-NULL: 100
55
+ Среднее: 0.000
56
+ Корреляция с оценкой: nan
57
+
58
+ has_punctuation:
59
+ Тип: float64
60
+ Не-NULL: 100
61
+ Среднее: 0.000
62
+ Корреляция с оценкой: nan
63
+
64
+ has_greeting:
65
+ Тип: float64
66
+ Не-NULL: 100
67
+ Среднее: 0.470
68
+ Корреляция с оценкой: -0.342
69
+
70
+ has_questions:
71
+ Тип: float64
72
+ Не-NULL: 100
73
+ Среднее: 0.920
74
+ Корреляция с оценкой: 0.179
75
+
76
+ has_description:
77
+ Тип: float64
78
+ Не-NULL: 100
79
+ Среднее: 0.310
80
+ Корреляция с оценкой: 0.226
81
+
82
+ dialog_initiation:
83
+ Тип: float64
84
+ Не-NULL: 25
85
+ Среднее: 0.968
86
+ Корреляция с оценкой: 0.363
87
+
88
+ question_type:
89
+ Тип: float64
90
+ Не-NULL: 100
91
+ Среднее: 2.500
92
+ Корреляция с оценкой: 0.146
93
+
94
+ response_adequacy:
95
+ Тип: float64
96
+ Не-NULL: 25
97
+ Среднее: 0.970
98
+ Корреляция с оценкой: 0.327
99
+
100
+ information_seeking:
101
+ Тип: float64
102
+ Не-NULL: 25
103
+ Среднее: 0.920
104
+ Корреляция с оценкой: -0.147
105
+
106
+ descriptive_detail:
107
+ Тип: float64
108
+ Не-NULL: 25
109
+ Среднее: 1.000
110
+ Корреляция с оценкой: nan
111
+
features_description_detailed.txt ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ПОДРОБНОЕ ОПИСАНИЕ ПРИЗНАКОВ
2
+ ==================================================
3
+
4
+ text_length:
5
+ Тип: int64
6
+ Не-NULL: 50
7
+ Среднее: 1676.120
8
+ Std: 1190.330
9
+ Min: 328.000
10
+ Max: 5002.000
11
+
12
+ word_count:
13
+ Тип: int64
14
+ Не-NULL: 50
15
+ Среднее: 265.600
16
+ Std: 196.751
17
+ Min: 46.000
18
+ Max: 820.000
19
+
20
+ sentence_count:
21
+ Тип: int64
22
+ Не-NULL: 50
23
+ Среднее: 47.720
24
+ Std: 39.596
25
+ Min: 1.000
26
+ Max: 157.000
27
+
28
+ avg_word_length:
29
+ Тип: float64
30
+ Не-NULL: 50
31
+ Среднее: 5.156
32
+ Std: 0.386
33
+ Min: 4.443
34
+ Max: 6.397
35
+
36
+ lexical_diversity:
37
+ Тип: float64
38
+ Не-NULL: 50
39
+ Среднее: 0.618
40
+ Std: 0.087
41
+ Min: 0.431
42
+ Max: 0.744
43
+
44
+ has_questions:
45
+ Тип: int64
46
+ Не-NULL: 50
47
+ Среднее: 0.920
48
+ Std: 0.274
49
+ Min: 0.000
50
+ Max: 1.000
51
+
52
+ has_exclamations:
53
+ Тип: int64
54
+ Не-NULL: 50
55
+ Среднее: 0.000
56
+ Std: 0.000
57
+ Min: 0.000
58
+ Max: 0.000
59
+
60
+ keyword_overlap:
61
+ Тип: float64
62
+ Не-NULL: 50
63
+ Среднее: 0.768
64
+ Std: 0.078
65
+ Min: 0.593
66
+ Max: 0.902
67
+
68
+ response_relevance:
69
+ Тип: float64
70
+ Не-NULL: 50
71
+ Среднее: 1.000
72
+ Std: 0.000
73
+ Min: 1.000
74
+ Max: 1.000
75
+
76
+ grammar_quality:
77
+ Тип: float64
78
+ Не-NULL: 50
79
+ Среднее: 0.682
80
+ Std: 0.146
81
+ Min: 0.419
82
+ Max: 0.868
83
+
84
+ has_punctuation:
85
+ Тип: float64
86
+ Не-NULL: 50
87
+ Среднее: 0.940
88
+ Std: 0.240
89
+ Min: 0.000
90
+ Max: 1.000
91
+
92
+ sentence_completeness:
93
+ Тип: float64
94
+ Не-NULL: 50
95
+ Среднее: 0.724
96
+ Std: 0.157
97
+ Min: 0.297
98
+ Max: 1.000
99
+
100
+ has_greeting:
101
+ Тип: float64
102
+ Не-NULL: 50
103
+ Среднее: 0.540
104
+ Std: 0.503
105
+ Min: 0.000
106
+ Max: 1.000
107
+
108
+ has_description:
109
+ Тип: float64
110
+ Не-NULL: 50
111
+ Среднее: 0.540
112
+ Std: 0.503
113
+ Min: 0.000
114
+ Max: 1.000
115
+
116
+ has_connectors:
117
+ Тип: float64
118
+ Не-NULL: 50
119
+ Среднее: 0.500
120
+ Std: 0.505
121
+ Min: 0.000
122
+ Max: 1.000
123
+
124
+ has_emotional_words:
125
+ Тип: float64
126
+ Не-NULL: 50
127
+ Среднее: 0.360
128
+ Std: 0.485
129
+ Min: 0.000
130
+ Max: 1.000
131
+
132
+ style_score:
133
+ Тип: float64
134
+ Не-NULL: 50
135
+ Среднее: 0.467
136
+ Std: 0.213
137
+ Min: 0.000
138
+ Max: 1.000
139
+
140
+ answer_length_sufficiency:
141
+ Тип: float64
142
+ Не-NULL: 50
143
+ Среднее: 1.000
144
+ Std: 0.000
145
+ Min: 1.000
146
+ Max: 1.000
147
+
148
+ content_richness:
149
+ Тип: float64
150
+ Не-NULL: 50
151
+ Среднее: 0.861
152
+ Std: 0.038
153
+ Min: 0.745
154
+ Max: 0.929
155
+
156
+ engagement_level:
157
+ Тип: float64
158
+ Не-NULL: 50
159
+ Среднее: 0.892
160
+ Std: 0.090
161
+ Min: 0.576
162
+ Max: 0.958
163
+
164
+ question_type:
165
+ Тип: float64
166
+ Не-NULL: 50
167
+ Среднее: 2.460
168
+ Std: 1.129
169
+ Min: 1.000
170
+ Max: 4.000
171
+
172
+ composite_quality_score:
173
+ Тип: float64
174
+ Не-NULL: 50
175
+ Среднее: 0.788
176
+ Std: 0.054
177
+ Min: 0.659
178
+ Max: 0.894
179
+
main.py ADDED
File without changes
minimal_app.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import subprocess
3
+ import sys
4
+
5
+
6
+ def install_package(package):
7
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package])
8
+
9
+
10
+ try:
11
+ from transformers import pipeline
12
+ except ImportError:
13
+ st.warning("Устанавливаем transformers...")
14
+ install_package("transformers")
15
+ from transformers import pipeline
16
+
17
+ st.title("Минимальное приложение с Hugging Face")
18
+
19
+
20
+ # Простая модель для теста
21
+ @st.cache_resource
22
+ def load_model():
23
+ try:
24
+ return pipeline("sentiment-analysis")
25
+ except Exception as e:
26
+ st.error(f"Ошибка загрузки модели: {e}")
27
+ return None
28
+
29
+
30
+ model = load_model()
31
+
32
+ if model:
33
+ text = st.text_input("Введите текст:", "I love this!")
34
+
35
+ if st.button("Анализировать") and text:
36
+ result = model(text)[0]
37
+ st.write(f"Результат: {result['label']}")
38
+ st.write(f"Уверенность: {result['score']:.4f}")
39
+ else:
40
+ st.error("Не удалось загрузить модель")
models/catboost_Q1.cbm ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7756476f07583a1134762daef9296d39f6b89c7fac6200a342c3cd1dcabd5a98
3
+ size 2223544
models/catboost_Q2.cbm ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:384cba685992c67888db73aa6e78ddc2d41725079df06186fab61e615c4cf3f2
3
+ size 2225560
models/catboost_Q3.cbm ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:29a7dd613ab06bf185f48e70a4179d83c629f84bba489ddcba151b662c6647fe
3
+ size 2227624
models/catboost_Q4.cbm ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0916b0b97fcdf71e14dd6b0b3e4f4a8d652a0717d87b9173dc094ac558ad1696
3
+ size 2228928
models/catboost_Q4_enhanced.cbm ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:058ddc80f29acf6eab048d425f0ede4f29145969e708daa5e48a94127b809e94
3
+ size 565688
pytest.ini ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ [pytest]
2
+ testpaths = tests
3
+ pythonpath = .
quick_test.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ### **3. `quick_test.py`** (быстрая проверка)
2
+ ```python
3
+ # !/usr/bin/env python3
4
+ """
5
+ Быстрая проверка работы системы
6
+ """
7
+
8
+ import subprocess
9
+ import sys
10
+ import os
11
+
12
+
13
+ def run_command(cmd):
14
+ """Запускает команду и возвращает результат"""
15
+ try:
16
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
17
+ return result.returncode == 0, result.stdout, result.stderr
18
+ except Exception as e:
19
+ return False, "", str(e)
20
+
21
+
22
+ def main():
23
+ print("🚀 БЫСТРАЯ ПРОВЕРКА СИСТЕМЫ")
24
+ print("=" * 50)
25
+
26
+ # 1. Проверяем зависимости
27
+ print("1. Проверка зависимостей...")
28
+ success, out, err = run_command(
29
+ "python -c \"import catboost, fastapi, streamlit; print('✅ Все зависимости установлены')\"")
30
+ if success:
31
+ print(" ✅ Все зависимости установлены")
32
+ else:
33
+ print(" ❌ Ошибка зависимостей:", err)
34
+ return
35
+
36
+ # 2. Проверяем модели
37
+ print("2. Проверка ML моделей...")
38
+ models = ["catboost_Q1.cbm", "catboost_Q2.cbm", "catboost_Q3.cbm", "catboost_Q4.cbm"]
39
+ all_models_exist = all(os.path.exists(f"models/{model}") for model in models)
40
+ if all_models_exist:
41
+ print(" ✅ Все ML модели найдены")
42
+ else:
43
+ print(" ❌ Не все модели найдены")
44
+ return
45
+
46
+ # 3. Проверяем данные
47
+ print("3. Проверка данных...")
48
+ if os.path.exists("data/raw/small.csv"):
49
+ print(" ✅ Тестовые данные найдены")
50
+ else:
51
+ print(" ⚠️ Тестовые данные не найдены")
52
+
53
+ print("\n🎉 СИСТЕМА ГОТОВА К РАБОТЕ!")
54
+ print("Запустите: docker-compose up")
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()
requirements.txt ADDED
Binary file (284 Bytes). View file
 
retrain_q4.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ from catboost import CatBoostRegressor
4
+ import sys
5
+ import os
6
+
7
+ sys.path.append('src')
8
+
9
+ from features_q4 import enhanced_q4_features
10
+ from features import build_baseline_features
11
+ from semantic_features import add_semantic_similarity
12
+ from data_cleaning import prepare_dataframe
13
+
14
+
15
+ def retrain_q4_model():
16
+ print("🔄 Переобучение модели Q4 с улучшенными фичами...")
17
+
18
+ # 1. Загрузи данные
19
+ df = pd.read_csv('data/raw/Данные для кейса.csv', sep=';')
20
+ print(f"📊 Загружено {len(df)} строк")
21
+
22
+ # 2. Подготовь данные только для Q4
23
+ df_clean = prepare_dataframe(df)
24
+ df_q4 = df_clean[df_clean['question_number'] == 4]
25
+ print(f"📋 Q4 данных: {len(df_q4)} строк")
26
+
27
+ # 3. Построй все фичи
28
+ print("🔨 Строим фичи...")
29
+ feats = build_baseline_features(df_q4)
30
+ feats = add_semantic_similarity(feats, verbose=False)
31
+ feats = enhanced_q4_features(feats)
32
+
33
+ # 4. Выдели фичи и целевую переменную
34
+ feature_cols = [c for c in feats.columns if c.startswith('q4_') or c in [
35
+ 'semantic_sim', 'ans_len_words', 'ans_n_sents', 'ans_ttr',
36
+ 'ans_short_sent_rt', 'ans_punct_rt', 'q_len_words'
37
+ ]]
38
+
39
+ X = feats[feature_cols].fillna(0)
40
+ y = feats['score'].fillna(0)
41
+
42
+ print(f"🎯 Фичей: {len(feature_cols)}, Примеров: {len(X)}")
43
+ print(f"📈 Фичи: {feature_cols}")
44
+
45
+ # 5. Обучи новую модель
46
+ print("🤖 Обучаем CatBoost...")
47
+ model = CatBoostRegressor(
48
+ iterations=500,
49
+ learning_rate=0.1,
50
+ depth=6,
51
+ verbose=100,
52
+ random_state=42
53
+ )
54
+
55
+ model.fit(X, y)
56
+
57
+ # 6. Сохрани модель
58
+ model.save_model('models/catboost_Q4_enhanced.cbm')
59
+ print("✅ Модель Q4 переобучена с улучшенными фичами!")
60
+
61
+ # 7. Проверим важность фич
62
+ feature_importance = pd.DataFrame({
63
+ 'feature': feature_cols,
64
+ 'importance': model.get_feature_importance()
65
+ }).sort_values('importance', ascending=False)
66
+
67
+ print("\n📊 Важность фич:")
68
+ print(feature_importance.head(10))
69
+
70
+
71
+ if __name__ == "__main__":
72
+ retrain_q4_model()
run.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import uvicorn
2
+
3
+ if __name__ == "__main__":
4
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
run_predict.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Упрощенный запуск предсказания
4
+ """
5
+ import os
6
+ import sys
7
+
8
+ # Устанавливаем PYTHONPATH
9
+ current_dir = os.path.dirname(os.path.abspath(__file__))
10
+ sys.path.insert(0, current_dir)
11
+
12
+
13
+ def main():
14
+ print("🚀 ЗАПУСК ПРЕДСКАЗАНИЯ")
15
+
16
+ # Импортируем после установки PYTHONPATH
17
+ from src.predict import pipeline_infer
18
+
19
+ # Запускаем предсказание
20
+ input_file = "data/raw/small.csv"
21
+ output_file = "predictions_final.csv"
22
+
23
+ print(f"📁 Входной файл: {input_file}")
24
+ print(f"📁 Выходной файл: {output_file}")
25
+
26
+ pipeline_infer(input_file, output_file)
27
+ print("🎉 ПРЕДСКАЗАНИЕ ЗАВЕРШЕНО!")
28
+
29
+
30
+ if __name__ == "__main__":
31
+ main()
runtime.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ python-3.10
serverless-container.yaml.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: exam-scorer-api
2
+ spec:
3
+ connectivity:
4
+ network_id: default
5
+ containers:
6
+ - name: api
7
+ image: cr.yandex/your-registry-id/exam-scorer:latest
8
+ command:
9
+ - python
10
+ - -m
11
+ - uvicorn
12
+ - app.main:api
13
+ - --host
14
+ - 0.0.0.0
15
+ - --port
16
+ - "8000"
17
+ ports:
18
+ - containerPort: 8000
19
+ protocol: TCP
20
+ resources:
21
+ memory: "2048MB"
22
+ cores: "1"
23
+ probes:
24
+ http:
25
+ path: /health
26
+ port: 8000
27
+ initialDelaySeconds: 10
28
+ periodSeconds: 5
setup.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import subprocess
4
+
5
+
6
+ def setup_environment():
7
+ """Устанавливает PYTHONPATH и возвращает команду для запуска"""
8
+ current_dir = os.path.dirname(os.path.abspath(__file__))
9
+
10
+ # Добавляем в PYTHONPATH
11
+ if current_dir not in sys.path:
12
+ sys.path.insert(0, current_dir)
13
+
14
+ # Устанавливаем переменную окружения для дочерних процессов
15
+ os.environ['PYTHONPATH'] = current_dir + os.pathsep + os.environ.get('PYTHONPATH', '')
16
+
17
+ print(f"✅ PYTHONPATH установлен: {current_dir}")
18
+ return current_dir
19
+
20
+
21
+ if __name__ == "__main__":
22
+ setup_environment()
23
+
24
+ # Теперь можно запускать predict.py
25
+ print("🚀 Запуск predict.py...")
26
+ try:
27
+ from src.predict import pipeline_infer
28
+
29
+ pipeline_infer("data/raw/small.csv", "predictions.csv")
30
+ print("✅ Предсказание завершено!")
31
+ except Exception as e:
32
+ print(f"❌ Ошибка: {e}")
simple_app.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import streamlit as st
3
+ from transformers import pipeline
4
+ import os
5
+
6
+ # Отключаем предупреждения
7
+ os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
8
+
9
+ # Простая конфигурация
10
+ st.set_page_config(
11
+ page_title="AI Model Demo",
12
+ page_icon="🤖",
13
+ layout="wide"
14
+ )
15
+
16
+ # Простой заголовок
17
+ st.title("🤖 Демо AI Моделей")
18
+ st.write("Тестирование моделей машинного обучения")
19
+
20
+ # Боковая панель
21
+ st.sidebar.header("Настройки")
22
+
23
+ # Выбор задачи
24
+ task = st.sidebar.selectbox(
25
+ "Выберите задачу:",
26
+ ["Анализ тональности", "Генерация текста", "Классификация"]
27
+ )
28
+
29
+ # Основной контент
30
+ if task == "Анализ тональности":
31
+ st.header("📊 Анализ тональности текста")
32
+ text = st.text_area("Введите текст:", "Я очень рад этому!")
33
+
34
+ if st.button("Анализировать"):
35
+ with st.spinner("Анализируем..."):
36
+ try:
37
+ classifier = pipeline("sentiment-analysis")
38
+ result = classifier(text)[0]
39
+ st.success(f"Результат: {result['label']}")
40
+ st.info(f"Уверенность: {result['score']:.4f}")
41
+ except Exception as e:
42
+ st.error(f"Ошибка: {e}")
43
+
44
+ elif task == "Генерация текста":
45
+ st.header("✍️ Генерация текста")
46
+ prompt = st.text_area("Введите начало текста:", "Искусственный интеллект")
47
+
48
+ if st.button("Сгенерировать"):
49
+ with st.spinner("Генерируем..."):
50
+ try:
51
+ generator = pipeline("text-generation", model="gpt2")
52
+ result = generator(prompt, max_length=100, num_return_sequences=1)
53
+ st.write("**Результат:**")
54
+ st.write(result[0]['generated_text'])
55
+ except Exception as e:
56
+ st.error(f"Ошибка: {e}")
57
+
58
+ elif task == "Классификация":
59
+ st.header("🏷️ Классификация текста")
60
+ text = st.text_area("Введите текст для классификации:", "Это потрясающий продукт!")
61
+
62
+ if st.button("Классифицировать"):
63
+ with st.spinner("Классифицируем..."):
64
+ try:
65
+ classifier = pipeline("text-classification")
66
+ results = classifier(text)
67
+ st.write("**Результаты:**")
68
+ for result in results:
69
+ st.write(f"- {result['label']}: {result['score']:.4f}")
70
+ except Exception as e:
71
+ st.error(f"Ошибка: {e}")
72
+
73
+ # Информация внизу
74
+ st.sidebar.markdown("---")
75
+ st.sidebar.info("Простое демо для тестирования моделей")
src/__init__.py ADDED
File without changes
src/add_q4_features.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/add_q4_features.py
2
+ from pathlib import Path
3
+ import pandas as pd
4
+ from src.features_q4 import q4_slot_features
5
+
6
+ ROOT = Path(__file__).resolve().parents[1]
7
+ INP = ROOT / "data" / "processed" / "features_with_semantics.csv" # уже есть
8
+ OUT = ROOT / "data" / "processed" / "features_with_semantics_q4.csv"
9
+
10
+ def main():
11
+ df = pd.read_csv(INP, encoding="utf-8-sig")
12
+ df2 = q4_slot_features(df)
13
+ OUT.parent.mkdir(parents=True, exist_ok=True)
14
+ df2.to_csv(OUT, index=False, encoding="utf-8-sig")
15
+ print("✅ Сохранено:", OUT)
16
+ print(df2[[
17
+ "question_number","semantic_sim",
18
+ "q4_slots_covered","q4_answered_personal","q4_non_cyr_ratio","score"
19
+ ]].head())
20
+
21
+ if __name__ == "__main__":
22
+ main()