Skip to content

Repository files navigation

medcost-prediction-app

Система прогнозирования годовых медицинских расходов пациента на основе табличных данных

📖 О проекте

MedCost Prediction App — это high-fidelity прототип цифрового ML-продукта, который позволяет ввести данные пациента, получить прогноз ожидаемых медицинских расходов, сохранить расчёт, получить аналитику по факторам риска и посмотреть историю сохранённых прогнозов.

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

🧑‍💻 Команда проекта

  • Team Lead - руководит проектом, определяет архитектуру, наводит на нужный путь и делает ревью.
  • BA/PM - готовит презентации, представляет проект на отчётных занятиях, отвечает на вопросы и получает обратную связь на защите проекта.
  • Data Scientist - проводит аналитику данных, готовит датасет для ML, строит модели ML.
  • Backend - разрабатывает бэкенд, отвечающий за ML-инференс, бизнес-логику и работу с БД.
  • Frontend - разрабатывает интерфейс прототипа: формы ввода, отображение результатов и навигацию по приложению.
  • QA - проводит тестирование прототипа, оценивает качество системы.

🚀 Запуск проекта

  1. Клонировать репозиторий:
git clone https://github.com/horacemtb/medcost-prediction-app.git
cd medcost-prediction-app
  1. Настроить переменные окружения:

Скопируйте файл .env.example в .env и подставьте свои значения:

cp .env.example .env

Файл .env не отслеживается git — ключи не попадут в репозиторий.

  1. Выполнить команду:
docker compose up --build

После запуска приложение будет доступно по адресу:

http://localhost:8501

Дополнительно backend доступен напрямую по адресу:

http://localhost:8000

Swagger / OpenAPI документация FastAPI:

http://localhost:8000/docs
  1. После завершения работы выполнить команду:
docker compose down

🔍 Исследование рынка и аудитории

Анализ текущего состояния рынка ДМС

Российский рынок добровольного медицинского страхования (ДМС) демонстрирует устойчивую тенденцию к росту. По данным аналитики, объем рынка в 2025 году превысил 320 млрд руб., при этом эксперты рейтингового агентства АКРА прогнозируют дальнейшее увеличение сегмента на 7% в 2026 году — до 342 млрд руб. Особую роль в структуре спроса на инновационные решения для андеррайтинга играет корпоративный сегмент, премии в котором за первое полугодие 2025 года достигли 127,8 млрд руб.

В текущих экономических условиях рынок сталкивается с рядом вызовов:

Медицинская инфляция: Рост стоимости медицинских услуг на 10–13% ежегодно создает риск недооценки расходов страховщика, даже при сохранении стабильных тарифов.

Ценовое давление: Прогнозируемый рост стоимости полисов на 7–15% в 2026 году вынуждает компании отказываться от «усредненных» тарифов в пользу риск-ориентированного ценообразования.

Критический уровень убыточности: Данные страхового брокера Remind указывают на то, что средний показатель убыточности (Loss Ratio) в РФ колеблется в диапазоне 95–97%, что ставит перед страховыми компаниями задачу повышения рентабельности через более точный отбор рисков.

Опыт внедрения ML-решений в андеррайтинг

Современный российский и международный опыт подтверждает, что внедрение ML/AI-решений в сегмент добровольного медицинского страхования (ДМС) обеспечивает измеримый финансовый и операционный эффект. Использование предиктивной аналитики позволяет трансформировать процесс андеррайтинга из экспертной оценки в точный инструмент управления рисками, что ведет к росту маржинальности и снижению убыточности (Loss Ratio).

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

Страховой Дом ВСК

Является наиболее релевантным российским ориентиром. Компания внедрила систему поддержки андеррайтинговых решений, которая позволила автоматизировать около 40% обращений по ДМС. Основные результаты внедрения:

Операционная эффективность: Сокращение времени обработки запросов в 15 раз.

Точность оценки: Рост точности андеррайтинга в 1,5–1,7 раза за счет перехода от субъективного мнения к статистическим моделям.

Бизнес-результат: Эффективная комбинация автоматизации и роста точности способствовала укреплению позиций компании в медицинском сегменте (чистая прибыль по блоку медицины за 2025 год составила 6,64 млрд руб.).

Webiomed

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

Экономический эффект: Заявленный совокупный эффект от внедрения решений составил 7,1 млрд руб.

Ориентир эффективности: До 277 млн руб. экономии на 1 млн населения в год.

Методология успеха: Точность выявления рисков достигается за счет обработки более 150 типов признаков пациента с использованием NLP-технологий.

Merit Medicine (США)

Кейс иллюстрирует потенциал глубокой оптимизации портфеля рисков с помощью AI-моделей:

Снижение убыточности: Использование модели позволило снизить коэффициент убыточности (Loss Ratio) с 83,6% до 55%.

Рост маржинальности: Маржа андеррайтинга (Underwriting Margin) увеличилась с 16,4% до 45%.

Ценность: Данный кейс служит важным обоснованием для проекта MedCost, так как на рынке ДМС любое улучшение качества отбора клиентов и точности тарификации мгновенно конвертируется в прямую прибыль.

Целевая аудитория

Cтраховой аналитик (андеррайтер):

Ключевые компетенции: Глубокое понимание факторов медицинского риска, опыт работы с историческими данными клиентов.

Используемый инструментарий: Excel, BI-системы, внутренние корпоративные порталы и калькуляторы.

Основные KPI: Удержание коэффициента убыточности (Loss Ratio) в заданных пределах, минимизация времени на обработку одной заявки, финансовая обоснованность страховой премии.

Боли и потребности: Необходимость ручного ввода данных из бумажных анкет, риск субъективной оценки (человеческий фактор), отсутствие быстрых инструментов для выявления скрытых корреляций в здоровье пациента.

Уровень доступа: Полный доступ к функционалу скоринга, аналитики и управления историей расчетов.

Менеджер по продажам/страховой агент:

Ключевые компетенции: Продажи, коммуникация, ведение переговоров.

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

Текущие ограничения: Длительное ожидание расчетов от аналитиков; непонимание механизмов ценообразования; риск ошибок при ручном предварительном расчете тарифа для клиента.

🎯 Цели и задачи

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

Цель продукта

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

Какие проблемы решает продукт

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

  • Ошибка I рода (переоценка рисков): предотвращение необоснованного завышения тарифа, ведущего к оттоку «здоровых» клиентов.

  • Ошибка II рода (недооценка рисков): выявление скрытых факторов риска, ведущих к скрытой убыточности (Loss Ratio).

Ускоряет время расчёта медицинских расходов пациента. Если медленно принимать решение => потеря клиентов. Возрастает скорость и масштаб принятия решений. Также аналитик при принятии решения видит основные факторы, повлиявшие на прогноз.

Требования к продукту

Функциональные требования

Функционал системы должен обеспечивать расчет прогноза, аналитику рисков и управление историей и отчетностью:

ФТ-1.1: Система должна предоставлять интерфейс для ручного ввода демографических и физиологических параметров пациента (ФИО, возраст, ИМТ, вредные привычки, уровень сахара и др.) с обязательной клиентской и серверной валидацией.

ФТ-1.2: Система должна поддерживать загрузку скан-копий или фотографий клиентских анкет с последующим автоматическим извлечением текста и предзаполнением полей формы.

ФТ-1.3: При отправке валидных данных система должна обращаться к ML-модели и возвращать расчетную стоимость медицинских расходов на следующий год, а также процентиль риска.

ФТ-1.4: Каждый успешный расчет должен автоматически фиксироваться в базе данных с присвоением уникального prediction_id.

ФТ-1.5: По запросу пользователя система должна рассчитывать и отображать влияние каждого отдельного признака пациента на итоговую сумму прогноза (на базе алгоритмов SHAP).

ФТ-1.6: Факторы риска должны быть отсортированы по степени их математического вклада в итоговую стоимость полиса (от наиболее критичных к наименее значимым).

ФТ-1.7: Система должна отображать табличный список всех ранее выполненных прогнозов с поддержкой пагинации.

ФТ-1.8: Пользователь должен иметь возможность искать расчеты по ФИО пациента и сортировать записи кликом по заголовкам столбцов (по возрастанию/убыванию даты, суммы, возраста).

ФТ-1.9: Система должна позволять перманентно удалять ошибочные прогнозы (с каскадным удалением связанных факторов риска из БД).

ФТ-1.10: Предусмотрена возможность генерации и скачивания официального отчета в формате PDF, содержащего ФИО, дату, сумму прогноза и ключевые риск-факторы.

Нефункциональные требования

НТ-1.1: Время ответа сервера на расчет прогноза (P95) не должно превышать 2000 мс для удаленного сервера (и ~600 мс при локальном тестировании).

НТ-1.2: Время начальной загрузки веб-интерфейса (P95) не должно превышать 3000 мс в 95-м процентиле при условии стандартного сетевого соединения.

НТ-1.3: Система должна обрабатывать не менее 15 запросов на прогнозирование в секунду.

НТ-1.4: OCR: Среднее время извлечения данных из одной отсканированной анкеты (Tesseract OCR) не должно превышать 4 сек.

НТ-1.5: Точность распознавания анкет должна составлять не менее 90% для печатных документов в идеальных условиях.

НТ-1.6: Частота ошибок на уровне символов не должна превышать 10% при распознавании документов.

НТ-1.7: Частота ошибок API не должна превышать 0.5% от общего количества запросов за расчетный период.

НТ-1.8: Инфраструктурный стек должен включать Yandex Cloud (2 сервера: CPU 16 ядер, RAM 32 GB, СХД 2 TB для хранения исторических данных), Managed PostgreSQL и интеграцию с DaData.

🎯 Результаты реализации продукта

Итогом проведенной работы стала разработка и успешный запуск прототипа medcost-prediction-app, который переводит концепцию риск-ориентированного ценообразования в готовый к эксплуатации IT-инструмент.

Реализация целевого процесса андеррайтинга

Разработанный сервис позволил устранить разрыв между текущим (ручным) и целевым (автоматизированным) состояниями процесса:

Исключение человеческого фактора: Бизнес получил инструмент, в котором первичная тарификация полиса ДМС осуществляется объективным алгоритмом, а не экспертной оценкой.

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

Метрики операционной эффективности

Основная цель внедрения системы — перевод процесса андеррайтинга из ручного режима в автоматизированный. Расчет показателей производился на основе деятельности средней страховой компании (60 аналитиков, 18 000 – 36 000 прогнозов в месяц):

Ключевые метрики:

Time Reduction % (Снижение времени андеррайтинга): Благодаря автоматизации процесса, время работы аналитика на один кейс сокращается с 60 до 10 минут.

