Spaces:
Sleeping
Sleeping
Upload 3 files
Browse files- app.py +217 -0
- requirements.txt +8 -0
- 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
|