BeatSense / README.md
devrup404's picture
fix: shorten short_description to comply with 60-char limit
9881306
|
Raw
History Blame Contribute Delete
21.4 kB
metadata
title: BeatSense
emoji: 🩺
colorFrom: blue
colorTo: indigo
sdk: docker
app_port: 7860
pinned: false
license: mit
short_description: Cardiovascular risk workstation (FastAPI + Next.js)

BeatSense

BeatSense β€” Cardiovascular Risk Intelligence

QR β€” Hugging Face Space

Scan to open the Hugging Face Space

English Β· EspaΓ±ol

Read on Medium

BeatSense is a cardiovascular risk assessment platform born from a question: what would a coronary risk tool actually feel like if it were built for the nurses station of a real hospital, instead of for a notebook demo?

Around a trained scikit-learn model (918 patients, twelve clinical features), it ships three coordinated surfaces: a FastAPI service that exposes the model and persists every assessment, a Next.js 16 clinical workstation with sidebar navigation, dark mode, English and Spanish, and a result rendered as a printable thermal-style ticket ready to clip to the patient record, plus a legacy Streamlit dashboard kept alive for quick demos. The model itself is a Logistic Regression with a custom 0.4 decision threshold, picked because in cardiology a false negative is far more dangerous than a false positive, an interpretable choice that recovers 94% of true positives.

The repo is end-to-end: notebooks for EDA and training, a typed REST API, a production-grade clinical UI, internationalization with zero hardcoded strings, and theme tokens out of the box.


Table of Contents

Overview Live Demo
Features Architecture
Project Structure Tech Stack
Dataset Model Selection
Getting Started Make Targets
API Reference Frontend Highlights
Internationalization Theming
Testing Docker

Overview

BeatSense predicts the probability of coronary heart disease from clinical patient data. It ships three independent surfaces sharing the same trained model:

  • A FastAPI REST backend that wraps the production model (models/modelo_coronario.pkl).
  • A Next.js 16 clinical workstation UI β€” sidebar, ticket-style report, dark mode, EN/ES β€” for nurses and clinicians.
  • A legacy Streamlit dashboard (still operational) for quick demos.

The trained model is a Logistic Regression with a custom decision threshold of 0.4, chosen because in cardiology the cost of a false negative (missing a sick patient) far exceeds that of a false positive.


Live Demo

Hugging Face Space


Features

Surface Highlights
πŸ€– ML pipeline EDA + survival analysis (Kaplan–Meier, Cox), preprocessing, 4 models compared, Optuna tuning, full notebooks.
πŸš€ REST API FastAPI service with /predict, /history, /stats, /eda, /model-info, persistent CSV history, CORS enabled.
🩺 Clinical UI Next.js 16 + Tailwind v4 + shadcn-style components. Sidebar navigation, nurse-station header, KPI dashboard, filtered patient table, EDA charts, model insights.
πŸ–¨οΈ Printable ticket Risk result rendered as a thermal-printer ticket: monospace font, dashed dividers, barcode, window.print().
🌍 i18n English (default) + Spanish, zero hardcoded strings, JSON dictionaries, cookie + Accept-Language detection.
πŸŒ“ Dark mode First-class next-themes integration, dark by default, no flash on first paint.
πŸ“¦ Containerized Docker / docker-compose for the Streamlit surface.
πŸ› οΈ Dual toolchain uv for Python, pnpm for the Next.js workspace. One make dev boots both servers.

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      HTTPS/REST       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Next.js 16 frontend     β”‚  ────────────────▢    β”‚ FastAPI service         β”‚
β”‚ β€’ App Router            β”‚                       β”‚ β€’ /predict              β”‚
β”‚ β€’ Server + Client       β”‚                       β”‚ β€’ /history /stats       β”‚
β”‚   Components            β”‚                       β”‚ β€’ /eda /model-info      β”‚
β”‚ β€’ TanStack Query        β”‚  ◀────────────────    β”‚ β€’ Loads .pkl + scaler   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      JSON              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚                                                β”‚
              β”‚ static assets                                   β”‚ joblib.load
              β–Ό                                                β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚ /public  β”‚                                β”‚ models/             β”‚
        β”‚ logu1.pngβ”‚                                β”‚  β€’ modelo_coronario β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                β”‚  β€’ scaler_coronario β”‚
                                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                              β–²
                                                              β”‚ persists
                                                              β”‚
                                                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                    β”‚ docs/                β”‚
                                                    β”‚ historial_pacientes  β”‚
                                                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Project Structure

