670 lines
31 KiB
Python
670 lines
31 KiB
Python
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout,
|
||
QPushButton, QLabel, QWidget, QMessageBox,
|
||
QTableWidget, QTableWidgetItem, QGroupBox,
|
||
QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit,
|
||
QTextEdit, QTabWidget, QFormLayout, QCheckBox,
|
||
QScrollArea, QFileDialog)
|
||
from PyQt5.QtCore import Qt
|
||
from PyQt5.QtGui import QColor, QFont
|
||
import numpy as np
|
||
import csv
|
||
|
||
|
||
class ExperimentDesignWindow(QMainWindow):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.setWindowTitle("Планирование эксперимента - Цифровой помощник биохимика")
|
||
self.setGeometry(200, 100, 1200, 800)
|
||
self.setStyleSheet("""
|
||
QMainWindow {
|
||
background-color: #f5f5f5;
|
||
}
|
||
QGroupBox {
|
||
font-weight: bold;
|
||
border: 2px solid #ccc;
|
||
border-radius: 5px;
|
||
margin-top: 10px;
|
||
padding-top: 10px;
|
||
}
|
||
QGroupBox::title {
|
||
subcontrol-origin: margin;
|
||
left: 10px;
|
||
padding: 0 5px 0 5px;
|
||
color: #1565C0;
|
||
}
|
||
QPushButton {
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #45a049;
|
||
}
|
||
QTableWidget {
|
||
gridline-color: #ddd;
|
||
}
|
||
QHeaderView::section {
|
||
background-color: #1565C0;
|
||
color: white;
|
||
padding: 8px;
|
||
border: none;
|
||
font-weight: bold;
|
||
}
|
||
""")
|
||
self._init_ui()
|
||
|
||
def _init_ui(self):
|
||
central_widget = QWidget()
|
||
self.setCentralWidget(central_widget)
|
||
layout = QVBoxLayout(central_widget)
|
||
|
||
# Заголовок
|
||
title_label = QLabel("📈 Планирование полнофакторного эксперимента (DoE)")
|
||
title_font = QFont()
|
||
title_font.setPointSize(18)
|
||
title_font.setBold(True)
|
||
title_label.setFont(title_font)
|
||
title_label.setAlignment(Qt.AlignCenter)
|
||
title_label.setStyleSheet("color: #2E7D32;")
|
||
layout.addWidget(title_label)
|
||
|
||
# Вкладки
|
||
tabs = QTabWidget()
|
||
|
||
# Вкладка 1: Параметры эксперимента
|
||
params_tab = QWidget()
|
||
params_layout = QVBoxLayout(params_tab)
|
||
|
||
# Группа факторов
|
||
factors_group = QGroupBox("Факторы эксперимента (независимые переменные)")
|
||
factors_layout = QVBoxLayout()
|
||
|
||
info_label = QLabel("Определите факторы, которые влияют на ваш эксперимент:")
|
||
info_label.setStyleSheet("color: #555; font-weight: normal;")
|
||
factors_layout.addWidget(info_label)
|
||
|
||
self.factors_table = QTableWidget()
|
||
# Новый порядок колонок: Фактор, Нулевой уровень, Шаг, Верхний (+1), Нижний (-1), Единица измерения
|
||
self.factors_table.setColumnCount(6)
|
||
self.factors_table.setHorizontalHeaderLabels(["Фактор", "Нулевой уровень (0)", "Шаг",
|
||
"Верхний уровень (+1)", "Нижний уровень (-1)", "Единица измерения"])
|
||
self.factors_table.setRowCount(2)
|
||
|
||
# Пример данных
|
||
sample_factors = [
|
||
["Температура", "31", "6", "37", "25", "°C"],
|
||
["pH", "7.0", "0.5", "7.5", "6.5", ""],
|
||
]
|
||
|
||
for i, factor in enumerate(sample_factors):
|
||
for j, value in enumerate(factor):
|
||
item = QTableWidgetItem(value)
|
||
if j in [3, 4]: # Верхний и нижний уровень - только для чтения
|
||
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
|
||
item.setBackground(QColor(240, 240, 240))
|
||
self.factors_table.setItem(i, j, item)
|
||
|
||
# Настройка ширины колонок
|
||
self.factors_table.setColumnWidth(0, 150)
|
||
self.factors_table.setColumnWidth(1, 120)
|
||
self.factors_table.setColumnWidth(2, 80)
|
||
self.factors_table.setColumnWidth(3, 120)
|
||
self.factors_table.setColumnWidth(4, 120)
|
||
self.factors_table.setColumnWidth(5, 100)
|
||
|
||
# Подключаем сигналы изменения ячеек
|
||
self.factors_table.itemChanged.connect(self.on_factor_changed)
|
||
|
||
factors_layout.addWidget(self.factors_table)
|
||
|
||
# Кнопки для управления факторами
|
||
factor_buttons = QHBoxLayout()
|
||
add_factor_btn = QPushButton("+ Добавить фактор")
|
||
add_factor_btn.clicked.connect(self.add_factor_row)
|
||
remove_factor_btn = QPushButton("- Удалить последний")
|
||
remove_factor_btn.clicked.connect(self.remove_factor_row)
|
||
factor_buttons.addWidget(add_factor_btn)
|
||
factor_buttons.addWidget(remove_factor_btn)
|
||
factor_buttons.addStretch()
|
||
factors_layout.addLayout(factor_buttons)
|
||
|
||
factors_group.setLayout(factors_layout)
|
||
params_layout.addWidget(factors_group)
|
||
|
||
# Группа настроек эксперимента
|
||
settings_group = QGroupBox("Настройки эксперимента")
|
||
settings_layout = QHBoxLayout()
|
||
|
||
center_layout = QHBoxLayout()
|
||
center_layout.addWidget(QLabel("Количество центральных точек:"))
|
||
self.center_points_spin = QSpinBox()
|
||
self.center_points_spin.setRange(0, 10)
|
||
self.center_points_spin.setValue(3)
|
||
self.center_points_spin.setToolTip("Повторные опыты в нулевой точке для оценки дисперсии")
|
||
center_layout.addWidget(self.center_points_spin)
|
||
settings_layout.addLayout(center_layout)
|
||
|
||
self.randomize_check = QCheckBox("Рэндомизировать порядок опытов")
|
||
self.randomize_check.setChecked(True)
|
||
settings_layout.addWidget(self.randomize_check)
|
||
|
||
settings_layout.addStretch()
|
||
settings_group.setLayout(settings_layout)
|
||
params_layout.addWidget(settings_group)
|
||
|
||
# Группа откликов
|
||
responses_group = QGroupBox("Отклики (зависимые переменные)")
|
||
responses_layout = QVBoxLayout()
|
||
|
||
self.responses_table = QTableWidget()
|
||
self.responses_table.setColumnCount(2)
|
||
self.responses_table.setHorizontalHeaderLabels(["Отклик", "Единица измерения"])
|
||
self.responses_table.setRowCount(2)
|
||
|
||
sample_responses = [
|
||
["Оптическая плотность (OD600)", ""],
|
||
["Концентрация целевого продукта", "мг/мл"]
|
||
]
|
||
|
||
for i, response in enumerate(sample_responses):
|
||
for j, value in enumerate(response):
|
||
self.responses_table.setItem(i, j, QTableWidgetItem(value))
|
||
|
||
responses_layout.addWidget(self.responses_table)
|
||
|
||
response_buttons = QHBoxLayout()
|
||
add_response_btn = QPushButton("+ Добавить отклик")
|
||
add_response_btn.clicked.connect(self.add_response_row)
|
||
remove_response_btn = QPushButton("- Удалить последний")
|
||
remove_response_btn.clicked.connect(self.remove_response_row)
|
||
response_buttons.addWidget(add_response_btn)
|
||
response_buttons.addWidget(remove_response_btn)
|
||
response_buttons.addStretch()
|
||
responses_layout.addLayout(response_buttons)
|
||
|
||
responses_group.setLayout(responses_layout)
|
||
params_layout.addWidget(responses_group)
|
||
|
||
tabs.addTab(params_tab, "📝 Параметры эксперимента")
|
||
|
||
# Вкладка 2: Матрица планирования
|
||
plan_tab = QWidget()
|
||
plan_layout = QVBoxLayout(plan_tab)
|
||
|
||
plan_info = QLabel("Полнофакторный план эксперимента с центральными точками")
|
||
plan_info.setAlignment(Qt.AlignCenter)
|
||
plan_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;")
|
||
plan_layout.addWidget(plan_info)
|
||
|
||
scroll = QScrollArea()
|
||
scroll.setWidgetResizable(True)
|
||
matrix_widget = QWidget()
|
||
matrix_layout = QVBoxLayout(matrix_widget)
|
||
|
||
self.design_matrix = QTableWidget()
|
||
matrix_layout.addWidget(self.design_matrix)
|
||
|
||
scroll.setWidget(matrix_widget)
|
||
plan_layout.addWidget(scroll)
|
||
|
||
buttons_layout = QHBoxLayout()
|
||
|
||
generate_btn = QPushButton("🔄 Сгенерировать план эксперимента")
|
||
generate_btn.clicked.connect(self.generate_design_matrix)
|
||
buttons_layout.addWidget(generate_btn)
|
||
|
||
export_btn = QPushButton("📊 Экспорт в CSV")
|
||
export_btn.clicked.connect(self.export_to_csv)
|
||
buttons_layout.addWidget(export_btn)
|
||
|
||
buttons_layout.addStretch()
|
||
plan_layout.addLayout(buttons_layout)
|
||
|
||
self.plan_info_label = QLabel("")
|
||
self.plan_info_label.setStyleSheet("color: #666; font-size: 12px; padding: 5px;")
|
||
plan_layout.addWidget(self.plan_info_label)
|
||
|
||
tabs.addTab(plan_tab, "📊 Матрица планирования")
|
||
|
||
# Вкладка 3: Анализ результатов
|
||
analysis_tab = QWidget()
|
||
analysis_layout = QVBoxLayout(analysis_tab)
|
||
|
||
analysis_info = QLabel("Введите результаты экспериментов для анализа")
|
||
analysis_info.setAlignment(Qt.AlignCenter)
|
||
analysis_info.setStyleSheet("font-weight: bold; font-size: 14px; padding: 10px;")
|
||
analysis_layout.addWidget(analysis_info)
|
||
|
||
self.results_table = QTableWidget()
|
||
analysis_layout.addWidget(self.results_table)
|
||
|
||
analyze_btn = QPushButton("📈 Провести регрессионный анализ")
|
||
analyze_btn.clicked.connect(self.perform_analysis)
|
||
analysis_layout.addWidget(analyze_btn)
|
||
|
||
self.analysis_output = QTextEdit()
|
||
self.analysis_output.setReadOnly(True)
|
||
self.analysis_output.setMaximumHeight(200)
|
||
analysis_layout.addWidget(self.analysis_output)
|
||
|
||
tabs.addTab(analysis_tab, "📐 Анализ результатов")
|
||
|
||
layout.addWidget(tabs)
|
||
|
||
btn_layout = QHBoxLayout()
|
||
|
||
save_btn = QPushButton("💿 Сохранить проект")
|
||
save_btn.clicked.connect(self.show_placeholder_message)
|
||
btn_layout.addWidget(save_btn)
|
||
|
||
load_btn = QPushButton("📂 Загрузить проект")
|
||
load_btn.clicked.connect(self.show_placeholder_message)
|
||
btn_layout.addWidget(load_btn)
|
||
|
||
btn_layout.addStretch()
|
||
|
||
close_btn = QPushButton("❌ Закрыть")
|
||
close_btn.clicked.connect(self.close)
|
||
btn_layout.addWidget(close_btn)
|
||
|
||
layout.addLayout(btn_layout)
|
||
|
||
def on_factor_changed(self, item):
|
||
"""При изменении нулевого уровня или шага пересчитываем верхний и нижний уровни"""
|
||
row = item.row()
|
||
col = item.column()
|
||
|
||
# Если изменили нулевой уровень (колонка 1) или шаг (колонка 2)
|
||
if col in [1, 2]:
|
||
try:
|
||
center = float(self.factors_table.item(row, 1).text()) if self.factors_table.item(row, 1) else 0
|
||
step = float(self.factors_table.item(row, 2).text()) if self.factors_table.item(row, 2) else 0
|
||
|
||
# Пересчитываем верхний и нижний уровни
|
||
high = center + step
|
||
low = center - step
|
||
|
||
# Обновляем ячейки (временно отключаем сигнал)
|
||
self.factors_table.blockSignals(True)
|
||
|
||
high_item = self.factors_table.item(row, 3)
|
||
if high_item:
|
||
high_item.setText(f"{high:.3f}".rstrip('0').rstrip('.'))
|
||
|
||
low_item = self.factors_table.item(row, 4)
|
||
if low_item:
|
||
low_item.setText(f"{low:.3f}".rstrip('0').rstrip('.'))
|
||
|
||
self.factors_table.blockSignals(False)
|
||
except ValueError:
|
||
pass
|
||
|
||
def add_factor_row(self):
|
||
"""Добавляет строку для нового фактора"""
|
||
row = self.factors_table.rowCount()
|
||
self.factors_table.insertRow(row)
|
||
self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}"))
|
||
self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
|
||
self.factors_table.setItem(row, 2, QTableWidgetItem("1"))
|
||
|
||
# Верхний и нижний уровни - только для чтения
|
||
high_item = QTableWidgetItem("1")
|
||
high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable)
|
||
high_item.setBackground(QColor(240, 240, 240))
|
||
self.factors_table.setItem(row, 3, high_item)
|
||
|
||
low_item = QTableWidgetItem("-1")
|
||
low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable)
|
||
low_item.setBackground(QColor(240, 240, 240))
|
||
self.factors_table.setItem(row, 4, low_item)
|
||
|
||
self.factors_table.setItem(row, 5, QTableWidgetItem(""))
|
||
|
||
def remove_factor_row(self):
|
||
"""Удаляет последнюю строку факторов"""
|
||
if self.factors_table.rowCount() > 1:
|
||
self.factors_table.removeRow(self.factors_table.rowCount() - 1)
|
||
|
||
def add_response_row(self):
|
||
"""Добавляет строку для нового отклика"""
|
||
row = self.responses_table.rowCount()
|
||
self.responses_table.insertRow(row)
|
||
self.responses_table.setItem(row, 0, QTableWidgetItem(f"Отклик_{row+1}"))
|
||
self.responses_table.setItem(row, 1, QTableWidgetItem(""))
|
||
|
||
def remove_response_row(self):
|
||
"""Удаляет последнюю строку откликов"""
|
||
if self.responses_table.rowCount() > 1:
|
||
self.responses_table.removeRow(self.responses_table.rowCount() - 1)
|
||
|
||
def get_factors_data(self):
|
||
"""Получает данные о факторах"""
|
||
factors = []
|
||
for row in range(self.factors_table.rowCount()):
|
||
try:
|
||
factor = {
|
||
'name': self.factors_table.item(row, 0).text() if self.factors_table.item(row, 0) else "",
|
||
'center': float(self.factors_table.item(row, 1).text()) if self.factors_table.item(row, 1) else 0,
|
||
'step': float(self.factors_table.item(row, 2).text()) if self.factors_table.item(row, 2) else 0,
|
||
'high': float(self.factors_table.item(row, 3).text()) if self.factors_table.item(row, 3) else 0,
|
||
'low': float(self.factors_table.item(row, 4).text()) if self.factors_table.item(row, 4) else 0,
|
||
'unit': self.factors_table.item(row, 5).text() if self.factors_table.item(row, 5) else ""
|
||
}
|
||
factors.append(factor)
|
||
except (ValueError, AttributeError):
|
||
continue
|
||
return factors
|
||
|
||
def calculate_factorial_design(self, factors):
|
||
"""Генерирует полнофакторный план 2^k с центральными точками"""
|
||
k = len(factors)
|
||
if k == 0:
|
||
return []
|
||
|
||
# Генерируем 2^k комбинаций
|
||
n_factorial = 2 ** k
|
||
design = []
|
||
|
||
for i in range(n_factorial):
|
||
experiment = {}
|
||
for j in range(k):
|
||
# Кодированный уровень (-1 или +1)
|
||
coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1
|
||
|
||
# Переводим в натуральные значения
|
||
if coded_level == -1:
|
||
natural_value = factors[j]['low']
|
||
else:
|
||
natural_value = factors[j]['high']
|
||
|
||
experiment[f"Фактор_{j+1}"] = {
|
||
'coded': coded_level,
|
||
'natural': natural_value,
|
||
'name': factors[j]['name'],
|
||
'unit': factors[j]['unit']
|
||
}
|
||
design.append(experiment)
|
||
|
||
# Добавляем центральные точки
|
||
n_center = self.center_points_spin.value()
|
||
for i in range(n_center):
|
||
center_experiment = {}
|
||
for j in range(k):
|
||
center_experiment[f"Фактор_{j+1}"] = {
|
||
'coded': 0,
|
||
'natural': factors[j]['center'],
|
||
'name': factors[j]['name'],
|
||
'unit': factors[j]['unit']
|
||
}
|
||
center_experiment['is_center'] = True
|
||
center_experiment['center_num'] = i + 1
|
||
design.append(center_experiment)
|
||
|
||
# Рэндомизация порядка опытов
|
||
if self.randomize_check.isChecked():
|
||
import random
|
||
random.shuffle(design)
|
||
|
||
return design
|
||
|
||
def generate_design_matrix(self):
|
||
"""Генерирует и отображает матрицу планирования"""
|
||
factors = self.get_factors_data()
|
||
|
||
if len(factors) == 0:
|
||
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!")
|
||
return
|
||
|
||
# Генерируем план
|
||
design = self.calculate_factorial_design(factors)
|
||
|
||
# Количество опытов
|
||
n_experiments = len(design)
|
||
n_factors = len(factors)
|
||
|
||
# Настройка таблицы
|
||
self.design_matrix.setRowCount(n_experiments)
|
||
self.design_matrix.setColumnCount(n_factors + 2)
|
||
|
||
# Заголовки
|
||
headers = ["№ опыта"] + [f["name"] for f in factors] + ["Тип точки"]
|
||
self.design_matrix.setHorizontalHeaderLabels(headers)
|
||
|
||
# Заполняем матрицу
|
||
for exp_idx, experiment in enumerate(design):
|
||
# Номер опыта
|
||
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
|
||
|
||
# Значения факторов
|
||
for factor_idx in range(n_factors):
|
||
factor_key = f"Фактор_{factor_idx + 1}"
|
||
value = experiment[factor_key]['natural']
|
||
unit = factors[factor_idx]['unit']
|
||
|
||
# Форматируем значение
|
||
if isinstance(value, float):
|
||
if value == int(value):
|
||
display_value = str(int(value))
|
||
else:
|
||
display_value = f"{value:.3f}".rstrip('0').rstrip('.')
|
||
else:
|
||
display_value = str(value)
|
||
|
||
if unit:
|
||
display_value += f" {unit}"
|
||
|
||
item = QTableWidgetItem(display_value)
|
||
|
||
# Подсветка центральных точек
|
||
if experiment.get('is_center', False):
|
||
item.setBackground(QColor(255, 255, 200))
|
||
|
||
self.design_matrix.setItem(exp_idx, factor_idx + 1, item)
|
||
|
||
# Тип точки
|
||
if experiment.get('is_center', False):
|
||
type_item = QTableWidgetItem(f"Центральная #{experiment['center_num']}")
|
||
type_item.setBackground(QColor(255, 255, 200))
|
||
else:
|
||
# Показываем комбинацию уровней
|
||
levels = []
|
||
for factor_idx in range(n_factors):
|
||
factor_key = f"Фактор_{factor_idx + 1}"
|
||
coded = experiment[factor_key]['coded']
|
||
levels.append("+" if coded == 1 else "-")
|
||
type_item = QTableWidgetItem(f"Факторная ({''.join(levels)})")
|
||
|
||
self.design_matrix.setItem(exp_idx, n_factors + 1, type_item)
|
||
|
||
# Настройка ширины колонок
|
||
self.design_matrix.resizeColumnsToContents()
|
||
|
||
# Обновляем информацию
|
||
n_factorial = 2 ** n_factors
|
||
n_center = self.center_points_spin.value()
|
||
self.plan_info_label.setText(
|
||
f"📊 План эксперимента: {n_factorial} факторных точек + {n_center} центральных точек = {n_experiments} опытов"
|
||
)
|
||
|
||
# Настраиваем таблицу для ввода результатов
|
||
self.setup_results_table(n_experiments)
|
||
|
||
QMessageBox.information(self, "Успех",
|
||
f"Сгенерирован план для {n_factors} факторов\n"
|
||
f"Факторных точек: {n_factorial}\n"
|
||
f"Центральных точек: {n_center}\n"
|
||
f"Всего опытов: {n_experiments}\n\n"
|
||
f"Центральные точки позволяют оценить дисперсию воспроизводимости")
|
||
|
||
def setup_results_table(self, n_experiments):
|
||
"""Настраивает таблицу для ввода результатов"""
|
||
n_responses = self.responses_table.rowCount()
|
||
|
||
self.results_table.setRowCount(n_experiments)
|
||
self.results_table.setColumnCount(n_responses + 1)
|
||
|
||
# Заголовки
|
||
headers = ["№ опыта"] + [self.responses_table.item(i, 0).text() if self.responses_table.item(i, 0) else f"Отклик_{i+1}"
|
||
for i in range(n_responses)]
|
||
self.results_table.setHorizontalHeaderLabels(headers)
|
||
|
||
# Заполняем номера опытов
|
||
for i in range(n_experiments):
|
||
self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1)))
|
||
|
||
# Настройка ширины колонок
|
||
self.results_table.setColumnWidth(0, 80)
|
||
for i in range(n_responses):
|
||
self.results_table.setColumnWidth(i + 1, 150)
|
||
|
||
def export_to_csv(self):
|
||
"""Экспортирует матрицу планирования в CSV"""
|
||
if self.design_matrix.rowCount() == 0:
|
||
QMessageBox.warning(self, "Предупреждение", "Сначала сгенерируйте план эксперимента!")
|
||
return
|
||
|
||
filename, _ = QFileDialog.getSaveFileName(
|
||
self,
|
||
"Сохранить план эксперимента",
|
||
"",
|
||
"CSV Files (*.csv);;All Files (*)"
|
||
)
|
||
|
||
if filename:
|
||
if not filename.lower().endswith('.csv'):
|
||
filename += '.csv'
|
||
|
||
try:
|
||
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
|
||
writer = csv.writer(f)
|
||
|
||
# Заголовки
|
||
headers = []
|
||
for j in range(self.design_matrix.columnCount()):
|
||
header_item = self.design_matrix.horizontalHeaderItem(j)
|
||
headers.append(header_item.text() if header_item else f"Колонка_{j+1}")
|
||
writer.writerow(headers)
|
||
|
||
# Данные
|
||
for i in range(self.design_matrix.rowCount()):
|
||
row = []
|
||
for j in range(self.design_matrix.columnCount()):
|
||
item = self.design_matrix.item(i, j)
|
||
row.append(item.text() if item else "")
|
||
writer.writerow(row)
|
||
|
||
QMessageBox.information(self, "Успех", f"План эксперимента сохранен в {filename}")
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить файл: {str(e)}")
|
||
|
||
def perform_analysis(self):
|
||
"""Проводит регрессионный анализ"""
|
||
n_responses = self.responses_table.rowCount()
|
||
|
||
if n_responses == 0:
|
||
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один отклик!")
|
||
return
|
||
|
||
# Собираем результаты
|
||
results = []
|
||
for i in range(self.results_table.rowCount()):
|
||
row_results = []
|
||
for j in range(1, self.results_table.columnCount()):
|
||
item = self.results_table.item(i, j)
|
||
if item and item.text():
|
||
try:
|
||
row_results.append(float(item.text()))
|
||
except ValueError:
|
||
row_results.append(None)
|
||
else:
|
||
row_results.append(None)
|
||
results.append(row_results)
|
||
|
||
# Проверяем, что все результаты введены
|
||
missing = False
|
||
for i, row in enumerate(results):
|
||
for j, val in enumerate(row):
|
||
if val is None:
|
||
missing = True
|
||
self.analysis_output.setText(f"❌ Ошибка: Не введены результаты для опыта {i+1}, отклик {j+1}")
|
||
return
|
||
|
||
# Анализ
|
||
self.analysis_output.clear()
|
||
self.analysis_output.append("=" * 60)
|
||
self.analysis_output.append("РЕЗУЛЬТАТЫ РЕГРЕССИОННОГО АНАЛИЗА")
|
||
self.analysis_output.append("=" * 60)
|
||
|
||
factors = self.get_factors_data()
|
||
design = self.calculate_factorial_design(factors)
|
||
|
||
for resp_idx in range(n_responses):
|
||
resp_name = self.responses_table.item(resp_idx, 0).text()
|
||
self.analysis_output.append(f"\n📊 Отклик: {resp_name}")
|
||
self.analysis_output.append("-" * 40)
|
||
|
||
# Собираем значения отклика
|
||
y_values = [results[i][resp_idx] for i in range(len(results))]
|
||
|
||
# Среднее значение
|
||
mean_y = np.mean(y_values)
|
||
self.analysis_output.append(f"Среднее значение: {mean_y:.4f}")
|
||
|
||
# Дисперсия
|
||
variance = np.var(y_values, ddof=1) if len(y_values) > 1 else 0
|
||
self.analysis_output.append(f"Общая дисперсия: {variance:.4f}")
|
||
|
||
# Стандартное отклонение
|
||
std_dev = np.std(y_values, ddof=1) if len(y_values) > 1 else 0
|
||
self.analysis_output.append(f"Стандартное отклонение: {std_dev:.4f}")
|
||
|
||
# Коэффициент вариации
|
||
if mean_y != 0:
|
||
cv = (std_dev / mean_y) * 100
|
||
self.analysis_output.append(f"Коэффициент вариации: {cv:.2f}%")
|
||
|
||
# Разделяем факторные и центральные точки
|
||
factorial_y = []
|
||
center_y = []
|
||
for i, exp in enumerate(design):
|
||
if exp.get('is_center', False):
|
||
center_y.append(y_values[i])
|
||
else:
|
||
factorial_y.append(y_values[i])
|
||
|
||
if len(center_y) > 1:
|
||
center_variance = np.var(center_y, ddof=1)
|
||
self.analysis_output.append(f"\nЦентральные точки (n={len(center_y)}):")
|
||
self.analysis_output.append(f" Среднее: {np.mean(center_y):.4f}")
|
||
self.analysis_output.append(f" Дисперсия воспроизводимости: {center_variance:.4f}")
|
||
self.analysis_output.append(f" Стандартное отклонение: {np.std(center_y, ddof=1):.4f}")
|
||
|
||
# Критерий Фишера для проверки адекватности
|
||
if len(factorial_y) > 0 and center_variance > 0:
|
||
factorial_variance = np.var(factorial_y, ddof=1) if len(factorial_y) > 1 else 0
|
||
if factorial_variance > 0:
|
||
fisher = max(factorial_variance, center_variance) / min(factorial_variance, center_variance)
|
||
self.analysis_output.append(f"\nКритерий Фишера (F-отношение): {fisher:.4f}")
|
||
if fisher < 4.0:
|
||
self.analysis_output.append(" ✅ Модель адекватна экспериментальным данным")
|
||
else:
|
||
self.analysis_output.append(" ⚠️ Модель может быть неадекватна, требуется проверка")
|
||
|
||
self.analysis_output.append("\n" + "=" * 60)
|
||
self.analysis_output.append("✅ Анализ завершен")
|
||
|
||
def show_placeholder_message(self):
|
||
"""Показывает сообщение о том, что функция в разработке"""
|
||
QMessageBox.information(
|
||
self,
|
||
"В разработке",
|
||
"🧪 Функция в стадии разработки!\n\nБлижайшие обновления:\n"
|
||
"✅ Экспорт в Excel\n"
|
||
"✅ Построение поверхностей отклика\n"
|
||
"✅ Графики главных эффектов\n"
|
||
"✅ Полный регрессионный анализ"
|
||
)
|