НМИЦ им. Бурденко · CatBoost · SHAP · 2026

Прогрессия после гамма-ножа

Реальные клинические данные, 583 пациента. Модель оценивает риск повторного роста метастазов. Потом я разобрал ее и понял, что почти все качество было нарисовано.

583
Пациента
настоящие карты, не синтетика
3
Версии проекта
соревнование, валидация, аудит
0.195
Дала одна колонка
по ROC-AUC, и знать ее было нельзя

Что за задача

Гамма-нож сводит 192 пучка излучения в одну точку. Опухоль в мозге облучают, здоровую ткань почти не задевают.

Схематичный мозг, пучки излучения сведены в одну точку

Проблема в том, что прогрессия все равно случается. Либо очаг снова растет в том же месте, либо появляются новые в другой части мозга. Универсального способа это предотвратить нет, поэтому важно заранее понимать, кого звать на контроль чаще.

Отсюда задача: до вмешательства оценить риск и назвать причину. Врачу мало числа 0.8, ему нужно понимать, почему у этого пациента риск высокий.

Данные настоящие, клинические: Центр гамма-нож при НМИЦ им. Бурденко, обезличенные карты. 872 строки, после фильтрации 583 пациента, 18 признаков. Работа в научной студии Центрального университета.

Схема: потоки признаков сходятся в общие узлы и выходят предсказанием

Формат был соревновательный: студенты решают одну задачу и сравниваются по метрике. Так что первая версия писалась ради места в таблице, и с этим мы почти справились.

А потом я сел смотреть, откуда берется такое качество. Все, что было дальше, уже не про метрику, а про честность.

Три версии одной работы

Все три ноутбука лежат в репозитории. Первый я не удалял: без него не видно, что поменялось и почему.

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

Признак, который был ответом

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

Колонка называется «Число РХ процедур на ГН», то есть сколько раз пациенту делали радиохирургию. Разложил ее по исходу.

Повторная процедура и прогрессия

РИС. 1
прогрессия былапрогрессии не было
Из 197 пациентов с повторной радиохирургией прогрессия у 191. Повторную процедуру назначают не просто так, а когда появился новый очаг или вырос старый. То есть повторная процедура и есть прогрессия, записанная другой колонкой.

Модель ничего не предсказывала. Она читала ответ из соседнего столбца.

Убираю одну колонку. Больше не меняю ничего.

Тот же протокол, те же гиперпараметры, то же разбиение. Меняется только список признаков, и вот что остается от качества.

Цена одной колонки

Базовый уровень PR-AUC равен доле положительного класса, здесь 0.657. Ниже него модель хуже константы.

Три набора признаков

РИС. 2
ROC-AUCPR-AUC
Листается вбок
0.195 ROC-AUC стоила одна колонка. Дальше PR-AUC 0.740 при базовом уровне 0.657, то есть модель почти не отличается от константы. Все качество первой версии держалось на ней.

Даты из будущего

Тот же вопрос ко всем остальным признакам. Знали ли мы значение на момент прогноза?

Флаги «была ОВГМ» и «была операция на ГМ» я ставил по факту наличия даты. Когда эта дата наступила, не проверял.

Сколько событий случилось уже после радиохирургии

РИС. 3
после первой РХдо нее
Из 80 облучений всего мозга 49 сделаны уже после гамма-ножа. Прогрессия у этой группы в 77.6 процента случаев против 65.2 в среднем. Причина та же: облучение всего мозга назначают, когда появились новые очаги. Флаг снова смотрит в будущее, просто слабее.

Пересчет обоих флагов на честные, со сравнением дат по каждой строке, дает плюс 0.006 ROC-AUC. По числу мелочь, но без нее нельзя.

Порог на своих же предсказаниях

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

Модель отдает вероятность, решение бинарное, порог надо где-то подобрать. Порог это такой же обучаемый параметр, как и все остальные. Если подбирать его на тех же предсказаниях, по которым потом отчитываешься, метрика получает фору.

Во второй версии подбор ушел во вложенные фолды: он видит только обучающую часть.

Схема вложенной кросс-валидации: пять внешних фолдов и три внутренних

