В предыдущих статьях я рассказывал, как появился веб-дневник диабета на Django и как постепенно оптимизировалась его производительность. Проект начинался довольно типично: пользователь вводил показатели сахара, записывал приемы пищи и дозы инсулина, а система сохраняла их в базе данных и отображала на графиках.
Со временем дневник перестал быть просто электронным блокнотом. Данных становилось все больше, а вместе с ними появлялись и новые вопросы. Главный из них звучал очень просто:
Что будет с уровнем глюкозы через 15, 30 или 45 минут?

Достаточно быстро стало понятно, что хранить историю измерений уже недостаточно. Пользователю важно не только видеть прошлое, но и понимать ближайшее будущее. Если система сможет заранее предупредить о возможном снижении сахара или его резком росте, то у человека появится время скорректировать дозу инсулина, принять углеводы или, наоборот, отказаться от дополнительной еды. Однако построить такой прогноз оказалось намного сложнее, чем просто обучить модель машинного обучения.
Почему нельзя просто использовать нейросеть
Когда речь заходит о прогнозировании временных рядов, первое, что приходит в голову, — использовать нейронные сети. В интернете можно найти множество примеров с LSTM, Transformer и другими современными архитектурами.
Проблема в том, что диабет — это не только временной ряд уровня глюкозы. На концентрацию сахара одновременно влияют десятки факторов:
текущий уровень глюкозы;
скорость его изменения;
инсулин, введенный несколько часов назад;
углеводы, которые еще продолжают всасываться;
жиры и белки, замедляющие усвоение пищи;
время суток;
чувствительность к инсулину;
физическая активность.
Если передать модели только историю значений глюкозы, она увидит лишь следствие происходящих процессов, но не их причины. Представьте две ситуации.
В первой человек только что съел большое количество углеводов.
Во второй — сделал корректирующий болюс инсулина.
В обоих случаях текущий уровень сахара может быть абсолютно одинаковым — например, 8 ммоль/л. Но дальнейшее развитие событий будет совершенно разным. В первом случае сахар, скорее всего, продолжит расти, а во втором — начнет снижаться. Для человека эта разница очевидна, потому что он знает, что происходило несколько минут назад. Для модели машинного обучения — нет. Поэтому первой задачей стало не обучение алгоритма, а создание математической модели, которая сможет описать текущее состояние организма значительно точнее, чем простая последовательность измерений CGM.
От сырых данных к математической модели
Вместо того чтобы использовать только последние значения глюкозы, я решил сформировать набор признаков, описывающих состояние организма в текущий момент времени.
Источников данных было несколько:
непрерывный мониторинг глюкозы (CGM);
записи о введенном инсулине;
журнал питания;
время каждого события.
Но сами по себе эти данные мало что говорят модели. Их необходимо превратить в осмысленные признаки. Например, уровень сахара 7,2 ммоль/л сам по себе практически бесполезен. Гораздо важнее понять:
сахар растет или падает;
насколько быстро происходит изменение;
сколько активного инсулина еще осталось в организме;
сколько углеводов еще не успело усвоиться;
сколько времени прошло после последнего болюса;
сколько времени прошло после последнего приема пищи.
Именно эти параметры позволяют оценить текущее состояние значительно точнее, чем одна цифра на экране глюкометра.
Поэтому следующим этапом проекта стало создание собственной математической модели обмена веществ, которая рассчитывает остаточное действие инсулина (IOB), количество еще не усвоенных углеводов (COB) и десятки дополнительных признаков для последующего прогнозирования.
Математическая модель вместо простых правил
Когда говорят о расчете доз инсулина, обычно вспоминают хлебные единицы (ХЕ). Действительно, этот показатель остается одним из основных ориентиров для людей с сахарным диабетом. Однако довольно быстро становится понятно, что одинаковое количество углеводов далеко не всегда вызывает одинаковую реакцию организма.
Например, яблоко и картофельное пюре могут содержать примерно одинаковое количество углеводов. Но скорость их усвоения будет совершенно разной. Еще сильнее ситуацию меняют жиры и белки. Добавьте к этому уже введенный инсулин, физическую активность или просто разное время суток — и становится очевидно, что простого подсчета хлебных единиц уже недостаточно.
Именно поэтому я отказался от идеи использовать только количество углеводов в качестве признака для модели. Вместо этого проект получил собственную математическую модель, которая постоянно оценивает текущее состояние организма. По сути, система в каждый момент времени отвечает на два вопроса:
сколько инсулина еще продолжает действовать;
сколько углеводов еще не успело попасть в кровь.
Эти два показателя известны как IOB (Insulin On Board) и COB (Carbs On Board).
Именно они стали одними из самых важных признаков для машинного обучения.
Почему линейная модель не работает
Самый простой способ посчитать остаток углеводов выглядит примерно так. Предположим, человек съел 6 ХЕ, а время полного усвоения блюда составляет три часа. Тогда можно считать, что каждые тридцать минут организм усваивает одинаковое количество углеводов. Такая модель очень проста, но почти не имеет ничего общего с реальной физиологией. На практике усвоение пищи начинается постепенно. Затем скорость увеличивается, достигает максимума примерно в середине процесса и только после этого начинает снижаться. Получается характерная колоколообразная кривая. Линейная модель полностью игнорирует этот эффект. В результате через час после еды она может серьезно ошибаться как в большую, так и в меньшую сторону. Для системы прогнозирования это критично, потому что ошибка на раннем этапе приводит к неправильному прогнозу уровня глюкозы уже через 15–30 минут.
Собственная модель расчета COB
В проекте используется нелинейная модель, в которой остаток углеводов уменьшается не с постоянной скоростью, а по гладкой кубической функции. Она имеет несколько важных свойств.
Во-первых, сразу после еды количество усвоенных углеводов растет медленно.
Во-вторых, затем скорость всасывания увеличивается и достигает максимума примерно в середине процесса.
И наконец, ближе к завершению усвоения снова плавно снижается.

