Оглавление

Введение

Представьте: пациент приходит на приём. Врач выслушивает жалобы и назначает обследование. Температура, общий анализ крови, рентген грудной клетки, УЗИ, мазок из горла – стандартная карточка. Часть из этого действительно нужна. Часть – назначается по привычке, «чтобы не пропустить».

Теперь вопрос: можно ли математически доказать какие симптомы несут реальную информацию о диагнозе, а какие – просто "шум"? Можно ли взять таблицу пациентов и получить на выходе точный ответ – вот эти три признака обязательны, этот четвёртый заменяем, а пятый почти бесполезен?

Оказывается, можно. И для этого не нужны нейросети, градиентный бустинг или сотни тысяч записей. Достаточно бинарной матрицы, красивой идеи из теории множеств и нескольких строк кода.

Сразу сделаю честную оговорку: я не врач и не претендую на медицинскую точность. Таблица пациентов в этой статье составлена по общедоступным представлениям из интернета – какие симптомы обычно сопровождают грипп, бронхит, пневмонию. Настоящие врачи составят такую матрицу куда точнее – на основе реальной клинической практики, статистики, протоколов. Моя цель другая: показать как математика может помочь врачу структурировать знания и найти в них закономерности. Не заменить клиническое мышление – а дать ему инструмент.

Медицина здесь – удобный и понятный пример. Тот же подход работает в промышленной диагностике, в анализе анкет, в задачах отбора признаков для машинного обучения, в системах поддержки принятия решений. Математика универсальна – меняется только предметная область.

В этой статье мы возьмём таблицу из 12 пациентов и 7 симптомов, переберём все возможные комбинации признаков, найдём те наборы которые позволяют однозначно поставить диагноз – и посчитаем вектор значимости каждого симптома. Реализацию сделаем в среде Engee на языке Julia.

Немного истории: Павлак и грубые множества

Чтобы понять откуда берётся наш алгоритм – нужно сделать небольшой экскурс в историю.

В 1982 году польский математик и информатик Здислав Павлак (Zdzisław Pawlak) опубликовал работу «Rough Sets». Это была короткая статья – меньше двадцати страниц. Но она дала начало целому направлению в анализе данных, которое сегодня насчитывает тысячи публикаций и десятки практических приложений.

Рисунок 1 – Здислав Павлак
Рисунок 1 – Здислав Павлак

Здислав родился в 1926 году в городе Лодзи. Он был одновременно математиком, логиком и пионером информатики в Польше – участвовал в создании одного из первых польских компьютеров в 1950-х годах. Но главным его вкладом в мировую науку стала именно теория грубых множеств, опубликованная когда ему было уже 56 лет.

Проблема которую он решал

До Павлака задача работы с неточными и неполными знаниями решалась двумя основными способами.

Первый – теория вероятностей. Если мы не уверены – присвоим вероятность. Грипп с вероятностью 0.7, ОРВИ с вероятностью 0.3. Подход работает, но требует численных оценок которых часто просто нет.

Второй – нечёткая логика Заде (1965). Вместо чёткого «да/нет» – степень принадлежности от 0 до 1. Температура «немного высокая» – это 0.6, «очень высокая» – это 0.95. Тоже работает, но требует экспертного задания функций принадлежности.

Павлак предложил принципиально иной подход: не размывать границы понятий и не вводить вероятности, а честно признать что некоторые объекты при имеющихся данных неразличимы – и работать с этим фактом напрямую.

Ключевая идея

Представьте что у вас есть таблица. Строки – объекты (пациенты). Столбцы – признаки (симптомы). Два объекта неразличимы относительно некоторого набора признаков если они совпадают по всем признакам из этого набора.

Например если мы смотрим только на температуру и кашель – то пациент с [температура=1, кашель=1] и другой пациент с [температура=1, кашель=1] неразличимы, даже если у них разные одышка и насморк.

Все объекты разбиваются на классы неразличимости. Внутри класса – объекты которых мы не можем отличить друг от друга при данном наборе признаков. Это называется отношением неразличимости.

Теперь появляется понятие нижнего и верхнего приближения множества. Нижнее приближение – это объекты которые мы точно можем отнести к нужному классу. Верхнее – объекты которые возможно к нему относятся. Разница между ними – это «граница неопределённости», та самая «грубость» в названии теории.