Сверху пять внешних фолдов, снизу три внутренних. Порог ищется только на нижних. Отложенная часть внешнего фолда не участвует ни в чем, кроме замера.

ПоказательС утечкойЧестно
ROC-AUC0.79170.7951
PR-AUC0.89730.8993
Balanced accuracy0.74220.7232

Первые две метрики не сдвинулись, потому что от порога не зависят. Просела ровно та, которую порог и определяет. 1.9 пункта первая версия получала даром.

А это вообще модель

После всех чисток осталось 0.612 по ROC-AUC. Похоже на шум, надо проверить.

Перестановочный тест

РИС. 4
Таргет перемешивается, протокол не меняется. Если бы протокол тек, качество осталось бы высоким. Двадцать перестановок дали среднее 0.494 и максимум 0.536, ни одна не дотянула до 0.612.

Второй тест там же: бутстрэп по 2000 ресэмплов дает 95-процентный интервал [0.568, 0.666]. Нижняя граница выше 0.5.

Сигнал есть, просто слабый. Это результат, а не поломка.

Откуда берется слабый сигнал

Убираем по группе признаков и смотрим, что просядет.

Абляция по группам

РИС. 5
Листается вбок
Просаживает только одна группа. Весь сигнал в объеме очагов и их числе, немного добавляет клиника.

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

Задача была поставлена неправильно

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

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

Три постановки одной задачи

РИС. 6
Локальный рецидив не предсказывается вообще, 0.509 это монетка. И это объяснимо: локальный контроль зависит от подведенной дозы, покрытия мишени и качества планирования, а этих колонок в данных нет. Дистантная прогрессия предсказывается лучше склейки, потому что склейка просто добавляла шум.

Попытка выжать больше

0.650 это мало. Прежде чем говорить про потолок, надо перебрать очевидное.

Что пробовал

РИС. 7
Листается вбок
Наведите на строку. Расширение набора признаков дало минус 0.004, подбор гиперпараметров плюс 0.002, ансамбли с бустингом внутри только разбавляют. А смена семейства дала 0.027 и почти два пункта balanced accuracy.

Объяснение простое. Бустинг дообучается на своих ошибках, а на 583 строках с шумным таргетом ошибки это в основном шум, и он его заучивает. Лес усредняет независимо обученные деревья, и шум гасится. Помогла не более сложная модель, а более устойчивая.

Выбирать семейство по отложенным метрикам это та же ошибка, что подбирать по ним порог, просто уровнем выше. Поэтому выбор модели тоже ушел внутрь фолда: на внутренних фолдах перебирается зоопарк, наружу выходит только замер выбранного. Из 25 фолдов бэггинг победил в 24.

0.666
ROC-AUC, вложенный выбор
0.732
PR-AUC при базовом 0.564
0.614
Balanced accuracy
24 / 25
фолдов выбрали бэггинг

Куда все пришло

Четыре шага от первой версии ко второй.

ROC-AUC по версиям

РИС. 8
Листается вбок
Правильная постановка задачи дала 0.038, правильное семейство моделей еще 0.016. К 0.795 это не возвращает и не должно: те 0.795 были не качеством модели, а утечкой.

Что переписано

Первая версия писалась на скорость и на место в таблице. Вторая и третья уже на то, чтобы числам можно было верить. Каждый пункт ниже это найденный баг, а не улучшение стиля.

Один препроцессинг вместо двух копий

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

Индексы после фильтрации

После выброса строк обязателен reset_index. Дальше в коде идут и обращения по позиции, и запись в массив тоже по позиции. При дырявом индексе предсказания молча попадают не в те строки: ошибка не падает, метрики становятся мусором.

ID для сабмита

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

Квантили винзоризации внутри фолда

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

Каждый замер по пяти разбиениям

При разбросе по фолдам около 0.05 одно разбиение не значит ничего. Все числа на этой странице это средние по пяти seed. Заодно выяснилось, что seed 42, по которому шел отчет первой версии, давал худшую balanced accuracy из пяти :)

Категории по типу колонки, а не списком

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

Чего третья версия не умеет

Названная слабость перестает быть уязвимостью.