Именно такая форма намного лучше соответствует реальному поведению большинства продуктов. Кроме того, время полного усвоения не является постоянным. Оно рассчитывается отдельно для каждого блюда на основании его состава. В системе уже хранится информация о хлебных единицах, белках, жирах, а также предполагаемом времени всасывания, которое определяется с учетом гликемического индекса и особенностей конкретного продукта. Таким образом, тарелка овсяной каши, банан и шоколад будут иметь одинаковое количество углеводов, но совершенно разные кривые поступления глюкозы в кровь. Это позволяет рассчитывать не просто количество съеденных углеводов, а количество углеводов, которые еще продолжают усваиваться именно сейчас. Именно это значение и становится признаком COB.
Остаточное действие инсулина
С инсулином ситуация очень похожая. Во многих приложениях предполагается, что после введения инсулин действует равномерно до окончания времени действия препарата. Но любой человек, использующий ультракороткий инсулин, знает, что это не так. Например, Новорапид начинает работать достаточно быстро, затем его действие усиливается, достигает максимума примерно через 70–90 минут, а затем постепенно снижается еще несколько часов. Если считать действие инсулина линейным, модель будет систематически ошибаться именно в тот момент, когда влияние инсулина наиболее выражено. Поэтому для расчета IOB я использовал аппроксимацию кривой действия ультракороткого инсулина. В расчетах учитываются длительность действия препарата, время достижения пика активности и последующее плавное затухание. В результате для каждой записи дневника можно определить не просто введенную дозу, а количество активного инсулина, которое еще остается в организме в данный момент. Именно это значение используется моделью прогнозирования вместо простой суммы последних инъекций. Кроме текущего значения IOB и COB система сразу рассчитывает их ожидаемые значения через 15, 30 и 45 минут. Благодаря этому модель машинного обучения получает информацию не только о настоящем состоянии организма, но и о том, как оно будет изменяться в ближайшем будущем. Это оказалось значительно полезнее, чем использование только исторических данных непрерывного мониторинга глюкозы.

Построение признаков
После появления моделей IOB и COB стало понятно, что простая последовательность значений сахара больше не является основным источником информации для алгоритма. Теперь задача состояла в том, чтобы максимально полно описать текущее состояние организма в виде набора числовых признаков.
В машинном обучении этот этап называется Feature Engineering, и именно он зачастую оказывает большее влияние на качество модели, чем выбор конкретного алгоритма. Я условно разделил все признаки на несколько групп.

