daswer123 commited on
Commit
f147e1e
·
verified ·
1 Parent(s): 1777cd7

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +217 -0
  2. requirements.txt +8 -0
  3. work.py +435 -0
app.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import os
4
+ from work import LDAAnalyzer
5
+ from datetime import datetime
6
+ import shutil
7
+
8
+ BASE_OUTPUT_DIR = "output"
9
+ os.makedirs(BASE_OUTPUT_DIR, exist_ok=True)
10
+
11
+ def create_output_dir():
12
+ """Создание директории для текущего анализа"""
13
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
14
+ output_dir = os.path.join(BASE_OUTPUT_DIR, timestamp)
15
+ os.makedirs(output_dir, exist_ok=True)
16
+ return output_dir
17
+
18
+ def show_columns(file):
19
+ """Получение списка колонок из загруженного файла"""
20
+ if file is None:
21
+ return gr.Dropdown(
22
+ choices=[],
23
+ value=None,
24
+ interactive=False,
25
+ label="Сначала загрузите файл"
26
+ )
27
+
28
+ try:
29
+ df = pd.read_excel(file.name)
30
+ columns = [f"{i}: {col}" for i, col in enumerate(df.columns)]
31
+ return gr.Dropdown(
32
+ choices=columns,
33
+ value=None,
34
+ interactive=True,
35
+ label="Выберите колонку для анализа"
36
+ )
37
+ except Exception as e:
38
+ return gr.Dropdown(
39
+ choices=[],
40
+ value=None,
41
+ interactive=False,
42
+ label=f"Ошибка чтения файла: {str(e)}"
43
+ )
44
+
45
+ def perform_analysis(file, selected_column, progress=gr.Progress()):
46
+ """Выполнение LDA анализа"""
47
+ if file is None or selected_column is None:
48
+ return ["⚠️ Ошибка: Загрузите файл и выберите колонку",
49
+ None, None, None, None, None]
50
+
51
+ try:
52
+ output_dir = create_output_dir()
53
+ input_file_path = os.path.join(output_dir, "data.xlsx")
54
+ shutil.copy2(file.name, input_file_path)
55
+
56
+ column_idx = int(selected_column.split(":")[0])
57
+
58
+ progress(0, desc="Инициализация...")
59
+ analyzer = LDAAnalyzer(input_file_path, column_idx)
60
+
61
+ # Загрузка данных
62
+ progress(0.2, desc="📂 Загрузка данных...")
63
+ analyzer.load_data()
64
+
65
+ # Подготовка данных
66
+ progress(0.4, desc="🔄 Подготовка данных...")
67
+ analyzer.prepare_data()
68
+
69
+ # Выполнение анализа
70
+ progress(0.6, desc="📊 Выполнение LDA анализа...")
71
+ analyzer.perform_lda()
72
+
73
+ # Получение и подготовка результатов перед сохранением
74
+ progress(0.8, desc="📊 Формирование результатов...")
75
+
76
+ # Получаем матрицы напрямую из анализатора
77
+ confusion_matrix, percentages, accuracy = analyzer.create_confusion_matrix()
78
+ coefficients = analyzer.get_coefficients()
79
+
80
+ # Подготовка данных для отображения
81
+
82
+ # 1. Матрица классификации
83
+ df1 = confusion_matrix.copy()
84
+ df1.index = [f"{i+1}.00" for i in range(len(df1))]
85
+ df1.insert(0, "Исходный", df1.index)
86
+ df1.insert(1, "Количество", df1["Всего"])
87
+
88
+ # 2. Проценты классификации
89
+ df2 = pd.DataFrame(percentages)
90
+ df2.index = [f"{i+1}.00" for i in range(len(df2))]
91
+ df2.columns = df1.columns[2:] # Используем те же заголовки
92
+ df2.insert(0, "Исходный", df2.index)
93
+ df2.insert(1, "Количество", confusion_matrix["Всего"])
94
+
95
+ # Добавляем строку с примечанием
96
+ note_row = pd.DataFrame({
97
+ "Исходный": f"* Примечание: {accuracy:.1f}% наблюдений классифицированы правильно.",
98
+ "Количество": "",
99
+ }, index=[""])
100
+ df2 = pd.concat([df2, note_row])
101
+
102
+ # 3. Коэффициенты
103
+ df3 = coefficients.copy()
104
+ df3.index.name = "Переменная"
105
+ df3 = df3.reset_index()
106
+
107
+ # Сохранение результатов
108
+ progress(0.9, desc="💾 Сохранение результатов...")
109
+ analyzer.save_results(output_dir)
110
+
111
+ # Пути к файлам
112
+ results_file = os.path.join(output_dir, 'lda_results.xlsx')
113
+ plot_file = os.path.join(output_dir, 'lda_visualization.png')
114
+
115
+ progress(1.0, desc="✅ Готово!")
116
+ return [
117
+ f"✅ Анализ успешно завершен!\nРезультаты сохранены в: {output_dir}",
118
+ df1,
119
+ df2,
120
+ df3,
121
+ plot_file,
122
+ results_file
123
+ ]
124
+
125
+ except Exception as e:
126
+ error_msg = f"❌ Ошибка при выполнении анализа: {str(e)}"
127
+ print(error_msg) # для отладки
128
+ return [error_msg, None, None, None, None, None]
129
+
130
+ with gr.Blocks(title="LDA Анализ", theme=gr.themes.Soft()) as demo:
131
+ gr.Markdown("""
132
+ # 📊 LDA Анализ
133
+ ### Загрузите Excel файл и выберите колонку для анализа
134
+ """)
135
+
136
+ with gr.Row():
137
+ with gr.Column(scale=1):
138
+ file_input = gr.File(
139
+ label="📑 Excel файл",
140
+ file_types=[".xlsx", ".xls"],
141
+ type="filepath"
142
+ )
143
+
144
+ with gr.Column(scale=1):
145
+ column_select = gr.Dropdown(
146
+ label="🎯 Выберите колонку",
147
+ choices=[],
148
+ interactive=False
149
+ )
150
+
151
+ with gr.Column(scale=1):
152
+ start_btn = gr.Button(
153
+ "▶️ Начать анализ",
154
+ variant="primary"
155
+ )
156
+
157
+ status = gr.Markdown("💡 Ожидание начала анализа...")
158
+
159
+ with gr.Tabs() as tabs:
160
+ with gr.Tab("📋 Матрица классификации"):
161
+ df1 = gr.Dataframe(
162
+ label="Матрица классификации",
163
+ headers=None,
164
+ datatype="number",
165
+ wrap=True,
166
+ )
167
+
168
+ with gr.Tab("📊 Проценты"):
169
+ df2 = gr.Dataframe(
170
+ label="Проценты классификации",
171
+ headers=None,
172
+ datatype="number",
173
+ wrap=True
174
+ )
175
+
176
+ with gr.Tab("📈 Коэффициенты"):
177
+ df3 = gr.Dataframe(
178
+ label="Коэффициенты функций",
179
+ headers=None,
180
+ datatype="number",
181
+ wrap=True
182
+ )
183
+
184
+ with gr.Tab("📉 Визуализация"):
185
+ with gr.Column():
186
+ results_plot = gr.Image(
187
+ label="График результатов",
188
+ show_label=True
189
+ )
190
+
191
+ with gr.Tab("📁 Файлы"):
192
+ with gr.Column():
193
+ results_file = gr.File(
194
+ label="📊 Скачать полный отчет",
195
+ show_label=True
196
+ )
197
+
198
+ # Обработчики событий
199
+ file_input.change(
200
+ fn=show_columns,
201
+ inputs=[file_input],
202
+ outputs=[column_select]
203
+ )
204
+
205
+ start_btn.click(
206
+ fn=perform_analysis,
207
+ inputs=[file_input, column_select],
208
+ outputs=[
209
+ status,
210
+ df1, df2, df3,
211
+ results_plot, results_file
212
+ ],
213
+ show_progress=True
214
+ )
215
+
216
+ if __name__ == "__main__":
217
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ numpy>=1.20.0
2
+ scikit-learn>=0.24.0
3
+ matplotlib>=3.3.0
4
+ seaborn>=0.11.0
5
+ xlsxwriter>=3.0.0
6
+ openpyxl>=3.0.0
7
+ gradio>=5.0.0
8
+ pandas==2.2.3
work.py ADDED
@@ -0,0 +1,435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
4
+ from sklearn.preprocessing import StandardScaler, LabelEncoder
5
+ from sklearn.decomposition import PCA
6
+ from sklearn.feature_selection import SelectKBest, f_classif
7
+ import matplotlib.pyplot as plt
8
+ import seaborn as sns
9
+ import logging
10
+ import os
11
+ from datetime import datetime
12
+ from typing import Dict, Tuple, List, Optional, Any
13
+ import xlsxwriter
14
+
15
+ class LDAAnalyzer:
16
+ """
17
+ Класс для выполнения линейного дискриминантного анализа (LDA)
18
+ с расширенной функциональностью и форматированным выводом результатов
19
+ """
20
+
21
+ def __init__(self, input_file: str, target_column: int):
22
+ """
23
+ Инициализация анализатора LDA
24
+
25
+ Args:
26
+ input_file (str): Путь к входному файлу Excel
27
+ target_column (int): Номер столбца для классификации
28
+ """
29
+ self.input_file = input_file
30
+ self.target_column = target_column
31
+ self.data = None
32
+ self.X = None
33
+ self.y = None
34
+ self.X_transformed = None
35
+ self.lda = None
36
+ self.scaler = StandardScaler()
37
+ self.label_encoder = LabelEncoder()
38
+ self.feature_names = None
39
+
40
+ # Настройка логирования
41
+ logging.basicConfig(
42
+ level=logging.INFO,
43
+ format='%(asctime)s - %(levelname)s - %(message)s',
44
+ handlers=[
45
+ logging.FileHandler('lda_analysis.log'),
46
+ logging.StreamHandler()
47
+ ]
48
+ )
49
+ self.logger = logging.getLogger(__name__)
50
+
51
+ # Цветовая схема для визуализации
52
+ self.colors = ['lightblue', 'green', 'purple', 'yellow',
53
+ 'red', 'orange', 'cyan', 'brown', 'pink']
54
+
55
+ self.logger.info(f"Инициализация LDA анализатора с файлом: {input_file}")
56
+
57
+ def validate_data(self) -> None:
58
+ """Валидация входных данных"""
59
+ if self.data is None:
60
+ raise ValueError("Данные не загружены")
61
+
62
+ # Проверка размерности
63
+ if self.data.shape[0] < 30:
64
+ raise ValueError("Недостаточно наблюдений (минимум 30)")
65
+
66
+ # Проверка пропущенных значений
67
+ if self.data.isnull().any().any():
68
+ raise ValueError("Обнаружены пропущенные значения")
69
+
70
+ # Проверка типов данных
71
+ numeric_cols = self.data.select_dtypes(include=[np.number]).columns
72
+ if len(numeric_cols) < self.data.shape[1] - 1: # -1 для целевой переменной
73
+ raise ValueError("Обнаружены нечисловые признаки")
74
+
75
+ def load_data(self) -> None:
76
+ """Загрузка данных из Excel файла"""
77
+ try:
78
+ self.logger.info("Загрузка данных...")
79
+
80
+ # Загрузка данных
81
+ self.data = pd.read_excel(self.input_file)
82
+
83
+ # Преобразование имен колонок
84
+ self.data.columns = [str(col) for col in self.data.columns]
85
+
86
+ # Попытка преобразовать все колонки (кроме целевой) в числовой формат
87
+ for col in self.data.columns:
88
+ if self.data.columns.get_loc(col) != self.target_column:
89
+ try:
90
+ self.data[col] = pd.to_numeric(self.data[col], errors='coerce')
91
+ except Exception as e:
92
+ self.logger.warning(f"Не удалось преобразовать колонку {col} в числовой формат: {str(e)}")
93
+
94
+ self.validate_data()
95
+ self.logger.info(f"Данные загружены. Размерность: {self.data.shape}")
96
+
97
+ except Exception as e:
98
+ self.logger.error(f"Ошибка при загрузке данных: {str(e)}")
99
+ raise
100
+
101
+
102
+
103
+ def prepare_data(self) -> None:
104
+ """Подготовка данных для анализа"""
105
+ try:
106
+ self.logger.info("Подготовка данных...")
107
+
108
+ # Разделение на признаки и целевую переменную
109
+ X = self.data.drop(self.data.columns[self.target_column], axis=1)
110
+ y = self.data.iloc[:, self.target_column]
111
+
112
+ # Преобразование имен колонок в строки
113
+ X.columns = X.columns.astype(str)
114
+
115
+ # Кодирование меток классов
116
+ self.y = self.label_encoder.fit_transform(y) + 1
117
+
118
+ # Преобразование в числовой формат
119
+ X = X.apply(pd.to_numeric, errors='coerce')
120
+
121
+ # Проверка на пропущенные значения после преобразования
122
+ if X.isnull().any().any():
123
+ raise ValueError("После преобразования в числовой формат появились пропущенные значения")
124
+
125
+ # Стандартизация признаков
126
+ self.X = self.scaler.fit_transform(X)
127
+
128
+ # Проверка количества классов и наблюдений в каждом классе
129
+ class_counts = pd.Series(self.y).value_counts()
130
+ if (class_counts < 5).any():
131
+ self.logger.warning("Некоторые классы имеют менее 5 наблюдений")
132
+
133
+ self.logger.info(f"Данные подготовлены. X: {self.X.shape}, y: {self.y.shape}")
134
+ self.logger.info(f"Количество классов: {len(np.unique(self.y))}")
135
+
136
+ except Exception as e:
137
+ self.logger.error(f"Ошибка при подготовке данных: {str(e)}")
138
+ raise
139
+
140
+ def perform_lda(self) -> None:
141
+ """Выполнение LDA анализа"""
142
+ try:
143
+ self.logger.info("Выполнение LDA анализа...")
144
+
145
+ # Инициализация и обучение LDA
146
+ self.lda = LinearDiscriminantAnalysis(solver='svd')
147
+ self.X_transformed = self.lda.fit_transform(self.X, self.y)
148
+
149
+ # Оценка качества модели
150
+ accuracy = self.lda.score(self.X, self.y)
151
+ self.logger.info(f"Общая точность модели: {accuracy:.3f}")
152
+
153
+ except Exception as e:
154
+ self.logger.error(f"Ошибка при выполнении LDA: {str(e)}")
155
+ raise
156
+
157
+ def create_confusion_matrix(self) -> Tuple[pd.DataFrame, List[List[str]], float]:
158
+ """
159
+ Создание матрицы ошибок и расчет процентов классификации
160
+
161
+ Returns:
162
+ tuple: (матрица ошибок, проценты, общая точность)
163
+ """
164
+ try:
165
+ self.logger.info("Создание матрицы ошибок...")
166
+
167
+ # Получение предсказаний
168
+ y_pred = self.lda.predict(self.X)
169
+
170
+ # Создание матрицы ошибок
171
+ classes = sorted(np.unique(self.y))
172
+ n_classes = len(classes)
173
+ confusion_matrix = np.zeros((n_classes, n_classes))
174
+
175
+ for i in range(len(self.y)):
176
+ confusion_matrix[self.y[i]-1][y_pred[i]-1] += 1
177
+
178
+ # Создание DataFrame для матрицы ошибок
179
+ columns = [f"{i+1}.00" for i in range(n_classes)]
180
+ index = [f"{i+1}.00" for i in range(n_classes)]
181
+
182
+ df_confusion = pd.DataFrame(confusion_matrix, columns=columns, index=index)
183
+
184
+ # Добавление столбца "Всего"
185
+ df_confusion['Всего'] = df_confusion.sum(axis=1)
186
+
187
+ # Расчет процентов
188
+ percentages = np.zeros((n_classes, n_classes + 1)) # +1 для столбца "Всего"
189
+ for i in range(n_classes):
190
+ row_sum = confusion_matrix[i].sum()
191
+ if row_sum > 0:
192
+ percentages[i, :-1] = (confusion_matrix[i] / row_sum) * 100
193
+ percentages[i, -1] = 100.0
194
+
195
+ # Форматирование процентов
196
+ percentage_rows = []
197
+ for row in percentages:
198
+ formatted_row = [f"{x:.1f}" for x in row]
199
+ percentage_rows.append(formatted_row)
200
+
201
+ # Расчет общей точности
202
+ accuracy = (np.sum(np.diag(confusion_matrix)) / np.sum(confusion_matrix)) * 100
203
+
204
+ self.logger.info(f"Процент правильной классификации: {accuracy:.1f}%")
205
+
206
+ return df_confusion, percentage_rows, accuracy
207
+
208
+ except Exception as e:
209
+ self.logger.error(f"Ошибка при создании матрицы ошибок: {str(e)}")
210
+ raise
211
+
212
+ def get_coefficients(self) -> pd.DataFrame:
213
+ """
214
+ Получение коэффициентов дискриминантных функций
215
+
216
+ Returns:
217
+ pd.DataFrame: таблица коэффициентов
218
+ """
219
+ try:
220
+ self.logger.info("Получение коэфф��циентов...")
221
+
222
+ # Получение коэффициентов и размерностей
223
+ n_features = self.X.shape[1]
224
+ n_classes = len(np.unique(self.y))
225
+ n_components = min(n_classes - 1, n_features)
226
+
227
+ # Создание списка имен переменных
228
+ var_names = [f"VAR{str(i+1).zfill(5)}" for i in range(n_features)]
229
+
230
+ # Создание DataFrame с коэффициентами
231
+ coef_data = []
232
+ for i in range(n_components):
233
+ row_data = {}
234
+ for j, var_name in enumerate(var_names):
235
+ row_data[var_name] = self.lda.coef_[i][j]
236
+ coef_data.append(row_data)
237
+
238
+ df_coef = pd.DataFrame(coef_data, index=[f"Функция {i+1}" for i in range(n_components)])
239
+
240
+ # Добавление константы (intercept)
241
+ const_data = {}
242
+ for j, var_name in enumerate(var_names):
243
+ const_data[var_name] = self.lda.intercept_[j] if j < len(self.lda.intercept_) else 0.0
244
+
245
+ const_df = pd.DataFrame([const_data], index=['Константа'])
246
+
247
+ # Объединение коэффициентов и константы
248
+ df_coef = pd.concat([df_coef, const_df])
249
+
250
+ # Округление значений
251
+ df_coef = df_coef.round(3)
252
+
253
+ self.logger.info("Коэффициенты получены")
254
+ return df_coef
255
+
256
+ except Exception as e:
257
+ self.logger.error(f"Ошибка при получении коэффициентов: {str(e)}")
258
+ raise
259
+
260
+ def create_visualization(self) -> plt.Figure:
261
+ """
262
+ Создание визуализации результатов
263
+
264
+ Returns:
265
+ plt.Figure: объект графика
266
+ """
267
+ try:
268
+ self.logger.info("Создание визуализации...")
269
+
270
+ fig = plt.figure(figsize=(12, 8))
271
+
272
+ # Построение точек для каждого класса
273
+ for class_num in np.unique(self.y):
274
+ mask = self.y == class_num
275
+ plt.scatter(
276
+ self.X_transformed[mask, 0],
277
+ self.X_transformed[mask, 1] if self.X_transformed.shape[1] > 1
278
+ else np.zeros_like(self.X_transformed[mask, 0]),
279
+ c=[self.colors[(class_num-1) % len(self.colors)]],
280
+ label=f'Группа {class_num}',
281
+ alpha=0.7
282
+ )
283
+
284
+ # Добавление центроидов
285
+ centroid = np.mean(self.X_transformed[mask, :2], axis=0)
286
+ plt.scatter(
287
+ centroid[0],
288
+ centroid[1] if self.X_transformed.shape[1] > 1 else 0,
289
+ c='black',
290
+ marker='s',
291
+ s=100
292
+ )
293
+ plt.annotate(
294
+ f'{class_num}',
295
+ (centroid[0], centroid[1]),
296
+ xytext=(5, 5),
297
+ textcoords='offset points',
298
+ fontsize=10,
299
+ bbox=dict(facecolor='white', edgecolor='none', alpha=0.7)
300
+ )
301
+
302
+ plt.xlabel('Первая каноническая функция')
303
+ plt.ylabel('Вторая каноническая функция')
304
+ plt.title('Канонические дискриминантные функции')
305
+ plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
306
+ plt.grid(True, alpha=0.3)
307
+ plt.tight_layout()
308
+
309
+ self.logger.info("Визуализация создана")
310
+ return fig
311
+
312
+ except Exception as e:
313
+ self.logger.error(f"Ошибка при создании визуализации: {str(e)}")
314
+ raise
315
+
316
+ def save_results(self, output_dir: str) -> None:
317
+ """
318
+ Сохранение всех результатов анализа
319
+
320
+ Args:
321
+ output_dir (str): директория для сохранения результатов
322
+ """
323
+ try:
324
+ self.logger.info(f"Сохранение результатов в {output_dir}...")
325
+
326
+ # Создание директории если её нет
327
+ os.makedirs(output_dir, exist_ok=True)
328
+
329
+ # Получение результатов
330
+ confusion_matrix, percentages, accuracy = self.create_confusion_matrix()
331
+ coefficients = self.get_coefficients()
332
+
333
+ # Сохранен��е в Excel
334
+ excel_path = os.path.join(output_dir, 'lda_results.xlsx')
335
+ with pd.ExcelWriter(excel_path, engine='xlsxwriter') as writer:
336
+ workbook = writer.book
337
+
338
+ # Форматы для Excel
339
+ header_format = workbook.add_format({
340
+ 'bold': True,
341
+ 'align': 'center',
342
+ 'valign': 'vcenter',
343
+ 'bg_color': '#D9D9D9',
344
+ 'border': 1
345
+ })
346
+
347
+ cell_format = workbook.add_format({
348
+ 'align': 'center',
349
+ 'border': 1
350
+ })
351
+
352
+ number_format = workbook.add_format({
353
+ 'align': 'center',
354
+ 'border': 1,
355
+ 'num_format': '0.000'
356
+ })
357
+
358
+ # 1. Матрица классификации
359
+ worksheet1 = workbook.add_worksheet('Матрица классификации')
360
+
361
+ # Записываем заголовки
362
+ headers = ['Исходный', 'Количество'] + \
363
+ [f'{i+1}.00' for i in range(len(confusion_matrix.columns)-1)] + \
364
+ ['Всего']
365
+ for col, header in enumerate(headers):
366
+ worksheet1.write(0, col, header, header_format)
367
+ worksheet1.set_column(col, col, 15)
368
+
369
+ # Записываем данные
370
+ for i, (index, row) in enumerate(confusion_matrix.iterrows()):
371
+ worksheet1.write(i+1, 0, index, cell_format)
372
+ worksheet1.write(i+1, 1, row['Всего'], cell_format)
373
+ for j, val in enumerate(row):
374
+ worksheet1.write(i+1, j+2, val, cell_format)
375
+
376
+ # 2. Проценты классификации
377
+ worksheet2 = workbook.add_worksheet('Проценты')
378
+
379
+ # Заголовки
380
+ for col, header in enumerate(headers):
381
+ worksheet2.write(0, col, header, header_format)
382
+ worksheet2.set_column(col, col, 15)
383
+
384
+ # Данные процентов
385
+ for i, row in enumerate(percentages):
386
+ worksheet2.write(i+1, 0, f"{i+1}.00", cell_format)
387
+ worksheet2.write(i+1, 1, confusion_matrix.iloc[i]['Всего'], cell_format)
388
+ for j, val in enumerate(row):
389
+ worksheet2.write(i+1, j+2, float(val.replace(',', '.')), number_format)
390
+
391
+ # Примечание
392
+ note_row = len(percentages) + 2
393
+ worksheet2.write(
394
+ note_row, 0,
395
+ f'* Примечание: {accuracy:.1f}% исходных сгруппированных наблюдений '
396
+ f'классифицированы правильно.',
397
+ workbook.add_format({'bold': True})
398
+ )
399
+
400
+ # 3. Коэффициенты функций
401
+ worksheet3 = workbook.add_worksheet('Коэффициенты')
402
+
403
+ # Записываем заголовки коэффициентов
404
+ worksheet3.write(0, 0, 'Переменная', header_format)
405
+ for i, col in enumerate(coefficients.columns):
406
+ worksheet3.write(0, i+1, col, header_format)
407
+ worksheet3.set_column(i+1, i+1, 15)
408
+
409
+ # Записываем данные коэффициентов
410
+ for i, (index, row) in enumerate(coefficients.iterrows()):
411
+ worksheet3.write(i+1, 0, index, cell_format)
412
+ for j, val in enumerate(row):
413
+ worksheet3.write(i+1, j+1, val, number_format)
414
+
415
+ # Добавляем примечание к коэффициентам
416
+ worksheet3.write(
417
+ len(coefficients)+1, 0,
418
+ '*Нестандартизованные коэффициенты',
419
+ workbook.add_format({'bold': True, 'italic': True})
420
+ )
421
+
422
+ # Сохранение визуализации
423
+ fig = self.create_visualization()
424
+ fig.savefig(
425
+ os.path.join(output_dir, 'lda_visualization.png'),
426
+ bbox_inches='tight',
427
+ dpi=300
428
+ )
429
+ plt.close(fig)
430
+
431
+ self.logger.info("Результаты успешно сохранены")
432
+
433
+ except Exception as e:
434
+ self.logger.error(f"Ошибка при сохранении результатов: {str(e)}")
435
+ raise