Добавлены пояснения к анализу

This commit is contained in:
2026-07-24 00:52:40 +05:00
parent 33fd9f1177
commit 901a5e5c7b
2 changed files with 328 additions and 24 deletions
+60 -24
View File
@@ -7,11 +7,13 @@
- проверку значимости коэффициентов (t-критерий), - проверку значимости коэффициентов (t-критерий),
- дисперсионный анализ (ANOVA) с разложением остатка на чистую ошибку и неадекватность (если есть центр), - дисперсионный анализ (ANOVA) с разложением остатка на чистую ошибку и неадекватность (если есть центр),
- оценку воспроизводимости по центральным точкам, - оценку воспроизводимости по центральным точкам,
- исключение незначимых факторов с пересчётом модели. - исключение незначимых факторов с пересчётом модели,
- интерпретацию результатов на естественном языке.
""" """
import numpy as np import numpy as np
from core.table_io import handle_table_output from core.table_io import handle_table_output
from core.interpreter import full_interpretation
from core.statistics import ( from core.statistics import (
compute_regression, compute_regression,
get_center_indices, get_center_indices,
@@ -19,7 +21,7 @@ from core.statistics import (
chi2_confidence_interval, chi2_confidence_interval,
get_t_critical, get_t_critical,
regression_summary, 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). и неадекватность (lack-of-fit).
4. Анализ центральных точек – оценка воспроизводимости. 4. Анализ центральных точек – оценка воспроизводимости.
5. Опция исключения незначимых факторов (p > alpha) и пересчёт упрощённой модели. 5. Опция исключения незначимых факторов (p > alpha) и пересчёт упрощённой модели.
6. Интерпретация результатов на естественном языке.
Аргументы: Аргументы:
response (dict): {'name': str, 'values': list} 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) df_res (int): степени свободы остатка (N - k - 1)
design (ExperimentDesign): объект с планом design (ExperimentDesign): объект с планом
alpha (float): уровень значимости alpha (float): уровень значимости
factor_names (list): имена факторов factor_names (list): имена факторов (без "Свободный член")
""" """
# Преобразуем значения отклика в массив numpy # Преобразуем значения отклика в массив numpy
y = np.array(response["values"]) 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): if not np.isnan(t_crit):
print(f"Критическое значение t (alpha={alpha}, df={df_res}): {t_crit:.4f}") print(f"Критическое значение t (alpha={alpha}, df={df_res}): {t_crit:.4f}")
return t_crit
# Возвращаем summary для дальнейшего использования
return t_crit, summary
# ---------- Шаг 2 (выполнение): вывод статистики для полной модели ---------- # ---------- Шаг 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) ---------- # ---------- Шаг 3: Дисперсионный анализ (ANOVA) ----------
# Определяем индексы строк плана, соответствующие центральным точкам (все активные факторы = 0) # Определяем индексы строк плана, соответствующие центральным точкам (все активные факторы = 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) handle_table_output(anova_headers, anova_rows, title="", ask_each_time=True)
# ---------- Шаг 4: Анализ центральных точек (воспроизводимость) ---------- # ---------- Шаг 4: Анализ центральных точек (воспроизводимость) ----------
# Инициализируем переменные для центра
center_mean = None
center_var = None
if len(center_indices) >= 2: if len(center_indices) >= 2:
y_center = y[center_indices] y_center = y[center_indices]
center_mean, center_var = compute_center_stats(y_center) 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): if all(significant):
print("Все факторы значимы. Упрощённая модель не отличается от полной.") 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"]
# Выбираем только значимые столбцы if np.isnan(beta_new).any():
X_new = X[:, significant] print("Ошибка при пересчёте упрощённой модели.")
# Пересчитываем регрессию else:
reg_new = compute_regression(X_new, y) # Обновляем список имён факторов
beta_new = reg_new["beta"] factor_names_new = ["Свободный член"] + [factor_names[i-1] for i in range(1, k+1) if significant[i]]
se_new = reg_new["se"] k_new = len(factor_names_new) - 1
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("\n=== Упрощённая модель (исключены незначимые факторы) ===")
print("Ошибка при пересчёте упрощённой модели.") # Выводим статистику для упрощённой модели
return 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_new = ["Свободный член"] + [factor_names[i-1] for i in range(1, k+1) if significant[i]] # Создаём полный список имён (включая свободный член) для интерпретатора
k_new = len(factor_names_new) - 1 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)
+268
View File
@@ -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)