# analysis.py """ Модуль анализа экспериментальных данных. Выполняет: - множественную линейную регрессию (МНК), - проверку значимости коэффициентов (t-критерий), - дисперсионный анализ (ANOVA) с разложением остатка на чистую ошибку и неадекватность (если есть центр), - оценку воспроизводимости по центральным точкам, - исключение незначимых факторов с пересчётом модели, - интерпретацию результатов на естественном языке. """ import numpy as np from core.table_io import handle_table_output from core.interpreter import full_interpretation from core.statistics import ( compute_regression, get_center_indices, compute_center_stats, chi2_confidence_interval, get_t_critical, regression_summary, anova_regression ) def analysis_menu(mixture, design, responses): """ Главное меню подсистемы анализа. Аргументы: mixture (Mixture): текущий состав смеси (нужен для имён факторов). design (ExperimentDesign): объект с шагами и матрицей плана. responses (list): список откликов, каждый как {'name': str, 'values': list}. Действия: - проверяет наличие плана и откликов; - определяет активные факторы (шаг > 0); - запрашивает уровень значимости alpha; - строит матрицу X (единичный столбец + активные факторы); - позволяет выбрать один отклик или все для последовательного анализа. """ # Проверка наличия данных if not design.plan or not responses: print("Нет данных для анализа: план или отклики пусты.") input("Нажмите Enter для продолжения...") return # Активные факторы – те, для которых задан шаг > 0 active_indices = design._active_indices() k = len(active_indices) if k < 2: print("Для анализа необходимо минимум 2 активных фактора.") input("Нажмите Enter для продолжения...") return # Имена активных факторов берём из смеси (индекс = idx + 1, т.к. 0 – растворитель) factor_names = [] for idx in active_indices: ing_index = idx + 1 if ing_index < len(mixture.ings): factor_names.append(mixture.ings[ing_index].name) else: factor_names.append(f"x{idx+1}") # Ввод уровня значимости alpha print("\nВведите уровень значимости alpha (например, 0.05, 0.01, 0.001):") while True: try: alpha_input = input("alpha (по умолчанию 0.05): ").strip() if alpha_input == "": alpha = 0.05 else: alpha = float(alpha_input) if alpha <= 0 or alpha >= 1: print("alpha должна быть в интервале (0, 1).") continue break except ValueError: print("Введите число.") # Матрица X: столбец единиц + столбцы активных факторов (в кодировке ±1) plan_matrix = np.array([[row[j] for j in active_indices] for row in design.plan]) N = plan_matrix.shape[0] X = np.hstack([np.ones((N, 1)), plan_matrix]) # размер N x (k+1) df_res = N - (k + 1) # степени свободы остатка для полной модели # Цикл выбора отклика while True: print("\n" + "=" * 50) print("АНАЛИЗ ЗНАЧЕНИЙ") print(f"Уровень значимости α = {alpha}") print("Доступные отклики:") for idx, resp in enumerate(responses, start=1): print(f"{idx}. {resp['name']}") print(f"{len(responses)+1}. Анализировать все отклики") print("(Enter - выход в главное меню)") print("=" * 50) choice = input("Выберите отклик для анализа: ").strip() if choice == "": break try: idx = int(choice) - 1 if idx < 0: print("Неверный номер.") continue if idx < len(responses): # Анализ одного отклика analyze_single_response(responses[idx], X, k, df_res, design, alpha, factor_names) input("Нажмите Enter для продолжения...") elif idx == len(responses): # Анализ всех откликов подряд for resp in responses: print(f"\n--- Анализ отклика '{resp['name']}' ---") analyze_single_response(resp, X, k, df_res, design, alpha, factor_names) input("Нажмите Enter для продолжения...") else: print("Неверный номер.") except ValueError: print("Введите число.") def analyze_single_response(response, X, k, df_res, design, alpha, factor_names): """ Проводит полный анализ одного отклика. Этапы: 1. Расчёт регрессии методом наименьших квадратов (МНК). 2. Вывод регрессионной статистики: R², скорректированный R², F-критерий, коэффициенты, их стандартные ошибки и t-статистики. 3. Дисперсионный анализ (ANOVA) с разложением сумм квадратов. Если есть центральные точки (>=2), остаток разбивается на чистую ошибку и неадекватность (lack-of-fit). 4. Анализ центральных точек – оценка воспроизводимости. 5. Опция исключения незначимых факторов (p > alpha) и пересчёт упрощённой модели. 6. Интерпретация результатов на естественном языке. Аргументы: response (dict): {'name': str, 'values': list} X (np.ndarray): матрица регрессоров (с единичным столбцом) k (int): число активных факторов df_res (int): степени свободы остатка (N - k - 1) design (ExperimentDesign): объект с планом alpha (float): уровень значимости factor_names (list): имена факторов (без "Свободный член") """ # Преобразуем значения отклика в массив numpy y = np.array(response["values"]) N = len(y) if N != X.shape[0]: print(f"Ошибка: число значений отклика ({N}) не соответствует числу опытов ({X.shape[0]})") return # ---------- Шаг 1: Расчёт регрессии ---------- reg = compute_regression(X, y) beta = reg["beta"] # коэффициенты (включая свободный член) se = reg["se"] # стандартные ошибки коэффициентов t_stats = reg["t_stats"] # t-статистики s2_res = reg["s2_res"] # дисперсия остатков df_res_actual = reg["df_res"] # степени свободы остатка (N - p) y_pred = reg["y_pred"] # предсказанные значения модели # Проверка на вырожденность матрицы if np.isnan(beta).any(): print("Ошибка при расчёте регрессии: матрица вырождена.") return # ---------- Шаг 2: Вспомогательная функция для вывода регрессионной статистики ---------- def print_and_get_tcrit(beta, se, t_stats, df_res, y_pred, y, k, factor_names, alpha): """ Выводит основную регрессионную статистику и возвращает критическое t. """ # Расчёт R², скорр. R² и F-критерия summary = regression_summary(y, y_pred, k, df_res) r2 = summary["r2"] r2_adj = summary["r2_adj"] f_stat = summary["f_stat"] f_pvalue = summary["f_pvalue"] print("\n=== Регрессионный анализ ===") print(f"Число опытов: {len(y)}") print(f"Число факторов: {k}") print(f"Степени свободы остатков: {df_res}") # Стандартная ошибка регрессии (корень из дисперсии остатков) print(f"Стандартная ошибка регрессии: {np.sqrt(s2_res) if not np.isnan(s2_res) else np.nan:.6f}") print(f"Множественный R: {np.sqrt(r2) if not np.isnan(r2) else np.nan:.6f}") print(f"R-квадрат: {r2:.6f}") print(f"Скорректированный R-квадрат: {r2_adj:.6f}") print(f"F-статистика: {f_stat:.4f}, p-value: {f_pvalue:.6f}") # Критическое значение t для двустороннего критерия t_crit = get_t_critical(df_res, alpha=alpha) print("\nКоэффициенты регрессии:") headers = ["Коэффициент", "Значение", "Станд. ошибка", "t-статистика", f"|t| > t_крит({alpha})?"] rows = [] coef_names = ["Свободный член"] + factor_names for i, name in enumerate(coef_names): sig = "Да" if (not np.isnan(t_stats[i]) and abs(t_stats[i]) > t_crit and not np.isnan(t_crit)) else "Нет" rows.append([ name, f"{beta[i]:.6f}", f"{se[i]:.6f}", f"{t_stats[i]:.4f}", sig ]) # Вывод таблицы с возможностью сохранения/копирования handle_table_output(headers, rows, title="", ask_each_time=True) if not np.isnan(t_crit): print(f"Критическое значение t (alpha={alpha}, df={df_res}): {t_crit:.4f}") # Возвращаем summary для дальнейшего использования return t_crit, summary # ---------- Шаг 2 (выполнение): вывод статистики для полной модели ---------- t_crit, summary_full = print_and_get_tcrit(beta, se, t_stats, df_res_actual, y_pred, y, k, factor_names, alpha) # Извлекаем показатели качества r2 = summary_full["r2"] r2_adj = summary_full["r2_adj"] f_stat = summary_full["f_stat"] f_pvalue = summary_full["f_pvalue"] # ---------- Шаг 3: Дисперсионный анализ (ANOVA) ---------- # Определяем индексы строк плана, соответствующие центральным точкам (все активные факторы = 0) center_indices = get_center_indices(design.plan, design._active_indices()) # Выполняем ANOVA (если центр есть, разлагаем остаток) anova = anova_regression(y, y_pred, X, center_indices if len(center_indices) >= 2 else None) print("\n=== Дисперсионный анализ (ANOVA) ===") anova_headers = ["Источник", "SS", "df", "MS", "F", "p-value"] anova_rows = [] # Регрессия anova_rows.append([ "Регрессия", f"{anova['SSR']:.6f}", f"{anova['df_reg']}", f"{anova['MSR']:.6f}", f"{anova['F_reg']:.4f}", f"{anova['p_reg']:.6f}" ]) # Остаток anova_rows.append([ "Остаток", f"{anova['SSE']:.6f}", f"{anova['df_res']}", f"{anova['MSE']:.6f}", "-", "-" ]) # Если есть центральные точки (>=2), добавляем разложение остатка if len(center_indices) >= 2: anova_rows.append([ " Чистая ошибка", f"{anova['SSPE']:.6f}", f"{anova['df_pe']}", f"{anova['MS_pe']:.6f}", "-", "-" ]) anova_rows.append([ " Неадекватность", f"{anova['SSLOF']:.6f}", f"{anova['df_lof']}", f"{anova['MS_lof']:.6f}", f"{anova['F_lof']:.4f}", f"{anova['p_lof']:.6f}" ]) # Итого (общая сумма квадратов) anova_rows.append([ "Итого", f"{anova['SST']:.6f}", f"{len(y)-1}", "-", "-", "-" ]) handle_table_output(anova_headers, anova_rows, title="", ask_each_time=True) # ---------- Шаг 4: Анализ центральных точек (воспроизводимость) ---------- # Инициализируем переменные для центра center_mean = None center_var = None if len(center_indices) >= 2: y_center = y[center_indices] center_mean, center_var = compute_center_stats(y_center) print("\n=== Анализ центральных точек ===") print(f"Количество центральных точек: {len(center_indices)}") print(f"Среднее значение отклика в центре: {center_mean:.6f}") print(f"Дисперсия в центре (несмещённая): {center_var:.6f}") print(f"Стандартное отклонение: {np.sqrt(center_var):.6f}") # Доверительный интервал для дисперсии (хи-квадрат) lower, upper = chi2_confidence_interval(center_var, len(center_indices), alpha=alpha) if not np.isnan(lower): print(f"Доверительный интервал для дисперсии (уровень доверия {1-alpha}): [{lower:.6f}, {upper:.6f}]") else: print("\nНедостаточно центральных точек (нужно минимум 2).") # ---------- Шаг 5: Опция исключения незначимых факторов ---------- if k > 0 and not np.isnan(t_crit): print("\nХотите исключить незначимые факторы и пересчитать модель?") choice = input("(y/n, по умолчанию n): ").strip().lower() if choice == 'y': # Маска: первый столбец (свободный член) всегда включаем significant = [True] + [abs(t_stats[i]) > t_crit for i in range(1, k+1)] # Если все факторы значимы, упрощённая модель не отличается if all(significant): print("Все факторы значимы. Упрощённая модель не отличается от полной.") else: # Выбираем только значимые столбцы X_new = X[:, significant] # Пересчитываем регрессию reg_new = compute_regression(X_new, y) beta_new = reg_new["beta"] se_new = reg_new["se"] t_stats_new = reg_new["t_stats"] df_res_new = reg_new["df_res"] y_pred_new = reg_new["y_pred"] if np.isnan(beta_new).any(): print("Ошибка при пересчёте упрощённой модели.") else: # Обновляем список имён факторов factor_names_new = ["Свободный член"] + [factor_names[i-1] for i in range(1, k+1) if significant[i]] k_new = len(factor_names_new) - 1 print("\n=== Упрощённая модель (исключены незначимые факторы) ===") # Выводим статистику для упрощённой модели print_and_get_tcrit(beta_new, se_new, t_stats_new, df_res_new, y_pred_new, y, k_new, factor_names_new[1:], alpha) print("\nСовет: для использования упрощённой модели в крутом восхождении установите шаги для исключённых факторов равными 0 в планировании эксперимента.") # ---------- Шаг 6: Интерпретация результатов ---------- # Создаём полный список имён (включая свободный член) для интерпретатора factor_names_full = ["Свободный член"] + factor_names # Формируем полное заключение с помощью интерпретатора interpretation = full_interpretation( r2=r2, r2_adj=r2_adj, f_stat=f_stat, f_pvalue=f_pvalue, beta=beta, t_stats=t_stats, p_values=None, # пока не вычисляем p-values для коэффициентов factor_names=factor_names_full, # <-- передаём полный список anova=anova, center_mean=center_mean, center_var=center_var, n_center=len(center_indices) if len(center_indices) >= 2 else 0, alpha=alpha ) print("\n" + interpretation)