Теория грубых множеств: поиск минимального набора симптомов
Автор
In [ ]:
using Combinatorics
# =========================================================
# Матрица симптомов: строки — пациенты, столбцы — симптомы
# 1 = симптом есть, 0 = симптома нет
#
# Меняй под свои данные — главное чтобы:
# - только 0 и 1
# - число строк в M совпадало с длиной labels
# =========================================================
M = [
1 0 0 1 1 1 0; # Пациент 1 — Грипп
1 1 1 1 1 0 0; # Пациент 2 — Пневмония
1 0 0 1 0 1 1; # Пациент 3 — ОРВИ
1 1 1 0 0 0 0; # Пациент 4 — Бронхит
0 1 1 0 1 0 0; # Пациент 5 — Бронхит
0 1 0 1 0 0 1; # Пациент 6 — Фарингит
1 0 0 0 1 1 1; # Пациент 7 — Грипп
0 0 0 1 0 1 1; # Пациент 8 — ОРВИ
1 1 1 0 1 0 0; # Пациент 9 — Пневмония
0 1 0 1 1 0 1; # Пациент 10 — Фарингит
1 1 0 0 1 0 1; # Пациент 11 — Грипп
0 0 0 0 1 1 0; # Пациент 12 — ОРВИ
]
# Диагнозы в числовом виде — по одному на каждую строку матрицы.
# Числа вместо строк: сравнивать 1 == 1 быстрее чем "Грипп" == "Грипп".
# 1=Грипп, 2=Пневмония, 3=ОРВИ, 4=Бронхит, 5=Фарингит
labels = [1, 2, 3, 4, 4, 5, 1, 3, 2, 5, 1, 3]
# Названия симптомов — используются только для красивого вывода в консоль.
# Порядок должен совпадать с порядком столбцов в матрице M.
symptom_names = [
"Температура", # столбец 1
"Кашель", # столбец 2
"Одышка", # столбец 3
"Боль в горле", # столбец 4
"Слабость", # столбец 5
"Насморк", # столбец 6
"Головная боль" # столбец 7
]
# Размер матрицы нужен везде дальше — достаём сразу
n_rows, n_cols = size(M)
# =========================================================
# Функция проверки согласованности набора симптомов.
#
# Идея: берём подматрицу (только выбранные столбцы),
# группируем пациентов с одинаковым профилем симптомов
# и проверяем — у всех в группе одинаковый диагноз?
#
# Если да — набор согласован, можно ставить диагноз.
# Если нет — конфликт: одни симптомы, разные диагнозы.
#
# Аргументы:
# sub — подматрица симптомов (только выбранные столбцы)
# labels — вектор диагнозов всех пациентов
#
# Возвращает:
# true — набор согласован
# false — найден конфликт
# =========================================================
function is_consistent(sub::Matrix{Int}, labels::Vector{Int})
# Получаем все уникальные строки подматрицы.
# Каждая уникальная строка — это уникальный «профиль симптомов».
# Например [1, 0, 1] = есть температура, нет кашля, есть одышка.
unique_rows = unique(eachrow(sub))
for urow in unique_rows
# Строим булев вектор: true на позиции i означает что
# профиль i-го пациента совпадает с текущим профилем urow.
# collect() нужен потому что eachrow возвращает views (представления),
# а не обычные массивы — без collect сравнение может дать неверный результат.
mask = [collect(row) == collect(urow) for row in eachrow(sub)]
# Собираем диагнозы всех пациентов у которых профиль совпал
group_labels = labels[mask]
# Если в группе больше одного уникального диагноза — конфликт.
# Одинаковые симптомы должны давать одинаковый диагноз — иначе
# набор признаков недостаточен для различения болезней.
if length(unique(group_labels)) > 1
# Ранняя остановка: нашли конфликт — дальше проверять не нужно.
# Это ускоряет работу при большом числе комбинаций.
return false
end
end
# Прошли все группы без конфликтов — набор согласован
return true
end
# =========================================================
# Основной перебор всех комбинаций столбцов.
#
# Идея: перебираем все способы убрать от 1 до n_cols-1 столбцов.
# Для каждого оставшегося набора проверяем согласованность.
# Валидные наборы сохраняем для подсчёта вектора значимости.
#
# Всего комбинаций: 2^n_cols - 2
# Минус 2 потому что не считаем:
# - пустое множество (0 симптомов — диагноз невозможен)
# - полное множество (все симптомы — исходная таблица, она заведомо согласована)
# =========================================================
valid_col_sets = Vector{Vector{Int}}() # список валидных наборов столбцов
actual_total = 0 # счётчик перебранных комбинаций
expected_total = 2^n_cols - 2 # сколько комбинаций должно быть
println("=" ^ 57)
println(" Пациентов : $n_rows")
println(" Симптомов : $n_cols")
println(" Комбинаций для перебора: 2^$n_cols - 2 = $expected_total")
println("=" ^ 57)
# Внешний цикл: перебираем количество удаляемых столбцов
# от 1 (убираем один) до n_cols-1 (оставляем один)
for n_remove in 1 : n_cols - 1
# combinations(1:n_cols, n_remove) генерирует все сочетания
# индексов столбцов которые будем удалять.
# Например при n_cols=7 и n_remove=2: [1,2],[1,3],...,[6,7]
for combo in combinations(1:n_cols, n_remove)
actual_total += 1
# setdiff возвращает то что осталось после удаления —
# индексы столбцов которые сохраняем в подматрице
cols_kept = setdiff(1:n_cols, combo)
# Вырезаем подматрицу: все строки, только выбранные столбцы
sub = M[:, cols_kept]
# Проверяем согласованность подматрицы
if is_consistent(sub, labels)
# Сохраняем индексы столбцов — не саму матрицу.
# Индексы занимают меньше памяти и дают всё необходимое
# для финального подсчёта вектора значимости.
push!(valid_col_sets, cols_kept)
end
end
end
# =========================================================
# Контрольная проверка полноты перебора.
# Если цифры совпали — ничего не пропущено.
# Если нет — где-то ошибка в логике цикла.
# =========================================================
println("\nПеребрано комбинаций : $actual_total")
if actual_total == expected_total
println("Все варианты перебраны ✓")
else
println("ОШИБКА: пропущено $(expected_total - actual_total) вариантов ✗")
end
println("Валидных наборов : $(length(valid_col_sets))\n")
# =========================================================
# Подсчёт вектора значимости симптомов.
#
# Для каждого симптома считаем в скольких валидных наборах
# он присутствует и делим на общее число валидных наборов.
#
# Результат — доля от 0 до 1:
# 1.0 — симптом присутствует во всех валидных наборах (обязателен)
# 0.5 — присутствует в половине наборов (заменяем)
# 0.0 — не встречается ни разу (бесполезен)
# =========================================================
counts = zeros(Int, n_cols) # счётчик для каждого симптома
n_valid = length(valid_col_sets) # общее число валидных наборов
# Проходим по всем валидным наборам и для каждого симптома
# в наборе увеличиваем его счётчик на единицу
for cols in valid_col_sets
for c in cols
counts[c] += 1
end
end
# Нормируем: делим каждый счётчик на число валидных наборов.
# Точка перед / — векторизованная операция в Julia:
# делим каждый элемент массива отдельно, а не массив целиком.
result_vector = counts ./ n_valid
# =========================================================
# Вывод результата в консоль.
# Для каждого симптома показываем долю, дробь и
# визуальный индикатор из символов █ и ░.
# =========================================================
println("Вектор значимости симптомов:")
println(" " * "─" ^ 58)
println(" $(rpad("Симптом", 16)) | $(rpad("Доля", 8)) | Дробь | Важность")
println(" " * "─" ^ 58)
for i in 1:n_cols
# Масштабируем долю от 0..1 в длину полоски от 0..20 символов
bar_len = round(Int, result_vector[i] * 20)
# Заполненные блоки — значимость, пустые — оставшееся место
bar = "█" ^ bar_len * "░" ^ (20 - bar_len)
println(" $(rpad(symptom_names[i], 16)) | " *
"$(rpad(round(result_vector[i], digits=4), 8)) | " *
"$(lpad(counts[i], 2))/$n_valid | $bar")
end
# Итоговый вектор в одну строку — удобно для копирования и сравнения
println("\nИтоговый вектор:")
println(round.(result_vector, digits=4))