История уровня глюкозы
Самый очевидный источник информации — последние измерения непрерывного мониторинга. В модели используются текущее значение сахара и двенадцать предыдущих измерений, что соответствует примерно 36 минутам истории CGM. На первый взгляд может показаться, что этого слишком мало. Однако эксперименты показали, что именно последние десятки минут содержат наиболее полезную информацию для краткосрочного прогнозирования. Более старая история практически не улучшала качество модели, зато увеличивала количество признаков и время обучения.
Скорость изменения сахара
Не менее важным оказался не сам уровень глюкозы, а скорость его изменения. Для этого рассчитываются изменения уровня сахара за несколько временных интервалов.
Например:
за последние 3 минуты;
за 9 минут;
за 15 минут;
за 30 минут.
Эти показатели позволяют понять, находится ли сахар в стабильном состоянии или уже начал быстро изменяться. Два человека могут иметь одинаковые 7,5 ммоль/л, но в первом случае сахар будет практически неизменным уже час, а во втором — стремительно расти после приема пищи. Для модели это две совершенно разные ситуации.
Ускорение изменения
Одной скорости оказалось недостаточно. Представим автомобиль. Если он едет со скоростью 60 км/ч, это еще не говорит о том, разгоняется он или тормозит. Точно так же и с сахаром. Поэтому дополнительно вычисляется ускорение — изменение скорости роста или снижения глюкозы. Этот признак помогает заранее обнаружить начало резкого подъема или падения еще до того, как это станет заметно по абсолютному уровню сахара.
Скользящие статистики
Следующая группа признаков описывает не отдельные точки, а поведение сахара за небольшой промежуток времени. Для последних 15 и 30 минут рассчитываются:
среднее значение;
стандартное отклонение.
Среднее значение показывает общий уровень сахара за последнее время, а стандартное отклонение позволяет понять, насколько стабильной была гликемия. Если разброс небольшой, значит сахар находится в относительно спокойном состоянии. Если же разброс резко увеличивается, модель получает дополнительный сигнал о начале активных изменений.
Инсулин и углеводы как признаки будущего
Самыми интересными признаками оказались рассчитанные значения IOB и COB. Но на этом вычисления не заканчиваются. Для каждой точки дневника система рассчитывает не только текущее количество активного инсулина и оставшихся углеводов, но и прогнозирует их значения через 15, 30 и 45 минут. Другими словами, модель получает информацию не только о настоящем состоянии организма, но и о том, как это состояние будет изменяться в ближайшем будущем. Например, если сейчас в организме остается 4 единицы активного инсулина, модель уже знает, что через полчаса его количество уменьшится примерно до 2,5 единиц.
То же самое касается и углеводов. Если прием пищи произошел недавно, система понимает, что через 30 минут большая часть углеводов еще будет продолжать всасываться. Такие признаки оказались значительно информативнее, чем простое время после последней инъекции или приема пищи.
Время после последних событий
Тем не менее полностью от временных признаков я не отказался. Дополнительно рассчитывается количество минут, прошедших после:
последнего болюса;
последнего приема пищи.
На первый взгляд кажется, что эта информация уже содержится в IOB и COB. Однако эксперименты показали, что эти признаки содержат дополнительную информацию. Например, сразу после введения инсулина остаточное количество IOB практически не изменяется, но сам факт недавней инъекции уже влияет на дальнейшее поведение модели. Аналогичная ситуация наблюдается и после еды. Поэтому оба признака были сохранены в итоговом наборе данных.
Время суток — тоже важный фактор
Еще один интересный признак оказался связан вовсе не с диабетом, а с биологическими ритмами человека. Практически каждый диабетик знает, что чувствительность к инсулину меняется в течение суток. У многих людей утром наблюдается так называемый «феномен утренней зари», когда уровень сахара начинает повышаться еще до завтрака. Вечером чувствительность к инсулину также может заметно отличаться от дневной. Поэтому время суток обязательно должно учитываться моделью. Однако здесь возникает небольшая математическая проблема. Если использовать обычный номер часа, то окажется, что 23:00 и 00:00 отличаются на 23 единицы, хотя в реальности между ними всего одна минута. Для алгоритма это создает искусственный разрыв. Чтобы избежать этой проблемы, время суток было представлено в виде двух циклических признаков — синуса и косинуса угла, соответствующего текущему времени.
Такой подход широко используется при работе с периодическими данными и позволяет превратить сутки в непрерывный цикл без резких скачков на границе дня. В результате модель начинает воспринимать 23:59 и 00:00 как практически соседние точки, что намного лучше соответствует реальности.
Почему пришлось отказаться от классических Django-шаблонов
Когда проект только начинался, интерфейс был построен совершенно стандартным для Django образом. Практически каждое действие пользователя открывало новую страницу. Нужно добавить прием пищи? Открывается отдельная форма. После сохранения выполняется POST-запрос, сервер обрабатывает данные, затем происходит перенаправление обратно в дневник и полная перерисовка страницы. Та же схема использовалась для инсулина, ингредиентов, блюд, физических нагрузок и любых других записей. Для небольшого проекта это абсолютно нормальная архитектура. Она простая, надежная и хорошо поддерживается встроенными возможностями Django. Но со временем система начала выполнять значительно больше вычислений. Теперь после каждого изменения необходимо было:
пересчитать остаточный инсулин (IOB);
пересчитать оставшиеся углеводы (COB);
обновить признаки для модели;
выполнить прогноз на несколько горизонтов;
перестроить график.
Полная перезагрузка страницы после каждого действия стала выглядеть совершенно неестественно. Особенно это ощущалось на телефоне. Пользователь нажимал кнопку «Добавить еду», переходил на новую страницу, заполнял форму, сохранял данные, ждал загрузки, возвращался обратно в дневник и снова искал глазами место, где остановился несколько секунд назад. Когда таких действий за день несколько десятков, интерфейс начинает мешать работе. Стало очевидно, что сам дневник должен превратиться из набора отдельных страниц в единое рабочее пространство.
Почему не React?
Наверное, самый частый вопрос, который мне задавали после перехода на новый интерфейс:
Почему не React?
На первый взгляд ответ очевиден. Если хочется получить современный интерфейс без перезагрузки страниц, большинство разработчиков сразу вспоминают React, Vue или Angular. Я тоже рассматривал этот вариант. Но довольно быстро понял, что для моего проекта это создаст больше проблем, чем преимуществ.
Во-первых, вся серверная логика уже была реализована на Django.
Во-вторых, существовала большая кодовая база с авторизацией, правами доступа, шаблонами, административной панелью и большим количеством готовых представлений.
Добавление полноценного фронтенд-приложения означало бы фактически поддержку двух отдельных проектов. Появилась бы необходимость синхронизировать версии API, настраивать отдельную сборку JavaScript, контролировать взаимодействие двух независимых приложений и поддерживать значительно более сложный процесс разработки. При этом требования к интерфейсу были достаточно скромными. Не требовалась сложная маршрутизация. Не было сотен независимых компонентов. Не было необходимости в глобальном состоянии приложения. Все, что было нужно, — быстро получать данные с сервера и обновлять только изменившиеся части страницы. Для этого возможностей обычного JavaScript оказалось более чем достаточно.
Django превратился в API
Вместо классических представлений, возвращающих HTML, постепенно появились небольшие API-методы. Каждый из них решал одну конкретную задачу.
Получить список ингредиентов.
Добавить новый прием пищи.
Изменить запись дневника.
Удалить болюс.
Получить новые точки графика.
Все они начали возвращать обычный JSON. Со стороны браузера все выглядело значительно проще. После нажатия кнопки JavaScript отправлял запрос через fetch(), получал ответ и обновлял только необходимые элементы интерфейса. Страница при этом вообще не перезагружалась. Такой подход оказался неожиданно удобным.