Павлак показал что с этими приближениями можно работать строго математически – выводить правила, находить зависимости, сокращать описания объектов. И всё это без вероятностей и без нечёткости.

Почему это важно

Теория грубых множеств привлекательна по нескольким причинам:

Не требует дополнительных знаний. Для вероятностного подхода нужны априорные распределения. Для нечёткой логики – функции принадлежности. Для грубых множеств нужна только сама таблица данных.

Интерпретируемость. Результат – конкретные наборы признаков и правила. Не «веса нейросети», а «для постановки диагноза достаточно проверить температуру и слабость».

Работает с малыми данными. Нейросетям нужны тысячи примеров. Грубые множества работают с таблицами в десятки строк.

Сегодня теория Павлака живёт в медицинской диагностике, в системах управления производством, в анализе финансовых рисков и в предобработке данных для машинного обучения. Но начиналось всё с простой таблицы на бумаге.

Ключевые понятия: что такое редукт?

Прежде чем переходить к коду – разберём три ключевых понятия. Без них результат будет набором цифр без смысла.

Согласованность таблицы

Таблица согласована если в ней нет двух объектов с одинаковыми условными признаками но разными целевыми классами.

Простой пример. Есть два пациента:

Температура

Кашель

Диагноз

Пациент А

1

1

Грипп

Пациент Б

1

1

ОРВИ

Таблица несогласована: одинаковые симптомы – разные диагнозы. Врач смотрит на одну и ту же картину и не может решить что именно у пациента. Значит этих двух симптомов недостаточно – нужно добавить ещё.

Если же:

Температура

Кашель

Насморк

Диагноз

Пациент А

1

1

1

Грипп

Пациент Б

1

1

0

ОРВИ

Теперь таблица согласована: добавили насморк – и пациенты стали различимы. Одинаковых строк с разными диагнозами нет.

Суперредукт

Суперредукт – любой согласованный набор признаков. Это может быть и три признака, и пять, и все семь. Главное условие – согласованность.

Полный набор всех симптомов – всегда суперредукт (если исходная таблица согласована). Но нас интересует не любой согласованный набор, а минимальный.

Редукт

Редукт – это минимальный суперредукт. Набор признаков который:

  • согласован – не порождает конфликтов в классификации;

  • минимален – если убрать любой один признак из набора, согласованность нарушится.

Это принципиально: редукт не просто «хороший» набор признаков – это набор в котором каждый элемент необходим. Лишних нет.

У одной таблицы может быть несколько редуктов. Это нормально и даже интересно: существуют разные минимальные способы описать объекты достаточно для классификации. Иногда можно использовать симптомы 1, 2, 5 – а иногда вместо симптома 2 подойдёт симптом 6.

Вектор значимости

Когда редуктов несколько – возникает вопрос: какой выбрать? И более интересный вопрос: какие признаки самые важные в целом?

Ответ – вектор значимости. Для каждого признака считаем в какой доле всех валидных наборов (редуктов и суперредуктов) он присутствует. Признак который входит во все наборы – обязателен. Признак который входит в половину – заменяем. Признак который почти нигде не нужен – слабый.

Формально:

\text{significance}(a_i) = \frac{|\{S \in \mathcal{V} : a_i \in S\}|}{|\mathcal{V}|},

где V – множество всех валидных (согласованных) наборов признаков.

Именно этот вектор мы и будем вычислять.

Постановка задачи

Итак, у нас есть таблица 12 пациентов. Для каждого зафиксировано наличие или отсутствие 7 симптомов (1 – есть, 0 – нет) и поставлен диагноз врачом.

Повторю оговорку: данные составлены по общим представлениям, а не по медицинским справочникам. Реальный врач с реальной базой данных составит эту матрицу точнее – и получит более содержательные результаты. Математика та же самая.

Таблица пациентов

Температура

Кашель

Одышка

Боль в горле

Слабость

Насморк

Головная боль

Диагноз

Пациент 1

1

0

0

1

1

1

0

Грипп

Пациент 2

1

1

1

1

1

0

0

Пневмония

Пациент 3

1

0

0

1

0

1

1

ОРВИ

Пациент 4

1

1

1

0

0

0

0

Бронхит

Пациент 5

0

1

1

0

1

0

0

Бронхит

Пациент 6

0

1

0

1

0

0

1

Фарингит

Пациент 7

1

0

0

0

