Spaces:
Build error
Build error
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
|