Django продолжил заниматься тем, что умеет лучше всего:
авторизацией;
работой с базой данных;
бизнес-логикой;
расчетами;
прогнозированием.
А браузер стал отвечать только за отображение информации. Получилась довольно необычная архитектура. Фактически это уже SPA, но без React, Vue и других тяжелых фронтенд-фреймворков. Вся клиентская часть состоит из обычного JavaScript, работающего поверх Django API.
Один API — множество возможностей
Еще один приятный эффект появился совершенно неожиданно. Когда вся работа начала выполняться через API, оказалось, что один и тот же серверный код можно использовать сразу в нескольких местах. Тот же метод, который возвращает данные для веб-интерфейса, может использоваться мобильным приложением.
Или Home Assistant.
Или внешней системой аналитики.
Или Telegram-ботом.
Или будущим десктопным клиентом.
Получилось, что переход на API решил не только проблему интерфейса. Он полностью разделил представление данных и бизнес-логику.
Все математические расчеты — IOB, COB, прогнозирование, построение признаков — теперь существуют независимо от способа отображения информации пользователю.
Именно такое разделение в дальнейшем позволило практически без изменений встроить модели машинного обучения в уже существующую архитектуру проекта. Следующим шагом стало обучение алгоритмов, которые научились использовать подготовленные признаки для прогнозирования уровня глюкозы на 15, 30 и 45 минут вперед.

