From 901a5e5c7be2137d76fe0f5e5644384dc31b7b14 Mon Sep 17 00:00:00 2001 From: Artemiy Date: Fri, 24 Jul 2026 00:52:40 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BF=D0=BE=D1=8F=D1=81=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BA=20=D0=B0=D0=BD=D0=B0=D0=BB=D0=B8=D0=B7=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- analysis.py | 84 ++++++++++---- core/interpreter.py | 268 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 328 insertions(+), 24 deletions(-) create mode 100644 core/interpreter.py diff --git a/analysis.py b/analysis.py index 96d4959..4d39c1e 100644 --- a/analysis.py +++ b/analysis.py @@ -7,11 +7,13 @@ - проверку значимости коэффициентов (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, @@ -19,7 +21,7 @@ from core.statistics import ( chi2_confidence_interval, get_t_critical, regression_summary, - anova_regression # функция ANOVA, добавленная в statistics.py + anova_regression ) @@ -134,6 +136,7 @@ def analyze_single_response(response, X, k, df_res, design, alpha, factor_names) и неадекватность (lack-of-fit). 4. Анализ центральных точек – оценка воспроизводимости. 5. Опция исключения незначимых факторов (p > alpha) и пересчёт упрощённой модели. + 6. Интерпретация результатов на естественном языке. Аргументы: response (dict): {'name': str, 'values': list} @@ -142,7 +145,7 @@ def analyze_single_response(response, X, k, df_res, design, alpha, factor_names) df_res (int): степени свободы остатка (N - k - 1) design (ExperimentDesign): объект с планом alpha (float): уровень значимости - factor_names (list): имена факторов + factor_names (list): имена факторов (без "Свободный член") """ # Преобразуем значения отклика в массив numpy y = np.array(response["values"]) @@ -209,10 +212,18 @@ def analyze_single_response(response, X, k, df_res, design, alpha, factor_names) if not np.isnan(t_crit): print(f"Критическое значение t (alpha={alpha}, df={df_res}): {t_crit:.4f}") - return t_crit + + # Возвращаем summary для дальнейшего использования + return t_crit, summary # ---------- Шаг 2 (выполнение): вывод статистики для полной модели ---------- - t_crit = print_and_get_tcrit(beta, se, t_stats, df_res_actual, y_pred, y, k, factor_names, alpha) + 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) @@ -275,6 +286,10 @@ def analyze_single_response(response, X, k, df_res, design, alpha, factor_names) 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) @@ -302,27 +317,48 @@ def analyze_single_response(response, X, k, df_res, design, alpha, factor_names) # Если все факторы значимы, упрощённая модель не отличается if all(significant): print("Все факторы значимы. Упрощённая модель не отличается от полной.") - return + 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"] - # Выбираем только значимые столбцы - 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 - if np.isnan(beta_new).any(): - print("Ошибка при пересчёте упрощённой модели.") - return + 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 в планировании эксперимента.") - # Обновляем список имён факторов - factor_names_new = ["Свободный член"] + [factor_names[i-1] for i in range(1, k+1) if significant[i]] - k_new = len(factor_names_new) - 1 + # ---------- Шаг 6: Интерпретация результатов ---------- + # Создаём полный список имён (включая свободный член) для интерпретатора + factor_names_full = ["Свободный член"] + factor_names - 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) + # Формируем полное заключение с помощью интерпретатора + 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Совет: для использования упрощённой модели в крутом восхождении установите шаги для исключённых факторов равными 0 в планировании эксперимента.") + print("\n" + interpretation) diff --git a/core/interpreter.py b/core/interpreter.py new file mode 100644 index 0000000..6abb96d --- /dev/null +++ b/core/interpreter.py @@ -0,0 +1,268 @@ +# interpreter.py +""" +Модуль интерпретации результатов статистического анализа. +Выдаёт содержательные комментарии на русском языке о качестве модели, +значимости факторов, адекватности и рекомендациях по дальнейшим действиям. +""" + +import numpy as np + + +def interpret_regression(r2, r2_adj, f_stat, f_pvalue, alpha): + """ + Интерпретирует качество регрессионной модели. + + Аргументы: + r2 (float): коэффициент детерминации + r2_adj (float): скорректированный R² + f_stat (float): F-статистика + f_pvalue (float): p-value для F-критерия + alpha (float): уровень значимости + + Возвращает: + str: текстовое заключение + """ + parts = [] + + # ----- Оценка значимости модели в целом ----- + if f_pvalue < alpha: + parts.append("✅ Модель статистически значима (p < α).") + else: + parts.append("❌ Модель статистически незначима (p ≥ α).") + + # ----- Оценка R² ----- + if r2 >= 0.9: + parts.append(f"📊 R² = {r2:.3f} — очень высокий, модель объясняет более 90% вариации.") + elif r2 >= 0.7: + parts.append(f"📊 R² = {r2:.3f} — хороший, модель объясняет {r2*100:.1f}% вариации.") + elif r2 >= 0.5: + parts.append(f"📊 R² = {r2:.3f} — умеренный, модель объясняет около {r2*100:.1f}% вариации.") + else: + parts.append(f"📊 R² = {r2:.3f} — низкий, модель плохо объясняет данные.") + + # ----- Сравнение R² и скорректированного R² ----- + diff = r2 - r2_adj + if diff > 0.1: + parts.append(f"⚠️ Разница между R² и скорректированным R² велика ({diff:.3f}) — возможно, модель переобучена или содержит лишние факторы.") + elif diff > 0.05: + parts.append(f"ℹ️ Разница между R² и скорректированным R² составляет {diff:.3f} — стоит проверить, все ли факторы значимы.") + else: + parts.append(f"✅ R² и скорректированный R² близки — модель хорошо сбалансирована.") + + return "\n".join(parts) + + +def interpret_coefficients(beta, t_stats, p_values, factor_names, alpha): + """ + Интерпретирует значимость коэффициентов регрессии. + + Аргументы: + beta (list): коэффициенты (включая свободный член) + t_stats (list): t-статистики для каждого коэффициента + p_values (list или None): p-values для каждого коэффициента + factor_names (list): имена факторов (первый должен быть "Свободный член") + alpha (float): уровень значимости + + Возвращает: + str: текстовое заключение + """ + parts = [] + significant = [] + not_significant = [] + + # Проверяем, что factor_names содержит "Свободный член" + if factor_names[0] != "Свободный член": + # Если нет, добавляем + factor_names = ["Свободный член"] + factor_names + + for i, name in enumerate(factor_names): + if i == 0: + continue # пропускаем свободный член + if i >= len(t_stats): + break + if p_values is not None and p_values[i] < alpha: + significant.append(name) + elif p_values is None and abs(t_stats[i]) > 2.0: # грубая оценка + significant.append(name) + else: + not_significant.append(name) + + if not significant: + parts.append("❌ Ни один фактор не является статистически значимым.") + else: + parts.append(f"✅ Значимые факторы: {', '.join(significant)}.") + + if not_significant: + parts.append(f"⚠️ Незначимые факторы: {', '.join(not_significant)}. Рекомендуется исключить их из модели.") + + # ----- Оценка силы влияния (по величине коэффициентов) ----- + if len(significant) >= 2: + coefs = {name: abs(beta[i]) for i, name in enumerate(factor_names) if name in significant} + if coefs: + max_name = max(coefs, key=coefs.get) + max_val = coefs[max_name] + parts.append(f"💪 Самый сильный фактор — {max_name} (коэффициент {max_val:.4f}).") + + return "\n".join(parts) + + +def interpret_anova(anova, alpha): + """ + Интерпретирует результаты дисперсионного анализа (ANOVA). + + Аргументы: + anova (dict): результат функции anova_regression + alpha (float): уровень значимости + + Возвращает: + str: текстовое заключение + """ + parts = [] + + # ----- Проверка адекватности (lack-of-fit) ----- + if not np.isnan(anova.get("F_lof", np.nan)): + p_lof = anova["p_lof"] + if p_lof < alpha: + parts.append(f"⚠️ Модель НЕ АДЕКВАТНА (p_lof = {p_lof:.4f} < {alpha}). Линейная модель плохо описывает данные — вероятно, присутствует кривизна.") + parts.append(" 🔄 Рекомендуется перейти к квадратичной модели (ЦКП, Бокса-Бенкена).") + else: + parts.append(f"✅ Модель адекватна (p_lof = {p_lof:.4f} ≥ {alpha}). Линейное приближение достаточно.") + else: + parts.append("ℹ️ Недостаточно центральных точек для оценки адекватности (lack-of-fit).") + + # ----- Сравнение дисперсий ----- + if not np.isnan(anova.get("MS_pe", np.nan)) and not np.isnan(anova.get("MS_lof", np.nan)): + ms_pe = anova["MS_pe"] + ms_lof = anova["MS_lof"] + if ms_lof > ms_pe * 3: + parts.append(f"⚠️ Дисперсия неадекватности ({ms_lof:.6f}) намного превышает чистую ошибку ({ms_pe:.6f}) — модель систематически ошибается.") + elif ms_lof > ms_pe: + parts.append(f"ℹ️ Дисперсия неадекватности ({ms_lof:.6f}) больше чистой ошибки ({ms_pe:.6f}), но разница не критична.") + else: + parts.append(f"✅ Дисперсия неадекватности ({ms_lof:.6f}) меньше или сравнима с чистой ошибкой ({ms_pe:.6f}).") + + return "\n".join(parts) + + +def interpret_center_points(center_mean, center_var, n_center, alpha): + """ + Интерпретирует анализ центральных точек. + + Аргументы: + center_mean (float): среднее отклика в центре + center_var (float): дисперсия в центре + n_center (int): количество центральных точек + alpha (float): уровень значимости + + Возвращает: + str: текстовое заключение + """ + parts = [] + + if n_center < 2: + parts.append("⚠️ Меньше двух центральных точек — оценка воспроизводимости невозможна.") + return "\n".join(parts) + + std = np.sqrt(center_var) if center_var > 0 else 0.0 + + parts.append(f"📌 Центральных точек: {n_center}") + parts.append(f"📌 Среднее отклика в центре: {center_mean:.6f}") + parts.append(f"📌 Стандартное отклонение в центре: {std:.6f}") + + # ----- Оценка воспроизводимости ----- + if center_var < 1e-6: + parts.append("✅ Дисперсия в центре очень мала — эксперимент высоковоспроизводим.") + elif center_var < 1e-4: + parts.append("✅ Дисперсия в центре мала — воспроизводимость хорошая.") + elif center_var < 1e-3: + parts.append("ℹ️ Дисперсия в центре умеренная — стоит обратить внимание на точность измерений.") + else: + parts.append("⚠️ Дисперсия в центре велика — возможны проблемы с воспроизводимостью эксперимента.") + + # ----- Относительная оценка (по сравнению со средним) ----- + if center_mean != 0: + rel_std = std / abs(center_mean) * 100 + if rel_std < 1: + parts.append(f"📊 Относительное стандартное отклонение: {rel_std:.2f}% — отличная точность.") + elif rel_std < 5: + parts.append(f"📊 Относительное стандартное отклонение: {rel_std:.2f}% — хорошая точность.") + elif rel_std < 10: + parts.append(f"📊 Относительное стандартное отклонение: {rel_std:.2f}% — удовлетворительная точность.") + else: + parts.append(f"⚠️ Относительное стандартное отклонение: {rel_std:.2f}% — низкая точность, возможно, есть выбросы или систематическая ошибка.") + + return "\n".join(parts) + + +def interpret_model_adequacy(anova, alpha): + """ + Даёт общую рекомендацию: переходить на квадратичную модель или нет. + + Аргументы: + anova (dict): результат ANOVA + alpha (float): уровень значимости + + Возвращает: + str: краткая рекомендация + """ + if not np.isnan(anova.get("p_lof", np.nan)): + p_lof = anova["p_lof"] + if p_lof < alpha: + return ( + "⚠️ РЕКОМЕНДАЦИЯ: модель НЕ АДЕКВАТНА.\n" + " Линейное приближение недостаточно. Перейдите к квадратичной модели\n" + " (Центральный композиционный план или план Бокса-Бенкена).\n" + " Это позволит точно определить положение экстремума." + ) + else: + return ( + "✅ РЕКОМЕНДАЦИЯ: модель АДЕКВАТНА.\n" + " Линейное приближение достаточно. Можете использовать крутое восхождение\n" + " для движения к оптимуму или уточнять модель дальше." + ) + else: + return ( + "ℹ️ Недостаточно данных для проверки адекватности.\n" + " Добавьте центральные точки в план, чтобы оценить воспроизводимость\n" + " и проверить, нужна ли квадратичная модель." + ) + + +def full_interpretation( + r2, r2_adj, f_stat, f_pvalue, + beta, t_stats, p_values, factor_names, + anova, center_mean, center_var, n_center, + alpha +): + """ + Собирает все интерпретации в единый отчёт. + + Аргументы: + все вышеперечисленные + + Возвращает: + str: полное заключение + """ + lines = [] + lines.append("=" * 60) + lines.append("📋 ИНТЕРПРЕТАЦИЯ РЕЗУЛЬТАТОВ") + lines.append("=" * 60) + + lines.append("\n▶ 1. КАЧЕСТВО МОДЕЛИ") + lines.append(interpret_regression(r2, r2_adj, f_stat, f_pvalue, alpha)) + + lines.append("\n▶ 2. ЗНАЧИМОСТЬ ФАКТОРОВ") + lines.append(interpret_coefficients(beta, t_stats, p_values, factor_names, alpha)) + + lines.append("\n▶ 3. АДЕКВАТНОСТЬ (ANOVA)") + lines.append(interpret_anova(anova, alpha)) + + if n_center >= 2: + lines.append("\n▶ 4. ВОСПРОИЗВОДИМОСТЬ") + lines.append(interpret_center_points(center_mean, center_var, n_center, alpha)) + + lines.append("\n▶ 5. ОБЩАЯ РЕКОМЕНДАЦИЯ") + lines.append(interpret_model_adequacy(anova, alpha)) + + lines.append("\n" + "=" * 60) + return "\n".join(lines)