Cost Saved per Month: При средней зарплате аналитика 100 000 руб./мес., экономия составляет 9 750 000 руб./мес.

OPEX (Операционные расходы): Суммарные затраты составляют 152 200 руб./мес., что обеспечивает высокую масштабируемость (до 500 000 запросов/мес.) и складываются из следующих компонентов:

  • Стоимость аренды двух серверов в облаке с заданными параметрами (Yandex Cloud) — 55 000 руб./мес.;
  • Стоимость аренды Managed PostgreSQL в облаке (Yandex Cloud) — 40 000 руб./мес.;
  • Интеграция с DaData: подсказки до 10 000 запросов в день — бесплатно; стандартизация — 7 200 руб./мес.;
  • Техническая поддержка (обновление, восстановление после сбоев, поддержка) — 50 000 руб./мес.

Cost per Inference: Стоимость одного обращения к модели составляет 4,1 руб.

ROI (Окупаемость): Месячный коэффициент окупаемости составляет 6306%, что подтверждает высокую финансовую эффективность продукта.

Архитектурные метрики надежности

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

1. Latency (время отклика): Время ответа системы на запрос расчёта прогноза.

  • Latency P95 = 57,237 мс (10 000 запросов на локальный сервер);
  • Для удалённого сервера Latency P95 ≈ 200 мс.

2. Throughput (Пропускная способность): Количество операций, обрабатываемых системой в единицу времени.

  • Throughput = 14 прогнозов/сек.

3. API Error Rate (Частота ошибок API): Отношение числа неуспешных запросов к числу успешных.

  • API Error Rate = 0,03 %.

4. Initial Load Time (Время начальной загрузки): Время загрузки веб-интерфейса при первом обращении пользователя.

  • Initial Load Time P95 = 2,23 сек.

Валидация экономической модели

Практическая реализация приложения подтвердила расчетные операционные показатели (OPEX):

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

Масштабируемость: Реализованное API способно выдерживать пиковые нагрузки и обрабатывать целевые объемы (до 36 000 прогнозов в месяц для штата из 60 аналитиков) без деградации производительности. Система масштабируема для крупных компаний: она позволяет использовать существующую инфраструктуру при увеличении нагрузки до 500 000 запросов в месяц.

Инструментарий для снижения убыточности (Loss Ratio)

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

Управление Ошибкой I рода: Система позволяет страховой компании предлагать более конкурентные и справедливые тарифы для клиентов с низким уровнем риска, повышая конверсию в покупку (удержание здоровых клиентов).

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

Конкурентные преимущества

В результате реализации продукта и исследования конкурентных кейсов были определены следующие преимущества:

  • Фокус на индивидуальное страхование;
  • Простой и понятный интерфейс для демонстрации базового сценария оценки;
  • Подходит для средних и малых компаний;
  • Сокращение времени оценки клиента до нескольких минут;
  • Потенциальное снижение ошибок ценообразования за счёт более обоснованной оценки расходов;
  • Высокая объяснимость прогнозов с помощью важности признаков;
  • Адаптивность к новым данным (повторное обучение модели).

Выводы исследований и реализации продукта

  • Рынок ДМС в России находится в фазе устойчивого роста.
  • Отечественные кейсы (ВСК, Webiomed) подтверждают эффективность ML-андеррайтинга.
  • Реализованный продукт устраняет главный риск андеррайтинга — субъективность эксперта, заменяя его алгоритом модели машинного обучения для получения первичной оценки расходов на медицинское обслуживание.
  • Подтверждены заявленные нефункциональные требования и метрики надёжности.
  • Продукт демонстрирует высокую экономическую эффективность: низкие операционные расходы и стоимость одного прогноза обеспечивают быструю окупаемость при масштабировании.

📚 Техническая документация проекта

🧰 Технологический стек

В проекте используется стек, ориентированный на развитие hi-fidelity прототипа в полноценный бизнес-инструмент: с понятной архитектурой, удобным локальным развёртыванием и возможностью дальнейшего масштабирования.

Frontend

  • React 18 — основа пользовательского интерфейса.
  • TypeScript — типизация клиентской логики и API-контрактов.
  • Vite — быстрый dev-сервер и сборка frontend-приложения.
  • React Router — маршрутизация между разделами интерфейса.
  • Tailwind CSS — утилитарная стилизация и адаптивный layout.
  • Recharts — визуализация статистики и графиков на дашборде.
  • Sonner — toast-уведомления для OCR, ошибок и пользовательских действий.

Почему выбран такой стек:

  • фронтенд организован по модульной структуре app / features / pages / widgets / shared;
  • API-контракты типизированы и переиспользуются в клиентской логике;
  • интерфейс собран из переиспользуемых виджетов и собственного UI-kit;
  • дашборд, история, отчёт анализа и OCR-flow развиваются независимо друг от друга;
  • React-приложение удобно масштабировать под новые сценарии без переписывания backend-части.

Backend

  • Python 3.11
  • FastAPI — реализация REST API.
  • Pydantic — валидация входных и выходных схем.
  • SQLAlchemy — ORM и работа с реляционной БД.
  • Uvicorn — ASGI-сервер.
  • python-multipart — загрузка файлов для OCR.

Почему выбран FastAPI:

  • высокая скорость разработки;
  • удобная декларативная валидация через Pydantic;
  • встроенная OpenAPI/Swagger документация;
  • простой и понятный способ описывать API для дальнейшего развития сервиса и интеграций.

ML и аналитика

  • scikit-learn — сохранённый pipeline модели и инференс.
  • joblib — загрузка обученного pipeline из .joblib.
  • SHAP — интерпретация предсказаний и расчёт top risk factors.
  • pandas / numpy — подготовка и агрегация табличных данных.

OCR и работа с документами

  • Tesseract OCR
  • pytesseract — Python-обёртка над Tesseract.
  • Pillow — загрузка и базовая обработка изображений.
  • fpdf2 — генерация PDF-отчётов.

База данных

  • PostgreSQL 16 — основная реляционная СУБД проекта.

Инфраструктура и развёртывание

  • Docker
  • Docker Compose
  • Nginx — обратный прокси-сервер для frontend и backend.

CI/CD и деплой

В проекте реализован базовый CI-конвейер на GitHub Actions. Он автоматически запускается при push и pull_request, собирает контейнеры через docker compose, выполняет тесты внутри Docker-окружения и затем корректно останавливает сервисы. Такой контур позволяет быстро проверять работоспособность backend-части и не допускать попадания в основную ветку изменений, которые ломают сборку или тесты.

На текущем этапе деплой ориентирован на локальное развёртывание через docker compose, однако архитектура проекта уже совместима с дальнейшим расширением CI/CD-контура. В следующей итерации конвейер может быть дополнен автоматической публикацией Docker-образов, развёртыванием на staging-среду, прогоном smoke-тестов после релиза и базовыми сценариями rollback.


🏗️ Архитектура решения

Общая идея

Система построена по классической клиент-серверной схеме с выделением уровня пользовательского интерфейса, backend-сервиса, базы данных и инфраструктурного слоя. Пользователь взаимодействует с web-интерфейсом на React + Tailwind CSS, который отправляет запросы к backend по REST API в формате JSON. Backend реализован на Python + FastAPI, отвечает за бизнес-логику, ML-инференс, OCR-интеграции, генерацию PDF-отчётов, работу с внешними сервисами и сохранение данных в PostgreSQL.

Инфраструктурно проект запускается в контейнеризированном окружении: frontend, backend, PostgreSQL и nginx поднимаются через docker compose, что обеспечивает воспроизводимое локальное развёртывание и единый способ запуска для всей команды. Внешний доступ к интерфейсу и проксирование запросов между сервисами организованы через nginx. Для контроля качества используется GitHub Actions, который выполняет сборку и тесты при изменениях в репозитории.

Схема архитектуры

Архитектура решения

Краткое описание компонентов системы

  • Frontend (React + Tailwind CSS) — пользовательский интерфейс системы: формы ввода, просмотр результатов, история прогнозов, аналитические экраны и работа с OCR-сценарием.
  • nginx — входная точка приложения, которая проксирует HTTP-запросы к frontend и backend-сервисам.
  • Backend (FastAPI) — центральный серверный слой, который реализует REST API, валидацию входных данных, ML-инференс, интерпретацию факторов риска, PDF-генерацию, OCR-flow и интеграцию с Dadata.
  • PostgreSQL — реляционная база данных, в которой хранятся карточки пациентов, история прогнозов, исходная синтетическая когорта и факторы риска.
  • GitHub Actions — CI-контур, автоматически проверяющий сборку контейнеров и прохождение тестов при изменениях в кодовой базе.

1. Клиентский слой

Frontend реализован на React и организован по модульной схеме:

  • app/ — корневой shell приложения, layout, router, глобальное состояние оболочки;
  • features/ — локальные прикладные фичи, например глобальный поиск пациента и подсказки адреса;
  • pages/ — страницы верхнего уровня;
  • widgets/ — крупные составные UI-блоки (форма прогноза, таблица истории, отчёт анализа, dashboard widgets);
  • shared/ — API-клиент, типы, утилиты, общий UI-kit, стили и иконки.

Основные пользовательские маршруты:

  • /dashboard — экран аналитики и статистики;
  • /predict — страница расчёта и просмотра итогового отчёта анализа;
  • /history — история прогнозов, фильтры, сортировка, удаление, открытие существующего отчёта;
  • /settings — служебная страница со статусом backend.

Дополнительно в интерфейсе реализованы:

  • глобальный поиск по ID или имени пациента в верхней панели;
  • боковая навигация с поддержкой сворачивания;
  • открытие существующего отчёта прямо из истории или через глобальный поиск;
  • переиспользуемый UI-kit для форм, таблиц, карточек, модальных окон и уведомлений.

2. Серверный слой

Backend организован вокруг модулей:

  • routers/ — HTTP-эндпойнты;
  • services/ — прикладная логика (ML, OCR, Dadata, PDF);
  • models.py — SQLAlchemy-модели;
  • schemas.py — Pydantic-схемы;
  • database.py — подключение к БД и сессии.

3. Слой данных

Используются четыре основные таблицы:

  • patients
  • predictions
  • risk_factors
  • synthetic_cohort

Роли таблиц описаны подробно в разделе про базу данных.

Логика взаимодействия компонентов