Машинное обучение: почему именно градиентный бустинг
Когда набор признаков был готов, возник следующий вопрос — какую модель использовать для прогнозирования. Сегодня большинство публикаций о прогнозировании временных рядов так или иначе сводятся к нейронным сетям. LSTM, GRU, Transformer — все эти архитектуры регулярно появляются в научных статьях и технических блогах. Однако мой проект имеет несколько особенностей.
Во-первых, объем данных одного пользователя относительно небольшой. Даже несколько лет непрерывного мониторинга — это далеко не те объемы информации, на которых раскрываются возможности глубоких нейронных сетей.
Во-вторых, большая часть информации уже содержится в подготовленных признаках. Модель получает не «сырую» последовательность значений сахара, а описание текущего состояния организма: остаточный инсулин, остаточные углеводы, скорость изменения глюкозы, статистику последних измерений и временные характеристики.
В такой ситуации деревья решений часто оказываются эффективнее сложных нейросетевых архитектур. Поэтому я остановился на двух алгоритмах градиентного бустинга — XGBoost и CatBoost.
Обе библиотеки давно зарекомендовали себя как одни из лучших решений для работы с табличными данными. Они хорошо справляются с нелинейными зависимостями, практически не требуют подготовки признаков и позволяют быстро экспериментировать с различными вариантами модели. Вместо того чтобы выбирать одну библиотеку заранее, я решил реализовать обучение сразу на обеих и сравнить их поведение на одинаковых данных.
Подготовка обучающей выборки
Каждая запись датасета представляет собой состояние организма в определенный момент времени. Для этой точки вычисляется полный набор признаков:
текущее значение сахара;
двенадцать предыдущих измерений;
скорость изменения;
ускорение;
скользящие средние;
остаточный инсулин;
остаточные углеводы;
время после еды;
время после болюса;
циклическое время суток.
После этого для каждой строки формируются сразу три целевых значения.
Первое — уровень глюкозы через 15 минут.
Второе — через 30 минут.
Третье — через 45 минут.
Таким образом одна и та же последовательность данных может использоваться для построения нескольких независимых моделей прогнозирования. Такой подход оказался значительно удобнее, чем попытка предсказать сразу всю будущую кривую изменения сахара.
Во-первых, каждая модель обучается на своей задаче.
Во-вторых, появляется возможность сравнивать качество прогнозов на разных временных горизонтах.
Например, предсказание на 15 минут обычно оказывается значительно точнее, чем прогноз почти на час вперед.
Остановка обучения без переобучения
Обе модели обучаются по похожей схеме. Максимальное число деревьев установлено достаточно большим — 1500. На первый взгляд это кажется избыточным. Но в действительности модель почти никогда не использует все деревья. Во время обучения одновременно контролируется качество на валидационной выборке. Если ошибка перестает уменьшаться в течение пятидесяти итераций подряд, обучение автоматически прекращается. Такой механизм называется Early Stopping. Он позволяет получить максимально точную модель, не допуская переобучения.
Это особенно важно для медицинских данных, где излишне сложная модель начинает запоминать случайный шум вместо реальных закономерностей.
Не все ошибки одинаково опасны
Во время экспериментов обнаружилась еще одна интересная особенность. Стандартные функции потерь считают все ошибки одинаковыми. Если модель ошиблась на 2 ммоль/л при уровне сахара 15 ммоль/л, и на те же 2 ммоль/л при сахаре 3,2 ммоль/л, классический RMSE воспримет обе ошибки как равнозначные. Но с медицинской точки зрения это совершенно не так. Если реальный сахар находится в зоне гипогликемии, а модель прогнозирует безопасное значение, пользователь может не получить своевременное предупреждение. Такая ошибка потенциально намного опаснее. Аналогично нежелательно недооценивать выраженную гипергликемию. Поэтому для XGBoost была реализована собственная функция потерь. Она увеличивает штраф за ошибки в наиболее опасных диапазонах. Если фактический уровень глюкозы ниже 3,9 ммоль/л, а модель прогнозирует более высокое значение, ошибка получает пятикратный вес. Если же уровень сахара превышает 11,1 ммоль/л, а модель его занижает, применяется трехкратный коэффициент. В результате алгоритм начинает уделять значительно больше внимания ситуациям, которые действительно важны с практической точки зрения.

