Compare commits

...

6 Commits

6 changed files with 751 additions and 55 deletions
+51
View File
@@ -0,0 +1,51 @@
ЛИЦЕНЗИОННОЕ СОГЛАШЕНИЕ
Copyright (c) 2026 Артемий (Колобов Артем Андреевич)
Данное лицензионное соглашение (далее — "Соглашение") регулирует использование программного обеспечения "Mixture Optimizer" (далее — "Программное обеспечение") и сопутствующей документации.
1. ОБЪЕМ ПРЕДОСТАВЛЯЕМЫХ ПРАВ
1.1. Автор предоставляет вам неисключительное, безвозмездное право использовать, копировать, модифицировать, объединять, публиковать, распространять и/или создавать производные работы на основе Программного обеспечения исключительно в некоммерческих целях.
1.2. Некоммерческими целями признаются:
- личное использование;
- образовательная деятельность;
- научно-исследовательская работа;
- участие в открытых некоммерческих проектах;
- любое иное использование, не направленное на получение прямой или косвенной коммерческой выгоды.
2. ОГРАНИЧЕНИЯ
2.1. Коммерческое использование Программного обеспечения или его частей строго запрещено без получения предварительного письменного разрешения от Автора.
2.2. Коммерческим использованием признается, в частности:
- продажа, сдача в аренду, лицензирование или иная передача Программного обеспечения за плату;
- использование Программного обеспечения в коммерческих продуктах или услугах;
- использование Программного обеспечения для извлечения прибыли любым способом.
2.3. Для получения разрешения на коммерческое использование необходимо направить запрос по электронной почте: [ваш email]. Автор оставляет за собой право устанавливать условия и стоимость коммерческой лицензии.
3. УСЛОВИЯ РАСПРОСТРАНЕНИЯ
3.1. При распространении Программного обеспечения или его производных работ в некоммерческих целях вы обязаны:
- сохранять настоящее Соглашение и уведомление об авторских правах во всех копиях;
- явно указывать, что Программное обеспечение распространяется под условиями данного Соглашения.
3.2. Запрещается изменять или удалять информацию об авторстве и условиях лицензии.
4. ОТКАЗ ОТ ГАРАНТИЙ
Программное обеспечение предоставляется "как есть" (AS IS) без каких-либо гарантий, явных или подразумеваемых, включая, но не ограничиваясь, гарантии товарной пригодности, соответствия конкретным целям и отсутствия нарушений. Автор не несет ответственности за любые убытки, включая прямые, косвенные, случайные или косвенные убытки, возникшие в результате использования или невозможности использования Программного обеспечения.
5. ПРИМЕНИМОЕ ПРАВО
Настоящее Соглашение регулируется и толкуется в соответствии с законодательством Российской Федерации. Все споры подлежат разрешению в суде по месту нахождения Автора.
6. КОНТАКТНАЯ ИНФОРМАЦИЯ
Автор: Колобов Артем Андреевич
Email: kolobov-artem@bk.ru
Веб-сайт/репозиторий: https://gitlab.artemkas.ru/artemiy/mixture_optimizer
Дата вступления в силу: 23 июля 2026 г.
+1 -1
View File
@@ -125,6 +125,6 @@ mixture_optimizer/
## Контакты и вклад
По вопросам коммерческого использования, предложений и сообщений об ошибках обращайтесь по адресу: [ваш email].
По вопросам коммерческого использования, предложений и сообщений об ошибках обращайтесь по адресу: kolobov-artem@bk.ru
Приветствуются сообщения об ошибках и предложения по улучшению через Issues и Merge Requests (для некоммерческих изменений).
+196 -14
View File
@@ -1,24 +1,53 @@
# 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
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:
@@ -26,6 +55,7 @@ def analysis_menu(mixture, design, responses):
input("Нажмите Enter для продолжения...")
return
# Имена активных факторов берём из смеси (индекс = idx + 1, т.к. 0 – растворитель)
factor_names = []
for idx in active_indices:
ing_index = idx + 1
@@ -34,6 +64,7 @@ def analysis_menu(mixture, design, responses):
else:
factor_names.append(f"x{idx+1}")
# Ввод уровня значимости alpha
print("\nВведите уровень значимости alpha (например, 0.05, 0.01, 0.001):")
while True:
try:
@@ -49,11 +80,13 @@ def analysis_menu(mixture, design, responses):
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])
df_res = N - (k + 1)
X = np.hstack([np.ones((N, 1)), plan_matrix]) # размер N x (k+1)
df_res = N - (k + 1) # степени свободы остатка для полной модели
# Цикл выбора отклика
while True:
print("\n" + "=" * 50)
print("АНАЛИЗ ЗНАЧЕНИЙ")
@@ -67,15 +100,19 @@ def analysis_menu(mixture, design, responses):
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)
@@ -85,26 +122,58 @@ def analysis_menu(mixture, design, responses):
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"]
s2_res = reg["s2_res"]
df_res_actual = reg["df_res"]
y_pred = reg["y_pred"]
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"]
@@ -115,13 +184,16 @@ def analyze_single_response(response, X, k, df_res, design, alpha, factor_names)
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 = []
@@ -135,14 +207,89 @@ def analyze_single_response(response, X, k, df_res, design, alpha, factor_names)
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}")
return t_crit
t_crit = print_and_get_tcrit(beta, se, t_stats, df_res_actual, y_pred, y, k, factor_names, alpha)
# Возвращаем 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)
@@ -151,32 +298,67 @@ def analyze_single_response(response, X, k, df_res, design, alpha, factor_names)
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("Все факторы значимы. Упрощённая модель не отличается от полной.")
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():
print("Ошибка при пересчёте упрощённой модели.")
return
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)
+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)
+180
View File
@@ -128,3 +128,183 @@ def compute_gradient_points(beta, base_levels, step_factor, num_steps, factor_na
point["Предсказанный отклик"] = predicted
points.append(point)
return points
def anova_regression(y, y_pred, X, center_indices=None):
"""
Выполняет полный дисперсионный анализ (ANOVA) для регрессионной модели.
Аргументы:
y – фактические значения отклика
y_pred – предсказанные моделью значения
X – матрица регрессоров (с единичным столбцом)
center_indices – индексы центральных точек (опционально)
Возвращает словарь с компонентами ANOVA:
SSR, SSE, SST, df_reg, df_res, df_pe, df_lof,
MSR, MSE, MS_pe, MS_lof,
F_regression, p_regression, F_lof, p_lof
"""
n = len(y)
p = X.shape[1] # число параметров (включая свободный член)
df_reg = p - 1 # степени свободы регрессии (без свободного члена)
df_res = n - p # степени свободы остатка
# Общая сумма квадратов (SST)
y_mean = np.mean(y)
SST = np.sum((y - y_mean)**2)
# Регрессионная сумма квадратов (SSR)
SSR = np.sum((y_pred - y_mean)**2)
# Остаточная сумма квадратов (SSE)
SSE = np.sum((y - y_pred)**2)
# Средние квадраты
MSR = SSR / df_reg if df_reg > 0 else np.nan
MSE = SSE / df_res if df_res > 0 else np.nan
# F-статистика для регрессии (значимость всей модели)
F_reg = MSR / MSE if MSE > 0 else np.nan
p_reg = 1 - stats.f.cdf(F_reg, df_reg, df_res) if not np.isnan(F_reg) and SCIPY_AVAILABLE else np.nan
# ----- Разложение остатка на чистую ошибку и неадекватность (если есть центр) -----
if center_indices is not None and len(center_indices) >= 2:
y_center = y[center_indices]
y_pred_center = y_pred[center_indices]
n_center = len(center_indices)
# Среднее в центре
y_center_mean = np.mean(y_center)
# Сумма квадратов чистой ошибки (SSPE) = сумма квадратов отклонений от среднего в центре
SSPE = np.sum((y_center - y_center_mean)**2)
df_pe = n_center - 1
MS_pe = SSPE / df_pe if df_pe > 0 else np.nan
# Сумма квадратов неадекватности (SSLOF) = SSE - SSPE
SSLOF = SSE - SSPE
df_lof = df_res - df_pe
MS_lof = SSLOF / df_lof if df_lof > 0 else np.nan
# F-статистика для проверки адекватности (lack-of-fit)
F_lof = MS_lof / MS_pe if MS_pe > 0 and not np.isnan(MS_lof) else np.nan
p_lof = 1 - stats.f.cdf(F_lof, df_lof, df_pe) if not np.isnan(F_lof) and SCIPY_AVAILABLE else np.nan
else:
SSPE = np.nan
df_pe = 0
MS_pe = np.nan
SSLOF = np.nan
df_lof = 0
MS_lof = np.nan
F_lof = np.nan
p_lof = np.nan
return {
"SSR": SSR, "SSE": SSE, "SST": SST,
"df_reg": df_reg, "df_res": df_res,
"MSR": MSR, "MSE": MSE,
"F_reg": F_reg, "p_reg": p_reg,
"SSPE": SSPE, "SSLOF": SSLOF,
"df_pe": df_pe, "df_lof": df_lof,
"MS_pe": MS_pe, "MS_lof": MS_lof,
"F_lof": F_lof, "p_lof": p_lof
}
# core/statistics.py (дополнение)
def build_quadratic_X(plan):
"""
Строит расширенную матрицу регрессоров для квадратичной модели.
plan: numpy array (N x k) – матрица плана в кодированных переменных.
Возвращает: X (N x p), где p = 1 + 2k + k*(k-1)//2
(свободный член, линейные, квадраты, попарные произведения)
"""
N, k = plan.shape
X = np.ones((N, 1)) # свободный член
X = np.hstack([X, plan]) # линейные (k столбцов)
X = np.hstack([X, plan**2]) # квадраты (k столбцов)
# взаимодействия (k*(k-1)//2 столбцов)
for i in range(k):
for j in range(i+1, k):
X = np.hstack([X, (plan[:, i] * plan[:, j]).reshape(-1, 1)])
return X
def fit_quadratic_model(plan, y):
"""
Выполняет регрессию для квадратичной модели по матрице плана.
Возвращает словарь с результатами (аналогично compute_regression, но с добавлением структуры).
"""
X = build_quadratic_X(plan)
reg = compute_regression(X, y) # используем существующую функцию
# Добавляем информацию о структуре модели
k = plan.shape[1]
p_total = X.shape[1]
reg["k"] = k
reg["X"] = X
reg["plan"] = plan
reg["p_total"] = p_total
# Можно добавить имена коэффициентов (для вывода)
coef_names = ["Свободный член"]
coef_names += [f"x{i+1}" for i in range(k)]
coef_names += [f"x{i+1}²" for i in range(k)]
for i in range(k):
for j in range(i+1, k):
coef_names.append(f"x{i+1}·x{j+1}")
reg["coef_names"] = coef_names
return reg
def analyze_quadratic_model(beta, k, X, y):
"""
Анализирует квадратичную модель: находит стационарную точку и тип экстремума.
beta – коэффициенты (включая все члены модели)
k – число факторов
X – матрица регрессоров (для расчёта предсказанных значений)
y отклик
Возвращает словарь с результатами.
"""
# Извлекаем коэффициенты
b0 = beta[0]
b_lin = beta[1:1+k] # линейные
b_quad = beta[1+k:1+2*k] # квадраты
b_inter = beta[1+2*k:] # взаимодействия
# Строим матрицу B (гессиан) для квадратичной формы
B = np.diag(2 * b_quad)
idx = 0
for i in range(k):
for j in range(i+1, k):
B[i, j] = b_inter[idx]
B[j, i] = b_inter[idx]
idx += 1
# Стационарная точка: x* = -0.5 * inv(B) * b_lin
try:
x_star = -0.5 * np.linalg.solve(B, b_lin)
except np.linalg.LinAlgError:
x_star = None
# Предсказанное значение в стационарной точке
if x_star is not None:
y_star = b0 + np.dot(b_lin, x_star) + np.dot(x_star, B @ x_star) / 2
else:
y_star = None
# Тип экстремума
if x_star is not None:
eigvals = np.linalg.eigvalsh(B)
if np.all(eigvals < 0):
extremum_type = "Максимум"
elif np.all(eigvals > 0):
extremum_type = "Минимум"
elif np.any(eigvals < 0) and np.any(eigvals > 0):
extremum_type = "Седловая точка"
else:
extremum_type = "Неопределён (есть нулевые собственные значения)"
else:
extremum_type = "Не найден (матрица B вырождена)"
return {
"x_star": x_star,
"y_star": y_star,
"extremum_type": extremum_type,
"B": B,
"eigenvalues": np.linalg.eigvalsh(B) if x_star is not None else None
}
+16 -1
View File
@@ -2,8 +2,11 @@
"""
Главное меню программы.
Загружает проект при старте и предоставляет доступ ко всем подменю.
Обрабатывает прерывание Ctrl+C для корректного выхода.
"""
from core.fs import load_project
import sys
from core.fs import load_project, save_project_partial
from editor import editor_menu
from plan import plan_menu
from response import response_menu
@@ -15,6 +18,7 @@ PROJECT_FILE = "project.json"
def main():
mixture, design, responses = load_project(PROJECT_FILE)
try:
while True:
print("\n" + "=" * 50)
print("ГЛАВНОЕ МЕНЮ")
@@ -42,6 +46,17 @@ def main():
ascent_menu(mixture, design, responses)
else:
print("Неверный пункт.")
except KeyboardInterrupt:
print("\n\nОбнаружено прерывание (Ctrl+C).")
# Спрашиваем, хочет ли пользователь сохранить текущий проект
save_choice = input("Сохранить текущий проект перед выходом? (y/n, Enter = нет): ").strip().lower()
if save_choice == 'y':
save_project_partial(PROJECT_FILE, mixture, design, responses)
print("Проект сохранён.")
else:
print("Выход без сохранения.")
print("До свидания!")
sys.exit(0)
if __name__ == "__main__":
main()