1

1

1

Грипп

Пациент 8

0

0

0

1

0

1

1

ОРВИ

Пациент 9

1

1

1

0

1

0

0

Пневмония

Пациент 10

0

1

0

1

1

0

1

Фарингит

Пациент 11

1

1

0

0

1

0

1

Грипп

Пациент 12

0

0

0

0

1

1

0

ОРВИ

Диагноз кодируется числом: 1 = Грипп, 2 = Пневмония, 3 = ОРВИ, 4 = Бронхит, 5 = Фарингит.

Как читать эту таблицу

Каждая строка – один пациент. Каждый столбец – один симптом. Единица означает «симптом есть», ноль – «симптома нет». Диагноз – то что врач поставил по совокупности всех данных.

Несколько наблюдений которые важны для понимания задачи:

Пациенты 4 и 5 оба с бронхитом, но симптомы разные. Пациент 4: температура, кашель, одышка – без слабости. Пациент 5: кашель, одышка, слабость – без температуры. Это нормально. Одна болезнь может проявляться по-разному у разных людей. Алгоритм это учитывает: он не требует чтобы пациенты с одинаковым диагнозом были неразличимы. Он требует только чтобы пациенты с одинаковыми симптомами имели одинаковый диагноз.

Пациенты 1, 7, 11 все грипп, но симптомы разные. Три варианта одной болезни. Алгоритм воспринимает их как три отдельных объекта – и это правильно.

Полный набор из 7 симптомов согласован. Это легко проверить вручную: нет двух строк с одинаковым набором нулей и единиц. Значит исходная задача корректно поставлена и у неё есть решение.

Формальная постановка

Дано:

  • Матрица M размером 12×7 – симптомы пациентов;

  • Вектор меток L длиной 12 – диагнозы.

Найти:

  • Все согласованные подмножества столбцов матрицы M;

  • Вектор значимости каждого столбца.

Алгоритм: как это работает?

Идея алгоритма прямолинейная – полный перебор с проверкой. Это не самый быстрый подход (об ограничениях поговорим позже), но самый прозрачный: видно каждый шаг, легко проверить результат.

Шаг 1. Подсчёт числа комбинаций

При n симптомах количество непустых собственных подмножеств равно:

2^n - 2

Вычитаем 2 потому что нас не интересуют два крайних случая: пустое множество (ноль симптомов – диагноз поставить невозможно) и полное множество (все симптомы – это исходная таблица, она заведомо согласована и не несёт нового знания).

При наших 7 симптомах: 27−2=126 комбинаций. Компьютер справится с ними за доли секунды.

Для наглядности: эти 126 комбинаций распределяются так:

Убираем столбцов

Остаётся столбцов

Комбинаций

1

6

7

2

5

21

3

4

35

4

3

35

5

2

21

6

1

7

Итого

126

Шаг 2. Построение подматрицы

Для каждой комбинации берём только те столбцы которые в неё вошли. Получаем подматрицу – уменьшенное представление таблицы пациентов.

Шаг 3. Проверка согласованности

Это главный шаг. Алгоритм:

  1. Находим все уникальные строки подматрицы – это уникальные «профили симптомов».

  2. Для каждого профиля собираем всех пациентов у которых он совпал.

  3. Смотрим – у всех в группе одинаковый диагноз?

  4. Если нашли группу с разными диагнозами – конфликт, набор не годится.

  5. Если прошли все группы без конфликтов – набор согласован.

Примеры: валидный набор

Убрали «Температуру» (столбец 1). Остались: «Кашель», «Одышка», «Боль в горле», «Слабость», «Насморк», «Головная боль».

Смотрим на пациентов 1 и 7 – оба Грипп:

Кашель

Одышка

Боль в горле

Слабость

Насморк

Гол. боль

Пациент 1

0

0

1

1

1

0

Пациент 7

0

0

0

1

1

1

Строки разные – они различимы даже без температуры!

Проверяем все 12 пациентов – нигде нет двух строк с одинаковыми симптомами и разными диагнозами. Набор валиден!

Примеры: невалидный набор

Убрали «Кашель», «Одышку», «Насморк» и «Головную боль». Остались: «Температура», «Боль в горле», «Слабость».

Температура

Боль в горле

Слабость

Диагноз

Пациент 1

1

1

1

Грипп

Пациент 2

1

1

1

