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 β Cardiovascular Risk Intelligence
Scan to open the Hugging Face Space
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
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?
- 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.
- No overfitting β train/test gap of only 0.007. 5-fold CV: 84.74% Β± 2.91%.
- Clinical transparency β linear coefficients let clinicians see exactly which features drive each prediction.
Full analysis:
Getting Started
Prerequisites
Install
# Python deps
make install
# Frontend deps
make frontend-install
Run everything (recommended)
make dev
This spawns:
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
| 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:
- locale cookie (set by the language switcher),
- Accept-Language header,
- 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.