Фактически модель обучается не просто минимизировать среднюю ошибку, а снижать вероятность потенциально опасных прогнозов.
Именно этот этап стал одним из самых интересных во всем проекте. Оказалось, что для медицинских задач зачастую недостаточно использовать стандартные алгоритмы «из коробки». Иногда гораздо важнее адаптировать сам процесс обучения под особенности предметной области, чтобы модель оптимизировала не абстрактную математическую метрику, а поведение, полезное для реального пользователя.
Что получилось в итоге
Когда вся система начала работать вместе, стало понятно, что машинное обучение — это лишь небольшая часть общей архитектуры. Прогноз не строится "из воздуха". Каждые несколько минут выполняется целая цепочка вычислений. Сначала из системы непрерывного мониторинга поступает новое значение уровня глюкозы. Затем из журнала извлекаются последние записи о введенном инсулине и приемах пищи. После этого рассчитываются остаточный инсулин (IOB), остаточные углеводы (COB), скорость изменения сахара, ускорение, статистические характеристики и остальные признаки. Полученный вектор передается модели машинного обучения. Она возвращает прогноз уровня глюкозы через 15, 30 и 45 минут. После этого Django API отправляет готовый результат браузеру, а интерфейс обновляет только график, не перезагружая страницу. На первый взгляд кажется, что это длинная цепочка вычислений. На практике все происходит настолько быстро, что пользователь воспринимает прогноз как часть обычного графика.

Ручной анализ данных: почему я не стал полагаться только на алгоритмы
Прежде чем перейти к прогнозированию уровня глюкозы, я добавил в систему еще один важный блок — инструменты ручного анализа данных. На первый взгляд может показаться, что при наличии моделей машинного обучения подобные функции уже не нужны. Однако на практике все оказалось наоборот. Любой алгоритм должен быть понятен пользователю. Если система просто сообщает: «через 30 минут ожидается 8,9 ммоль/л», возникает закономерный вопрос — почему именно такой прогноз? Поэтому еще до появления прогнозирования я решил добавить инструменты, позволяющие самостоятельно анализировать накопленные данные. Они помогают не только пользователям, но и мне самому при разработке и проверке новых алгоритмов.
Статистика за разные периоды
Первой такой функцией стал расчет основных показателей компенсации диабета сразу за несколько временных интервалов. Пользователь может мгновенно посмотреть статистику за последние 7, 14 или 21 день, не формируя отдельные отчеты. В интерфейсе отображаются наиболее важные показатели:
TIR (Time In Range) — процент времени, проведенного в целевом диапазоне глюкозы;
КЧИ — коэффициент чувствительности к инсулину;
УК - углеводный коэффициент.
Такая статистика позволяет быстро оценить, улучшается ли компенсация диабета или, наоборот, появляются тревожные тенденции. Например, снижение TIR при одновременном увеличении количества инсулина может говорить о необходимости пересмотра терапии, а резкие изменения количества потребляемых углеводов помогают объяснить изменение общей картины гликемии.
Коридор сахара вместо сотен отдельных точек
Еще одна проблема дневников непрерывного мониторинга заключается в огромном количестве данных. За неделю система непрерывного мониторинга глюкозы формирует тысячи измерений. Если вывести их все на один график, увидеть общую закономерность практически невозможно. Поэтому в интерфейсе появилась кнопка «Коридор сахара». После выбора периода — 7, 14 или 21 день — все измерения накладываются друг на друга по времени суток. Вместо множества отдельных кривых пользователь получает своеобразную «область» изменения глюкозы, которая сразу показывает, в какие часы сахар наиболее стабилен, а где наблюдается наибольшая вариабельность. Одновременно рассчитываются два важных показателя.
Коэффициент вариабельности (CV) характеризует стабильность уровня глюкозы. Чем меньше его значение, тем ровнее протекает компенсация диабета. Высокая вариабельность часто оказывается не менее важной проблемой, чем высокий средний сахар.
Второй показатель — расчетный HbA1c (GMI). Он позволяет оценить предполагаемое значение гликированного гемоглобина на основании данных непрерывного мониторинга, не дожидаясь лабораторного анализа.
Таким образом пользователь получает не только красивый график, но и объективные числовые показатели качества компенсации.