BeatSense/
β”œβ”€β”€ api/                              # FastAPI backend
β”‚   └── main.py                       # /predict, /history, /stats, /eda, /model-info
β”‚
β”œβ”€β”€ app.py                            # Legacy Streamlit app (still works)
β”‚
β”œβ”€β”€ frontend/                         # Next.js 16 clinical workstation
β”‚   β”œβ”€β”€ public/
β”‚   β”‚   └── logu1.png                 # Logo (auto-cropped for tight framing)
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ app/
β”‚   β”‚   β”‚   β”œβ”€β”€ layout.tsx            # Root layout β€” ThemeProvider lives here
β”‚   β”‚   β”‚   β”œβ”€β”€ globals.css           # Tailwind v4 + design tokens + print CSS
β”‚   β”‚   β”‚   └── [lang]/               # Locale-scoped routes
β”‚   β”‚   β”‚       β”œβ”€β”€ layout.tsx        # CRM shell (Sidebar + Header) + Providers
β”‚   β”‚   β”‚       β”œβ”€β”€ page.tsx          # Dashboard (KPIs + recent + pie chart)
β”‚   β”‚   β”‚       β”œβ”€β”€ new-evaluation/   # Clinical form β†’ ticket result
β”‚   β”‚   β”‚       β”œβ”€β”€ patients/         # Filterable history table + CSV export
β”‚   β”‚   β”‚       β”œβ”€β”€ eda/              # Dataset analytics (Recharts)
β”‚   β”‚   β”‚       β”œβ”€β”€ model/            # Model metrics + comparison
β”‚   β”‚   β”‚       └── settings/         # Language + theme switcher
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”‚   β”œβ”€β”€ ui/                   # Button, Card, Input, Label, Select, Badge
β”‚   β”‚   β”‚   β”œβ”€β”€ layout/               # Sidebar, Header, PageHeader, toggles
β”‚   β”‚   β”‚   β”œβ”€β”€ forms/                # EvaluationForm (RHF + Zod)
β”‚   β”‚   β”‚   β”œβ”€β”€ ticket/               # Printable result ticket
β”‚   β”‚   β”‚   β”œβ”€β”€ charts/               # EDA charts
β”‚   β”‚   β”‚   β”œβ”€β”€ dashboard/            # Dashboard client widgets
β”‚   β”‚   β”‚   β”œβ”€β”€ patients/             # Patient table
β”‚   β”‚   β”‚   β”œβ”€β”€ model/                # Model insights
β”‚   β”‚   β”‚   β”œβ”€β”€ settings/             # Settings panel
β”‚   β”‚   β”‚   └── providers.tsx         # ThemeRoot + I18nProvider + QueryClient
β”‚   β”‚   β”œβ”€β”€ i18n/
β”‚   β”‚   β”‚   β”œβ”€β”€ config.ts             # Locale list + defaultLocale
β”‚   β”‚   β”‚   β”œβ”€β”€ context.tsx           # Client-side useT() hook
β”‚   β”‚   β”‚   β”œβ”€β”€ dictionaries.ts       # Server-only loader
β”‚   β”‚   β”‚   └── dictionaries/
β”‚   β”‚   β”‚       β”œβ”€β”€ en.json
β”‚   β”‚   β”‚       └── es.json
β”‚   β”‚   β”œβ”€β”€ lib/
β”‚   β”‚   β”‚   β”œβ”€β”€ api.ts                # Typed REST client (TanStack-friendly)
β”‚   β”‚   β”‚   └── utils.ts              # cn() helper
β”‚   β”‚   └── proxy.ts                  # Next 16 middleware: locale routing
β”‚   β”œβ”€β”€ package.json
β”‚   └── tsconfig.json
β”‚
β”œβ”€β”€ data/                             # Heart Disease dataset (918 records)
β”‚   β”œβ”€β”€ heart.csv                     # Raw
β”‚   β”œβ”€β”€ heart_procesado_g.csv         # Processed by Gema
β”‚   β”œβ”€β”€ heart_procesado_i.csv         # Processed by Isrodam
β”‚   └── heart_processed_R.csv         # Processed by Roberto
β”‚
β”œβ”€β”€ models/                           # Persisted scikit-learn artifacts
β”‚   β”œβ”€β”€ modelo_coronario.pkl
β”‚   β”œβ”€β”€ scaler_coronario.pkl
β”‚   └── model_R.pkl
β”‚
β”œβ”€β”€ notebook/                         # EDA + training notebooks
β”‚   β”œβ”€β”€ eda_heart_disease_p.ipynb     # EDA + survival analysis (Paloma)
β”‚   β”œβ”€β”€ eda_heart_failure_g.ipynb     # EDA (Gema)
β”‚   β”œβ”€β”€ eda_heart_failure_i.ipynb     # EDA (Isrodam)
β”‚   β”œβ”€β”€ eda_heart_failure_R.ipynb     # EDA (Roberto)
β”‚   β”œβ”€β”€ entrenamiento_modelo_final.ipynb
β”‚   β”œβ”€β”€ entrenamiento_modelo_g.ipynb
β”‚   └── entrenamiento_modelo_i.ipynb
β”‚
β”œβ”€β”€ report/                           # Evaluation reports (PDF)
β”œβ”€β”€ docs/historial_pacientes.csv      # Patient history (written by /predict)
β”œβ”€β”€ src/img/qr_app.png                # QR code to live demo
β”œβ”€β”€ assets/                           # Static assets (lottie, etc.)
β”œβ”€β”€ .streamlit/                       # Streamlit theming
β”œβ”€β”€ Makefile                          # uv + pnpm orchestration
β”œβ”€β”€ dockerfile
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ pyproject.toml                    # uv-managed Python deps
└── uv.lock