Обычный сценарий расчёта

  1. Пользователь открывает страницу /predict.
  2. Frontend отображает структурированную форму с персональными, медицинскими и поведенческими признаками.
  3. После отправки формы React-приложение вызывает backend endpoint /api/predict.
  4. Backend валидирует payload и передаёт признаки в ML-пайплайн.
  5. ML-модель рассчитывает predicted_cost.
  6. Backend сохраняет прогноз в predictions и возвращает prediction_id.
  7. Frontend переключает страницу из режима ввода в режим отчёта анализа.
  8. Далее frontend запрашивает детали прогноза, факторы риска и итоговую оценку риска через:
    • /api/predictions/{prediction_id}
    • /api/predictions/{prediction_id}/assessment
  9. Пользователь получает единый отчёт анализа: прогноз, факторы влияния, карточку пациента, итоговую категорию риска и рекомендации.
  10. Из того же интерфейса доступны повторный расчёт и экспорт отчёта в PDF.

OCR-сценарий

  1. Пользователь загружает изображение анкеты.
  2. Frontend отправляет файл в /api/ocr/patient-form.
  3. Backend распознаёт поля анкеты.
  4. React подставляет распознанные значения в форму.
  5. Пользователь проверяет и при необходимости исправляет поля.
  6. После этого выполняется обычный прогноз через /api/predict.

Чем обусловлен выбор архитектуры:

  • компоненты разделены по ответственности;
  • backend можно развивать независимо от frontend;
  • контейнеризация делает развёртывание воспроизводимым;
  • API-контракт прозрачен и типизирован;
  • архитектура остаётся достаточно простой для текущей стадии продукта, но при этом допускает дальнейшее развитие;
  • решение не перегружено избыточным числом микросервисов и лишней инфраструктурной сложностью.

🗃️ База данных

Выбор БД

Для проекта выбрана PostgreSQL, потому что она хорошо подходит под реляционную модель данных приложения:

  1. Чёткая схема и связи

    • у проекта есть сущности patients, predictions, risk_factors, synthetic_cohort;
    • между ними есть естественные связи one-to-many;
    • такая структура лучше укладывается в реляционную СУБД, чем в документоориентированную.
  2. Поддержка транзакций

    • важно для сценариев, где нужно атомарно сохранить пациента и прогноз;
    • помогает избегать частично сохранённых данных.
  3. Удобная работа с SQLAlchemy

    • PostgreSQL — один из самых типовых вариантов для Python + SQLAlchemy + FastAPI.
  4. Лучше, чем SQLite, для многоконтейнерного сервиса

    • SQLite удобна для маленьких локальных скриптов, но хуже подходит для клиент-серверного сценария с отдельным backend-контейнером и отдельной БД;
    • PostgreSQL лучше соответствует целевой архитектуре проекта.
  5. Более уместна, чем NoSQL, для текущей модели

    • в проекте нет необходимости хранить произвольные документы без схемы;
    • здесь важнее связи, индексы и предсказуемая структура данных.

Почему не SQLite

SQLite могла бы подойти для самой первой прототипной версии, но:

  • она файловая и не даёт такой же модели серверной БД;
  • хуже отражает реальную продуктовую архитектуру;
  • менее удобна для расширения схемы и многопользовательских сценариев.

Почему не MongoDB

MongoDB имеет смысл, когда:

  • структура данных плохо формализуется;
  • нужен документный формат без строгих связей.

В данном проекте, наоборот, данные хорошо структурированы, а связи между сущностями прозрачны.


Схема таблиц

1. patients

Таблица карточек пациентов.

Один человек хранится один раз.
Используется для хранения идентификационных данных пациента.

Поле Тип Описание
id integer, PK Внутренний идентификатор пациента
full_name string ФИО пациента
snils string, unique, not null Внешний идентификатор пациента для поиска, дедупликации и связывания расчётов
phone string, nullable Телефон пациента
address text, nullable Адрес пациента
created_at datetime Дата создания записи
updated_at datetime Дата последнего обновления

snils не используется как primary key. Внутренним ключом остаётся id, а snils — бизнес-идентификатор для поиска существующего пациента.

2. predictions

Таблица истории расчётов.

Один расчёт = одна запись.
Один пациент может иметь много записей в predictions.

Поле Тип Описание
id integer, PK Идентификатор прогноза
patient_id FK → patients.id Ссылка на карточку пациента
full_name string Snapshot имени на момент расчёта
age integer Возраст
gender integer Пол (0/1)
bmi float Индекс массы тела
smoker boolean Курение
diabetes boolean Диабет
hypertension boolean Гипертония
heart_disease boolean Болезни сердца
asthma boolean Астма
physical_activity_level string Уровень физической активности
daily_steps integer Число шагов в день
sleep_hours float Продолжительность сна
stress_level integer Уровень стресса
doctor_visits_per_year integer Визитов к врачу в год
hospital_admissions integer Госпитализаций
medication_count integer Количество лекарств
city_type string Тип населённого пункта
previous_year_cost float Расходы за прошлый год
predicted_cost float Рассчитанный прогноз
created_at datetime Дата расчёта

3. risk_factors

Таблица факторов риска для конкретного прогноза.

Поле Тип Описание
id integer, PK Идентификатор записи
prediction_id FK → predictions.id Ссылка на прогноз
feature_name string Имя признака
feature_value string Значение признака
shap_value float Вклад признака в прогноз
rank integer Позиция фактора в top-N

4. synthetic_cohort

Таблица с исходной синтетической когортой.

Используется для:

  • расчёта перцентилей;
  • референсной аналитики;
  • статистики на дашборде.
Поле Тип Описание
id integer, PK Идентификатор baseline-записи
age integer, nullable Возраст
gender string, nullable Пол
bmi float, nullable BMI
smoker boolean, nullable Курение
diabetes boolean, nullable Диабет
hypertension boolean, nullable Гипертония
heart_disease boolean, nullable Болезни сердца
asthma boolean, nullable Астма
physical_activity_level string, nullable Физическая активность
daily_steps integer, nullable Шаги
sleep_hours float, nullable Сон
stress_level integer, nullable Стресс
doctor_visits_per_year integer, nullable Визиты к врачу
hospital_admissions integer, nullable Госпитализации
medication_count integer, nullable Количество лекарств
city_type string, nullable Тип населённого пункта
previous_year_cost float, nullable Расходы за прошлый год
annual_medical_cost float, nullable Фактический baseline-cost
created_at datetime Дата загрузки записи

Связи между таблицами

ER-диаграмма БД

Пример связи patientspredictions

  • в patients пациент хранится один раз;
  • в predictions один и тот же пациент может иметь несколько расчётов;
  • связь идёт через predictions.patient_id = patients.id.

Это позволяет:

  • искать предыдущий прогноз не по имени, а по patient_id;
  • не путать тёзок;
  • хранить историю расчётов отдельно от карточки пациента.

🔌 REST API и контракт взаимодействия

Общие принципы

  • формат обмена: JSON
  • загрузка анкеты для OCR: multipart/form-data
  • PDF-экспорт: binary response (application/pdf)
  • основной namespace: /api
  • спецификация и тестирование: Swagger / OpenAPI на /docs

Список endpoint’ов

Метод Endpoint Назначение
GET /api/health Проверка доступности backend
POST /api/predict Создать новый прогноз
PUT /api/predictions/{prediction_id}/recalculate Пересчитать существующий прогноз
GET /api/predictions/{prediction_id} Получить детали прогноза
GET /api/predictions/{prediction_id}/factors Получить топ-факторы
GET /api/predictions/{prediction_id}/assessment Получить категорию риска / перцентиль / рекомендацию
GET /api/predictions/{prediction_id}/pdf Скачать PDF-отчёт
GET /api/history Получить историю прогнозов
DELETE /api/history/{prediction_id} Удалить прогноз
POST /api/ocr/patient-form Распознать поля анкеты
POST /api/percentile Рассчитать перцентиль для заданного predicted_cost
GET /api/stats/overview Получить агрегированную статистику для дашборда

Основные схемы запросов и ответов

POST /api/predict

Создаёт новый прогноз.

Request body:

{
  "full_name": "Иван Иванов",
  "snils": "123-456-789 00",
  "phone": "+7-900-000-00-00",
  "address": "г. Москва",
  "age": 45,
  "gender": 1,
  "bmi": 27.5,
  "smoker": false,
  "diabetes": false,
  "hypertension": false,
  "heart_disease": false,
  "asthma": false,
  "physical_activity_level": "Medium",
  "daily_steps": 6000,
  "sleep_hours": 7.0,
  "stress_level": 4,
  "doctor_visits_per_year": 2,
  "hospital_admissions": 0,
  "medication_count": 1,
  "city_type": "Urban",
  "previous_year_cost": 12000.0
}

Response:

{
  "prediction_id": 101,
  "full_name": "Иван Иванов",
  "predicted_cost": 14530.42,
  "patient_id": 5,
  "created_at": "2026-04-20T10:15:00"
}

GET /api/predictions/{prediction_id}

Возвращает полные детали прогноза и список факторов риска.

GET /api/predictions/{prediction_id}/assessment

Возвращает:

  • перцентиль;
  • категорию риска;
  • текстовую рекомендацию.

GET /api/history

Поддерживает query-параметр:

  • search — поиск по ID или ФИО;
  • limit — ограничение количества записей.

Пример:

GET /api/history?search=Иванов&limit=100

POST /api/ocr/patient-form

Принимает файл изображения анкеты и возвращает:

  • fields — распознанные поля;
  • raw_text — сырой OCR-текст;
  • warnings — предупреждения по распознаванию и валидации.

GET /api/stats/overview

Возвращает агрегированную статистику для дашборда frontend-приложения.

Ответ состоит из двух блоков:

  • synthetic — baseline-метрики по synthetic_cohort;
  • predictions — live-метрики по пользовательским прогнозам.

Frontend использует эти данные для построения нескольких виджетов:

  • сводного обзора;
  • профиля рисков;
  • сравнения прогнозов с исторической выборкой;
  • распределения по полу;
  • монитора объяснимости по топ факторам.

Дополнительно в блоке predictions используется поле high_cost_prediction_share — доля прогнозов выше 90-го перцентиля исторических расходов.


Примеры запросов

Проверка healthcheck

curl http://localhost:8000/api/health

Создание прогноза

curl -X POST http://localhost:8000/api/predict \
  -H "Content-Type: application/json" \
  -d '{
    "full_name": "Иван Иванов",
    "snils": "123-456-789 00",
    "phone": "+7-900-000-00-00",
    "address": "г. Москва",
    "age": 45,
    "gender": 1,
    "bmi": 27.5,
    "smoker": false,
    "diabetes": false,
    "hypertension": false,
    "heart_disease": false,
    "asthma": false,
    "physical_activity_level": "Medium",
    "daily_steps": 6000,
    "sleep_hours": 7.0,
    "stress_level": 4,
    "doctor_visits_per_year": 2,
    "hospital_admissions": 0,
    "medication_count": 1,
    "city_type": "Urban",
    "previous_year_cost": 12000.0
  }'

