Реальные клинические данные, 583 пациента. Модель оценивает риск повторного роста метастазов. Потом я разобрал ее и понял, что почти все качество было нарисовано.
Гамма-нож сводит 192 пучка излучения в одну точку. Опухоль в мозге облучают, здоровую ткань почти не задевают.
Проблема в том, что прогрессия все равно случается. Либо очаг снова растет в том же месте, либо появляются новые в другой части мозга. Универсального способа это предотвратить нет, поэтому важно заранее понимать, кого звать на контроль чаще.
Отсюда задача: до вмешательства оценить риск и назвать причину. Врачу мало числа 0.8, ему нужно понимать, почему у этого пациента риск высокий.
Данные настоящие, клинические: Центр гамма-нож при НМИЦ им. Бурденко, обезличенные карты. 872 строки, после фильтрации 583 пациента, 18 признаков. Работа в научной студии Центрального университета.
Формат был соревновательный: студенты решают одну задачу и сравниваются по метрике. Так что первая версия писалась ради места в таблице, и с этим мы почти справились.
А потом я сел смотреть, откуда берется такое качество. Все, что было дальше, уже не про метрику, а про честность.
Все три ноутбука лежат в репозитории. Первый я не удалял: без него не видно, что поменялось и почему.
Признаки из дат, Optuna, три бустинга, ансамбль, SHAP. Считалось на место в таблице. Внутри три утечки и два бага, про которые я тогда не знал.
Открыть ноутбук Версия 2 · валидацияПорог ушел во вложенные фолды, квантили считаются внутри фолда, препроцессинг сведен в одну функцию. Плюс бейслайн, пять сидов и калибровка.
Открыть ноутбук Версия 3 · аудитКаждому признаку один вопрос: знали ли мы его значение на момент прогноза. Дальше новый таргет, зоопарк моделей и вложенный выбор.
Открыть ноутбукМетрики трех версий между собой не сравниваются. Первая посчитана на признаке, которого знать было нельзя, вторая на нем же, но с честным порогом, третья без него и на другом таргете. Про это вся страница ниже.
Началось с того, что один признак весил в семь раз больше следующего. Такой отрыв это всегда повод присмотреться.
Колонка называется «Число РХ процедур на ГН», то есть сколько раз пациенту делали радиохирургию. Разложил ее по исходу.
Модель ничего не предсказывала. Она читала ответ из соседнего столбца.
Убираю одну колонку. Больше не меняю ничего.
Тот же протокол, те же гиперпараметры, то же разбиение. Меняется только список признаков, и вот что остается от качества.Базовый уровень PR-AUC равен доле положительного класса, здесь 0.657. Ниже него модель хуже константы.
Тот же вопрос ко всем остальным признакам. Знали ли мы значение на момент прогноза?
Флаги «была ОВГМ» и «была операция на ГМ» я ставил по факту наличия даты. Когда эта дата наступила, не проверял.
Пересчет обоих флагов на честные, со сравнением дат по каждой строке, дает плюс 0.006 ROC-AUC. По числу мелочь, но без нее нельзя.
Третья утечка не в данных, а в протоколе, и она была с самого начала.
Модель отдает вероятность, решение бинарное, порог надо где-то подобрать. Порог это такой же обучаемый параметр, как и все остальные. Если подбирать его на тех же предсказаниях, по которым потом отчитываешься, метрика получает фору.
Во второй версии подбор ушел во вложенные фолды: он видит только обучающую часть.
Сверху пять внешних фолдов, снизу три внутренних. Порог ищется только на нижних. Отложенная часть внешнего фолда не участвует ни в чем, кроме замера.
| Показатель | С утечкой | Честно |
|---|---|---|
| ROC-AUC | 0.7917 | 0.7951 |
| PR-AUC | 0.8973 | 0.8993 |
| Balanced accuracy | 0.7422 | 0.7232 |
Первые две метрики не сдвинулись, потому что от порога не зависят. Просела ровно та, которую порог и определяет. 1.9 пункта первая версия получала даром.
После всех чисток осталось 0.612 по ROC-AUC. Похоже на шум, надо проверить.
Второй тест там же: бутстрэп по 2000 ресэмплов дает 95-процентный интервал [0.568, 0.666]. Нижняя граница выше 0.5.
Сигнал есть, просто слабый. Это результат, а не поломка.
Убираем по группе признаков и смотрим, что просядет.
И отдельно неприятное про себя. Пять признаков, которые я считал из семи дат, не дают ничего: без них качество даже чуть выше. Вся возня с датами в первой версии была впустую.
Таргет первой версии склеивал два разных исхода в один. Это четвертая ошибка, и она уже не про утечку.
Локальный рецидив это рост в уже облученной зоне. Дистантные метастазы это новый очаг в другом месте мозга. Механизмы разные, значит и предсказуемость разная. Проверил по отдельности.
0.650 это мало. Прежде чем говорить про потолок, надо перебрать очевидное.
Объяснение простое. Бустинг дообучается на своих ошибках, а на 583 строках с шумным таргетом ошибки это в основном шум, и он его заучивает. Лес усредняет независимо обученные деревья, и шум гасится. Помогла не более сложная модель, а более устойчивая.
Выбирать семейство по отложенным метрикам это та же ошибка, что подбирать по ним порог, просто уровнем выше. Поэтому выбор модели тоже ушел внутрь фолда: на внутренних фолдах перебирается зоопарк, наружу выходит только замер выбранного. Из 25 фолдов бэггинг победил в 24.
Четыре шага от первой версии ко второй.
Первая версия писалась на скорость и на место в таблице. Вторая и третья уже на то, чтобы числам можно было верить. Каждый пункт ниже это найденный баг, а не улучшение стиля.
В первой версии подготовка данных была скопирована в два места, для обучения и для теста, и копии со временем разошлись: на тесте объем приводился к целому, а на обучении логарифмировался. Модель училась на одном распределении, а предсказывала на другом. Теперь это одна функция, и расхождение невозможно по построению, а не по внимательности.
После выброса строк обязателен reset_index. Дальше в коде идут и обращения по позиции, и запись в массив тоже по позиции. При дырявом индексе предсказания молча попадают не в те строки: ошибка не падает, метрики становятся мусором.
Генерировались как последовательность от единицы, хотя часть строк к тому моменту уже отфильтровалась. Предсказания уезжали не к тем пациентам. Теперь ID берутся из исходного файла до препроцессинга.
Считались по всей выборке, то есть значение из отложенной части попадало в статистику, по которой обрезались обучающие данные. Теперь порог обрезки берется только из обучающей части.
При разбросе по фолдам около 0.05 одно разбиение не значит ничего. Все числа на этой странице это средние по пяти seed. Заодно выяснилось, что seed 42, по которому шел отчет первой версии, давал худшую balanced accuracy из пяти :)
Наборы признаков в работе разные, а список категориальных колонок был захардкожен. Один новый строковый признак, и обучение падает или, хуже, молча кодирует его как число. Теперь список считается из типов.
Названная слабость перестает быть уязвимостью.