Пневмония

Одинаковые симптомы – разные диагнозы. Конфликт!

Логика понятна: если убрать слишком много симптомов, разные болезни начинают «сливаться» – врач не может их различить.

Шаг 4. Подсчёт вектора значимости

Собрали все валидные наборы. Теперь для каждого симптома считаем в скольких из них он присутствует и делим на общее число валидных наборов:

\text{significance}(x_i) = \frac{\text{число валидных наборов содержащих } x_i}{\text{всего валидных наборов}}

Результат – число от 0 до 1. Чем ближе к 1 – тем важнее симптом.

Реализация в Engee

Всю описанную математику мы реализовали в среде Engee на языке Julia. Engee – это облачная среда динамического моделирования, доступная бесплатно прямо в браузере. Никакой установки, никаких зависимостей – открыл и считаешь. Именно поэтому порог входа минимальный: если вы знакомы с основами программирования, разобраться в коде не составит труда.

Структура кода

Скрипт разбит на четыре логических блока. Разберём каждый подробно.

Блок 1. Входные данные

using Combinatorics

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 — ОРВИ
]

labels = [1, 2, 3, 4, 4, 5, 1, 3, 2, 5, 1, 3]

symptom_names = [
    "Температура", "Кашель", "Одышка",
    "Боль в горле", "Слабость", "Насморк", "Головная боль"
]

n_rows, n_cols = size(M)

Первая строка – using Combinatorics: подключает стандартный пакет Julia для работы с комбинаторными объектами. Он даёт функциюcombinations(1:n, k),которая автоматически генерирует все сочетания изnпоk.

Все данные собраны в самом начале файла – намеренно. Хотите поменять задачу: другие болезни, другие симптомы, другое число пациентов – меняете только Mlabels и symptom_names. Остальной код не трогаете. Это называется разделением данных и логики.

Диагнозы закодированы числами, а не строками. «Грипп», «ОРВИ» оставлены в комментариях для читаемости – но в вычислениях используются числа: сравнивать 1 == 1 быстрее и надёжнее чем сравнивать "Грипп" == "Грипп".

Блок 2. Функция проверки согласованности

function is_consistent(sub::Matrix{Int}, labels::Vector{Int})
    unique_rows = unique(eachrow(sub))

    for urow in unique_rows
        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

Это сердце всего алгоритма. Функция принимает подматрицу симптомов и вектор диагнозов, возвращает true если набор согласован и false если найден конфликт.

Разберём построчно.

unique(eachrow(sub)) – итератор по строкам матрицы, из которых выбираются неповторяющиеся. Каждая уникальная строка – это уникальный «профиль симптомов». Если два пациента имеют одинаковый профиль – они попадут в одну группу.

mask = [collect(row) == collect(urow) for row in eachrow(sub)] – булев вектор длиной 12. Истина на позиции i означает что профиль i- го пациента совпадает с текущим проверяемым профилем. collect здесь необходим: eachrow возвращает представления (views), а не обычные массивы, и прямое сравнение без collect может дать неожиданный результат.

group_labels = labels[mask] – диагнозы всех пациентов из текущей группы.

length(unique(group_labels)) > 1 – если в группе больше одного уникального диагноза, значит нашли конфликт: одинаковые симптомы – разные диагнозы.

Одна деталь заслуживает отдельного внимания:

return false

Как только найден первый конфликт – функция немедленно возвращаетfalseи не проверяет остальные группы. Это ранняя остановка: зачем продолжать, если набор уже отбракован? При 126 комбинациях это незаметно, но при тысячах – даёт ощутимый выигрыш в скорости.

Блок 3. Основной перебор

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)

for n_remove in 1 : n_cols - 1
    for combo in combinations(1:n_cols, n_remove)
        actual_total += 1
        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")

Два вложенных цикла. Внешний идёт по количеству удаляемых столбцов – от одного до n_cols - 1. Внутренний перебирает все комбинации через combinations – элегантно и без лишнего кода.

setdiff(1:n_cols, combo) возвращает индексы столбцов которые остаются после удаления. Например если убираем столбцы [2, 5] из семи – остаются [1, 3, 4, 6, 7].

M[:, cols_kept]– срез матрицы по нужным столбцам. Все строки, только выбранные столбцы.