Получение факторов риска

curl http://localhost:8000/api/predictions/101/factors

Получение percentile

curl -X POST http://localhost:8000/api/percentile \
  -H "Content-Type: application/json" \
  -d '{"predicted_cost": 14530.42}'

Получение OCR-результата

curl -X POST http://localhost:8000/api/ocr/patient-form \
  -F "file=@patient_form.png"

Экспорт PDF

curl http://localhost:8000/api/predictions/101/pdf --output prediction-report.pdf

🧠 OCR-подсистема

Назначение

OCR-подсистема нужна для автоматического распознавания данных из анкеты пациента и автозаполнения формы расчёта на frontend.

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

Используемые библиотеки

  • Tesseract OCR — движок распознавания текста;
  • pytesseract — Python-обёртка для вызова Tesseract из backend-кода;
  • Pillow — чтение изображения и приведение его к RGB-формату перед OCR.

Какие поля поддерживаются

OCR-сервис распознаёт следующие поля:

  • full_name
  • snils
  • address
  • phone
  • age
  • gender
  • bmi
  • smoker
  • diabetes
  • hypertension
  • heart_disease
  • asthma
  • physical_activity_level
  • daily_steps
  • sleep_hours
  • stress_level
  • doctor_visits_per_year
  • hospital_admissions
  • medication_count
  • city_type
  • previous_year_cost

Как работает OCR-алгоритм

1. Загрузка изображения

Backend получает файл через multipart/form-data и пытается открыть его как изображение.

Если файл пустой, повреждён или не может быть прочитан как изображение, API возвращает ошибку чтения изображения анкеты.

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

2. OCR текста через Tesseract

Для распознавания используется Tesseract OCR со следующими настройками:

  • основной язык: rus;
  • fallback-язык: rus_standard;
  • OCR config: --oem 1.

На основном проходе Tesseract строит два результата:

  • raw_text — полный распознанный текст анкеты через image_to_string(...);
  • ocr_data — набор распознанных текстовых фрагментов с координатами через image_to_data(...).

raw_text используется для извлечения большинства текстовых и числовых полей.

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

3. Парсинг текстовых и числовых полей

После OCR backend извлекает большинство полей из raw_text.

Для этого используются:

  • нормализация текста;
  • регулярные выражения;
  • отдельные функции очистки и нормализации значений;
  • приведение распознанных строк к внутреннему формату backend и ML-модели.

На этом этапе координаты OCR-фрагментов не используются для большинства обычных текстовых полей. Основная логика здесь строится на анализе распознанного сплошного текста.

3.1. Как извлекаются значения полей

Для каждого поля задан свой шаблон поиска по тексту анкеты.

Например, алгоритм ищет в raw_text строки или фрагменты, соответствующие полям:

  • Возраст;
  • Пол;
  • BMI;
  • Уровень физической активности;
  • Шагов в день;
  • Часы сна;
  • Уровень стресса;
  • Визитов к врачу в год;
  • Госпитализаций;
  • Количество лекарств;
  • Тип населённого пункта;
  • Расходы за прошлый год.

После нахождения текстового значения backend пытается привести его к нужному типу и формату.

Для ФИО используется отдельная логика:

  1. алгоритм ищет строку с заголовком ФИО пациента;
  2. затем берёт значение после двоеточия;
  3. если значение не найдено в той же строке, проверяет несколько ближайших следующих строк;
  4. найденная строка очищается от лишних символов, не относящихся к имени.

Для опциональных полей address и phone используется отдельный блок извлечения. Если эти поля не распознаны, они не считаются критической ошибкой.

3.2. Нормализация значений

После извлечения значения приводятся к внутреннему формату:

  • СНИЛС нормализуется к виду XXX-XXX-XXX XX;
  • телефон очищается от лишних символов;
  • пол преобразуется в числовой формат:
    • женский0;
    • мужской1;
  • уровень физической активности преобразуется в категории:
    • низкийLow;
    • среднийMedium;
    • высокийHigh;
  • тип населённого пункта преобразуется в категории:
    • город → Urban;
    • пригород → Semi-Urban;
    • сельский населённый пункт / деревня → Rural;
  • целочисленные признаки приводятся к int;
  • дробные числовые признаки приводятся к float.

Если значение найдено, но не может быть корректно нормализовано, backend добавляет предупреждение в warnings.

4. Отдельная обработка чекбоксов

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

К бинарным полям относятся:

  • smoker
  • diabetes
  • hypertension
  • heart_disease
  • asthma

Для этих признаков backend анализирует не только текст, но и расположение элементов на изображении.

Алгоритм работает следующим образом:

  1. Tesseract возвращает OCR-данные через image_to_data(...).
  2. Backend извлекает из них распознанные слова и координаты их bounding boxes.
  3. Слова группируются в строки по OCR-метаданным.
  4. На изображении ищутся квадратные области, похожие на чекбоксы.
  5. Для каждого бинарного поля ищется строка с его названием, например курение, диабет, гипертония, болезни сердца, астма.
  6. В этой строке алгоритм пытается найти чекбоксы, соответствующие вариантам Да и Нет.
  7. Если подписи Да и Нет не удалось надёжно сопоставить по словам, используется fallback-логика: берутся два последних чекбокса в строке.
  8. Для найденных чекбоксов анализируется внутренняя область квадрата и определяется, отмечен он или нет.
  9. Если отмечен вариант Да, поле получает значение true.
  10. Если отмечен вариант Нет или не отмечен Да, поле получает значение false.

Если в одном поле отмечены оба варианта, backend добавляет предупреждение и возвращает для этого поля false.

Если строка чекбокса или сами чекбоксы не найдены, backend также добавляет предупреждение.

5. Fallback OCR

Если после основного текстового парсинга остаются предупреждения, сервис делает дополнительный OCR-проход с fallback-языком rus_standard.

Fallback-проход используется для повторного извлечения текстовых и числовых полей из распознанного текста.

Важно: fallback OCR применяется именно к текстовому распознаванию через image_to_string(...). Для чекбоксов отдельный fallback image_to_data(...) не строится: чекбоксы обрабатываются по OCR-данным основного прохода.

После fallback-прохода backend объединяет результаты:

  • значения, найденные на fallback-проходе, могут дополнить основной результат;
  • значения, уже найденные на основном проходе, сохраняют приоритет;
  • предупреждения по полям, которые удалось восстановить, фильтруются.

6. Валидация распознанных полей

После извлечения и нормализации выполняется валидация распознанных значений.

Проверяются:

  • типы данных;
  • допустимые числовые диапазоны;
  • допустимые категории;
  • корректность бинарных значений;
  • корректность значений gender, physical_activity_level и city_type.

Для числовых полей заданы допустимые диапазоны.

Например:

  • age: от 18 до 100;
  • daily_steps: от 0 до 50000;
  • stress_level: от 1 до 10;
  • doctor_visits_per_year: от 0 до 100;
  • hospital_admissions: от 0 до 50;
  • medication_count: от 0 до 100;
  • bmi: от 10.0 до 60.0;
  • sleep_hours: от 0.0 до 24.0;
  • previous_year_cost: от 0.0 и выше.

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

7. Ответ API

На выходе OCR API возвращает объект со следующими полями:

{
  "fields": {},
  "raw_text": "",
  "warnings": []
}

Где:

  • fields — словарь успешно распознанных и провалидированных значений;
  • raw_text — полный текст, полученный на основном OCR-проходе;
  • warnings — список предупреждений о полях, которые не удалось распознать, нормализовать или провалидировать.

Frontend использует fields для автозаполнения формы расчёта.

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

Схема OCR-потока

OCR pipeline

Почему выбран Tesseract OCR

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

Основные причины выбора:

  • open-source и бесплатность — Tesseract распространяется свободно и не требует оплаты за использование или зависимости от коммерческого OCR API;
  • локальный запуск — Tesseract работает внутри backend-контейнера и не требует внешнего API;
  • простая интеграция — его легко встроить в существующий Python/FastAPI backend;
  • доступ к координатам текста — помимо распознанных слов можно получить bounding boxes и использовать их для привязки значений к полям анкеты;
  • прозрачный пайплайн — Tesseract хорошо сочетается с разбором документа с учётом его структуры, координат текстовых блоков и расположения полей.;
  • предсказуемость и воспроизводимость — для структурированной анкеты rule-based обработка поверх Tesseract даёт более контролируемый результат.

Для оценки OCR-контура в проекте учитываются базовые метрики качества распознавания текста.

  • Accuracy — доля правильно распознанных символов или слов относительно эталонного текста. Для печатных документов в хороших условиях ориентировочно составляет 95–98%.
  • CER (Character Error Rate) — доля ошибок на уровне символов, включая замены, удаления и вставки. Для фотографий документов ориентировочное значение составляет 1.5–2.5%.

Источник: https://www.researchgate.net/publication/380896842_Issledovania_metodov_raspoznavania_tekstovyh_dokumentov_s_ispolzovaniem_komputernogo_zrenia

Эти значения используются как практические ориентиры для оценки качества OCR на структурированных анкетах. В прикладном сценарии проекта дополнительно важна не только общая точность распознавания текста, но и корректность извлечения конкретных полей формы.

Использование LLM в качестве основного OCR-слоя не было выбрано, так как для анкеты фиксированного шаблона это было бы избыточным решением. Такой подход потребовал бы более сложной инфраструктуры, дал бы менее детерминированный результат и усложнил бы отладку. В текущей архитектуре связка Tesseract + постобработка правилом лучше соответствует задаче.


🧩 Интеграция с Dadata

Назначение

В проекте подготовлен отдельный сервисный модуль dadata_service.py, который нужен для нормализации и обогащения идентификационных полей пациента.

Поддерживаются функции:

  • clean_full_name(...)
  • clean_address(...)
  • clean_phone(...)
  • suggest_address(...)

Что именно делает сервис

1. Нормализация ФИО

Используется endpoint Dadata Cleaner name, чтобы привести имя к более стандартизованному виду.

2. Нормализация адреса

Используется endpoint Cleaner address, чтобы уменьшить вариативность адресных строк.

3. Нормализация телефона

Используется endpoint Cleaner phone.

4. Подсказки по адресу

Используется endpoint Suggestions API suggest/address.

Как подключается

Dadata активируется через переменные окружения:

  • DADATA_API_KEY
  • DADATA_API_SECRET

Если ключи не заданы:

  • backend не падает;
  • сервис просто возвращает исходные значения или пустой результат.