Tech Stack

Backend / ML

  • Python 3.11 managed with uv
  • FastAPI + Uvicorn β€” REST API
  • scikit-learn β€” Logistic Regression, RF, KNN; StandardScaler
  • XGBoost β€” alternative model evaluated
  • Optuna β€” hyperparameter search
  • pandas / numpy β€” data wrangling
  • joblib β€” model persistence
  • Streamlit β€” legacy interactive dashboard
  • pytest β€” tests

Frontend

  • Next.js 16 (App Router, Turbopack, Server Components)
  • React 19 + TypeScript 5
  • Tailwind CSS v4 with custom design tokens
  • shadcn-style UI built on Radix UI primitives
  • TanStack Query 5 β€” server-state caching
  • React Hook Form 7 + Zod 4 β€” typed forms with runtime validation
  • Recharts β€” analytics charts
  • next-themes β€” dark/light theme
  • next-intl-style i18n using Next.js native dictionaries
  • lucide-react β€” icon set
  • pnpm 10 β€” workspace package manager

DevOps

  • Make β€” single command interface
  • Docker / docker-compose β€” containerised Streamlit deployment
  • GitHub Actions β€” CI/CD
  • Render β€” production hosting (Streamlit demo)

Dataset

918 records, 12 features:

Variable Description Type
Age Patient age Numeric
Sex Sex (M/F) Categorical
ChestPainType Type (ATA, NAP, ASY, TA) Categorical
RestingBP Resting blood pressure (mm Hg) Numeric
Cholesterol Serum cholesterol (mg/dl) Numeric
FastingBS Fasting blood sugar > 120 mg/dl (0/1) Binary
RestingECG Resting ECG result Categorical
MaxHR Max heart rate achieved Numeric
ExerciseAngina Exercise-induced angina (Y/N) Binary
Oldpeak ST depression Numeric
ST_Slope ST segment slope Categorical
HeartDisease Target β€” heart disease (0/1) Binary

The production model uses 12 engineered features (Age, Sex, MaxHR, FastingBS, ExerciseAngina, one-hot of ChestPainType, one-hot of ST_Slope). Only Age and MaxHR are scaled.


Model Selection

Model Accuracy Recall (class 1) ROC-AUC Notes
Logistic Regression (threshold 0.4) 88 % 0.94 0.926 βœ… Selected
Random Forest ~87 % ~0.88 ~0.91 Higher raw accuracy, lower recall
K-NN ~82 % ~0.83 ~0.87 Scale-sensitive
XGBoost ~86 % ~0.87 ~0.90 Strong, less interpretable

