| --- |
| title: BeatSense |
| emoji: 🩺 |
| colorFrom: blue |
| colorTo: indigo |
| sdk: docker |
| app_port: 7860 |
| pinned: false |
| license: mit |
| short_description: Cardiovascular risk workstation (FastAPI + Next.js) |
| --- |
| |
| <p align="center"> |
| <img src="src/img/logo.png" alt="BeatSense" width="260" /> |
| </p> |
|
|
| <h1 align="center">BeatSense — Cardiovascular Risk Intelligence</h1> |
|
|
| <p align="center"> |
| <a href="https://huggingface.co/spaces/devrup404/BeatSense"> |
| <img src="src/img/qr_hf.png" alt="QR — Hugging Face Space" width="160"/> |
| </a> |
| </p> |
| |
| <p align="center"> |
| <sub><b>Scan to open the Hugging Face Space</b></sub> |
| </p> |
|
|
| <p align="center"> |
| <a href="README.md"><img src="https://img.shields.io/badge/English-0d1a1f?style=flat&logoColor=7fd1c6&labelColor=0a1014" alt="English" /></a> |
| · |
| <a href="README.es.md"><img src="https://img.shields.io/badge/Espa%C3%B1ol-0d1a1f?style=flat&logoColor=7fd1c6&labelColor=0a1014" alt="Español" /></a> |
| </p> |
|
|
| <p align="center"> |
| <img src="https://img.shields.io/badge/Python-3.11-0a1014?logo=python&logoColor=7fd1c6&labelColor=0d1a1f" /> |
| <img src="https://img.shields.io/badge/FastAPI-0.115-0a1014?logo=fastapi&logoColor=7fd1c6&labelColor=0d1a1f" /> |
| <img src="https://img.shields.io/badge/scikit--learn-1.8-0a1014?logo=scikitlearn&logoColor=7fd1c6&labelColor=0d1a1f" /> |
| <img src="https://img.shields.io/badge/XGBoost-3.x-0a1014?logoColor=7fd1c6&labelColor=0d1a1f" /> |
| <img src="https://img.shields.io/badge/Optuna-4.x-0a1014?logoColor=7fd1c6&labelColor=0d1a1f" /> |
| <img src="https://img.shields.io/badge/uv-package%20mgr-0a1014?logoColor=7fd1c6&labelColor=0d1a1f" /> |
| <img src="https://img.shields.io/badge/Next.js-16-0a1014?logo=nextdotjs&logoColor=7fd1c6&labelColor=0d1a1f" /> |
| <img src="https://img.shields.io/badge/React-19-0a1014?logo=react&logoColor=7fd1c6&labelColor=0d1a1f" /> |
| <img src="https://img.shields.io/badge/TypeScript-5-0a1014?logo=typescript&logoColor=7fd1c6&labelColor=0d1a1f" /> |
| <img src="https://img.shields.io/badge/Tailwind-v4-0a1014?logo=tailwindcss&logoColor=7fd1c6&labelColor=0d1a1f" /> |
| <img src="https://img.shields.io/badge/TanStack%20Query-5-0a1014?logoColor=7fd1c6&labelColor=0d1a1f" /> |
| <img src="https://img.shields.io/badge/Recharts-2-0a1014?logoColor=7fd1c6&labelColor=0d1a1f" /> |
| <img src="https://img.shields.io/badge/pnpm-10-0a1014?logo=pnpm&logoColor=7fd1c6&labelColor=0d1a1f" /> |
| <img src="https://img.shields.io/badge/Streamlit-1.x-0a1014?logo=streamlit&logoColor=7fd1c6&labelColor=0d1a1f" /> |
| <img src="https://img.shields.io/badge/Docker-compose-0a1014?logo=docker&logoColor=7fd1c6&labelColor=0d1a1f" /> |
| </p> |
|
|
| <p align="center"> |
| <a href="https://medium.com/@devrup404/most-clinical-machine-learning-prototypes-share-the-same-fate-2c357e91e112"><img src="https://img.shields.io/badge/Medium-Read%20the%20article-0a1014?logo=medium&logoColor=7fd1c6&labelColor=0d1a1f" alt="Read on Medium" /></a> |
| </p> |
|
|
| > **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 |
| |
| <p> |
| <a href="https://huggingface.co/spaces/devrup404/BeatSense"><img src="https://img.shields.io/badge/Hugging%20Face-Open%20Space-0a1014?style=flat&logo=huggingface&logoColor=7fd1c6&labelColor=0d1a1f" alt="Hugging Face Space" /></a> |
| </p> |
| |
| --- |
| |
| ## 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: |
| > |
| > <a href="notebook/entrenamiento_modelo_final.ipynb"><img src="https://img.shields.io/badge/notebook-entrenamiento__modelo__final.ipynb-0a1014?style=flat&logo=jupyter&logoColor=7fd1c6&labelColor=0d1a1f" alt="Notebook" /></a> |
| > <a href="report/entrenamiento_modelo_final.pdf"><img src="https://img.shields.io/badge/report-entrenamiento__modelo__final.pdf-0a1014?style=flat&logo=adobeacrobatreader&logoColor=7fd1c6&labelColor=0d1a1f" alt="Report" /></a> |
|
|
| --- |
|
|
| ## Getting Started |
|
|
| ### Prerequisites |
|
|
| - Python >=3.11, <3.13 |
| - <a href="https://docs.astral.sh/uv/"><img src="https://img.shields.io/badge/uv-Python%20toolchain-0a1014?style=flat&logoColor=7fd1c6&labelColor=0d1a1f" alt="uv" /></a> |
| - <a href="https://pnpm.io/"><img src="https://img.shields.io/badge/pnpm-%E2%89%A510%20Node.js%20toolchain-0a1014?style=flat&logo=pnpm&logoColor=7fd1c6&labelColor=0d1a1f" alt="pnpm" /></a> |
| - Node.js >=20 |
|
|
| ### Install |
|
|
| ```bash |
| # Python deps |
| make install |
| |
| # Frontend deps |
| make frontend-install |
| ``` |
|
|
| ### Run everything (recommended) |
|
|
| ```bash |
| make dev |
| ``` |
|
|
| This spawns: |
|
|
| <p> |
| <a href="http://localhost:8000/docs"><img src="https://img.shields.io/badge/FastAPI-localhost:8000/docs-0a1014?style=flat&logo=fastapi&logoColor=7fd1c6&labelColor=0d1a1f" alt="FastAPI" /></a> |
| <a href="http://localhost:3000"><img src="https://img.shields.io/badge/Next.js-localhost:3000-0a1014?style=flat&logo=nextdotjs&logoColor=7fd1c6&labelColor=0d1a1f" alt="Next.js" /></a> |
| </p> |
|
|
| ### Run individually |
|
|
| ```bash |
| 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 |
|
|
| <p> |
| <b>Base URL</b> |
| <a href="http://localhost:8000"><img src="https://img.shields.io/badge/localhost:8000-0a1014?style=flat&logoColor=7fd1c6&labelColor=0d1a1f" alt="localhost:8000" /></a> |
| </p> |
|
|
| | 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 |
|
|
| ```json |
| { |
| "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: |
|
|
| ```json |
| { |
| "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 |
|
|
| ```bash |
| make test # pytest |
| cd frontend && pnpm build # type-check + production build |
| ``` |
|
|
| --- |
|
|
| ## Docker |
|
|
| ```bash |
| 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. |
|
|
| --- |
|
|
|
|