Какие плюсы даёт интеграция с Dadata

  • снижение количества дублей пациентов — за счёт более точной нормализации и сопоставления персональных данных;
  • повышение качества карточек patients — данные становятся более полными, единообразными и удобными для последующей обработки;
  • улучшение качества данных после OCR — распознанные значения можно дополнительно очищать и приводить к более надёжному формату;
  • более удобный пользовательский сценарий — адреса и другие поля можно быстрее и точнее заполнять на frontend.

📦 Контейнеризация и развёртывание

Общая схема

Проект поднимается через Docker Compose и состоит из четырёх сервисов:

  • db
  • backend
  • frontend-react
  • nginx

docker-compose.yml

db

  • образ: postgres:16
  • хранит все данные приложения;
  • имеет healthcheck, чтобы backend стартовал после готовности БД.

backend

  • собирается из src/backend/Dockerfile;
  • поднимает FastAPI на порту 8000;
  • получает:
    • DATABASE_URL
    • MODEL_PATH

frontend-react

  • собирается из src/frontend-react/Dockerfile;
  • запускает Vite dev-сервер на 8501 внутри контейнера.

nginx

  • проксирует:
    • /frontend-react
    • /api/backend
  • наружу публикуется на порту 8501.

Схема контейнеров

Контейнерная схема

Backend Dockerfile

Backend image:

  • основан на python:3.11-slim;
  • устанавливает tesseract-ocr, tesseract-ocr-rus, fonts-dejavu-core;
  • обновляет русский tessdata-файл;
  • ставит Python-зависимости;
  • копирует backend-код и артефакт модели;
  • запускает uvicorn.

Frontend Dockerfile

Frontend image:

  • основан на node:20-alpine;
  • устанавливает npm-зависимости;
  • копирует React/Vite-проект;
  • запускает Vite dev server.

Для production-режима React-приложение можно собирать в static build и отдавать напрямую из Nginx. В текущей версии выбран более гибкий способ: frontend работает в отдельном контейнере, а Nginx проксирует к нему запросы. Такой вариант удобен для активной разработки и дальнейшего перехода к production-контуру.

Nginx