Why Logistic Regression?

  1. Highest recall (0.94) β€” at threshold 0.4 it catches 94% of truly sick patients. A false negative in cardiology is far worse than a false positive.
  2. No overfitting β€” train/test gap of only 0.007. 5-fold CV: 84.74% Β± 2.91%.
  3. Clinical transparency β€” linear coefficients let clinicians see exactly which features drive each prediction.

Full analysis:

Notebook Report


Getting Started

Prerequisites

  • Python >=3.11, <3.13
  • uv
  • pnpm
  • Node.js >=20

Install

# Python deps
make install

# Frontend deps
make frontend-install

Run everything (recommended)

make dev

This spawns:

FastAPI Next.js

Run individually

make api          # FastAPI only
make frontend     # Next.js only
make streamlit    # Legacy Streamlit dashboard

Make Targets

Target Purpose
make install uv sync β€” install Python deps
make frontend-install pnpm install β€” install frontend deps
make dev Run both FastAPI + Next.js in parallel
make api Run FastAPI only (uvicorn :8000)
make frontend Run Next.js only (pnpm dev :3000)
make frontend-build Production build of Next.js
make streamlit Run legacy Streamlit app
make docker-up / docker-down / docker-build / docker-logs Docker Compose lifecycle
make jupyter Launch JupyterLab
make test Run pytest
make clean Remove caches and .next/

API Reference

Base URL  localhost:8000

Method Path Description
GET /health Liveness + model load status
POST /predict Run cardiovascular risk prediction
GET /history Return all saved patient assessments
GET /stats Aggregate KPIs (totals, high-risk %, today)
GET /eda Aggregated EDA payload for charts
GET /model-info Metrics of the selected model + alternatives

POST /predict payload

{
  "patient_id": "P-001",
  "age": 55,
  "sex": "M",
  "chest_pain_type": "ASY",
  "max_hr": 130,
  "fasting_bs": 0,
  "exercise_angina": "Y",
  "st_slope": "Flat",
  "resting_bp": 140,
  "cholesterol": 240,
  "resting_ecg": "Normal",
  "oldpeak": 1.2
}

Response:

{
  "patient_id": "P-001",
  "timestamp": "2026-06-08T10:32:41",
  "probability": 94.28,
  "classification": "HIGH",
  "threshold": 40.0,
  "probabilities": [0.057, 0.943]
}

Frontend Highlights

  • /[lang] Clinical dashboard β€” KPI cards, recent assessments, risk distribution pie.
  • /[lang]/new-evaluation Clinical form in three sections (Identification, Vitals, Symptoms/ECG) with RHF + Zod validation. On submit, the answer renders as a printable ticket (window.print() switches the print stylesheet so only the ticket prints, like a thermal receipt).
  • /[lang]/patients Searchable, filterable history table (high / low / all). CSV export client-side.
  • /[lang]/eda Charts: age histogram, sex distribution, chest pain distribution, scatter Max HR vs Age coloured by outcome.
  • /[lang]/model Selected model card, comparison bar chart, rationale paragraph.
  • /[lang]/settings Language + theme switcher.

The layout is a CRM/EHR shell with a sticky dark sidebar (logo + navigation + institution badge), a header bar with patient search, shift indicator, language switch, theme toggle and a faux nurse-station avatar.


Internationalization

  • Built on Next.js 16 native dictionaries (app/[lang]/...).
  • All UI strings live in frontend/src/i18n/dictionaries/{en,es}.json.
  • Locale is resolved in this order by frontend/src/proxy.ts:
    1. locale cookie (set by the language switcher),
    2. Accept-Language header,
    3. fallback to English.
  • useT() exposes the dictionary to client components.

Add a new locale by creating {xx}.json, adding xx to locales in src/i18n/config.ts, and registering it in src/i18n/dictionaries.ts.


Theming

  • Dark mode is the default.
  • Theme tokens are defined as CSS variables in globals.css (:root for light, .dark for dark) and bridged to Tailwind v4 via @theme inline.
  • ThemeProvider is mounted at the root layout to avoid hydration issues with next-themes.

Testing

make test                  # pytest
cd frontend && pnpm build  # type-check + production build

Docker

make docker-up      # Build and run Streamlit container
make docker-logs    # Stream logs
make docker-down    # Stop

Containerising the Next.js + FastAPI duo is on the roadmap.