Мини-график после каждого приема пищи
Наиболее интересной функцией, которой я сам пользуюсь практически ежедневно, стал небольшой четырехчасовой график, отображаемый непосредственно рядом с каждой записью о приеме пищи. После открытия записи система автоматически строит изменение уровня глюкозы в течение четырех часов после начала еды. Это позволяет буквально за несколько секунд ответить на вопросы, которые раньше требовали долгого поиска по дневнику.
Например:
насколько сильно поднялся сахар после конкретного блюда;
правильно ли была рассчитана доза инсулина;
не оказался ли болюс слишком ранним или слишком поздним;
подходит ли выбранное время всасывания для этого продукта.
Фактически каждая запись превращается в небольшой эксперимент с мгновенной обратной связью.
Именно эта функция впоследствии оказалась очень полезной и при разработке математической модели. Анализируя сотни подобных мини-графиков, удалось уточнить параметры всасывания различных продуктов и проверить корректность расчета IOB и COB до того, как они начали использоваться в моделях машинного обучения.

Почему проект продолжает развиваться
Во время разработки я несколько раз убеждался, что самая сложная часть подобных проектов — вовсе не машинное обучение. Гораздо сложнее правильно описать предметную область. Если ошибиться в расчете IOB или COB, добавить неудачные признаки или неправильно подготовить данные, никакой алгоритм уже не сможет компенсировать эти ошибки. И наоборот.
Даже относительно простая модель начинает показывать хорошие результаты, если получает качественную информацию о состоянии организма. Именно поэтому большая часть времени была потрачена не на подбор гиперпараметров XGBoost или CatBoost, а на разработку математической модели, подготовку признаков и постоянную проверку полученных результатов на реальных данных. Эта работа продолжается и сейчас. Постепенно появляются новые признаки, уточняются модели действия инсулина и всасывания углеводов, тестируются различные способы подготовки данных. Каждое подобное изменение сначала проходит проверку на исторических данных, а уже затем попадает в рабочую систему.
Что можно улучшить дальше
Несмотря на уже работающую систему прогнозирования, проект еще далек от завершения. Одно из наиболее интересных направлений — персонализация моделей. Сегодня алгоритм использует единый набор признаков и одинаковые принципы расчета для всех пользователей. Однако каждый человек реагирует на пищу и инсулин по-своему. Даже одинаковая доза Новорапида у двух людей может привести к совершенно разным изменениям уровня глюкозы. То же самое относится и к продуктам питания. Поэтому следующим этапом развития станет автоматическая адаптация модели под конкретного пользователя. Со временем система сможет самостоятельно уточнять скорость всасывания различных блюд, длительность действия инсулина и другие индивидуальные параметры на основании накопленной истории наблюдений. Еще одно перспективное направление — объяснимость прогнозов. Недостаточно просто показать пользователю число «через 30 минут ожидается 9,8 ммоль/л». Гораздо полезнее объяснить, почему модель пришла именно к такому выводу.
Например:
Прогноз повышен из-за большого количества еще не усвоенных углеводов.
или
Ожидается снижение сахара из-за сохраняющегося действия последнего болюса.
Подобные пояснения значительно повышают доверие к системе и помогают пользователю лучше понимать собственный организм.
Заключение
Когда я начинал писать этот проект, мне казалось, что основная задача — создать удобный электронный дневник диабета. Сегодня я понимаю, что дневник был лишь отправной точкой. Постепенно вокруг него появилась целая экосистема.
Сначала — интеграция с системами непрерывного мониторинга глюкозы и инсулиновыми помпами.
Затем — математические модели действия инсулина и усвоения углеводов.
После этого — переход от классических Django-шаблонов к современному интерфейсу, работающему через API без перезагрузки страниц.
И наконец — машинное обучение, которое использует все накопленные данные для прогнозирования уровня глюкозы.
Интересно, что самым ценным результатом проекта для меня стало вовсе не использование XGBoost или CatBoost. Главный вывод оказался гораздо проще.
Качество модели определяется не только выбранным алгоритмом, но прежде всего пониманием предметной области.
Машинное обучение не заменяет знания о физиологии человека. Оно лишь позволяет эффективнее использовать эти знания, если они уже заложены в систему. Именно поэтому разработка медицинских приложений оказывается такой интересной задачей. Здесь приходится одновременно быть разработчиком, аналитиком данных, математиком и немного исследователем.
И, пожалуй, именно это делает подобные проекты по-настоящему увлекательными.