Конфигурация infra/nginx/default.conf делает две вещи:

  1. проксирует /api/* на backend:8000;
  2. проксирует все остальные запросы на frontend-react:8501.

Это позволяет использовать один внешний адрес:

http://localhost:8501

для всего приложения.


🔐 Переменные окружения

Backend

Переменная Назначение
DATABASE_URL Строка подключения к PostgreSQL
MODEL_PATH Путь к сохранённому .joblib pipeline
DADATA_API_KEY Ключ доступа к Dadata Cleaner / Suggestions
DADATA_API_SECRET Секрет Dadata

Frontend

Переменная Назначение
VITE_API_BASE_URL Базовый URL backend API

В текущем nginx-сценарии frontend может работать и с пустым VITE_API_BASE_URL, используя относительные пути /api/*.


📊 Дашборд и статистики

Дашборд во frontend построен на endpoint:

GET /api/stats/overview

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

1. synthetic

Референсная статистика по таблице synthetic_cohort:

  • количество записей;
  • средние и медианные расходы annual_medical_cost;
  • счётчики по ключевым факторам риска:
    • курение,
    • диабет,
    • гипертония,
    • болезни сердца,
    • астма;
  • распределение по полу;
  • гистограмма исторических расходов.

2. predictions

Live-статистика по пользовательским прогнозам:

  • количество сохранённых прогнозов;
  • средний и медианный predicted_cost;
  • доля прогнозов выше 90-го перцентиля исторической выборки (high_cost_prediction_share);
  • гистограмма предсказанных расходов;
  • топ-факторы по таблице risk_factors.

3. Виджеты dashboard

Текущая frontend-реализация использует эти данные в следующих виджетах:

  • Сводный обзор — объёмы данных, средние значения, гистограммы исторических данных и прогнозов;
  • Профиль рисков — доля пациентов с ключевыми факторами риска;
  • Отклонение прогноза от исторической выборки — сравнение среднего, медианы и доли дорогих случаев;
  • Распределение по полу — сегментация исторической выборки;
  • Монитор объяснимости — частота появления признаков в топ факторов риска.

Почему статистики разделены

Это принципиально важно для интерпретации:

  • synthetic_cohort — референсная baseline-когорта с историческими данными;
  • predictions — реальные пользовательские расчёты, сделанные в приложении.

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


🧪 Отладка и ручная проверка

Проверка состояния контейнеров

docker compose ps

Логи backend

docker compose logs -f backend

Подключение к PostgreSQL

docker compose exec db psql -U postgres -d medcost_db

Полезные SQL-команды

Показать таблицы:

\dt

Посмотреть структуру:

\d patients
\d predictions
\d risk_factors
\d synthetic_cohort

Проверить связку пациентов и прогнозов:

SELECT
    p.id AS prediction_id,
    p.patient_id,
    p.full_name,
    pt.full_name AS patient_name,
    pt.snils,
    p.predicted_cost,
    p.created_at
FROM predictions p
LEFT JOIN patients pt ON p.patient_id = pt.id
ORDER BY p.id DESC;

Проверить дубли по snils:

SELECT snils, COUNT(*)
FROM patients
WHERE snils IS NOT NULL
GROUP BY snils
HAVING COUNT(*) > 1;

📈 Алгоритмы машинного обучения

Данный раздел описывает ML-исследование по прогнозированию годовых медицинских расходов пациента (annual_medical_cost) на основе табличных данных. Основной фокус — ноутбук full-research.ipynb; дополнительные ноутбуки используются как проверка выводов на более крупном и сложном датасете.

Исследование состоит из четырёх ноутбуков:

Ноутбук Роль
full-research.ipynb Основное исследование на исходном синтетическом датасете
additional-research-all-features.ipynb Дополнительный эксперимент на расширенном датасете с максимально широким набором признаков
additional-research.ipynb Дополнительный эксперимент на расширенном leakage-aware наборе признаков
additional-research-common-features.ipynb Проверка моделей на признаках, сопоставимых с исходным датасетом

1. Бизнес-задача

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

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

Основная ML-гипотеза состоит в том, что модели машинного обучения смогут лучше уловить структуру данных, чем простые фиксированные правила, потому что они:

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

Поэтому в исследовании сравниваются разные классы моделей: от интерпретируемых линейных моделей до более гибких tree-based и boosting-алгоритмов.

В рамках проекта решается задача регрессии:

X -> annual_medical_cost

где:

X — набор признаков пациента;
annual_medical_cost — ожидаемые годовые медицинские расходы.

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

annual_medical_cost = annual_medical_cost / (1 - insurance_coverage_pct / 100)

После корректировки признаки insurance_type и insurance_coverage_pct были исключены из модели, так как в целевом сценарии пациент может ещё не иметь выбранной страховки.


2. Данные

2.1. Основной синтетический датасет

Основное исследование выполнено на синтетическом датасете:

  • наблюдений: 5 000;
  • исходных признаков: 20;
  • пропуски: отсутствуют;
  • целевая переменная: annual_medical_cost.

Группы признаков:

Группа Признаки
Демография age, gender, bmi
Образ жизни smoker, physical_activity_level, daily_steps, sleep_hours, stress_level
Заболевания diabetes, hypertension, heart_disease, asthma
Медицинская нагрузка doctor_visits_per_year, hospital_admissions, medication_count
Локация city_type
Исторические расходы previous_year_cost
Target annual_medical_cost

2.2. Расширенный датасет

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

  • наблюдений: 100 000;
  • признаков: 50+;
  • целевая переменная: annual_medical_cost.

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

Группа Признаки
Демография и социально-экономические показатели person_id, age, sex, region, urban_rural, income, education, marital_status, employment_status, household_size, dependents
Образ жизни bmi, smoker, alcohol_freq
Клинические показатели и риск systolic_bp, diastolic_bp, ldl, hba1c, risk_score, is_high_risk
Хронические состояния chronic_count, hypertension, diabetes, asthma, copd, cardiovascular_disease, cancer_history, kidney_disease, liver_disease, arthritis, mental_health
Медицинская нагрузка visits_last_year, hospitalizations_last_3yrs, days_hospitalized_last_3yrs, medication_count
Медицинские процедуры proc_imaging_count, proc_surgery_count, proc_physio_count, proc_consult_count, proc_lab_count, had_major_procedure
Страховой план plan_type, network_tier, deductible, copay, policy_term_years, policy_changes_last_2yrs, provider_quality
Плата за страховку и стаховые выплаты annual_premium, monthly_premium, claims_count, avg_claim_amount, total_claims_paid
Целевая переменная annual_medical_cost

person_id является техническим идентификатором и не используется как признак в моделировании. annual_medical_cost используется как целевая переменная.

В разных версиях дополнительного исследования использовались разные наборы признаков:

  • additional-research-common-features — только признаки, сопоставимые с исходным синтетическим датасетом;
  • additional-research — расширенный leakage-safe набор без policy-, premium- и claims-признаков;
  • additional-research-all-features — максимально полный набор доступных признаков, за исключением технического идентификатора и отдельных явно дублирующих признаков.

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


3. Краткие выводы EDA

3.1. Основной синтетический датасет

Основные наблюдения:

  • annual_medical_cost имеет умеренную правостороннюю асимметрию: среднее около 19 123, медиана около 18 538, skew около 0.56.
  • Самые сильные связи с таргетом по phi_k:
    • hospital_admissions — около 0.82;
    • heart_disease — около 0.31;
    • previous_year_cost — около 0.30;
    • smoker и medication_count — около 0.24.
  • Наиболее выраженная линейная связь с расходами у hospital_admissions: Pearson около 0.85, Spearman около 0.83.
  • Категориальные медицинские признаки (smoker, diabetes, hypertension, heart_disease) визуально повышают уровень расходов.

Матрица зависимостей phi_k для исходного датасета

3.2. Расширенные данные

В расширенном датасете EDA показал:

  • annual_medical_cost сильно скошен вправо: основная масса пациентов имеет небольшие расходы, но есть длинный хвост дорогих случаев.
  • Бинарные медицинские признаки сильно несбалансированы: положительный класс встречается редко, но часто связан с более высокими расходами.
  • В leakage-aware версии наиболее заметные связи с таргетом имеют:
    • risk_score;
    • days_hospitalized_last_3yrs;
    • chronic_count;
    • visits_last_year;
    • hospitalizations_last_3yrs.
  • В all-features версии наиболее сильные связи дают claims- и premium-признаки:
    • annual_premium;
    • monthly_premium;
    • total_claims_paid;
    • avg_claim_amount.

4. Подготовка данных и предобработка признаков

4.1. Разделение данных

Во всех исследованиях использовался фиксированный train/test split:

  • train: 80%;
  • test: 20%;
  • random_state = 42.

Для сохранения похожего распределения целевой переменной в train и test использовалась стратификация по бинам таргета (pd.qcut).

Test set откладывался до финального этапа. Подбор гиперпараметров, выбор модели, OOF-предсказания и веса ансамблей считались только на train на кросс-валидации.

4.2. Кросс-валидация

Для промежуточной оценки качества использовалась 5-фолдовая кросс-валидация:

KFold(n_splits=5, shuffle=True, random_state=42)

Основная метрика подбора — MAE.

4.3. Пайплайны предобработки

В проекте сознательно используются разные пайплайны предобработки для разных семейств моделей.

Линейные модели

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

  • числовые признаки:
    • SimpleImputer(strategy='median');
    • для асимметричных признаков — log1p;
    • StandardScaler;
  • бинарные признаки:
    • SimpleImputer(strategy='most_frequent');
  • категориальные признаки:
    • SimpleImputer;
    • OneHotEncoder;
  • в части расширенных экспериментов таргет также логарифмировался через TransformedTargetRegressor.

Цель такой обработки — сделать признаки сопоставимыми по масштабу и улучшить устойчивость регуляризованных линейных моделей.

Random Forest / ExtraTrees / HistGradientBoosting

Для sklearn моделей на основе деревьев масштабирование не требуется:

  • отсутствующие значения в числовых признаках заменяются медианой;
  • бинарные признаки остаются как 0/1;
  • категориальные признаки кодируются через OneHotEncoder;
  • ordinal-признаки, если есть естественный порядок, могут кодироваться через OrdinalEncoder.

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

CatBoost

Для CatBoost использовался отдельный пайплайн:

  • числовые признаки передаются как числовые;
  • бинарные признаки остаются как 0/1;
  • категориальные признаки передаются в CatBoost нативно через cat_features.

Это важно, потому что CatBoost умеет эффективно работать с категориальными признаками без ручного one-hot encoding.


5. Модели

В исследовании использовались несколько классов моделей.

Линейные модели

Модель Идея
LinearRegression Базовая линейная регрессия без регуляризации
Ridge Линейная модель с L2-регуляризацией; стабилизирует коэффициенты при коррелированных признаках
Lasso Линейная модель с L1-регуляризацией; может занулять часть коэффициентов
ElasticNet Комбинация L1 и L2; балансирует отбор признаков и устойчивость
LassoSelect + Ridge Сначала Lasso выполняет отбор признаков; за счёт L1-регуляризации часть коэффициентов зануляется или становится очень маленькой, после чего в модель передаются только наиболее значимые признаки; затем на отобранном наборе признаков обучается Ridge, который более устойчиво оценивает коэффициенты за счёт L2-регуляризации

Линейные модели полезны как интерпретируемый baseline: по коэффициентам можно понять направление влияния признаков.

Модели на основе деревьев и бустинговые модели

Модель Идея
RandomForestRegressor Ансамбль независимых деревьев, обученных на bootstrap-сэмплах; хорошо снижает variance
ExtraTreesRegressor Похож на Random Forest, но добавляет больше случайности в разбиения
HistGradientBoostingRegressor Градиентный бустинг на гистограммных признаках; эффективен на больших табличных данных
CatBoostRegressor Градиентный бустинг с нативной обработкой категориальных признаков

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


6. Метрики

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

MAE

MAE = mean(|y - y_pred|)

Главная метрика проекта. Показывает среднюю абсолютную ошибку в денежных единицах.

RMSE

RMSE = sqrt(mean((y - y_pred)^2))

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

R² = 1 - SS_res / SS_total

Показывает долю дисперсии таргета, объяснённую моделью. Чем ближе к 1, тем лучше.

MAPE

MAPE = mean(|y - y_pred| / y)

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


7. Подбор гиперпараметров

Линейные модели

Для линейных моделей использовался GridSearchCV.

Подбирались:

  • alpha для Ridge и Lasso;
  • alpha и l1_ratio для ElasticNet;
  • параметры отбора признаков для LassoSelect + Ridge (подбирались alpha для Lasso-selector, порог отбора признаков threshold в SelectFromModel, а также alpha для финальной Ridge-модели).

Random Forest

Для Random Forest использовался GridSearchCV.

Основные параметры:

  • n_estimators;
  • max_depth;
  • min_samples_split;
  • min_samples_leaf;
  • max_features.

HistGradientBoostingRegressor

Для HistGradientBoosting использовалась Optuna.

Подбирались:

  • loss;
  • learning_rate;
  • max_leaf_nodes;
  • max_depth;
  • min_samples_leaf;
  • l2_regularization;
  • max_bins;
  • max_features.

Также использовалась ранняя остановка:

max_iter = 5000
early_stopping = True
validation_fraction = 0.15
n_iter_no_change = 50

CatBoost

Для CatBoost также использовалась Optuna.

Подбирались:

  • depth;
  • learning_rate;
  • l2_leaf_reg;
  • random_strength;
  • bagging_temperature;
  • border_count.

Особое внимание уделялось ранней остановке: на каждом CV-фолде модель получала eval_set, состоящий только из валидационной части текущего фолда. Это важно, чтобы не допустить утечки из валидации в обучение.


8. Feature importance и отбор признаков

В основном исследовании важность признаков оценивалась несколькими способами.

Линейные коэффициенты

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

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

Нативная важность признаков

Для моделей на основе деревьев использовались встроенные оценки важности:

  • feature_importances_ для Random Forest;
  • Нативная важность признаков в CatBoost.

Permutation importance

Permutation importance оценивает, насколько ухудшается качество модели, если перемешать значения конкретного признака. Если качество сильно падает, признак важен.

SHAP

SHAP использовался для более детальной интерпретации CatBoost.

SHAP показывает вклад каждого признака в конкретный прогноз. Среднее mean(|SHAP value|) использовалось как глобальная оценка важности.

Оценка важности признаков по SHAP для CatBoost

CatBoost на сокращённом наборе признаков

По SHAP была обучена отдельная версия CatBoost на сокращённом наборе признаков. В неё вошли признаки с заметным вкладом:

  • age;
  • bmi;
  • doctor_visits_per_year;
  • hospital_admissions;
  • medication_count;
  • previous_year_cost;
  • stress_level;
  • smoker;
  • diabetes;
  • hypertension;
  • heart_disease;
  • asthma.

Цель — проверить, можно ли упростить модель без потери качества.


9. Результаты основного исследования full-research.ipynb

9.1. Кросс-валидация

На этапе кросс-валидации все модели оценивались на train-части данных с использованием 5-фолдовой кросс-валидации. Основной метрикой для выбора моделей была MAE, так как она напрямую показывает среднюю ошибку прогноза в денежных единицах.

Лучшие CV-результаты:

Модель CV MAE CV RMSE CV R² CV MAPE
CatBoost SHAP-selected 42.65 163.55 0.9992 0.0021
CatBoost 43.27 159.98 0.9992 0.0022
HistGradientBoosting 288.09 513.70 0.9944 0.0158
Random Forest 1021.99 1361.16 0.9612 0.0580
ElasticNet 1039.63 1452.68 0.9561 0.0542
Ridge 1043.46 1415.15 0.9584 0.0565
Lasso 1055.32 1406.55 0.9589 0.0583
Linear Regression 1058.67 1405.78 0.9589 0.0589
LassoSelect + Ridge 1101.17 1495.55 0.9535 0.0591

По результатам CV лучше всего показали себя две CatBoost-модели: полная версия и версия на сокращённом наборе признаков, отобранных по SHAP importance. Разница между ними небольшая: SHAP-selected версия немного лучше по MAE, а полная CatBoost-модель немного лучше по RMSE.

Линейные модели и Random Forest заметно уступают CatBoost и HistGradientBoosting, хотя всё ещё показывают высокое значение . Это связано с тем, что исходный синтетический датасет содержит достаточно сильные закономерности, но CatBoost лучше всего улавливает нелинейности и взаимодействия признаков.

9.2. Финальная test-оценка

Финальная оценка проводилась на отложенной test-выборке, которая не использовалась ни при подборе гиперпараметров, ни при отборе признаков.

Оценка выполнялась не только точечно. Для каждой модели были рассчитаны основные метрики, а затем с помощью бутстрэпа (1000 итераций) по объектам тестовой выборки были построены 95% доверительные интервалы. Это позволяет оценить устойчивость качества модели, а не полагаться только на одно значение метрики.

Значения ниже представлены в формате:

bootstrap mean [95% confidence interval]
Модель MAE RMSE MAPE
CatBoost 35.26 [29.85; 44.13] 117.64 [42.76; 215.90] 0.9997 [0.9991; 1.0000] 0.0018 [0.0017; 0.0020]
CatBoost SHAP-selected 37.09 [28.64; 49.94] 176.75 [48.73; 330.49] 0.9993 [0.9979; 1.0000] 0.0018 [0.0016; 0.0021]
HistGradientBoosting 286.16 [256.78; 324.12] 591.25 [381.34; 863.46] 0.9929 [0.9860; 0.9970] 0.0151 [0.0141; 0.0162]
Random Forest 991.31 [938.70; 1050.68] 1334.38 [1246.41; 1437.86] 0.9654 [0.9606; 0.9695] 0.0569 [0.0537; 0.0604]
Best Linear ElasticNet 1083.21 [1011.97; 1158.43] 1598.57 [1421.91; 1797.68] 0.9503 [0.9406; 0.9584] 0.0550 [0.0524; 0.0576]

Дополнительно было выполнено попарное бутстрэп-сравнение по MAE относительно лучшей модели на test — CatBoostRegressor.

Модель Сравнение с Разница MAE 95% CI
CatBoost SHAP-selected CatBoost 1.82 [-2.87; 7.15]
HistGradientBoosting CatBoost 250.89 [225.97; 282.90]
Random Forest CatBoost 956.04 [903.74; 1013.85]
Best Linear ElasticNet CatBoost 1047.94 [979.15; 1120.15]

Главный вывод: полная CatBoost-модель и CatBoost SHAP-selected (с сокращённым набором признаков) показывают практически одинаковое качество по MAE. Доверительный интервал разницы между ними включает 0, поэтому статистически значимого отличия по MAE не выявлено.

С учётом сопоставимого качества и меньшего числа признаков, CatBoost SHAP-selected можно рассматривать как более компактную и интерпретируемую версию финальной модели. При этом полная CatBoost-модель остаётся лучшей по RMSE.

MAE на тесте с bootstrap CI

Диагностика прогнозов модели CatBoost SHAP-selected

9.3. Почему в основном исследовании не использовались ансамбли

На основном синтетическом датасете CatBoost фактически решил задачу почти идеально. MAE около 35 при среднем таргете около 19 000 выглядит очень сильным результатом.

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

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


10. Дополнительное исследование additional-research-all-features.ipynb

10.1. Цель

Цель all-features эксперимента — проверить модели на более крупном датасете с 100 000 пациентов и существенно более широким набором признаков.

В этом исследовании используются демографические, социально-экономические, клинические, поведенческие признаки, признаки медицинской нагрузки, процедуры, policy- и claims-признаки.

В рабочей логике такой эксперимент интерпретируется как upper-bound сценарий: он показывает, какого качества можно достичь при использовании максимально богатого описания пациента. Claims- и premium-признаки особенно информативны, но требуют аккуратной интерпретации.

10.2. Базовые модели

В all-features версии использовались:

  • лучшая линейная модель (Lasso);
  • RandomForestRegressor;
  • ExtraTreesRegressor;
  • HistGradientBoostingRegressor;
  • CatBoostRegressor.

Все модели обучались на одном train/test split, а test использовался только для финальной оценки.

10.3. Ансамбли моделей

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

Для этого сначала строились out-of-fold предсказания на train:

каждый объект train получает прогноз от модели, которая не видела этот объект при обучении

OOF-прогнозы использовались для:

  • оценки качества базовых моделей;
  • выбора лучшей одиночной модели;
  • расчёта весов ансамблей;
  • обучения мета-модели в стекинге.

Тестовая выборка при расчёте весов не использовалась.

Simple Average Ensemble

Самый простой ансамбль:

prediction = mean(prediction_1, ..., prediction_K)

Все модели получают одинаковый вес.

Плюс: простой и устойчивый baseline.
Минус: слабые модели получают такой же вес, как сильные.

Inverse-Error Weighted Ensemble

Вес модели обратно пропорционален её OOF-ошибке, т.е.:

меньше OOF-ошибка → больше вес модели в ансамбле

Сначала для каждой модели считаем обратную ошибку:

raw_weight_i = 1 / error_i

где error_i — OOF-ошибка модели, например MAE.

Затем нормируем веса так, чтобы их сумма была равна 1:

weight_i = raw_weight_i / sum(raw_weight_all_models)

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

В коде также используется небольшой eps, чтобы избежать деления на ноль, и параметр power, который управляет жёсткостью весов:

raw_weight_i = 1 / (error_i + eps) ** power

Если power = 1, веса считаются обычным способом.

Если power > 1, слабые модели штрафуются сильнее.

Если power < 1, веса становятся более равномерными.

Pseudo-BMA (Bayesian Model Averaging)

Pseudo-BMA — байесовски-мотивированное усреднение моделей.

В строгом Bayesian Model Averaging нужно задавать полноценную вероятностную модель, априорные распределения (prior) для параметров и самих моделей, а затем считать предельное правдоподобие (marginal likelihood).

Иными словами:

  • prior — это начальное предположение о том, насколько вероятна модель или значения её параметров до просмотра данных;
  • likelihood — это то, насколько хорошо модель объясняет наблюдаемые данные;
  • marginal likelihood — это итоговая оценка качества модели с учётом всех возможных значений её параметров.

Для моделей вроде CatBoost, Random Forest и HistGradientBoostingRegressor напрямую посчитать marginal likelihood сложно, потому что это не классические байесовские вероятностные модели, а алгоритмы, которые в первую очередь дают точечные прогнозы.

Поэтому в проекте используется практическое приближение: качество модели оценивается через out-of-fold ошибку (OOF-ошибку). Чем меньше OOF-ошибка, тем выше доверие к модели и тем больший вес она получает в pseudo-BMA ансамбле:

prior weights → pseudo-likelihood по OOF-качеству → posterior-like weights

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

log_likelihood_i = -0.5 * n * log(MSE_i + eps)

Равный prior для K моделей:

P(M_i) = 1 / K
log_prior_i = -log(K)

Затем веса считаются через softmax:

w_i = softmax(log_prior_i + log_likelihood_i / temperature)

Strict pseudo-BMA

В strict-версии:

temperature = 1

Если одна модель лучше по MSE/RMSE, её преимущество усиливается на большом числе наблюдений. Поэтому веса могут "схлопнуться" в одну модель.

Это ожидаемое поведение, а не ошибка.

Tempered pseudo-BMA

В tempered-версии используется большая temperature, например близкая к числу наблюдений:

temperature = n

Это смягчает веса:

  • сильные модели всё ещё получают больший вес;
  • слабые модели не обнуляются полностью;
  • ансамбль остаётся более распределённым.

Стекинг с Ridge-регрессией в качестве мета-модели

Стекинг строится следующим образом:

  1. Базовые модели дают OOF-прогнозы;
  2. Эти прогнозы становятся признаками для мета-модели;
  3. Мета-модель обучается предсказывать y_train;
  4. На тесте получаем прогнозы базовых моделей, затем применяем мета-модель.

В качестве мета-модели используется Ridge-регрессия, потому что она:

  • простая;
  • устойчивая;
  • хорошо работает с коррелированными прогнозами;
  • интерпретируема.

10.4. Результаты all-features эксперимента

Финальные метрики на тесте с 95% доверительными интервалами, посчитанными через бутстрэп:

Модель MAE RMSE MAPE
HistGradientBoosting 922.3163 [898.8607; 946.0279] 1870.2138 [1758.4014; 1995.1415] 0.6305 [0.5989; 0.6602] 0.3976 [0.3886; 0.4066]
Inverse-MAE Ensemble 941.6170 [920.3699; 963.7813] 1788.7334 [1685.4449; 1910.5479] 0.6620 [0.6297; 0.6912] 0.4763 [0.4652; 0.4878]
Tempered pseudo-BMA 942.5632 [921.3391; 964.7130] 1786.8958 [1683.4046; 1908.7776] 0.6627 [0.6303; 0.6920] 0.4786 [0.4674; 0.4903]
Simple Average 944.7929 [923.5948; 967.0521] 1798.4914 [1695.7615; 1919.9604] 0.6583 [0.6261; 0.6874] 0.4757 [0.4647; 0.4873]
ExtraTrees 968.5187 [947.5694; 989.6288] 1753.8019 [1653.1874; 1870.7874] 0.6750 [0.6439; 0.7040] 0.5227 [0.5096; 0.5357]
Stacking Ridge 968.8035 [948.5503; 990.2147] 1750.1748 [1655.5189; 1868.4229] 0.6763 [0.6452; 0.7060] 0.5217 [0.5085; 0.5351]
Random Forest 968.8993 [948.4722; 990.5416] 1749.9930 [1654.0317; 1869.7095] 0.6764 [0.6445; 0.7059] 0.5262 [0.5131; 0.5393]
Strict pseudo-BMA 968.8993 [948.4722; 990.5416] 1749.9930 [1654.0317; 1869.7095] 0.6764 [0.6445; 0.7059] 0.5262 [0.5131; 0.5393]
CatBoost 970.7137 [950.1367; 992.1131] 1771.5367 [1673.5285; 1888.6354] 0.6684 [0.6374; 0.6969] 0.5243 [0.5118; 0.5372]
BestLinear Lasso 1247.2992 [1221.1807; 1274.6771] 2316.3234 [2205.2872; 2440.5431] 0.4334 [0.4033; 0.4625] 0.5103 [0.5012; 0.5193]

Главный вывод:

  • по MAE лучшей моделью стал HistGradientBoostingRegressor;
  • ансамбли не улучшили MAE относительно лучшей одиночной модели;
  • RandomForestRegressor, StackingRidge и ExtraTreesRegressor лучше выглядят по RMSE/R², то есть лучше контролируют крупные ошибки;
  • StrictPseudoBMA схлопнулся в RandomForestRegressor, так как pseudo-likelihood через MSE/RMSE отдал почти весь вес модели с лучшим RMSE/R².

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

Метрика MAE на тесте с bootstrap CI для all-features эксперимента


11. Остальные дополнительные эксперименты

11.1. additional-research.ipynb

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

Итоговые результаты на тесте:

Модель MAE RMSE MAPE
HistGradientBoosting 1641.83 2911.34 0.1049 0.7276
BestLinear Lasso 1649.47 2915.29 0.1025 0.7318
Inverse-MAE Ensemble 1683.52 2815.19 0.1631 0.8877
Simple Average 1685.78 2813.74 0.1639 0.8926
Tempered pseudo-BMA 1687.08 2812.95 0.1644 0.8953
CatBoost 1758.29 2799.96 0.1721 1.0146
Random Forest 1761.26 2798.88 0.1727 1.0198

Вывод: без сильных предикторов задача становится заметно сложнее. По MAE снова выигрывает HistGradientBoostingRegressor, а CatBoost и Random Forest лучше контролируют RMSE/R². Ансамбли не дают явного преимущества перед одиночными моделями, но имеют сбалансированные показатели по совокупности метрик.

11.2. additional-research-common-features.ipynb

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

Итоговые результаты на тесте:

Модель MAE RMSE MAPE
HistGradientBoosting 1679.98 2975.33 0.0651 0.7562
BestLinear ElasticNet 1686.87 2978.70 0.0630 0.7605
Inverse-MAE Ensemble 1724.62 2877.81 0.1254 0.9350
Simple Average 1727.15 2876.22 0.1264 0.9408
CatBoost 1803.42 2857.43 0.1377 1.0706

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


12. Почему бустинг-модели хорошо подходят для задачи медицинских расходов

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

Это ожидаемо для медицинских расходов, потому что:

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

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

Похожие выводы встречаются в аналогичных исследованиях:

  • Orji & Ukwandu сравнивали XGBoost, GBM и Random Forest для прогнозирования стоимости медицинской страховки и показали сильные результаты ансамблевых методов на основе деревьев: https://arxiv.org/abs/2311.14139
  • В исследовании для определения пациентов с высокими медицинскими расходами использовались Random Forest, Gradient Boosting Machine, ANN и логистическая регрессия для прогноза таких пациентов на основе данных о страховании: https://pmc.ncbi.nlm.nih.gov/articles/PMC9847900/
  • В работе по прогнозированию размера страховых выплат также рассматриваются XGBoost, Random Forest и линейная регрессия как основные регрессионные подходы: https://www.atlantis-press.com/article/126020623.pdf

13. Мониторинг дрейфа данных: PSI

Для продакшн-сценария важно не только обучить модель, но и отслеживать, не изменилось ли распределение входных данных относительно датасета, на котором обучалась модель. Для этого можно использовать метрику Population Stability Index (PSI).

PSI сравнивает распределение признака в исходной выборке и новой выборке:

PSI = sum_i (p_i - q_i) * ln(p_i / q_i)

где:

  • p_i — доля объектов в бине или категории на бейслайне;
  • q_i — доля объектов в том же бине или категории на новой выборке.

Для числовых признаков значения обычно бьются на бины. Для категориальных — сравниваются категории.

Типовые эвристики:

PSI Интерпретация
< 0.10 распределение стабильно
0.10–0.25 умеренный дрейф
> 0.25 сильный дрейф, нужно расследование

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

Для PSI используются признаки, которые применялись в основном ML-моделировании на синтетическом датасете:

  • числовые признаки: age, bmi, daily_steps, sleep_hours, doctor_visits_per_year, hospital_admissions, medication_count, previous_year_cost, stress_level;
  • бинарные признаки: gender, smoker, diabetes, hypertension, heart_disease, asthma;
  • категориальные признаки: physical_activity_level, city_type.

Бинарные признаки при расчёте PSI рассматриваются как категориальные, так как для них важно сравнивать доли классов 0 и 1.

Для числовых признаков:

  1. По train-выборке строятся интервалы, например по квантилям.
  2. Для каждого интервала считается доля объектов в train и test.
  3. Затем сравниваются доли в соответствующих интервалах.

Для категориальных признаков:

  1. Берутся все категории, встречающиеся в train или test.
  2. Для каждой категории считается доля объектов в train и test.
  3. Затем сравниваются распределения категорий.

Общая идея:

если распределения train и test похожи → PSI близок к 0
если распределения заметно отличаются → PSI растёт

Мониторинг дрейфа данных через PSI

Если PSI по важному признаку становится высоким, это может означать:

  • изменение состава пациентов;
  • изменение источника данных;
  • изменение правил заполнения признака;
  • сдвиг в поведении пользователей или медицинском процессе;
  • потенциальное ухудшение качества модели.

В этом случае стоит дополнительно проверить качество модели на новых данных и при необходимости обучить модель заново на новых данных.


14. Итоговые рекомендации

  1. Для синтетического основного датасета лучшая модель — CatBoostRegressor. Версия CatBoostRegressor_SHAP_selected почти не уступает полной модели и выглядит предпочтительной, если важна компактность.

  2. Для более реалистичного расширенного датасета лучшей моделью по MAE оказался HistGradientBoostingRegressor. Он даёт лучший баланс качества, скорости и простоты реализации.

  3. Если важнее контролировать крупные ошибки, дополнительно стоит смотреть на RandomForestRegressor или ExtraTreesRegressor, так как они часто дают лучший RMSE/R².

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

  5. Для практического ML-пайплайна рекомендуется:

    • фиксировать тестовую выборку один раз;
    • использовать разные методы предобработки для линейных моделей, деревьев и бустингов;
    • подбирать гиперпараметры только на обучающей выборке через кросс-валидации;
    • рассчитывать OOF-прогнозы для ансамблей;
    • использовать бутстрэп с доверительными интервалами и попарные сравнения для финальной оценки;
    • мониторить дрейф данных в продакшн через PSI.
  6. Для интерпретации стоит использовать SHAP и permutation importance: они помогают понять, какие признаки действительно влияют на прогноз, и позволяют строить версии моделей с сокрашённым набором признаков.


🖼️ Основной функционал и пользовательские сценарии

1. Основной процесс расчета прогноза

Основной процесс расчета прогноза

  1. Аналитик открывает вкладку "Новый прогноз".
  2. Веб-интерфейс загружает форму.
  3. Аналитик заполняет поля (возраст, пол, ИМТ и т.д.) и нажимает кнопку "Рассчитать".
  4. Веб-интерфейс отправляет данные (JSON) в Систему (Backend API).
  5. Система (Backend) валидирует данные, обогащает их и вызывает ML-модель для инференса.
  6. ML-модель возвращает рассчитанный прогноз (число).
  7. Система (Backend) формирует ответ и отправляет его обратно во Веб-интерфейс.
  8. Веб-интерфейс отображает результат на экране аналитика.
  9. Одновременно Система (Backend) отправляет команду на сохранение (INSERT) в Базу данных.
  10. База данных подтверждает сохранение записи.

2. Просмотр отчёта анализа и факторов риска

Получение факторов риска

  1. Аналитик выполняет расчёт прогноза на странице "Новый прогноз".
  2. После успешного расчёта интерфейс переключается из режима формы в режим отчёта анализа.
  3. Frontend запрашивает у Backend полные детали созданного прогноза.
  4. Backend возвращает:
    • данные пациента;
    • рассчитанный прогноз;
    • список факторов риска.
  5. Если факторы риска ещё не были рассчитаны ранее, Backend дополнительно выполняет SHAP-объяснение, сохраняет top risk factors в БД и затем возвращает их в ответе.
  6. Frontend отправляет отдельный запрос на получение итоговой оценки риска.
  7. Backend возвращает:
    • категорию риска;
    • перцентиль;
    • текстовую рекомендацию.
  8. Веб-интерфейс отображает единый отчёт анализа.
  9. В отчёте аналитик видит:
    • итоговую стоимость;
    • ключевые факторы влияния на прогноз;
    • данные пациента;
    • итоговую категорию риска;
    • рекомендации по кейсу.
  10. Из того же интерфейса аналитик может запустить перерасчёт или экспортировать отчёт в PDF.

3. История, поиск и открытие существующего отчёта

История и поиск

  1. Аналитик открывает раздел "История".
  2. Frontend отправляет запрос GET /api/history в Backend.
  3. Backend получает данные из БД и возвращает список сохранённых прогнозов.
  4. Интерфейс отображает таблицу с основными параметрами расчётов.
  5. Аналитик может:
    • выполнить поиск по имени или ID;
    • отфильтровать записи по диапазону стоимости и дат;
    • отсортировать таблицу по ФИО, возрасту, прогнозу или дате.
  6. При вводе значения в строку поиска Frontend отправляет запрос GET /api/history?search=... и получает отфильтрованный список записей.
  7. Фильтрация по диапазону стоимости и дат, а также сортировка выполняются на Frontend по уже загруженному массиву данных.
  8. При клике по строке таблицы Frontend запрашивает детали выбранного прогноза.
  9. Backend возвращает полную информацию по записи.
  10. Frontend отправляет отдельный запрос на получение итоговой оценки риска для выбранного прогноза.
  11. Backend возвращает:
    • категорию риска;
    • перцентиль;
    • текстовую рекомендацию.
  12. Frontend открывает страницу /predict в режиме готового отчёта анализа.
  13. Аналитик может изучить сохранённый отчёт, экспортировать его в PDF или перейти к перерасчёту.

4. Экспорт отчёта в PDF

Формирование отчёта в виде PDF-файла

  1. Аналитик открывает готовый отчёт анализа на странице прогноза.
  2. В header отчёта доступна кнопка "Экспорт в PDF".
  3. Frontend отправляет запрос GET /api/predictions/{prediction_id}/pdf.
  4. Backend формирует PDF-отчёт на основе сохранённого прогноза.
  5. В PDF включаются:
    • данные пациента;
    • рассчитанный прогноз;
    • оценка риска;
    • ключевые факторы влияния;
    • дата формирования отчёта.
  6. Backend возвращает файл с Content-Type: application/pdf.
  7. Браузер инициирует скачивание отчёта.

5. Удаление прогноза из истории

Удаление прогноза из истории

  1. Аналитик Открывает раздел "История".
  2. Система показывает таблицу прогнозов.
  3. Аналитик находит строку с ошибочным прогнозом, в каждой строке есть кнопка/иконка "Удалить" (корзина).
  4. Аналитик нажимает на иконку корзины.
  5. Frontend показывает модальное окно: "Удалить прогноз для пациента Иванов И.?" с кнопками Да / Нет.
  6. Аналитик нажимает Да.
  7. Frontend отправляет DELETE-запрос на /history/{prediction_id}.
  8. Backend удаляет запись из БД (SQL DELETE).
  9. БД подтверждает удаление.
  10. Backend возвращает статус 200 OK.
  11. Система показывает уведомление: "Прогноз удален" Таблица обновляется (строка исчезает).
  12. Альтернативный поток: Аналитик нажимает Нет → модальное окно закрывается, удаление не происходит.

6. Сортировка в истории

Сортировка в истории

  1. Аналитик смотрит на таблицу истории, заголовки столбцов: Дата, Пациент, Возраст, Прогноз, Решение
  2. Аналитик нажимает на заголовок "Прогноз"
  3. Frontend сортирует уже загруженный массив данных по полю predicted_cost
  4. Рядом с заголовком появляется иконка ▼, система перерисовывает таблицу
  5. Аналитик нажимает на заголовок "Прогноз" еще раз
  6. Процесс повторяется, иконка меняется на ▲, таблица показывает сначала самые дешевые

7. Фильтры во вкладке История

Фильтры во вкладке История

  1. Аналитик открывает раздел "История" -> "Фильтры".
  2. Аналитик вводит минимальное значение прогноза в ячейку "Прогноз от".
  3. Frontend фильтрует загруженный массив данных по полю predicted_cost и отображает только те строки, где "Прогноз" больше или равен введённому значению.
  4. Аналитик вводит максимальное значение прогноза в ячейку "Прогноз до".
  5. Frontend фильтрует массив данных по полю predicted_cost и отображает только те строки, где "Прогноз" меньше или равен введённому значению.
  6. Аналитик вводит раннюю дату прогноза в ячейку слева в формате дата.
  7. Frontend фильтрует массив данных по полю created_at и отображает только те строки, где "Дата" больше или равна введённой даты.
  8. Аналитик вводит позднюю дату прогноза в ячейку справа в формате дата.
  9. Frontend фильтрует массив данных по полю created_at и отображает только те строки, где "Дата" меньше или равна введённой даты.
  10. Аналитик нажимает на "Сбросить фильтры".
  11. Frontend восстанавливает исходный массив и очищает поля ввода.

8. Загрузка анкеты

Загрузка анкеты

  1. Аналитик открывает вкладку "Новый прогноз".
  2. Аналитик нажимает на кнопку "Распознать анкету".
  3. Frontend открывает диалог выбора файла из локального хранилища.
  4. Аналитик выбирает файл для загрузки.
  5. Frontend загружает файл и отправляет на Backend.
  6. Backend с помощью OCR распознаёт данные и формирует массив данных для Frontend.
  7. Backend возвращает JSON с распознанными полями.
  8. Frontend заполняет поля анкеты полученными данными.
  9. Аналитик проверяет и при необходимости корректирует заполненные поля.
  10. Аналитик нажимает "Рассчитать".

About

Прототип системы прогнозирования годовых медицинских расходов пациента на основе табличных данных

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages