--- 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 ```bash # Python deps make install # Frontend deps make frontend-install ``` ### Run everything (recommended) ```bash make dev ``` This spawns:

FastAPI Next.js

### 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

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 ```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. ---