push!(valid_col_sets, cols_kept)– добавляем индексы валидного набора в список. Именно индексы, а не саму подматрицу: они занимают меньше памяти и содержат всё необходимое для финального подсчёта.

После перебора – контрольная проверка: сравниваем ожидаемое и реальное число комбинаций. Если цифры совпали – ничего не пропущено. Простая но надёжная страховка от ошибки в логике цикла.

Блок 4. Вектор значимости и вывод

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

result_vector = counts ./ n_valid

println("Вектор значимости симптомов:")
println("  " * "─" ^ 58)
println("  $(rpad("Симптом", 16)) | $(rpad("Доля", 8)) | Дробь  | Важность")
println("  " * "─" ^ 58)

for i in 1:n_cols
    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))

counts = zeros(Int, n_cols)– массив из семи нулей, по одному на каждый симптом. Будем накапливать в него счётчики.

Двойной цикл проходит по всем валидным наборам и для каждого симптома в наборе увеличивает его счётчик на единицу. После цикла counts[i] содержит число валидных наборов в которых симптом i присутствует.

counts ./ n_valid– поэлементное деление на количество валидных наборов. Точка перед / в Julia означает векторизованную операцию: делим каждый элемент массива, а не массив целиком.

Результат запуска

После запуска консоль выводит:

=========================================================
 Пациентов : 12
 Симптомов : 7
 Комбинаций для перебора: 2^7 - 2 = 126
=========================================================

Перебрано комбинаций : 126
Все варианты перебраны ✓
Валидных наборов     : 17

Вектор значимости симптомов:
  ──────────────────────────────────────────────────────────
  Симптом          | Доля     | Дробь  | Важность
  ──────────────────────────────────────────────────────────
  Температура      | 1.0      | 17/17  | ████████████████████
  Кашель           | 0.6471   | 11/17  | █████████████░░░░░░░
  Одышка           | 0.6471   | 11/17  | █████████████░░░░░░░
  Боль в горле     | 0.4706   |  8/17  | █████████░░░░░░░░░░░
  Слабость         | 1.0      | 17/17  | ████████████████████
  Насморк          | 0.6471   | 11/17  | █████████████░░░░░░░
  Головная боль    | 0.6471   | 11/17  | █████████████░░░░░░░

Итоговый вектор:
[1.0, 0.6471, 0.6471, 0.4706, 1.0, 0.6471, 0.6471]

Результат и интерпретация

Алгоритм нашёл 17 валидных наборов симптомов из 126 возможных. Разберём что это означает на практике – по каждому симптому отдельно, а потом сделаем общий вывод.

Температура – 1.0 (17/17)

Температура присутствует абсолютно во всех 17 валидных наборах. Это строгий математический результат: не существует ни одного согласованного набора симптомов который обходился бы без неё.

Почему? Посмотрим на матрицу внимательно. Температура делит всех пациентов на две большие группы:

Есть температура: Грипп (пациенты 1, 7, 11), Пневмония (2, 9), ОРВИ (3), Бронхит (4)

Нет температуры: Бронхит (5), Фарингит (6, 10), ОРВИ (8, 12)

Это фундаментальное разделение. Без него пациенты из разных групп начинают «смешиваться» – и никакая комбинация оставшихся симптомов не может надёжно их разграничить. Температура – это первый и самый грубый фильтр в нашей таблице.

Слабость – 1.0 (17/17)

Аналогичная ситуация. Слабость тоже входит во все 17 валидных наборов – убрать её невозможно.

Её роль другая: там где температура уже разделила группы, слабость разграничивает внутри них. Например среди пациентов с температурой – у пациента 4 (Бронхит) слабости нет, у пациента 2 (Пневмония) есть. Без этого признака они могут слиться при определённых комбинациях остальных симптомов.

Интересно что два обязательных признака – температура и слабость: не самые очевидные с медицинской точки зрения. Слабость есть при очень многих болезнях, и интуитивно кажется что она должна быть малоинформативной. Но математика говорит обратное: именно в контексте этой конкретной таблицы она незаменима.

Кашель, Одышка, Насморк, Головная боль – 0.647 (11/17)

Все четыре симптома получили одинаковую оценку – 11 из 17 валидных наборов. Это не случайное совпадение.

Эти четыре признака взаимозаменяемы в определённой мере. Существуют валидные наборы которые включают любые три из них – и при этом остаются согласованными. То есть иногда можно не проверять кашель если есть одышка, насморк и головная боль. Или не проверять насморк если есть кашель, одышка и головная боль.

