Загрузка данных
При проведении объединения файлов Эксель в один сводный файл выдало ошибку:
После заполнения матриц все сработало. Думаю надо ставить проверку и предупреждение
Traceback (most recent call last):
File "E:\Anaconda3\Lib\tkinter\__init__.py", line 2068, in __call__
return self.func(*args)
~~~~~~~~~^^^^^^^
File "E:\dozor_env\Lib\site-packages\customtkinter\windows\widgets\ctk_button.py", line 564, in _on_release
self._command()
~~~~~~~~~~~~~^^
File "E:\dukora\modules\experts\aggregate_tab_inter.py", line 153, in aggregate_data
success = self.controller.aggregate_data(output_file_path, input_files)
File "E:\dukora\modules\experts\aggregate_tab_work.py", line 101, in aggregate_data
messagebox.showerror("Ошибка данных", "В выбранных файлах нет матриц.")
^^^^^^^^^^
NameError: name 'messagebox' is not defined
Я вайб кодер. Но мне кажется здесь ошибка была или из-за того что матрица не оказалась строго квадратной. Когда я добавил для квадрата одну строку и заполнил матрицу - все сработало без ошибки. Нужно сделать в коде исправление на проверку того что я написал, чтобы программа не падала, а предупреждала, что матрица не соответствует квадрату.
Кроме того, при проверке на согласование транзитивности выдало следующую ошибку -
(dozor_env) (base) E:\dukora>python main.py
[INFO] Пользователь '1' успешно авторизован.
[GUI SELECT] Нажат Обзор...
[PREPARE FAIL]: name 'np' is not defined
Для устранения этих ошибок показываю код.
# ============================================
# dukora\modules\experts\aggregate_tab_inter.ру
# ============================================
import customtkinter as ctk
from tkinter import messagebox
from pathlib import Path
# Исправлено: указываем правильную подпапку "excel"
from modules.utils.resources import resource_path
from modules.experts.aggregate_tab_work import AggregateWorkController
class AggregateExpertTab(ctk.CTkFrame):
def __init__(self, master):
super().__init__(master)
self.controller = AggregateWorkController()
# Основной контейнер с отступами
main_container = ctk.CTkFrame(self, fg_color="transparent")
main_container.pack(fill="both", expand=True, padx=10, pady=10)
# --- СЕТКА ИНТЕРФЕЙСА ---
# Колонки: 0-Заголовок (фикс), 1-Поле ввода (растягивается), 2-Кнопка Обзор (фикс)
main_container.grid_columnconfigure(0, weight=0, minsize=140)
main_container.grid_columnconfigure(1, weight=1)
main_container.grid_columnconfigure(2, weight=0, minsize=100)
# Строка 0: Заголовок блока
lbl_create = ctk.CTkLabel(main_container, text="АГРЕГИРОВАНИЕ ФАЙЛОВ:", font=ctk.CTkFont(size=16, weight="bold"))
lbl_create.grid(row=0, column=0, columnspan=3, sticky="w", pady=(0, 10))
# Строка 1: Блок создания файла и указания пути (вложенная сетка)
frame_file = ctk.CTkFrame(main_container, fg_color="transparent")
frame_file.grid(row=1, column=0, columnspan=3, sticky="ew", pady=5)
frame_file.grid_columnconfigure(0, weight=0, minsize=140)
frame_file.grid_columnconfigure(1, weight=1)
frame_file.grid_columnconfigure(2, weight=0, minsize=100)
t_lbl_1 = ctk.CTkLabel(frame_file, text="Создать файл:", anchor="w")
t_lbl_1.grid(row=0, column=0, sticky="w", padx=(0, 5), pady=5)
self.new_file_name_entry = ctk.CTkEntry(frame_file, width=250)
self.new_file_name_entry.grid(row=0, column=1, sticky="ew", padx=5, pady=5)
btn_create = ctk.CTkButton(frame_file, text="Создать", command=self.handle_create_file_request, width=100)
btn_create.grid(row=0, column=2, sticky="e", padx=5, pady=5)
t_lbl_2 = ctk.CTkLabel(frame_file, text="Указать путь:", anchor="w")
t_lbl_2.grid(row=1, column=0, sticky="w", padx=(0, 5), pady=5)
self.target_file_entry = ctk.CTkEntry(frame_file, width=250)
self.target_file_entry.grid(row=1, column=1, sticky="ew", padx=5, pady=5)
browse_target_button = ctk.CTkButton(frame_file, text="Обзор...", command=self.select_target_file, width=100)
browse_target_button.grid(row=1, column=2, sticky="e", padx=5, pady=5)
# Строка 2: Выбор исходных файлов экспертов (вложенная сетка)
frame_select = ctk.CTkFrame(main_container, fg_color="transparent")
frame_select.grid(row=2, column=0, columnspan=3, sticky="ew", pady=5)
frame_select.grid_columnconfigure(0, weight=0, minsize=140)
frame_select.grid_columnconfigure(1, weight=1)
frame_select.grid_columnconfigure(2, weight=0, minsize=100)
t_lbl_3 = ctk.CTkLabel(frame_select, text="Выбрать файлы:", anchor="w")
t_lbl_3.grid(row=0, column=0, sticky="w", padx=(0, 5), pady=5)
self.file_path_entry = ctk.CTkEntry(frame_select, width=250)
self.file_path_entry.grid(row=0, column=1, sticky="ew", padx=5, pady=5)
browse_source_button = ctk.CTkButton(frame_select, text="Обзор...", command=self.select_files_for_aggregation, width=100)
browse_source_button.grid(row=0, column=2, sticky="e", padx=5, pady=5)
# Строка 3: Кнопка объединения во всю ширину
aggregate_button = ctk.CTkButton(
main_container,
text="Объединить информацию",
command=self.aggregate_data,
fg_color="#28a745", hover_color="#218838",
font=ctk.CTkFont(size=13, weight="bold"),
height=45
)
aggregate_button.grid(row=3, column=0, columnspan=3, sticky="ew", pady=(15, 15))
# Настройка растягивания строк внутри вложенных фреймов для адаптивности
for frm in [frame_file, frame_select]:
frm.grid_rowconfigure(0, weight=1)
frm.grid_rowconfigure(1, weight=1)
# Настройка весов строк основного контейнера
main_container.grid_rowconfigure(1, weight=0)
main_container.grid_rowconfigure(2, weight=0)
main_container.grid_rowconfigure(3, weight=0)
# === МЕТОДЫ УПРАВЛЕНИЯ СОБЫТИЯМИ ИНТЕРФЕЙСА ===
def handle_create_file_request(self):
"""
Вызывается при нажатии кнопки "Создать".
Передает имя в контроллер и отображает полный путь созданного файла.
"""
filename = self.new_file_name_entry.get().strip()
result_path = self.controller.create_empty_excel_file(filename)
if result_path:
# Очищаем поле целевого пути и вставляем туда только что созданный файл
self.target_file_entry.delete(0, "end")
self.target_file_entry.insert(0, str(result_path))
def select_target_file(self):
"""
Открывает проводник для выбора существующего Excel-файла
(куда будет сохранен результат или который будет перезаписан).
"""
from tkinter import filedialog
initial_dir = resource_path("excel")
file_path = filedialog.askopenfilename(title="Укажите целевой Excel-файл", initialdir=str(initial_dir), filetypes=[("Excel Files", "*.xlsx")])
if file_path:
self.target_file_entry.delete(0, "end")
self.target_file_entry.insert(0, file_path)
def select_files_for_aggregation(self):
"""
Открывает множественный выбор файлов-источников (экспертных оценок).
Путь к папке по умолчанию ведет в /experts от корня проекта.
"""
print("[GUI SELECT] Нажат Обзор...")
from tkinter import filedialog
initial_dir = resource_path("experts")
try:
file_paths = filedialog.askopenfilenames(title="Выберите файлы экспертов", initialdir=str(initial_dir), filetypes=[("Excel Files", "*.xlsx")])
if not file_paths:
return
# Сохраняем список путей внутрь контроллера бизнес-логики
self.controller.input_files = list(file_paths)
display_text = f"... ({len(file_paths)} files)"
self.file_path_entry.delete(0, "end")
self.file_path_entry.insert(0, display_text)
except Exception as e:
print(f"[GUI FATAL ERROR]: {e}")
messagebox.showerror("Критическая ошибка", f"Не удалось открыть проводник.\n{e}")
def aggregate_data(self):
"""
Главная кнопка действия. Проверяет заполненность полей и запускает логику сведения данных.
"""
output_file_path = self.target_file_entry.get().strip()
if not output_file_path:
messagebox.showwarning("Предупреждение", "Сначала укажите целевой файл!")
return
input_files = getattr(self.controller, 'input_files', [])
if not input_files or len(input_files) == 0:
messagebox.showwarning("Внимание", "Сначала выберите исходные файлы кнопкой 'Обзор...'.")
return
success = self.controller.aggregate_data(output_file_path, input_files)
if success:
messagebox.showinfo("Готово", f"Сводный отчет успешно сохранен в:\n{output_file_path}")
# ===========================================
# ===========================================
# dukora\modules\experts\aggregate_tab_work.py
# ===========================================
from openpyxl import load_workbook, Workbook
import os
import re
import zipfile
import shutil
import tempfile
from statistics import multimode, mean
from collections import defaultdict
from pathlib import Path
from modules.utils.resources import resource_path
class AggregateWorkController:
def __init__(self):
self.input_files = []
self._loaded_wbs = {}
def create_empty_excel_file(self, filename):
clean_filename = filename.strip()
if not clean_filename.endswith('.xlsx'):
clean_filename += '.xlsx'
if not clean_filename:
messagebox.showwarning("Предупреждение", "Имя файла не может быть пустым.")
return None
invalid_chars_pattern = r'[\\/:*?"<>|]'
if re.search(invalid_chars_pattern, clean_filename):
messagebox.showerror('Ошибка', 'Имя файла содержит недопустимые символы \\ / : * ? " < > |')
return None
experts_folder = resource_path("excel")
try:
os.makedirs(experts_folder, exist_ok=True)
except OSError as e:
messagebox.showerror("Ошибка доступа", f"Не удалось создать папку 'excel':\n{e}")
return None
full_path = experts_folder / clean_filename
wb = Workbook()
if "Sheet" in wb.sheetnames:
del wb["Sheet"]
wb.create_sheet(title="Данные")
wb.save(full_path)
wb.close()
return full_path
def _load_all_workbooks(self):
errors = []
loaded_count = 0
for fp in self.input_files:
try:
# Читаем значения формул (data_only=True), но игнорируем типы данных
wb = load_workbook(fp, data_only=True, read_only=False)
self._loaded_wbs[fp] = wb
loaded_count += 1
except Exception as e:
errors.append((Path(fp).name, str(e)))
return True if loaded_count > 0 else False, errors
def _get_mode_or_neutral(self, values):
numeric_values = [v for v in values if isinstance(v, (int, float))]
if not numeric_values:
return 1.0
modes = multimode(numeric_values)
if len(modes) == 1:
return modes[0]
return mean(modes)
def aggregate_data(self, output_file_path, input_files):
self._loaded_wbs.clear()
all_loaded, errors = self._load_all_workbooks()
if not all_loaded:
messagebox.showerror("Ошибка загрузки", f"Ни один файл не был загружен.\nПримеры ошибок: {errors[:3]}")
return False
sheets_by_name = defaultdict(list)
sheet_dimensions = {}
# --- 1. ПРОХОД: Ищем размеры ---
for fp, wb in self._loaded_wbs.items():
for sheet_name in wb.sheetnames:
ws = wb[sheet_name]
n_indicators = 0
while True:
cell_val = ws.cell(row=2, column=4 + n_indicators).value
s = "" if cell_val is None else str(cell_val).strip().replace(',', '.')
try:
float(s)
n_indicators += 1
except ValueError:
break
if n_indicators >= 2:
sheets_by_name[sheet_name].append(ws)
sheet_dimensions[sheet_name] = n_indicators
if not sheets_by_name:
messagebox.showerror("Ошибка данных", "В выбранных файлах нет матриц.")
return False
out_wb = Workbook()
if "Sheet" in out_wb.sheetnames:
del out_wb["Sheet"]
matrix_data_store = {}
# --- 2. ПРОХОД: Считаем данные ---
for group_name, worksheets in sheets_by_name.items():
n = sheet_dimensions[group_name]
# === ВОТ ЭТИ СТРОКИ БЫЛИ ПРОПУЩЕНЫ (фикс ошибки) ===
MATRIX_START_ROW = 2
MATRIX_START_COL = 4
# ==================================================
current_matrix = [[None] * n for _ in range(n)]
for i in range(n):
for j in range(n):
if i == j:
val = 1.0
elif i < j:
values_list = []
for ws in worksheets:
raw = ws.cell(row=MATRIX_START_ROW + i, column=MATRIX_START_COL + j).value
clean_s = "" if raw is None else str(raw).strip().replace(',', '.').replace(' ', '')
try:
num = float(clean_s)
values_list.append(num)
except ValueError:
continue
val = self._get_mode_or_neutral(values_list)
else:
val = None
current_matrix[i][j] = val
# Заполняем низ обратными величинами
EPSILON = 1e-9
for i in range(n):
for j in range(i):
upper = current_matrix[j][i]
if upper is None or abs(upper) < EPSILON:
current_matrix[i][j] = 1.0
else:
current_matrix[i][j] = round(1.0 / upper, 6)
matrix_data_store[group_name] = current_matrix
# --- ЗАПИСЬ РЕЗУЛЬТАТА ---
temp_fd, temp_path = tempfile.mkstemp(suffix='.xlsx')
os.close(temp_fd)
try:
for group_name, matrix in matrix_data_store.items():
n = len(matrix)
out_ws = out_wb.create_sheet(title=group_name[:31])
source_ws = sheets_by_name[group_name][0]
headers = ["№ п/п", "Наименование показателя", "Код"]
for col_idx, header in enumerate(headers, start=1):
out_ws.cell(row=1, column=col_idx, value=header)
for row_idx in range(1, n + 1):
out_ws.cell(row=row_idx + 1, column=1, value=str(row_idx))
out_ws.cell(row=row_idx + 1, column=2, value=source_ws.cell(row=row_idx + 1, column=2).value)
out_ws.cell(row=row_idx + 1, column=3, value=source_ws.cell(row=row_idx + 1, column=3).value)
codes = [source_ws.cell(row=1, column=4 + c).value for c in range(n)]
for c_idx, code in enumerate(codes):
out_ws.cell(row=1, column=4 + c_idx, value=code)
# Записываем числа
for i in range(n):
for j in range(n):
out_ws.cell(row=MATRIX_START_ROW + i, column=MATRIX_START_COL + j, value=float(matrix[i][j]))
out_wb.save(temp_path)
out_wb.close()
target_path = Path(output_file_path)
os.makedirs(target_path.parent, exist_ok=True)
shutil.move(temp_path, target_path)
except Exception as e:
messagebox.showerror("Ошибка записи", f"Технический сбой: {e}")
if os.path.exists(temp_path):
os.remove(temp_path)
return False
for wb in self._loaded_wbs.values():
wb.close()
self._loaded_wbs.clear()
return True
# =========================================
# =========================================
# dukora\modules\experts\check_tab_inter.py
# =========================================
import customtkinter as ctk
from tkinter import messagebox # Для работы showerror/showinfo
from pathlib import Path
# Используем ресурсный путь до корня экспертов
from modules.utils.resources import resource_path
from modules.experts.check_tab_work import CheckWorkController
class CheckExpertTab(ctk.CTkFrame):
def __init__(self, master):
super().__init__(master)
self.controller = CheckWorkController()
main_container = ctk.CTkFrame(self, fg_color="transparent")
main_container.pack(fill="both", expand=True, padx=10, pady=10)
main_container.grid_columnconfigure(0, weight=0, minsize=160)
main_container.grid_columnconfigure(1, weight=1)
main_container.grid_columnconfigure(2, weight=0, minsize=140)
FONT_LABEL = ctk.CTkFont(size=13)
lbl_file = ctk.CTkLabel(main_container, text="Выбрать файл:", anchor="w", font=FONT_LABEL)
lbl_file.grid(row=0, column=0, sticky="w", ipady=2, pady=(0, 15))
self.file_path_entry = ctk.CTkEntry(main_container, width=400)
self.file_path_entry.grid(row=0, column=1, sticky="ew", padx=5, pady=(0, 15)) # Исправлено на padx/pady
browse_btn = ctk.CTkButton(main_container, text="Обзор...", command=self.select_check_file, width=120)
browse_btn.grid(row=0, column=2, sticky="e", ipady=2, pady=(0, 15))
lbl_group = ctk.CTkLabel(main_container, text="Выбрать группу (лист):", anchor="w", font=FONT_LABEL)
lbl_group.grid(row=1, column=0, sticky="w", ipady=2, pady=(0, 15))
self.group_cb = ctk.CTkComboBox(master=main_container, state="readonly", width=400)
# Подписываемся на событие изменения выбора листа для контроллера
self.group_cb.bind("<<ComboboxSelected>>", lambda e: setattr(self.controller, "current_sheet_name", self.group_cb.get()))
self.group_cb.grid(row=1, column=1, sticky="ew", padx=5, ipady=2, pady=(0, 15))
check_btn = ctk.CTkButton(main_container, text="Проверить на транзитивность", command=self.run_transitivity_check, width=200)
check_btn.grid(row=2, column=0, sticky="w", padx=(0, 5), ipady=2, pady=(0, 15))
self.trans_var = ctk.StringVar(value="")
result_entry = ctk.CTkEntry(
master=main_container,
textvariable=self.trans_var,
justify="center",
width=100,
height=35,
state="disabled" # Чтобы пользователь не мог редактировать результат руками
)
result_entry.grid(row=2, column=1, sticky="ew", padx=5, pady=(0, 15))
percent_lbl = ctk.CTkLabel(main_container, text="%", anchor="w", font=FONT_LABEL)
percent_lbl.grid(row=2, column=2, sticky="w", padx=5, ipady=2, pady=(0, 15)) # Исправлена опечатка lbg -> lbl
btn_fix_norm_frame = ctk.CTkFrame(main_container, fg_color="transparent")
btn_fix_norm_frame.grid(row=3, column=0, columnspan=3, sticky="ew", pady=(20, 0))
fix_btn = ctk.CTkButton(btn_fix_norm_frame, text="Исправить транзитивность", fg_color="#28a745", hover_color="#218838", command=self.fix_transitivity)
fix_btn.pack(side="left", expand=True, fill="x", padx=5, ipady=2)
norm_btn = ctk.CTkButton(btn_fix_norm_frame, text="Нормализовать", fg_color="#dc3545", hover_color="#c82333", command=self.normalize_data)
norm_btn.pack(side="left", expand=True, fill="x", padx=5, ipady=2)
prepare_btn = ctk.CTkButton(
master=btn_fix_norm_frame,
text="Привести к согласованию",
fg_color="#ff9900",
hover_color="#cc7a00",
command=self.prepare_and_normalize
)
prepare_btn.pack(side="right", expand=True, fill="x", padx=5, ipady=2)
for r in [0, 1, 2, 3]:
main_container.grid_rowconfigure(r, weight=0)
# Изначально список листов пуст. Он заполняется при выборе файла.
self.group_cb.configure(values=[])
# === МЕТОДЫ УПРАВЛЕНИЯ ===
def refresh_groups(self):
sheets = []
if self.controller.current_file_path:
sheets = self.controller.get_sheet_names(self.controller.current_file_path)
self.group_cb.configure(values=sheets)
if sheets:
self.group_cb.set(sheets[0])
def select_check_file(self):
initial_dir = str(resource_path("experts"))
file_path = ctk.filedialog.askopenfilename(title="Выберите Excel-файл", initialdir=initial_dir, filetypes=[("Excel Files", "*.xlsx")])
if file_path:
self.file_path_entry.delete(0, "end")
self.file_path_entry.insert(0, file_path)
self.controller.current_file_path = file_path
# Обновляем список листов сразу после выбора файла
self.refresh_groups()
def run_transitivity_check(self):
selected_sheet = self.group_cb.get()
file_path = self.file_path_entry.get().strip()
if not file_path or not selected_sheet or selected_sheet == "Нет листов":
messagebox.showwarning("Внимание", "Сначала выберите файл!")
return
self.trans_var.set("") # Очищаем поле перед проверкой
percent_raw = self.controller.check_transitivity(file_path, selected_sheet)
self.trans_var.set(percent_raw)
def fix_transitivity(self):
selected_sheet = self.group_cb.get()
file_path = self.file_path_entry.get().strip()
if not file_path or not selected_sheet or selected_sheet == "Нет листов":
messagebox.showwarning("Внимание", "Сначала выберите файл!")
return
success = self.controller.apply_transitivity_corrections(file_path, selected_sheet)
if success:
# Сообщаем пользователю о сохранении изменений.
# Поле процентов обнуляется, так как текущее значение устарело.
# Пользователь должен нажать "Проверить" снова.
messagebox.showinfo("Готово", f"Нарушения транзитивности исправлены.\nДля точного процента нажмите 'Проверить'.")
else:
messagebox.showerror("Ошибка", "Не удалось исправить данные.")
def normalize_data(self):
selected_sheet = self.group_cb.get()
file_path = self.file_path_entry.get().strip()
if not file_path or not selected_sheet or selected_sheet == "Нет листов":
messagebox.showwarning("Внимание", "Сначала выберите файл!")
return
self.controller.current_file_path = file_path
self.controller.current_sheet_name = selected_sheet
success, message = self.controller.normalize_group_data()
if success:
messagebox.showinfo("Готово", message)
else:
msg_type = messagebox.showerror
if "уже проведена" in message or "очистить" in message or "квадратн" in message:
msg_type = messagebox.showwarning
msg_type("Статус операции", message)
def prepare_and_normalize(self):
selected_sheet = self.group_cb.get()
file_path = self.file_path_entry.get().strip()
if not file_path or not selected_sheet or selected_sheet == "Нет листов":
messagebox.showwarning("Внимание", "Сначала выберите файл!")
return
self.trans_var.set("")
success_prepare = self.controller.prepare_matrix_for_consistency(file_path, selected_sheet)
if success_prepare:
success_norm, message = self.controller.normalize_group_data()
if success_norm:
messagebox.showinfo("Готово", f"{message}\n\nМатрица перестроена для достижения высокой согласованности. Нажмите 'Проверить', чтобы увидеть точный процент транзитивности.")
else:
messagebox.showerror("Ошибка при записи:", message)
else:
messagebox.showerror("Ошибка", "Не удалось подготовить данные.")
# =========================================
# =====================================
# dukora\modules\experts\check_tab_work
# =====================================
from openpyxl import load_workbook, Workbook
import os
from pathlib import Path
class CheckWorkController:
def __init__(self):
self.current_file_path = ""
self.current_sheet_name = ""
# === НОВЫЙ МЕТОД: Жесткая привязка к шкале Саати ===
def _quantize_value(self, val):
"""
Приводит любое число к ближайшей точке нашей шкалы: 0.33, 1.0 или 3.0
"""
if val is None:
return None
v = float(val)
# Если значение очень близко к 3 (например 2.95 - 3.04)
if abs(v - 3.0) < 0.1:
return 3.0
# Если значение очень близко к 1 (например 0.95 - 1.04)
if abs(v - 1.0) < 0.1:
return 1.0
# Если значение очень близко к 0.33 (обратное к 3)
if abs(v - 0.33) < 0.05:
return 0.33
# Если вдруг попало что-то другое (ошибка ввода), возвращаем как есть
return v
def get_sheet_names(self, file_path):
try:
wb = load_workbook(file_path, read_only=True)
names = wb.sheetnames
wb.close()
return names
except Exception as e:
print(f"[LOAD SHEETS ERROR]: {e}")
return []
def _parse_cell_value(self, raw_val):
if raw_val is None:
return None
s = str(raw_val).strip().upper()
if s == "":
return None
cleaned = "".join([c for c in s if c.isdigit() or c in ".-"])
try:
num = float(cleaned)
# Сразу нормализуем считанное число
return self._quantize_value(num)
except ValueError:
return None
def check_transitivity(self, file_path, sheet_name):
"""
Проверка транзитивности через построение рейтинга.
Гарантирует корректную оценку после применения радикального исправления.
"""
n = 0
try:
wb = load_workbook(file_path, data_only=True)
if sheet_name not in wb.sheetnames:
return "Ошибка"
ws = wb[sheet_name]
# Определяем размер матрицы
while True:
cell_val = ws.cell(row=2, column=4 + n).value
parsed = self._parse_cell_value(cell_val)
if parsed is None:
break
n += 1
if n < 3:
wb.close()
return "100%"
MATRIX_START_ROW = 2
MATRIX_START_COL = 4
values = [[None] * n for _ in range(n)]
# Считываем всю матрицу в память как числа из нашей шкалы {0.33, 1, 3}
for i in range(n):
for j in range(n):
cell = ws.cell(row=MATRIX_START_ROW + i, column=j+MATRIX_START_COL).value
values[i][j] = self._parse_cell_value(cell)
total_triads = 0
consistent_triads = 0
# === НОВАЯ ЛОГИКА ПРОВЕРКИ ПО РАНГАМ ===
# Строим рейтинг показателей прямо здесь, в момент проверки.
# Суммируем строки (как при нормализации).
row_sums = [sum([val or 0 for val in row]) for row in values]
# Сортируем индексы от самого сильного показателя к слабому.
ranking = sorted(range(len(row_sums)), key=lambda x: -row_sums[x])
# Проверка всех возможных треугольников
for i in range(n):
for j in range(n):
if i == j: continue
for k in range(n):
if k == i or k == j: continue
a = values[i][j]
b = values[j][k]
c = values[i][k]
total_triads += 1
# Логика простая: смотрим на позиции в рейтинге.
# Если позиция i выше позиции k в рейтинге,
# то значение в ячейке i-k ДОЛЖНО быть большим (>= 1).
# Иначе оно должно быть малым (< 1).
should_be_greater = (ranking.index(i) < ranking.index(k))
# Значение в ячейке действительно большое?
is_greater = (a >= 1)
# Транзитивность соблюдена, если ранг соответствует значению ячейки.
if should_be_greater == is_greater:
consistent_triads += 1
wb.close()
if total_triads == 0:
return "100%"
percent = int((consistent_triads / total_triads) * 100)
return f"{percent}%"
except Exception as e:
print(f"[CHECK TRANS CRITICAL FAIL]: {e}")
return "Ошибка"
def apply_transitivity_corrections(self, file_path, sheet_name):
"""
Радикальное исправление транзитивности через построение идеальной иерархии
на основе нормализованных весов (аналогично методу "Нормализовать").
Гарантирует высокую согласованность.
"""
try:
wb = load_workbook(file_path)
if sheet_name not in wb.sheetnames:
print("[ERROR] Лист не найден.")
return False
ws = wb[sheet_name]
# Определяем размер матрицы
n = 0
while True:
cell_val = ws.cell(row=2, column=4 + n).value
parsed = self._parse_cell_value(cell_val)
if parsed is None:
break
n += 1
MATRIX_START_ROW = 2
MATRIX_START_COL = 4
# Считываем текущую матрицу в память
matrix = [[self._parse_cell_value(ws.cell(row=i+MATRIX_START_ROW, column=j+MATRIX_START_COL).value) for j in range(n)] for i in range(n)]
### Шаг 1: Нормализация (тот же принцип, что в кнопке "Нормализовать")
# Вычисляем сумму каждой строки
row_sums = [sum([val or 0 for val in row]) for row in matrix]
# Проверка на нулевую сумму (на всякий случай)
total_sum = sum(row_sums)
if abs(total_sum) < 1e-9:
raise ValueError("Сумма всех оценок равна нулю!")
# Создаём нормализированную матрицу
normalized_matrix = []
for i in range(n):
row = []
for j in range(n):
numerator = matrix[i][j] or 0
denominator = row_sums[j] or 1 # Защита от деления на ноль
row.append(numerator / denominator)
normalized_matrix.append(row)
# Теперь вычисляем итоговый вес каждого критерия как сумму его СТОЛБЦА в нормализированной матрице
criterion_weights = [sum(col) for col in zip(*normalized_matrix)]
### Шаг 2: Построение рейтинга
# Сортируем индексы показателей по убыванию их веса
ranking = sorted(range(len(criterion_weights)), key=lambda x: -criterion_weights[x])
### Шаг 3: Построение идеально согласованной матрицы
fixed_matrix = []
for i in range(n):
row = []
for j in range(n):
if i == j:
# Диагональ всегда единица
row.append(1.0)
elif ranking[i] < ranking[j]:
# Строка важнее столбца -> ставим максимум из шкалы
row.append(3.0)
else:
# Столбец важнее строки или они равны -> ставим минимум
row.append(1/3)
fixed_matrix.append(row)
### Шаг 4: Запись новой матрицы в файл
for i in range(n):
for j in range(n):
ws.cell(row=i+MATRIX_START_ROW, column=j+MATRIX_START_COL, value=fixed_matrix[i][j])
# Сохраняем изменения
wb.save(file_path)
wb.close()
return True
except Exception as e:
print(f"[FIX LOAD/SAVE CRITICAL FAIL]: {e}")
return False
import numpy as np # Для работы с матрицей
def prepare_matrix_for_consistency(self, file_path, sheet_name):
"""
Подготавливает матрицу к состоянию максимального согласия методом Саати.
Алгоритм:
1. Находит главный собственный вектор текущей матрицы.
2. Строит новую матрицу, где каждое отношение является отношением этих весов.
3. Округляет результаты до нашей шкалы {0.33, 1, 3}.
4. Записывает результат в файл.
"""
try:
wb = load_workbook(file_path)
if sheet_name not in wb.sheetnames:
print("[ERROR] Лист не найден.")
return False
ws = wb[sheet_name]
# Определяем размер матрицы
n = 0
while True:
cell_val = ws.cell(row=2, column=4 + n).value
parsed = self._parse_cell_value(cell_val)
if parsed is None:
break
n += 1
MATRIX_START_ROW = 2
MATRIX_START_COL = 4
# Считываем текущую матрицу в память
matrix = []
for i in range(n):
row = []
for j in range(n):
raw_val = ws.cell(row=i+MATRIX_START_ROW, column=j+MATRIX_START_COL).value
# Парсер должен возвращать только наши числа или None
num = self._parse_cell_value(raw_val)
# Защищаемся от случайных текстовых ячеек
row.append(num if isinstance(num, (int, float)) else 0)
matrix.append(row)
# Преобразуем её в NumPy-массив для линейной алгебры
np_matrix = np.array(matrix)
# Шаг 1: Находим главный собственный вектор
eigenvalues, eigenvectors = np.linalg.eig(np_matrix)
# Собственные числа могут быть комплексными из-за погрешностей округления,
# поэтому берём их абсолютные значения.
max_eigenvalue_idx = np.argmax(np.abs(eigenvalues))
eigenvector = eigenvectors[:, max_eigenvalue_idx].real
# Шаг 2: НОРМАЛИЗУЕМ вектор (делим каждый элемент на сумму всех элементов)
weights = eigenvector / np.sum(eigenvector)
# Теперь у нас есть идеальный набор весов [w1, w2, ..., wn].
# Идеальная матрица парных сравнений строится как M[i][j] = wi / wj.
# Шаг 3: Строим новую матрицу
fixed_matrix = []
for i in range(n):
row = []
for j in range(n):
# Диагональ всегда единица
if i == j:
value = 1.0
else:
# Отношение весов
ratio = weights[i] / weights[j]
# Округляем до нашей шкалы
# Здесь можно выбрать стратегию:
# Вариант A: Оставлять дробное число (лучше сохраняет информацию эксперта)
# Вариант B: Принудительно ставить 0.33/1/3 (более жёсткая логика)
# Я выбрал вариант B, так как он лучше подходит под твою задачу.
if ratio > 1.5: # Точка переключения между 1 и 3
value = 3.0
elif ratio < 0.67: # Точка переключения между 1 и 0.33
value = 0.33
else:
value = 1.0
row.append(value)
fixed_matrix.append(row)
# Шаг 4: Запись новой матрицы в файл
for i in range(n):
for j in range(n):
ws.cell(row=i+MATRIX_START_ROW, column=j+MATRIX_START_COL, value=fixed_matrix[i][j])
# Сохранение изменений
wb.save(file_path)
wb.close()
return True
except Exception as e:
print(f"[PREPARE FAIL]: {e}")
return False
def normalize_group_data(self):
if not self.current_file_path or not self.current_sheet_name:
return False, "Файл не выбран"
try:
wb = load_workbook(self.current_file_path)
if self.current_sheet_name not in wb.sheetnames:
return False, "Лист не найден"
ws = wb[self.current_sheet_name]
n = 0
while True:
cell_val = ws.cell(row=2, column=4 + n).value
if self._parse_cell_value(cell_val) is not None:
n += 1
else:
break
if n < 2:
wb.close()
return False, "Недостаточно данных (менее 2x2)"
MATRIX_START_COL = 4
HEADER_ROW = 1
DATA_START_ROW = 2
target_col = MATRIX_START_COL + n
max_search_limit = MATRIX_START_COL + n + 20
while target_col < max_search_limit:
header_cell = ws.cell(row=HEADER_ROW, column=target_col).value
if header_cell is None or ("нормализ" in str(header_cell).lower()):
break
target_col += 1
if target_col >= max_search_limit:
wb.close()
return False, "Нет свободного места для колонки весов."
ws.cell(row=HEADER_ROW, column=target_col, value="Нормализованный вес")
row_sums = []
for i in range(n):
row_sum = 0.0
for j in range(n):
val = self._parse_cell_value(ws.cell(row=DATA_START_ROW + i, column=MATRIX_START_COL + j).value)
row_sum += val if val is not None else 0.0
row_sums.append(row_sum)
total_sum = sum(row_sums)
if abs(total_sum) < 1e-9:
wb.close()
return False, "Сумма всех оценок равна нулю."
for i in range(n):
weight = round((row_sums[i] / total_sum), 4)
ws.cell(row=DATA_START_ROW + i, column=target_col, value=weight)
wb.save(self.current_file_path)
wb.close()
col_letter = ws.cell(row=HEADER_ROW, column=target_col).column_letter
return True, f"Веса успешно рассчитаны (колонка {col_letter})"
except Exception as e:
print(f"[NORM ERROR]: {e}")
return False, f"Ошибка при записи: {str(e)}"
# ==============================================================
# ========================================
# dukora\modules\experts\experts_window.py
# ========================================
import customtkinter as ctk
# Абсолютные импорты согласно вашему правилу
from modules.experts.create_tab_inter import CreateExpertTab
from modules.experts.aggregate_tab_inter import AggregateExpertTab
from modules.experts.check_tab_inter import CheckExpertTab
class ExpertsWindow(ctk.CTkToplevel):
def __init__(self, master):
super().__init__(master)
self.title("Эксперты")
window_width = 650
initial_height = 355
# Горизонтальный центр экрана
screen_w = self.winfo_screenwidth()
x_cord = int((screen_w - window_width) / 2)
# Фиксированный отступ от верхнего края (под панель задач Windows)
taskbar_offset_top = 40
y_cord = taskbar_offset_top
self.geometry(f"{window_width}x{initial_height}+{x_cord}+{y_cord}")
self.resizable(True, False)
self.grab_set()
self.transient(master)
main_container = ctk.CTkFrame(self, corner_radius=10)
main_container.pack(padx=10, pady=10, fill="both", expand=True)
title = ctk.CTkLabel(main_container, text="Модуль экспертов", font=ctk.CTkFont(size=20, weight="bold"))
title.pack(pady=(0, 15))
accordion_frame = ctk.CTkFrame(main_container)
accordion_frame.pack(fill="both", expand=True, pady=(10, 0))
button_frame = ctk.CTkFrame(main_container, fg_color="transparent")
self.accordion_data = {}
self.currently_open_button = None
# Заголовки аккордеона
create_item = self._create_accordion_header(accordion_frame, "Создание файлов для экспертов")
agg_item = self._create_accordion_header(accordion_frame, "Агрегирование файлов")
check_item = self._create_accordion_header(accordion_frame, "Проверка файлов")
# Наполнение контентом через наши новые файлы
self._fill_create_section(create_item["content"])
self._fill_aggregate_section(agg_item["content"])
self._fill_check_section(check_item["content"])
for item in [create_item, agg_item, check_item]:
btn = item["button"]
content = item["content"]
self.accordion_data[btn] = {"content": content}
btn.configure(command=lambda b=btn, c=content: self.toggle_section(b, c))
self.toggle_section(create_item["button"], create_item["content"])
button_frame.pack(fill="x", side="bottom")
close_btn = ctk.CTkButton(button_frame, text="Закрыть", width=120, command=self.destroy)
close_btn.pack(side="right", padx=10, pady=10)
def toggle_section(self, button, content):
is_currently_open = (self.currently_open_button == button)
if self.currently_open_button and not is_currently_open:
prev_content = self.accordion_data[self.currently_open_button]["content"]
prev_content.pack_forget()
self.currently_open_button.configure(fg_color="#34495e")
if is_currently_open:
content.pack_forget()
button.configure(fg_color="#34495e")
self.currently_open_button = None
new_geo = self._calculate_window_geometry(collapsed=True)
else:
content.pack(fill="x", padx=5, pady=(0, 5), anchor="center")
button.configure(fg_color="#2c3e50")
self.currently_open_button = button
new_geo = self._calculate_window_geometry(collapsed=False)
self.after_idle(lambda g=new_geo: self.geometry(g))
def _calculate_window_geometry(self, collapsed=False):
current_width = self.winfo_width()
if current_width == 1:
current_width = 650
header_height = (50 * len(self.accordion_data)) + 30
BOTTOM_RESERVED_SPACE = 70
if collapsed or not self.currently_open_button:
target_height = 355
else:
data = self.accordion_data[self.currently_open_button]
content_widget = data["content"]
self.update_idletasks()
open_content_height = content_widget.winfo_reqheight()
target_height = header_height + open_content_height + 120 + BOTTOM_RESERVED_SPACE
max_h = self.winfo_screenheight() - 50
final_height = min(target_height, max_h)
return f"{current_width}x{final_height}"
def _create_accordion_header(self, parent, title):
item_container = ctk.CTkFrame(parent, fg_color="#f0f2f5", border_color="#d1d8e0", border_width=1, corner_radius=8)
item_container.pack(fill="x", pady=5)
header_btn = ctk.CTkButton(
item_container,
text=title,
height=45,
font=ctk.CTkFont(weight="bold", size=16),
anchor="w",
fg_color="#34495e",
hover_color="#3c5a72",
)
header_btn.pack(fill="x")
content_frame = ctk.CTkFrame(item_container, fg_color="transparent", corner_radius=0)
return {"button": header_btn, "content": content_frame}
def _fill_create_section(self, frame):
panel = CreateExpertTab(frame)
panel.pack(fill="both", expand=True, padx=10, pady=10)
def _fill_aggregate_section(self, frame):
panel = AggregateExpertTab(frame)
panel.pack(fill="both", expand=True, padx=10, pady=10)
def _fill_check_section(self, frame):
panel = CheckExpertTab(frame)
panel.pack(fill="both", expand=True, padx=10, pady=10)
# ============================================================