Именно поэтому каждый из них отсутствует ровно в 6 валидных наборах из 17 – в тех случаях когда трое оставшихся справляются без него.

Для практики это означает: если пациент по какой-то причине не может описать один из этих симптомов – алгоритм не сломается. Есть запасные варианты.

Боль в горле – 0.471 (8/17)

Наименее информативный симптом. Меньше половины валидных наборов его включают – значит в большинстве случаев он просто не нужен.

Причина хорошо видна в таблице: боль в горле присутствует у пациентов с Гриппом (1, 2), ОРВИ (3, 8) и Фарингитом (6, 10) одновременно. Она плохо разграничивает диагнозы – есть и тут, и там. Остальные симптомы справляются с различением лучше, поэтому боль в горле легко исключается из набора без потери согласованности.

Это, пожалуй, самый неожиданный результат. Боль в горле – один из первых симптомов о которых спрашивает врач. А математика говорит: в контексте данной таблицы он наименее важен.

Итоговая картина

Сведём всё в одну таблицу:

Симптом

Значимость

Роль

Температура

1.00

Обязателен – первичный фильтр

Слабость

1.00

Обязателен – уточняющий фильтр

Кашель

0.65

Важен, но заменяем

Одышка

0.65

Важен, но заменяем

Насморк

0.65

Важен, но заменяем

Головная боль

0.65

Важен, но заменяем

Боль в горле

0.47

Наименее информативен

Если переформулировать практически:

Минимальный набор симптомов для однозначной постановки диагноза по данной таблице это температура и слабость плюс любые три из четырёх: кашель, одышка, насморк, головная боль. Боль в горле при этом можно не проверять вовсе.

Важная оговорка

Повторю то что сказал во введении – но теперь это важнее чем когда-либо.

Эти выводы справедливы только для данной конкретной таблицы. Мы работаем с 12 пациентами составленными по общим представлениям из интернета. Реальная клиническая картина сложнее: болезни протекают по-разному, симптомы перекрываются иначе, выборка из 12 человек статистически ничтожна.

Настоящий врач с реальной базой данных – скажем, 500 пациентов с подтверждёнными диагнозами – получит совершенно другие числа. И они будут куда более содержательными. Математический метод тот же самый. Качество результата определяется качеством данных.

Именно в этом и есть главная идея статьи: метод работает, инструмент готов. Осталось подставить правильные данные.

Заключение

Мы прошли путь от простого вопроса – какие симптомы реально важны: до математически обоснованного ответа с конкретными числами и работающим кодом.

Главный вывод: за задачей отбора признаков стоит красивая и строгая математика, которой уже больше сорока лет. Теория грубых множеств Павлака, опубликованная в 1982 году, даёт инструмент который работает без нейросетей, без больших данных и без сложных вычислений – только таблица, логика и перебор.

Результат получился неожиданным даже для такой простой таблицы: температура и слабость оказались обязательными признаками, а боль в горле – наименее информативным симптомом, хотя интуиция подсказывала бы обратное. Именно в этом и есть ценность формального метода: он не доверяет интуиции, он считает.

Повторю оговорку которую делал на протяжении всей статьи: наша таблица составлена по общим представлениям, а не по клинической практике. Настоящий врач с реальной базой данных получит другие числа – и они будут куда содержательнее. Математика та же самая. Качество результата определяется качеством данных. Именно поэтому метод имеет смысл не сам по себе, а как инструмент в руках специалиста – врача, инженера, аналитика – который понимает предметную область и умеет правильно составить таблицу.

Если тема заинтересовала – следующий шаг это матрица различимости Павлака. Это более элегантный математический способ найти все редукты без полного перебора: вместо 2n−2 проверок строится одна матрица размером n×n из которой редукты извлекаются алгебраически. При большом числе признаков это принципиально быстрее. А ещё дальше –жадные и генетические алгоритмы поиска редуктов, которые работают даже когда полный перебор невозможен.

И напоследок: медицина здесь была лишь удобным примером. Та же матрица, тот же код, те же формулы – работают для диагностики промышленного оборудования, для анализа анкет, для отбора признаков перед обучением классификатора. Математика универсальна. Осталось только найти свою таблицу.

Полезные ссылки

Готовая модель в Engee