feature: MIMIC-IV Replay Scenario Generator
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A production-quality clinical backend built with ASP.NET Core 8, PostgreSQL, Apache Kafka, RabbitMQ, Elasticsearch, Redis, and MinIO. The domain models the observe-alert-acknowledge lifecycle at the center of any clinical monitoring system: patient encounters, continuous vital sign and lab result ingest, real-time sepsis and NEWS2 scoring, and clinician notification with automatic escalation.
|
A production-quality clinical backend built with ASP.NET Core 8, PostgreSQL, Apache Kafka, RabbitMQ, Elasticsearch, Redis, and MinIO. The domain models the observe-alert-acknowledge lifecycle at the center of any clinical monitoring system: patient encounters, continuous vital sign and lab result ingest, real-time sepsis and NEWS2 scoring, and clinician notification with automatic escalation.
|
||||||
|
|
||||||
**Implementation status:** Thirty-two planned phases are complete through Phase 34 (plus Phases 20–23) — from schema and CRUD through Kafka, Elasticsearch CQRS, sepsis detection, RabbitMQ paging with DLQ escalation, reconciliation jobs, Prometheus/Grafana observability, the MinIO Parquet data lake, clinical data model expansion, warning alerts and orders, the NEWS2 composite scoring engine, trend detection with alert suppression, qSOFA bedside screening, medication administration with alert correlation annotations, the console replay simulator, the **Vue 3 ward dashboard**, clinician feedback mode, **Glasgow Coma Scale (GCS) scoring**, **SOFA organ-dysfunction scoring with baseline tracking and delta sepsis alerts**, the **Sepsis-3 clinical refactor** (SIRS removed, qSOFA repositioned as screening, SOFA delta ≥ 2 triggers bundles), **frontend GCS entry and SOFA display**, **expanded simulator scenarios with clinical validation**, the **Site & Gateway Registry** with dual authentication, shared clinical sync contracts, and fleet health Prometheus gauges, the **Ward Gateway Service** (local-first clinical path with offline buffering and central sync), the **FHIR R4 Inbound Facade** for EHR integration, **Role-Based Access Control (RBAC) with clinical audit logging**, the **Dashboard Gap Analysis Fixes** (SOFA/GCS/qSOFA history charts, patient banner, encounter timeline, medication markers on vital charts), the **Enhanced Dashboard** (department overview, sepsis bundle board, critical alert notifications, shift handoff reports, vitals entry form, sortable/filterable ward table), **Degraded Operations Visibility** (gateway fleet operations panel, stale gateway auto-detection, discharge summary API, admin panels for user/threshold/audit/reconciliation management, degraded-mode banner), **Alert Quality Analytics** (server-side clinician feedback with `AlertFeedback` entity, `AlertQualityAggregatorService` background metrics, quality metrics API, Grafana alert quality dashboard), and **Explainable Alerts** (immutable JSONB `explanation` on composite alerts with score contributors, trend context, structured medication context, and bedside `NarrativeSummary`; `AlertResponse` DTO on GET/list/acknowledge/resolve; dashboard `AlertReasoning.vue`; ES indexer and data lake propagation; ward gateway sync). Post-phase hardening includes health check endpoints, Kafka poison pill protection, outbox dead-letter with retry tracking, data lake partial-commit safety, MRN sequence-based generation, FHIR bundle transaction rollback, **FHIR R4 read/search endpoints** (Patient and Encounter), **alert threshold deletion with audit trail**, **FHIR API key rotation** (constant-time multi-key validation), **authorization failure logging** with Prometheus metrics, **JWT signing key validation** at startup, and **concurrency hardening** (transactional sepsis bundle creation, unique active encounter constraint). See [Implemented Phases](#implemented-phases) for the full breakdown. Guides: [dashboard-guide.md](docs/dashboard-guide.md) (technical), [clinical-testing-guide.md](docs/clinical-testing-guide.md) (doctors & nurses).
|
**Implementation status:** Thirty-three planned phases are complete through Phase 35 (plus Phases 20–23) — from schema and CRUD through Kafka, Elasticsearch CQRS, sepsis detection, RabbitMQ paging with DLQ escalation, reconciliation jobs, Prometheus/Grafana observability, the MinIO Parquet data lake, clinical data model expansion, warning alerts and orders, the NEWS2 composite scoring engine, trend detection with alert suppression, qSOFA bedside screening, medication administration with alert correlation annotations, the console replay simulator, the **Vue 3 ward dashboard**, clinician feedback mode, **Glasgow Coma Scale (GCS) scoring**, **SOFA organ-dysfunction scoring with baseline tracking and delta sepsis alerts**, the **Sepsis-3 clinical refactor** (SIRS removed, qSOFA repositioned as screening, SOFA delta ≥ 2 triggers bundles), **frontend GCS entry and SOFA display**, **expanded simulator scenarios with clinical validation**, the **Site & Gateway Registry** with dual authentication, shared clinical sync contracts, and fleet health Prometheus gauges, the **Ward Gateway Service** (local-first clinical path with offline buffering and central sync), the **FHIR R4 Inbound Facade** for EHR integration, **Role-Based Access Control (RBAC) with clinical audit logging**, the **Dashboard Gap Analysis Fixes** (SOFA/GCS/qSOFA history charts, patient banner, encounter timeline, medication markers on vital charts), the **Enhanced Dashboard** (department overview, sepsis bundle board, critical alert notifications, shift handoff reports, vitals entry form, sortable/filterable ward table), **Degraded Operations Visibility** (gateway fleet operations panel, stale gateway auto-detection, discharge summary API, admin panels for user/threshold/audit/reconciliation management, degraded-mode banner), **Alert Quality Analytics** (server-side clinician feedback with `AlertFeedback` entity, `AlertQualityAggregatorService` background metrics, quality metrics API, Grafana alert quality dashboard), **Explainable Alerts** (immutable JSONB `explanation` on composite alerts with score contributors, trend context, structured medication context, and bedside `NarrativeSummary`; `AlertResponse` DTO on GET/list/acknowledge/resolve; dashboard `AlertReasoning.vue`; ES indexer and data lake propagation; ward gateway sync), and **MIMIC-IV Replay Scenario Generator** (offline CLI tool converting real de-identified ICU data from MIT PhysioNet into VigilCare scenario JSONs; streaming CSV parser for 668K-row chartevents; 17 chart + 8 lab item ID mappings to VigilCare observation codes; GCS text-to-numeric conversion; Fahrenheit-to-Celsius; blood pressure deduplication preferring non-invasive over arterial; 10-observation cluster limit enforcement; `mimic-list` and `mimic-generate` CLI commands with Spectre.Console output; 100 patients / 140 ICU stays available for replay through NEWS2, SOFA, GCS, qSOFA, trend detection, and alerting). Post-phase hardening includes health check endpoints, Kafka poison pill protection, outbox dead-letter with retry tracking, data lake partial-commit safety, MRN sequence-based generation, FHIR bundle transaction rollback, **FHIR R4 read/search endpoints** (Patient and Encounter), **alert threshold deletion with audit trail**, **FHIR API key rotation** (constant-time multi-key validation), **authorization failure logging** with Prometheus metrics, **JWT signing key validation** at startup, and **concurrency hardening** (transactional sepsis bundle creation, unique active encounter constraint). See [Implemented Phases](#implemented-phases) for the full breakdown. Guides: [dashboard-guide.md](docs/dashboard-guide.md) (technical), [clinical-testing-guide.md](docs/clinical-testing-guide.md) (doctors & nurses).
|
||||||
|
|
||||||
## Domain Model — How It Maps to a Real Clinical System
|
## Domain Model — How It Maps to a Real Clinical System
|
||||||
|
|
||||||
@@ -92,7 +92,8 @@ An `OutboxEvent` is written in the same transaction as any observation or alert,
|
|||||||
- **Degraded Operations Visibility** — `GatewayStaleDetectorService` auto-marks gateways OFFLINE when heartbeat exceeds configurable `StaleThresholdMinutes`; `OperationsController` (`GET /operations/gateways`, `GET /operations/gateways/{id}`, `GET /operations/sites/{siteId}/summary`) provides fleet management API; `DischargeSummaryService` with `GET /encounters/{id}/discharge-summary` (info) and `GET /encounters/{id}/discharge-summary/content` (MinIO PDF download); `DischargeSummaryPanel.vue` on patient detail; `DegradedModeBanner.vue` warns when gateways are offline; `GatewayOperations.vue` operations dashboard
|
- **Degraded Operations Visibility** — `GatewayStaleDetectorService` auto-marks gateways OFFLINE when heartbeat exceeds configurable `StaleThresholdMinutes`; `OperationsController` (`GET /operations/gateways`, `GET /operations/gateways/{id}`, `GET /operations/sites/{siteId}/summary`) provides fleet management API; `DischargeSummaryService` with `GET /encounters/{id}/discharge-summary` (info) and `GET /encounters/{id}/discharge-summary/content` (MinIO PDF download); `DischargeSummaryPanel.vue` on patient detail; `DegradedModeBanner.vue` warns when gateways are offline; `GatewayOperations.vue` operations dashboard
|
||||||
- **User Management** — `UsersController` (`GET /users`, `POST /users`, `PATCH /users/{id}`) for admin user account CRUD; `UserService` with BCrypt password hashing; `UserManagementView.vue` with `UserFormModal.vue` (create/edit users, role assignment, active toggle)
|
- **User Management** — `UsersController` (`GET /users`, `POST /users`, `PATCH /users/{id}`) for admin user account CRUD; `UserService` with BCrypt password hashing; `UserManagementView.vue` with `UserFormModal.vue` (create/edit users, role assignment, active toggle)
|
||||||
- **Admin Dashboard Panels** — `ThresholdManagementView.vue` with `ThresholdFormModal.vue` (create/edit alert thresholds); `AuditLogView.vue` (filterable audit log viewer with action/entity/user/date filters); `ReconciliationView.vue` (safety finding viewer); sidebar navigation with role-aware admin section; `CollapsibleSection.vue` and `SeverityBadge.vue` UI components
|
- **Admin Dashboard Panels** — `ThresholdManagementView.vue` with `ThresholdFormModal.vue` (create/edit alert thresholds); `AuditLogView.vue` (filterable audit log viewer with action/entity/user/date filters); `ReconciliationView.vue` (safety finding viewer); sidebar navigation with role-aware admin section; `CollapsibleSection.vue` and `SeverityBadge.vue` UI components
|
||||||
- **Console Replay Simulator** — standalone `VigilCare.Simulator` .NET console app replays JSON scenario files against the live API with configurable speed (`--speed 0` instant, `60` = 60× faster); commands: `replay`, `replay-all`, `validate`, `dry-run`; optional `--poll` shows alerts, NEWS2, GCS, SOFA, and sepsis bundle state during replay; `--gateway` targets the ward gateway (`http://localhost:5081`) with `--encounter-id`, `--skip-setup`, and `--gateway-token`; `alert_ack` events poll for open alerts on central before acknowledging (handles async alert pipeline at `--speed 0`); `ExpectedOutcomeValidator` validates alert `narrativeContains` on key scenarios; twelve sample scenarios in `VigilCare.Simulator/Scenarios/List/` (including ward outage reconnect, GCS neurological decline, SOFA sepsis progression, and SpO₂/FiO₂ fallback); user guide in `docs/simulator-guide.md`
|
- **Console Replay Simulator** — standalone `VigilCare.Simulator` .NET console app replays JSON scenario files against the live API with configurable speed (`--speed 0` instant, `60` = 60× faster); commands: `replay`, `replay-all`, `validate`, `dry-run`, `mimic-list`, `mimic-generate`; optional `--poll` shows alerts, NEWS2, GCS, SOFA, and sepsis bundle state during replay; `--gateway` targets the ward gateway (`http://localhost:5081`) with `--encounter-id`, `--skip-setup`, and `--gateway-token`; `alert_ack` events poll for open alerts on central before acknowledging (handles async alert pipeline at `--speed 0`); `ExpectedOutcomeValidator` validates alert `narrativeContains` on key scenarios; twelve sample scenarios in `VigilCare.Simulator/Scenarios/List/` (including ward outage reconnect, GCS neurological decline, SOFA sepsis progression, and SpO₂/FiO₂ fallback); MIMIC-IV scenario generation from real ICU data; user guide in `docs/simulator-guide.md`
|
||||||
|
- **MIMIC-IV Scenario Generator** — offline CLI tool that reads MIMIC-IV CSV files (`docs/MIMIC-IV/`, 100 patients, 140 ICU stays, 668K chart events, 107K lab events) and generates VigilCare scenario JSONs; `mimic-list <dir>` displays a Spectre.Console table of available stays with demographics, care unit, LOS, and outcome; `mimic-generate <dir> --stay-id <id>` produces a scenario with options for `--max-hours`, `--no-medications`, `--no-labs`, `--validate`; streaming CSV parser (`MimicCsvReader`) filters 668K-row chartevents by stay_id and item ID set at the string level before allocating records; `MimicItemMap` maps 17 chart event items (vitals, GCS, FiO₂, PaO₂, labs) and 8 lab event items to VigilCare observation codes; GCS text labels ("Obeys Commands" → 6, "To Speech" → 3) resolved from `valuenum` with text-to-numeric fallback dictionary; Fahrenheit temperature converted to Celsius; blood pressure deduplication prefers non-invasive (NBP) over arterial (ABP); 10-observation-per-cluster limit enforced by priority-based splitting (vitals first, labs spill to next offset); medications from prescriptions with dose parsing; generated scenarios pass `ScenarioValidator` and replay through the standard `replay` command
|
||||||
- **RabbitMQ Notification Workers** — `NotificationPublisherService` reads `alert.generated` from Kafka and publishes paging jobs to `alerts.paging.queue`; `PagingWorkerService` sends the page and waits for acknowledgment; if no ack arrives before timeout it NACKs to `alerts.paging.dlq` with `x-message-ttl = 300000ms`; if the host is stopping, in-flight paging messages are NACKed with `requeue=true` so they are retried after restart and do not false-escalate; `EscalationWorkerService` pages the on-call backup and sets alert status to `escalated`; `DischargeSummaryWorkerService` reads `encounter.status.changed`, generates a discharge summary, and stores it in MinIO under `/discharge-summaries/{encounterId}/summary.pdf`
|
- **RabbitMQ Notification Workers** — `NotificationPublisherService` reads `alert.generated` from Kafka and publishes paging jobs to `alerts.paging.queue`; `PagingWorkerService` sends the page and waits for acknowledgment; if no ack arrives before timeout it NACKs to `alerts.paging.dlq` with `x-message-ttl = 300000ms`; if the host is stopping, in-flight paging messages are NACKed with `requeue=true` so they are retried after restart and do not false-escalate; `EscalationWorkerService` pages the on-call backup and sets alert status to `escalated`; `DischargeSummaryWorkerService` reads `encounter.status.changed`, generates a discharge summary, and stores it in MinIO under `/discharge-summaries/{encounterId}/summary.pdf`
|
||||||
- **Data Lake Writer** — `DataLakeWriterService` (consumer group `data-lake-writer`) buffers `observation.recorded`, `alert.generated`, and `encounter.status.changed` events, flushes date-partitioned Parquet files to MinIO (`/observations/`, `/alerts/`, `/encounters/`), and commits Kafka offsets only for topic-partitions where all uploads succeeded; failed partition buffers are retained in memory and retried on the next flush cycle (prevents data loss from partial upload failures); shutdown flush uses an uncanceled token so MinIO writes complete on Ctrl+C; `kafka_partition` and `kafka_offset` columns provide audit lineage
|
- **Data Lake Writer** — `DataLakeWriterService` (consumer group `data-lake-writer`) buffers `observation.recorded`, `alert.generated`, and `encounter.status.changed` events, flushes date-partitioned Parquet files to MinIO (`/observations/`, `/alerts/`, `/encounters/`), and commits Kafka offsets only for topic-partitions where all uploads succeeded; failed partition buffers are retained in memory and retried on the next flush cycle (prevents data loss from partial upload failures); shutdown flush uses an uncanceled token so MinIO writes complete on Ctrl+C; `kafka_partition` and `kafka_offset` columns provide audit lineage
|
||||||
- **Reconciliation Jobs** — three scheduled checks: (1) unacknowledged CRITICAL alerts older than 30 minutes, (2) pending orders without results after 4 hours, (3) active inpatients with no observation in 2 hours; each finding creates a `reconciliation_alerts` row and publishes to RabbitMQ
|
- **Reconciliation Jobs** — three scheduled checks: (1) unacknowledged CRITICAL alerts older than 30 minutes, (2) pending orders without results after 4 hours, (3) active inpatients with no observation in 2 hours; each finding creates a `reconciliation_alerts` row and publishes to RabbitMQ
|
||||||
@@ -569,8 +570,8 @@ VigilCare.WardGateway.Tests/ # Phase 21 — ward gateway i
|
|||||||
├── WardGatewayLocalPathTests.cs # Local observation ingest, warning alerts, buffered sync items
|
├── WardGatewayLocalPathTests.cs # Local observation ingest, warning alerts, buffered sync items
|
||||||
└── WardGatewayPartitionTests.cs # Network partition simulation — offline buffering and sync upload
|
└── WardGatewayPartitionTests.cs # Network partition simulation — offline buffering and sync upload
|
||||||
|
|
||||||
VigilCare.Simulator/ # Phase 16, 29, 34 — console replay simulator (HTTP-only, no direct DB/Kafka)
|
VigilCare.Simulator/ # Phase 16, 29, 34, 35 — console replay simulator (HTTP-only, no direct DB/Kafka)
|
||||||
├── Program.cs # CLI: replay, replay-all, validate, dry-run
|
├── Program.cs # CLI: replay, replay-all, validate, dry-run, mimic-list, mimic-generate
|
||||||
├── Commands/ # System.CommandLine command handlers
|
├── Commands/ # System.CommandLine command handlers
|
||||||
├── Client/
|
├── Client/
|
||||||
│ ├── VigilCareApiClient.cs # Typed HTTP client for all API endpoints (incl. GCS, SOFA)
|
│ ├── VigilCareApiClient.cs # Typed HTTP client for all API endpoints (incl. GCS, SOFA)
|
||||||
@@ -578,8 +579,16 @@ VigilCare.Simulator/ # Phase 16, 29, 34 — console
|
|||||||
├── Engine/ReplayEngine.cs # Scenario replay with speed multiplier + event logging
|
├── Engine/ReplayEngine.cs # Scenario replay with speed multiplier + event logging
|
||||||
├── Output/SimulatorConsole.cs # Colored output with GCS/SOFA score + explanation display
|
├── Output/SimulatorConsole.cs # Colored output with GCS/SOFA score + explanation display
|
||||||
├── Polling/ApiPoller.cs # Optional post-event alert/score/bundle/GCS/SOFA polling
|
├── Polling/ApiPoller.cs # Optional post-event alert/score/bundle/GCS/SOFA polling
|
||||||
|
├── Mimic/ # Phase 35 — MIMIC-IV scenario generator
|
||||||
|
│ ├── MimicCsvReader.cs # Streaming CSV parser with header-index lookup and filter predicate
|
||||||
|
│ ├── MimicItemMap.cs # MIMIC item ID → VigilCare observation code mappings (17 chart + 8 lab)
|
||||||
|
│ ├── MimicCareUnitMap.cs # ICU care unit → department + tag mapping
|
||||||
|
│ ├── MimicDataLoader.cs # Data access layer with streaming filters for chartevents/labevents
|
||||||
|
│ ├── MimicScenarioBuilder.cs # Core algorithm: BP dedup, cluster limits, offset conversion
|
||||||
|
│ ├── MimicListCommand.cs # CLI: mimic-list — Spectre.Console table of available stays
|
||||||
|
│ └── MimicGenerateCommand.cs # CLI: mimic-generate — produces scenario JSON from a stay ID
|
||||||
├── Scenarios/ # schema.json, ScenarioLoader, ScenarioValidator, ExpectedOutcomeValidator
|
├── Scenarios/ # schema.json, ScenarioLoader, ScenarioValidator, ExpectedOutcomeValidator
|
||||||
└── Scenarios/List/ # Twelve sample scenarios (sepsis, GCS, SOFA, NEWS2, stable, ward outage, …)
|
└── Scenarios/List/ # Twelve sample scenarios + MIMIC-generated scenarios
|
||||||
|
|
||||||
vigilcare-dashboard/ # Phases 17–19, 22, 23, 27–28, 31, 33–34 — Vue 3 ward dashboard SPA
|
vigilcare-dashboard/ # Phases 17–19, 22, 23, 27–28, 31, 33–34 — Vue 3 ward dashboard SPA
|
||||||
├── src/
|
├── src/
|
||||||
@@ -830,6 +839,21 @@ dotnet run --project VigilCare.Simulator -- replay \
|
|||||||
|
|
||||||
Other commands: `validate <file>`, `dry-run <file>`, `replay-all <directory>`. See `docs/simulator-guide.md` for the full user guide.
|
Other commands: `validate <file>`, `dry-run <file>`, `replay-all <directory>`. See `docs/simulator-guide.md` for the full user guide.
|
||||||
|
|
||||||
|
**MIMIC-IV real patient data (Phase 35):** generate and replay scenarios from de-identified ICU records:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List 140 available ICU stays across 100 patients
|
||||||
|
dotnet run --project VigilCare.Simulator -- mimic-list docs/MIMIC-IV/
|
||||||
|
|
||||||
|
# Generate a 24-hour scenario from a CVICU patient
|
||||||
|
dotnet run --project VigilCare.Simulator -- mimic-generate docs/MIMIC-IV/ \
|
||||||
|
--stay-id 32604416 --max-hours 24 --validate
|
||||||
|
|
||||||
|
# Replay the generated scenario
|
||||||
|
dotnet run --project VigilCare.Simulator -- replay \
|
||||||
|
VigilCare.Simulator/Scenarios/List/mimic-s32604416.json --speed 0 --poll
|
||||||
|
```
|
||||||
|
|
||||||
**Ward outage reconnect (Phase 24):** scenario `ward-outage-reconnect-01.json` exercises critical hyperkalemia alerting and nurse acknowledgment during a central outage, then sync back to central when connectivity returns. Manual procedure in `docs/simulator-guide.md` §10; automated end-to-end check:
|
**Ward outage reconnect (Phase 24):** scenario `ward-outage-reconnect-01.json` exercises critical hyperkalemia alerting and nurse acknowledgment during a central outage, then sync back to central when connectivity returns. Manual procedure in `docs/simulator-guide.md` §10; automated end-to-end check:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -2226,7 +2250,7 @@ Observation history uses cursor pagination on `(recorded_at DESC, id DESC)`. Off
|
|||||||
|
|
||||||
## Implemented Phases
|
## Implemented Phases
|
||||||
|
|
||||||
Thirty-two phases from the project roadmap are implemented and verified, including the **Site & Gateway Registry** (Phase 20), the **Ward Gateway Service** (Phase 21), the **Dashboard Gap Analysis Fixes** (Phase 22), the **Degraded Operations Visibility** (Phase 23), the **Sepsis-3 clinical refactor** (Phases 27–29), the **FHIR R4 Inbound Facade** (Phase 30), **RBAC with clinical audit logging** (Phase 31), the **Alert Quality Analytics** (Phase 33), the **Explainable Alerts** (Phase 34), and the **Enhanced Dashboard** (department overview, sepsis bundle board, critical alert notifications, shift handoff reports, vitals entry, sortable/filterable ward table). Integration tests (`dotnet test`) and per-phase verification scripts cover Phases 8–15, 20–23, 25–31, 33–34. Phases 17–19 add the Vue dashboard and clinician feedback (Vitest in `vigilcare-dashboard/`).
|
Thirty-three phases from the project roadmap are implemented and verified, including the **Site & Gateway Registry** (Phase 20), the **Ward Gateway Service** (Phase 21), the **Dashboard Gap Analysis Fixes** (Phase 22), the **Degraded Operations Visibility** (Phase 23), the **Sepsis-3 clinical refactor** (Phases 27–29), the **FHIR R4 Inbound Facade** (Phase 30), **RBAC with clinical audit logging** (Phase 31), the **Alert Quality Analytics** (Phase 33), the **Explainable Alerts** (Phase 34), the **MIMIC-IV Replay Scenario Generator** (Phase 35), and the **Enhanced Dashboard** (department overview, sepsis bundle board, critical alert notifications, shift handoff reports, vitals entry, sortable/filterable ward table). Integration tests (`dotnet test`) and per-phase verification scripts cover Phases 8–15, 20–23, 25–31, 33–35. Phases 17–19 add the Vue dashboard and clinician feedback (Vitest in `vigilcare-dashboard/`).
|
||||||
|
|
||||||
| Phase | Feature | Status |
|
| Phase | Feature | Status |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -2262,6 +2286,7 @@ Thirty-two phases from the project roadmap are implemented and verified, includi
|
|||||||
| 23 | **Degraded Operations Visibility** — `GatewayStaleDetectorService` background service auto-marks gateways OFFLINE when heartbeat exceeds configurable `StaleThresholdMinutes`; `OperationsController` exposes gateway fleet listing (`GET /operations/gateways` with status/site filters), gateway detail (`GET /operations/gateways/{id}`), and site summary (`GET /operations/sites/{siteId}/summary`); `DischargeSummaryService` with `GET /encounters/{id}/discharge-summary` (info) and `GET /encounters/{id}/discharge-summary/content` (MinIO PDF download); `UsersController` (`GET /users`, `POST /users`, `PATCH /users/{id}`) for admin user account management; frontend: `GatewayOperations.vue` operations dashboard, `DegradedModeBanner.vue` warning banner, `DischargeSummaryPanel.vue` on patient detail, `ThresholdManagementView.vue` with `ThresholdFormModal.vue`, `UserManagementView.vue` with `UserFormModal.vue`, `AuditLogView.vue`, `ReconciliationView.vue`; role-aware admin sidebar navigation; `roleAccess.js` composable; `useChartTheme.js`, `useFocusTrap.js`, `useApiMode.js` composables; `CollapsibleSection.vue`, `SeverityBadge.vue` UI components; `OperationsApiTests`; `run-phase23-verification.sh` | Done |
|
| 23 | **Degraded Operations Visibility** — `GatewayStaleDetectorService` background service auto-marks gateways OFFLINE when heartbeat exceeds configurable `StaleThresholdMinutes`; `OperationsController` exposes gateway fleet listing (`GET /operations/gateways` with status/site filters), gateway detail (`GET /operations/gateways/{id}`), and site summary (`GET /operations/sites/{siteId}/summary`); `DischargeSummaryService` with `GET /encounters/{id}/discharge-summary` (info) and `GET /encounters/{id}/discharge-summary/content` (MinIO PDF download); `UsersController` (`GET /users`, `POST /users`, `PATCH /users/{id}`) for admin user account management; frontend: `GatewayOperations.vue` operations dashboard, `DegradedModeBanner.vue` warning banner, `DischargeSummaryPanel.vue` on patient detail, `ThresholdManagementView.vue` with `ThresholdFormModal.vue`, `UserManagementView.vue` with `UserFormModal.vue`, `AuditLogView.vue`, `ReconciliationView.vue`; role-aware admin sidebar navigation; `roleAccess.js` composable; `useChartTheme.js`, `useFocusTrap.js`, `useApiMode.js` composables; `CollapsibleSection.vue`, `SeverityBadge.vue` UI components; `OperationsApiTests`; `run-phase23-verification.sh` | Done |
|
||||||
| 33 | **Alert Quality Analytics** — `AlertFeedback` entity with per-user-per-alert constraint; `POST /alerts/{id}/feedback` server-side feedback submission with `alerts:feedback` permission (Nurse, Physician, Admin); `AlertQualityMetric` entity stores per-alert-type quality snapshots (acknowledgement rate, false positive rate, useful rate, would-act rate, avg seconds to acknowledge/resolve); `AlertQualityAggregatorService` background service computes metrics periodically; `AlertQualityMetricsController` exposes `GET /alerts/quality-metrics` (time-range + alert type filter) and `GET /alerts/quality-metrics/summary`; `AlertFeedbackConfiguration` and `AlertQualityMetricConfiguration` EF Core configs; `SubmitAlertFeedbackRequestValidator`; Prometheus `alert_quality_useful_rate` and `alert_quality_false_positive_rate` gauges; Grafana `alert-quality-dashboard.json`; frontend `AlertQualityAnalytics.vue` with `AlertQualityChart.vue` and `alertQuality` Pinia store; `AlertQualityAnalyticsTests`; `run-phase33-verification.sh` | Done |
|
| 33 | **Alert Quality Analytics** — `AlertFeedback` entity with per-user-per-alert constraint; `POST /alerts/{id}/feedback` server-side feedback submission with `alerts:feedback` permission (Nurse, Physician, Admin); `AlertQualityMetric` entity stores per-alert-type quality snapshots (acknowledgement rate, false positive rate, useful rate, would-act rate, avg seconds to acknowledge/resolve); `AlertQualityAggregatorService` background service computes metrics periodically; `AlertQualityMetricsController` exposes `GET /alerts/quality-metrics` (time-range + alert type filter) and `GET /alerts/quality-metrics/summary`; `AlertFeedbackConfiguration` and `AlertQualityMetricConfiguration` EF Core configs; `SubmitAlertFeedbackRequestValidator`; Prometheus `alert_quality_useful_rate` and `alert_quality_false_positive_rate` gauges; Grafana `alert-quality-dashboard.json`; frontend `AlertQualityAnalytics.vue` with `AlertQualityChart.vue` and `alertQuality` Pinia store; `AlertQualityAnalyticsTests`; `run-phase33-verification.sh` | Done |
|
||||||
| 34 | **Explainable Alerts** — `AlertExplanation` value object (`ScoreContributor`, `TrendContext`, `MedicationContext`, `NarrativeSummary`); JSONB `ClinicalAlert.Explanation` column (immutable at creation); contributor builders for NEWS2, SOFA, GCS; `TrendContextBuilder`; `AlertExplanationBuilder` + `ClinicalAlertFactory`; NEWS2, SOFA, GCS, and `TrendDetector` wire explanation and include `explanation` in `alert.generated` outbox; `MedicationCorrelationHelper.TryGetContextAsync()` for structured medication context; `AlertResponse` DTO + `AlertResponseMapper`; GET/list/acknowledge/resolve return `AlertResponse`; ES indexer projects `NarrativeSummary`; data lake Parquet `explanation_json`; ward gateway `LocalClinicalAlert.ExplanationJson` + sync; dashboard `AlertReasoning.vue` + `alertExplanation.js`; simulator `ExpectedOutcomeValidator` with `narrativeContains`; `ExplainableAlertsTests`; `run-phase34-verification.sh` | Done |
|
| 34 | **Explainable Alerts** — `AlertExplanation` value object (`ScoreContributor`, `TrendContext`, `MedicationContext`, `NarrativeSummary`); JSONB `ClinicalAlert.Explanation` column (immutable at creation); contributor builders for NEWS2, SOFA, GCS; `TrendContextBuilder`; `AlertExplanationBuilder` + `ClinicalAlertFactory`; NEWS2, SOFA, GCS, and `TrendDetector` wire explanation and include `explanation` in `alert.generated` outbox; `MedicationCorrelationHelper.TryGetContextAsync()` for structured medication context; `AlertResponse` DTO + `AlertResponseMapper`; GET/list/acknowledge/resolve return `AlertResponse`; ES indexer projects `NarrativeSummary`; data lake Parquet `explanation_json`; ward gateway `LocalClinicalAlert.ExplanationJson` + sync; dashboard `AlertReasoning.vue` + `alertExplanation.js`; simulator `ExpectedOutcomeValidator` with `narrativeContains`; `ExplainableAlertsTests`; `run-phase34-verification.sh` | Done |
|
||||||
|
| 35 | **MIMIC-IV Replay Scenario Generator** — offline CLI tool in `VigilCare.Simulator/Mimic/` that reads MIMIC-IV CSV files (100 patients, 140 ICU stays, 668K chart events, 107K lab events from `docs/MIMIC-IV/`) and generates standard VigilCare scenario JSONs; `MimicCsvReader` streaming CSV parser with header-index lookup and filter predicate (memory-efficient for large files); `MimicItemMap` maps 17 chart event items (vitals, GCS, FiO₂, PaO₂, labs) and 8 lab event items to VigilCare observation codes with GCS text-to-numeric fallback dictionary, Fahrenheit-to-Celsius conversion, and blood pressure priority (non-invasive preferred over arterial); `MimicCareUnitMap` maps care units to departments and generates tags; `MimicDataLoader` streams chartevents/labevents/prescriptions filtered by stay_id + item ID set; `MimicScenarioBuilder` deduplicates BP, enforces 10-observation-per-cluster limit with priority-based splitting, computes offsetMinutes, parses medication doses; `MimicListCommand` (`mimic-list`) displays Spectre.Console table of available stays; `MimicGenerateCommand` (`mimic-generate`) produces scenario JSON with `--max-hours`, `--no-medications`, `--no-labs`, `--validate` options; generated scenarios pass `ScenarioValidator` and replay through the standard `replay` command | Done |
|
||||||
|
|
||||||
**Ward dashboard:** backend APIs (`GET /encounters` ward list with extended summary fields including SOFA/GCS/attending/admitted-at, `GET /qsofa/current`, `GET /qsofa/history`, `GET /gcs/history`, `GET /sepsis-bundles` hospital-wide list, `GET /operations/gateways` fleet management, `GET /users` user management, `GET /alerts/quality-metrics` alert quality, `GET /alerts/{id}` with structured explanation, CORS) and frontend SPA — `EncountersListTests`, `QsofaCurrentTests`, `GapAnalysisFixTests`, `OperationsApiTests`, `AlertQualityAnalyticsTests`, `ExplainableAlertsTests`, `vigilcare-dashboard` Vitest suite (replay scrubbing, feedback store, FeedbackButtons, FeedbackSummary, alert components, AlertReasoning, charts, ward table, ward sort, ward filter, department format, sepsis format, alert acknowledge, critical alert detect, handoff report, vitals form, GCS entry/history, SOFA panel/history, qSOFA history, scores panel, alert labels, PatientBanner, EncounterTimeline, medication chart markers, AcknowledgeModal, CriticalAlertBanner, DepartmentOverviewView, SepsisBoardView, VitalsEntryForm, useAlertStore, useWardStore, roleAccess, ThresholdManagementView, DischargeSummaryPanel, GatewayOperations, alertQuality).
|
**Ward dashboard:** backend APIs (`GET /encounters` ward list with extended summary fields including SOFA/GCS/attending/admitted-at, `GET /qsofa/current`, `GET /qsofa/history`, `GET /gcs/history`, `GET /sepsis-bundles` hospital-wide list, `GET /operations/gateways` fleet management, `GET /users` user management, `GET /alerts/quality-metrics` alert quality, `GET /alerts/{id}` with structured explanation, CORS) and frontend SPA — `EncountersListTests`, `QsofaCurrentTests`, `GapAnalysisFixTests`, `OperationsApiTests`, `AlertQualityAnalyticsTests`, `ExplainableAlertsTests`, `vigilcare-dashboard` Vitest suite (replay scrubbing, feedback store, FeedbackButtons, FeedbackSummary, alert components, AlertReasoning, charts, ward table, ward sort, ward filter, department format, sepsis format, alert acknowledge, critical alert detect, handoff report, vitals form, GCS entry/history, SOFA panel/history, qSOFA history, scores panel, alert labels, PatientBanner, EncounterTimeline, medication chart markers, AcknowledgeModal, CriticalAlertBanner, DepartmentOverviewView, SepsisBoardView, VitalsEntryForm, useAlertStore, useWardStore, roleAccess, ThresholdManagementView, DischargeSummaryPanel, GatewayOperations, alertQuality).
|
||||||
|
|
||||||
@@ -2287,6 +2312,8 @@ Thirty-two phases from the project roadmap are implemented and verified, includi
|
|||||||
|
|
||||||
**Explainable Alerts (Phase 34):** Composite alerts (NEWS2, SOFA, GCS, rapid deterioration) carry an immutable JSONB `explanation` snapshot at creation — score contributors with raw values and normal ranges, trend context (percent change, duration, direction), structured medication context, and a bedside `NarrativeSummary`. `AlertResponse` exposes explanation on GET/list/acknowledge/resolve. Downstream consumers (Elasticsearch indexer, data lake Parquet, ward gateway sync, Kafka `alert.generated`) propagate explanation without breaking legacy consumers. Dashboard `AlertReasoning.vue` renders structured reasoning. Simulator validates `narrativeContains` on key scenarios.
|
**Explainable Alerts (Phase 34):** Composite alerts (NEWS2, SOFA, GCS, rapid deterioration) carry an immutable JSONB `explanation` snapshot at creation — score contributors with raw values and normal ranges, trend context (percent change, duration, direction), structured medication context, and a bedside `NarrativeSummary`. `AlertResponse` exposes explanation on GET/list/acknowledge/resolve. Downstream consumers (Elasticsearch indexer, data lake Parquet, ward gateway sync, Kafka `alert.generated`) propagate explanation without breaking legacy consumers. Dashboard `AlertReasoning.vue` renders structured reasoning. Simulator validates `narrativeContains` on key scenarios.
|
||||||
|
|
||||||
|
**MIMIC-IV Replay Scenario Generator (Phase 35):** Offline CLI tool that converts real de-identified ICU data from MIT's MIMIC-IV dataset into VigilCare scenario JSONs. `mimic-list` browses 140 ICU stays across 100 patients with demographics, care unit, LOS, and outcome. `mimic-generate` produces a scenario from a specific stay ID with options for duration capping, medication/lab exclusion, and inline validation. The streaming CSV parser handles 668K-row chartevents efficiently by filtering at the string level before allocating records. Item mappings cover 17 chart event items (vitals, GCS text-to-numeric, FiO₂, PaO₂, ICU labs) and 8 lab event items (creatinine, platelets, bilirubin, lactate, WBC, potassium, glucose, PaO₂). Blood pressure deduplication prefers non-invasive over arterial readings. The 10-observation-per-cluster limit is enforced by priority-based splitting (vitals first, labs spill to the next offset). Generated scenarios are structurally identical to hand-crafted ones and replay through the existing `replay` command, driving NEWS2, SOFA, GCS, qSOFA, trend detection, and alerting on real patient trajectories.
|
||||||
|
|
||||||
**Post-phase hardening (after Phase 31):**
|
**Post-phase hardening (after Phase 31):**
|
||||||
- **FHIR R4 read/search** — `FhirReadController` adds `GET /fhir/R4/Patient/{id}`, `GET /fhir/R4/Patient` (search by `identifier`), `GET /fhir/R4/Encounter/{id}`, `GET /fhir/R4/Encounter` (search by `patient`/`status`); new `fhir:read` permission for Admin and Integration roles; CapabilityStatement updated to advertise `read` and `searchType` interactions for Patient and Encounter
|
- **FHIR R4 read/search** — `FhirReadController` adds `GET /fhir/R4/Patient/{id}`, `GET /fhir/R4/Patient` (search by `identifier`), `GET /fhir/R4/Encounter/{id}`, `GET /fhir/R4/Encounter` (search by `patient`/`status`); new `fhir:read` permission for Admin and Integration roles; CapabilityStatement updated to advertise `read` and `searchType` interactions for Patient and Encounter
|
||||||
- **Alert threshold deletion** — `DELETE /alert-thresholds/{id}` with `THRESHOLD_DELETED` audit action and Redis cache invalidation
|
- **Alert threshold deletion** — `DELETE /alert-thresholds/{id}` with `THRESHOLD_DELETED` audit action and Redis cache invalidation
|
||||||
|
|||||||
@@ -38,7 +38,12 @@ public class VigilCareApiClient
|
|||||||
public async Task<PatientResponse> RegisterPatientAsync(RegisterPatientRequest req)
|
public async Task<PatientResponse> RegisterPatientAsync(RegisterPatientRequest req)
|
||||||
{
|
{
|
||||||
var response = await _http.PostAsJsonAsync("/api/v1/patients", req);
|
var response = await _http.PostAsJsonAsync("/api/v1/patients", req);
|
||||||
response.EnsureSuccessStatusCode();
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
throw new HttpRequestException(
|
||||||
|
$"Register patient failed ({(int)response.StatusCode} {response.StatusCode}): {body}");
|
||||||
|
}
|
||||||
var envelope = await response.Content.ReadFromJsonAsync<ApiResponse<PatientResponse>>();
|
var envelope = await response.Content.ReadFromJsonAsync<ApiResponse<PatientResponse>>();
|
||||||
return envelope!.Data!;
|
return envelope!.Data!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
public static class MimicCareUnitMap
|
||||||
|
{
|
||||||
|
public static string ToVigilCareDepartment(string mimicCareUnit)
|
||||||
|
{
|
||||||
|
// All MIMIC ICU stays map to ICU in VigilCare
|
||||||
|
return "Icu";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToVigilCareEncounterType(string admissionType)
|
||||||
|
{
|
||||||
|
if (admissionType.Contains("EMER", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "Emergency";
|
||||||
|
return "Inpatient";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<string> GetTags(string mimicCareUnit, int hospitalExpireFlag)
|
||||||
|
{
|
||||||
|
var tags = new List<string> { "mimic-iv", "real-data", "icu" };
|
||||||
|
|
||||||
|
var unit = mimicCareUnit.ToUpperInvariant();
|
||||||
|
if (unit.Contains("CARDIAC") || unit.Contains("CVICU") || unit.Contains("CCU") || unit.Contains("CORONARY"))
|
||||||
|
tags.Add("cardiac");
|
||||||
|
if (unit.Contains("NEURO"))
|
||||||
|
tags.Add("neuro");
|
||||||
|
if (unit.Contains("SURG") || unit.Contains("TSICU"))
|
||||||
|
tags.Add("surgical");
|
||||||
|
if (unit.Contains("TRAUMA"))
|
||||||
|
tags.Add("trauma");
|
||||||
|
if (unit.Contains("MICU") || unit.Contains("MEDICAL"))
|
||||||
|
tags.Add("medical");
|
||||||
|
|
||||||
|
if (hospitalExpireFlag == 1)
|
||||||
|
tags.Add("expired");
|
||||||
|
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
public static class MimicCsvReader
|
||||||
|
{
|
||||||
|
public static IEnumerable<T> Read<T>(
|
||||||
|
string filePath,
|
||||||
|
Func<string[], Dictionary<string, int>, T?> parser,
|
||||||
|
Func<string[], Dictionary<string, int>, bool>? filter = null)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(filePath);
|
||||||
|
var headerLine = reader.ReadLine();
|
||||||
|
if (headerLine is null) yield break;
|
||||||
|
|
||||||
|
var headers = BuildHeaderIndex(headerLine);
|
||||||
|
|
||||||
|
while (reader.ReadLine() is { } line)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||||
|
var fields = line.Split(',');
|
||||||
|
|
||||||
|
if (filter is not null && !filter(fields, headers))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var record = parser(fields, headers);
|
||||||
|
if (record is not null)
|
||||||
|
yield return record;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<T> ReadAll<T>(
|
||||||
|
string filePath,
|
||||||
|
Func<string[], Dictionary<string, int>, T?> parser)
|
||||||
|
{
|
||||||
|
return Read(filePath, parser).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, int> BuildHeaderIndex(string headerLine)
|
||||||
|
{
|
||||||
|
var headers = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var columns = headerLine.Split(',');
|
||||||
|
for (var i = 0; i < columns.Length; i++)
|
||||||
|
headers[columns[i].Trim()] = i;
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Col(string[] fields, Dictionary<string, int> headers, string name)
|
||||||
|
{
|
||||||
|
return headers.TryGetValue(name, out var idx) && idx < fields.Length
|
||||||
|
? fields[idx].Trim()
|
||||||
|
: string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int? ColInt(string[] fields, Dictionary<string, int> headers, string name)
|
||||||
|
{
|
||||||
|
var val = Col(fields, headers, name);
|
||||||
|
return int.TryParse(val, out var result) ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decimal? ColDecimal(string[] fields, Dictionary<string, int> headers, string name)
|
||||||
|
{
|
||||||
|
var val = Col(fields, headers, name);
|
||||||
|
return decimal.TryParse(val, System.Globalization.NumberStyles.Any,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var result) ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DateTime? ColDateTime(string[] fields, Dictionary<string, int> headers, string name)
|
||||||
|
{
|
||||||
|
var val = Col(fields, headers, name);
|
||||||
|
return DateTime.TryParse(val, System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
System.Globalization.DateTimeStyles.None, out var result) ? result : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
public record MimicPatient(int SubjectId, string Gender, int AnchorAge, int AnchorYear, string? Dod);
|
||||||
|
|
||||||
|
public record MimicAdmission(
|
||||||
|
int SubjectId, int HadmId,
|
||||||
|
DateTime AdmitTime, DateTime DischTime, DateTime? DeathTime,
|
||||||
|
string AdmissionType, string? AdmissionLocation, string? DischargeLocation,
|
||||||
|
int HospitalExpireFlag);
|
||||||
|
|
||||||
|
public record MimicIcuStay(
|
||||||
|
int SubjectId, int HadmId, int StayId,
|
||||||
|
string FirstCareUnit, string LastCareUnit,
|
||||||
|
DateTime InTime, DateTime OutTime, decimal Los);
|
||||||
|
|
||||||
|
public record MimicChartEvent(
|
||||||
|
int StayId, DateTime ChartTime, int ItemId,
|
||||||
|
string? TextValue, decimal? ValueNum);
|
||||||
|
|
||||||
|
public record MimicLabEvent(
|
||||||
|
int HadmId, DateTime ChartTime, int ItemId,
|
||||||
|
decimal? ValueNum, string? ValueUom);
|
||||||
|
|
||||||
|
public record MimicPrescription(
|
||||||
|
int HadmId, DateTime StartTime,
|
||||||
|
string Drug, string? DoseValRx, string? DoseUnitRx, string? Route);
|
||||||
|
|
||||||
|
public class MimicDataLoader
|
||||||
|
{
|
||||||
|
private readonly string _dataDir;
|
||||||
|
|
||||||
|
public MimicDataLoader(string dataDir)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(dataDir))
|
||||||
|
throw new DirectoryNotFoundException($"MIMIC data directory not found: {dataDir}");
|
||||||
|
_dataDir = dataDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Path(string fileName) => System.IO.Path.Combine(_dataDir, fileName);
|
||||||
|
|
||||||
|
public List<MimicPatient> LoadPatients()
|
||||||
|
{
|
||||||
|
return MimicCsvReader.ReadAll(Path("patients.csv"), (f, h) =>
|
||||||
|
{
|
||||||
|
var id = MimicCsvReader.ColInt(f, h, "subject_id");
|
||||||
|
var age = MimicCsvReader.ColInt(f, h, "anchor_age");
|
||||||
|
var year = MimicCsvReader.ColInt(f, h, "anchor_year");
|
||||||
|
if (id is null || age is null || year is null) return null;
|
||||||
|
return new MimicPatient(
|
||||||
|
id.Value,
|
||||||
|
MimicCsvReader.Col(f, h, "gender"),
|
||||||
|
age.Value,
|
||||||
|
year.Value,
|
||||||
|
MimicCsvReader.Col(f, h, "dod") is { Length: > 0 } dod ? dod : null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MimicAdmission> LoadAdmissions()
|
||||||
|
{
|
||||||
|
return MimicCsvReader.ReadAll(Path("admissions.csv"), (f, h) =>
|
||||||
|
{
|
||||||
|
var subjectId = MimicCsvReader.ColInt(f, h, "subject_id");
|
||||||
|
var hadmId = MimicCsvReader.ColInt(f, h, "hadm_id");
|
||||||
|
var admitTime = MimicCsvReader.ColDateTime(f, h, "admittime");
|
||||||
|
var dischTime = MimicCsvReader.ColDateTime(f, h, "dischtime");
|
||||||
|
if (subjectId is null || hadmId is null || admitTime is null || dischTime is null)
|
||||||
|
return null;
|
||||||
|
return new MimicAdmission(
|
||||||
|
subjectId.Value, hadmId.Value,
|
||||||
|
admitTime.Value, dischTime.Value,
|
||||||
|
MimicCsvReader.ColDateTime(f, h, "deathtime"),
|
||||||
|
MimicCsvReader.Col(f, h, "admission_type"),
|
||||||
|
MimicCsvReader.Col(f, h, "admission_location") is { Length: > 0 } loc ? loc : null,
|
||||||
|
MimicCsvReader.Col(f, h, "discharge_location") is { Length: > 0 } dloc ? dloc : null,
|
||||||
|
MimicCsvReader.ColInt(f, h, "hospital_expire_flag") ?? 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MimicIcuStay> LoadIcuStays()
|
||||||
|
{
|
||||||
|
return MimicCsvReader.ReadAll(Path("icustays.csv"), (f, h) =>
|
||||||
|
{
|
||||||
|
var subjectId = MimicCsvReader.ColInt(f, h, "subject_id");
|
||||||
|
var hadmId = MimicCsvReader.ColInt(f, h, "hadm_id");
|
||||||
|
var stayId = MimicCsvReader.ColInt(f, h, "stay_id");
|
||||||
|
var inTime = MimicCsvReader.ColDateTime(f, h, "intime");
|
||||||
|
var outTime = MimicCsvReader.ColDateTime(f, h, "outtime");
|
||||||
|
if (subjectId is null || hadmId is null || stayId is null
|
||||||
|
|| inTime is null || outTime is null)
|
||||||
|
return null;
|
||||||
|
return new MimicIcuStay(
|
||||||
|
subjectId.Value, hadmId.Value, stayId.Value,
|
||||||
|
MimicCsvReader.Col(f, h, "first_careunit"),
|
||||||
|
MimicCsvReader.Col(f, h, "last_careunit"),
|
||||||
|
inTime.Value, outTime.Value,
|
||||||
|
MimicCsvReader.ColDecimal(f, h, "los") ?? 0m);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<MimicChartEvent> StreamChartEvents(int stayId)
|
||||||
|
{
|
||||||
|
var stayIdStr = stayId.ToString();
|
||||||
|
return MimicCsvReader.Read(
|
||||||
|
Path("chartevents.csv"),
|
||||||
|
parser: (f, h) =>
|
||||||
|
{
|
||||||
|
var itemId = MimicCsvReader.ColInt(f, h, "itemid");
|
||||||
|
if (itemId is null || !MimicItemMap.AllChartItemIds.Contains(itemId.Value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var chartTime = MimicCsvReader.ColDateTime(f, h, "charttime");
|
||||||
|
if (chartTime is null) return null;
|
||||||
|
|
||||||
|
var valueNum = MimicCsvReader.ColDecimal(f, h, "valuenum");
|
||||||
|
var textValue = MimicCsvReader.Col(f, h, "value");
|
||||||
|
|
||||||
|
if (MimicItemMap.IsGcsItem(itemId.Value))
|
||||||
|
{
|
||||||
|
var gcsVal = MimicItemMap.ResolveGcsValue(itemId.Value, textValue, valueNum);
|
||||||
|
if (gcsVal is null) return null;
|
||||||
|
return new MimicChartEvent(stayId, chartTime.Value, itemId.Value, textValue, gcsVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueNum is null) return null;
|
||||||
|
|
||||||
|
return new MimicChartEvent(stayId, chartTime.Value, itemId.Value, textValue, valueNum);
|
||||||
|
},
|
||||||
|
filter: (f, h) =>
|
||||||
|
{
|
||||||
|
var sid = MimicCsvReader.Col(f, h, "stay_id");
|
||||||
|
return sid == stayIdStr;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<MimicLabEvent> StreamLabEvents(int hadmId, DateTime? after = null, DateTime? before = null)
|
||||||
|
{
|
||||||
|
var hadmIdStr = hadmId.ToString();
|
||||||
|
return MimicCsvReader.Read(
|
||||||
|
Path("labevents.csv"),
|
||||||
|
parser: (f, h) =>
|
||||||
|
{
|
||||||
|
var itemId = MimicCsvReader.ColInt(f, h, "itemid");
|
||||||
|
if (itemId is null || !MimicItemMap.AllLabItemIds.Contains(itemId.Value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var chartTime = MimicCsvReader.ColDateTime(f, h, "charttime");
|
||||||
|
if (chartTime is null) return null;
|
||||||
|
if (after.HasValue && chartTime.Value < after.Value) return null;
|
||||||
|
if (before.HasValue && chartTime.Value > before.Value) return null;
|
||||||
|
|
||||||
|
var valueNum = MimicCsvReader.ColDecimal(f, h, "valuenum");
|
||||||
|
if (valueNum is null) return null;
|
||||||
|
|
||||||
|
return new MimicLabEvent(
|
||||||
|
hadmId, chartTime.Value, itemId.Value, valueNum,
|
||||||
|
MimicCsvReader.Col(f, h, "valueuom") is { Length: > 0 } uom ? uom : null);
|
||||||
|
},
|
||||||
|
filter: (f, h) =>
|
||||||
|
{
|
||||||
|
var hid = MimicCsvReader.Col(f, h, "hadm_id");
|
||||||
|
return hid == hadmIdStr;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<MimicPrescription> StreamPrescriptions(
|
||||||
|
int hadmId, DateTime? after = null, DateTime? before = null)
|
||||||
|
{
|
||||||
|
var hadmIdStr = hadmId.ToString();
|
||||||
|
return MimicCsvReader.Read(
|
||||||
|
Path("prescriptions.csv"),
|
||||||
|
parser: (f, h) =>
|
||||||
|
{
|
||||||
|
var startTime = MimicCsvReader.ColDateTime(f, h, "starttime");
|
||||||
|
if (startTime is null) return null;
|
||||||
|
if (after.HasValue && startTime.Value < after.Value) return null;
|
||||||
|
if (before.HasValue && startTime.Value > before.Value) return null;
|
||||||
|
|
||||||
|
var drug = MimicCsvReader.Col(f, h, "drug");
|
||||||
|
if (string.IsNullOrWhiteSpace(drug)) return null;
|
||||||
|
|
||||||
|
return new MimicPrescription(
|
||||||
|
hadmId, startTime.Value, drug,
|
||||||
|
MimicCsvReader.Col(f, h, "dose_val_rx") is { Length: > 0 } d ? d : null,
|
||||||
|
MimicCsvReader.Col(f, h, "dose_unit_rx") is { Length: > 0 } u ? u : null,
|
||||||
|
MimicCsvReader.Col(f, h, "route") is { Length: > 0 } r ? r : null);
|
||||||
|
},
|
||||||
|
filter: (f, h) =>
|
||||||
|
{
|
||||||
|
var hid = MimicCsvReader.Col(f, h, "hadm_id");
|
||||||
|
return hid == hadmIdStr;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
public static class MimicGenerateCommand
|
||||||
|
{
|
||||||
|
public static Command Create()
|
||||||
|
{
|
||||||
|
var dataDirArg = new Argument<DirectoryInfo>("mimic-dir",
|
||||||
|
"Path to directory containing MIMIC-IV CSV files");
|
||||||
|
var stayIdOpt = new Option<int>("--stay-id", "ICU stay ID to generate scenario for")
|
||||||
|
{ IsRequired = true };
|
||||||
|
var maxHoursOpt = new Option<int?>("--max-hours", "Limit scenario duration in hours");
|
||||||
|
var noMedsOpt = new Option<bool>("--no-medications", () => false,
|
||||||
|
"Exclude medication events");
|
||||||
|
var noLabsOpt = new Option<bool>("--no-labs", () => false,
|
||||||
|
"Exclude lab observations");
|
||||||
|
var outputOpt = new Option<string?>("--output", "Output file path (default: auto-named)");
|
||||||
|
var validateOpt = new Option<bool>("--validate", () => false,
|
||||||
|
"Run scenario validation after generation");
|
||||||
|
|
||||||
|
var command = new Command("mimic-generate",
|
||||||
|
"Generate a VigilCare scenario JSON from a MIMIC-IV ICU stay")
|
||||||
|
{
|
||||||
|
dataDirArg, stayIdOpt, maxHoursOpt, noMedsOpt, noLabsOpt, outputOpt, validateOpt
|
||||||
|
};
|
||||||
|
|
||||||
|
command.SetHandler(context =>
|
||||||
|
{
|
||||||
|
var dataDir = context.ParseResult.GetValueForArgument(dataDirArg);
|
||||||
|
var stayId = context.ParseResult.GetValueForOption(stayIdOpt);
|
||||||
|
var maxHours = context.ParseResult.GetValueForOption(maxHoursOpt);
|
||||||
|
var noMeds = context.ParseResult.GetValueForOption(noMedsOpt);
|
||||||
|
var noLabs = context.ParseResult.GetValueForOption(noLabsOpt);
|
||||||
|
var outputPath = context.ParseResult.GetValueForOption(outputOpt);
|
||||||
|
var validate = context.ParseResult.GetValueForOption(validateOpt);
|
||||||
|
|
||||||
|
var loader = new MimicDataLoader(dataDir.FullName);
|
||||||
|
|
||||||
|
AnsiConsole.MarkupLine($"[bold]Loading MIMIC-IV data for stay {stayId}...[/]");
|
||||||
|
|
||||||
|
var stays = loader.LoadIcuStays();
|
||||||
|
var stay = stays.FirstOrDefault(s => s.StayId == stayId);
|
||||||
|
if (stay is null)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine($"[red]ICU stay {stayId} not found.[/]");
|
||||||
|
var available = stays.Select(s => s.StayId).OrderBy(x => x).ToList();
|
||||||
|
AnsiConsole.MarkupLine($"Available stay IDs: {string.Join(", ", available.Take(20))}...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var admissions = loader.LoadAdmissions();
|
||||||
|
var admission = admissions.FirstOrDefault(a => a.HadmId == stay.HadmId);
|
||||||
|
if (admission is null)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine($"[red]Admission {stay.HadmId} not found.[/]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var patients = loader.LoadPatients();
|
||||||
|
var patient = patients.FirstOrDefault(p => p.SubjectId == stay.SubjectId);
|
||||||
|
if (patient is null)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine($"[red]Patient {stay.SubjectId} not found.[/]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.MarkupLine(
|
||||||
|
$" Patient: [cyan]{patient.SubjectId}[/] ({patient.Gender}, ~{patient.AnchorAge}y)");
|
||||||
|
AnsiConsole.MarkupLine(
|
||||||
|
$" Stay: [cyan]{stay.StayId}[/] in {stay.FirstCareUnit}");
|
||||||
|
AnsiConsole.MarkupLine(
|
||||||
|
$" LOS: {stay.Los:F1} days ({stay.InTime:g} → {stay.OutTime:g})");
|
||||||
|
AnsiConsole.MarkupLine(
|
||||||
|
$" Outcome: {(admission.HospitalExpireFlag == 1 ? "[red]Expired[/]" : "Survived")}");
|
||||||
|
|
||||||
|
var options = new MimicGenerateOptions(
|
||||||
|
MaxHours: maxHours,
|
||||||
|
IncludeMedications: !noMeds,
|
||||||
|
IncludeLabs: !noLabs);
|
||||||
|
|
||||||
|
AnsiConsole.MarkupLine("\n[bold]Generating scenario...[/]");
|
||||||
|
|
||||||
|
var builder = new MimicScenarioBuilder(loader);
|
||||||
|
var (scenario, warnings) = builder.Build(stay, admission, patient, options);
|
||||||
|
|
||||||
|
foreach (var warning in warnings)
|
||||||
|
AnsiConsole.MarkupLine($" [yellow]WARNING:[/] {Markup.Escape(warning)}");
|
||||||
|
|
||||||
|
var obsCount = scenario.Events.Count(e => e.Type == "observation");
|
||||||
|
var medCount = scenario.Events.Count(e => e.Type == "medication");
|
||||||
|
AnsiConsole.MarkupLine(
|
||||||
|
$" Events: [green]{obsCount}[/] observations, [green]{medCount}[/] medications");
|
||||||
|
AnsiConsole.MarkupLine(
|
||||||
|
$" Duration: {scenario.Scenario.DurationMinutes} minutes " +
|
||||||
|
$"({scenario.Scenario.DurationMinutes / 60.0:F1} hours)");
|
||||||
|
|
||||||
|
if (validate)
|
||||||
|
{
|
||||||
|
var errors = ScenarioValidator.Validate(scenario);
|
||||||
|
if (errors.Count == 0)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine(" [green]Validation: PASSED[/]");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine($" [red]Validation: {errors.Count} error(s)[/]");
|
||||||
|
foreach (var err in errors)
|
||||||
|
AnsiConsole.MarkupLine($" [red]• {Markup.Escape(err)}[/]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonOptions = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = true,
|
||||||
|
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(scenario, jsonOptions);
|
||||||
|
|
||||||
|
var filePath = outputPath
|
||||||
|
?? Path.Combine("Scenarios", "List", $"mimic-s{stayId}.json");
|
||||||
|
|
||||||
|
var dir = Path.GetDirectoryName(filePath);
|
||||||
|
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
|
||||||
|
File.WriteAllText(filePath, json);
|
||||||
|
AnsiConsole.MarkupLine($"\n[bold green]Scenario written to:[/] {filePath}");
|
||||||
|
AnsiConsole.MarkupLine($" Replay with: [dim]dotnet run -- replay {filePath}[/]");
|
||||||
|
});
|
||||||
|
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
public static class MimicItemMap
|
||||||
|
{
|
||||||
|
public record ChartItemMapping(
|
||||||
|
string Code, string Unit, string Source,
|
||||||
|
int Priority = 0,
|
||||||
|
bool ConvertFahrenheit = false);
|
||||||
|
|
||||||
|
public record LabItemMapping(string Code, string Unit);
|
||||||
|
|
||||||
|
private static readonly Dictionary<int, ChartItemMapping> ChartMappings = new()
|
||||||
|
{
|
||||||
|
[220045] = new("HEART_RATE", "bpm", "Device"),
|
||||||
|
[220210] = new("RESP_RATE", "/min", "Device"),
|
||||||
|
[220179] = new("SYSTOLIC_BP", "mmHg", "Device"),
|
||||||
|
[220180] = new("DIASTOLIC_BP", "mmHg", "Device"),
|
||||||
|
[220050] = new("SYSTOLIC_BP", "mmHg", "Device", Priority: 1),
|
||||||
|
[220051] = new("DIASTOLIC_BP", "mmHg", "Device", Priority: 1),
|
||||||
|
[223762] = new("TEMP_C", "°C", "Manual"),
|
||||||
|
[223761] = new("TEMP_C", "°C", "Manual", ConvertFahrenheit: true),
|
||||||
|
[220277] = new("SPO2", "%", "Device"),
|
||||||
|
[223835] = new("FIO2_PCT", "%", "Device"),
|
||||||
|
[220739] = new("GCS_EYE", "score", "Manual"),
|
||||||
|
[223900] = new("GCS_VERBAL", "score", "Manual"),
|
||||||
|
[223901] = new("GCS_MOTOR", "score", "Manual"),
|
||||||
|
[220615] = new("CREATININE_MG_DL", "mg/dL", "Lab"),
|
||||||
|
[225690] = new("BILIRUBIN_MG_DL", "mg/dL", "Lab"),
|
||||||
|
[225678] = new("PLATELET_K_UL", "k/µL", "Lab"),
|
||||||
|
[220224] = new("PAO2_MMHG", "mmHg", "Lab"),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<int, LabItemMapping> LabMappings = new()
|
||||||
|
{
|
||||||
|
[50912] = new("CREATININE_MG_DL", "mg/dL"),
|
||||||
|
[51704] = new("PLATELET_K_UL", "k/µL"),
|
||||||
|
[50885] = new("BILIRUBIN_MG_DL", "mg/dL"),
|
||||||
|
[50813] = new("LACTATE_MMOL_L", "mmol/L"),
|
||||||
|
[51301] = new("WBC_K_UL", "k/µL"),
|
||||||
|
[50971] = new("POTASSIUM_MEQ_L", "mEq/L"),
|
||||||
|
[50931] = new("GLUCOSE_MG_DL", "mg/dL"),
|
||||||
|
[50821] = new("PAO2_MMHG", "mmHg"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// GCS text → numeric fallback (in case valuenum is missing)
|
||||||
|
private static readonly Dictionary<string, int> GcsEyeText = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["None"] = 1, ["No Response"] = 1,
|
||||||
|
["To Pain"] = 2,
|
||||||
|
["To Speech"] = 3,
|
||||||
|
["Spontaneously"] = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, int> GcsVerbalText = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["No Response"] = 1, ["No Response-ETT"] = 1,
|
||||||
|
["Incomprehensible sounds"] = 2, ["Incomprehensible Sounds"] = 2,
|
||||||
|
["Inappropriate Words"] = 3,
|
||||||
|
["Confused"] = 4,
|
||||||
|
["Oriented"] = 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, int> GcsMotorText = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["No response"] = 1, ["No Response"] = 1,
|
||||||
|
["Abnormal extension"] = 2, ["Abnormal Extension"] = 2,
|
||||||
|
["Abnormal Flexion"] = 3,
|
||||||
|
["Flex-withdraws"] = 3, ["Flex-Withdraws"] = 3,
|
||||||
|
["Localizes Pain"] = 4,
|
||||||
|
["Obeys Commands"] = 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly HashSet<int> AllChartItemIds = new(ChartMappings.Keys);
|
||||||
|
public static readonly HashSet<int> AllLabItemIds = new(LabMappings.Keys);
|
||||||
|
|
||||||
|
public static bool TryMapChartEvent(int itemId, out ChartItemMapping mapping)
|
||||||
|
=> ChartMappings.TryGetValue(itemId, out mapping!);
|
||||||
|
|
||||||
|
public static bool TryMapLabEvent(int itemId, out LabItemMapping mapping)
|
||||||
|
=> LabMappings.TryGetValue(itemId, out mapping!);
|
||||||
|
|
||||||
|
public static decimal ConvertValue(decimal rawValue, ChartItemMapping mapping)
|
||||||
|
{
|
||||||
|
if (mapping.ConvertFahrenheit)
|
||||||
|
return Math.Round((rawValue - 32m) * 5m / 9m, 1);
|
||||||
|
return rawValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decimal? ResolveGcsValue(int itemId, string? textValue, decimal? numericValue)
|
||||||
|
{
|
||||||
|
if (numericValue.HasValue && numericValue.Value > 0)
|
||||||
|
return numericValue.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(textValue))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var lookup = itemId switch
|
||||||
|
{
|
||||||
|
220739 => GcsEyeText,
|
||||||
|
223900 => GcsVerbalText,
|
||||||
|
223901 => GcsMotorText,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lookup is not null && lookup.TryGetValue(textValue, out var score))
|
||||||
|
return score;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsGcsItem(int itemId) => itemId is 220739 or 223900 or 223901;
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
public static class MimicListCommand
|
||||||
|
{
|
||||||
|
public static Command Create()
|
||||||
|
{
|
||||||
|
var dataDirArg = new Argument<DirectoryInfo>("mimic-dir",
|
||||||
|
"Path to directory containing MIMIC-IV CSV files");
|
||||||
|
var subjectIdOpt = new Option<int?>("--subject-id", "Filter to a specific patient");
|
||||||
|
var stayIdOpt = new Option<int?>("--stay-id", "Filter to a specific ICU stay");
|
||||||
|
|
||||||
|
var command = new Command("mimic-list",
|
||||||
|
"List available MIMIC-IV patients and ICU stays")
|
||||||
|
{
|
||||||
|
dataDirArg, subjectIdOpt, stayIdOpt
|
||||||
|
};
|
||||||
|
|
||||||
|
command.SetHandler(context =>
|
||||||
|
{
|
||||||
|
var dataDir = context.ParseResult.GetValueForArgument(dataDirArg);
|
||||||
|
var subjectId = context.ParseResult.GetValueForOption(subjectIdOpt);
|
||||||
|
var stayId = context.ParseResult.GetValueForOption(stayIdOpt);
|
||||||
|
|
||||||
|
var loader = new MimicDataLoader(dataDir.FullName);
|
||||||
|
var patients = loader.LoadPatients().ToDictionary(p => p.SubjectId);
|
||||||
|
var admissions = loader.LoadAdmissions().ToDictionary(a => a.HadmId);
|
||||||
|
var stays = loader.LoadIcuStays();
|
||||||
|
|
||||||
|
if (subjectId.HasValue)
|
||||||
|
stays = stays.Where(s => s.SubjectId == subjectId.Value).ToList();
|
||||||
|
if (stayId.HasValue)
|
||||||
|
stays = stays.Where(s => s.StayId == stayId.Value).ToList();
|
||||||
|
|
||||||
|
var table = new Table()
|
||||||
|
.Border(TableBorder.Rounded)
|
||||||
|
.Title("[bold]MIMIC-IV ICU Stays[/]");
|
||||||
|
|
||||||
|
table.AddColumn("StayId");
|
||||||
|
table.AddColumn("SubjectId");
|
||||||
|
table.AddColumn("HadmId");
|
||||||
|
table.AddColumn("Gender");
|
||||||
|
table.AddColumn("Age");
|
||||||
|
table.AddColumn("Care Unit");
|
||||||
|
table.AddColumn("Admission");
|
||||||
|
table.AddColumn("LOS (d)");
|
||||||
|
table.AddColumn("Expired");
|
||||||
|
|
||||||
|
foreach (var stay in stays.OrderBy(s => s.SubjectId).ThenBy(s => s.InTime))
|
||||||
|
{
|
||||||
|
var pt = patients.GetValueOrDefault(stay.SubjectId);
|
||||||
|
var adm = admissions.GetValueOrDefault(stay.HadmId);
|
||||||
|
|
||||||
|
table.AddRow(
|
||||||
|
stay.StayId.ToString(),
|
||||||
|
stay.SubjectId.ToString(),
|
||||||
|
stay.HadmId.ToString(),
|
||||||
|
pt?.Gender ?? "?",
|
||||||
|
pt?.AnchorAge.ToString() ?? "?",
|
||||||
|
Markup.Escape(stay.FirstCareUnit),
|
||||||
|
adm?.AdmissionType ?? "?",
|
||||||
|
stay.Los.ToString("F1"),
|
||||||
|
adm?.HospitalExpireFlag == 1 ? "[red]Yes[/]" : "No");
|
||||||
|
}
|
||||||
|
|
||||||
|
AnsiConsole.Write(table);
|
||||||
|
|
||||||
|
var patientCount = stays.Select(s => s.SubjectId).Distinct().Count();
|
||||||
|
AnsiConsole.MarkupLine(
|
||||||
|
$"\nFound [bold]{stays.Count}[/] ICU stays across [bold]{patientCount}[/] patients.");
|
||||||
|
});
|
||||||
|
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
public record MimicGenerateOptions(
|
||||||
|
int? MaxHours = null,
|
||||||
|
bool IncludeMedications = true,
|
||||||
|
bool IncludeLabs = true);
|
||||||
|
|
||||||
|
public class MimicScenarioBuilder
|
||||||
|
{
|
||||||
|
private readonly MimicDataLoader _loader;
|
||||||
|
|
||||||
|
private static readonly string[] ObservationPriority =
|
||||||
|
[
|
||||||
|
"HEART_RATE", "RESP_RATE", "SYSTOLIC_BP", "DIASTOLIC_BP", "TEMP_C", "SPO2",
|
||||||
|
"GCS_EYE", "GCS_VERBAL", "GCS_MOTOR",
|
||||||
|
"FIO2_PCT", "PAO2_MMHG",
|
||||||
|
"CREATININE_MG_DL", "BILIRUBIN_MG_DL", "PLATELET_K_UL",
|
||||||
|
"WBC_K_UL", "POTASSIUM_MEQ_L", "LACTATE_MMOL_L", "GLUCOSE_MG_DL",
|
||||||
|
"URINE_OUTPUT_ML_H"
|
||||||
|
];
|
||||||
|
|
||||||
|
public MimicScenarioBuilder(MimicDataLoader loader)
|
||||||
|
{
|
||||||
|
_loader = loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public (ScenarioFile Scenario, List<string> Warnings) Build(
|
||||||
|
MimicIcuStay stay, MimicAdmission admission, MimicPatient patient,
|
||||||
|
MimicGenerateOptions options)
|
||||||
|
{
|
||||||
|
var warnings = new List<string>();
|
||||||
|
var scenarioStart = stay.InTime;
|
||||||
|
var scenarioEnd = stay.OutTime;
|
||||||
|
|
||||||
|
if (options.MaxHours.HasValue)
|
||||||
|
{
|
||||||
|
var maxEnd = scenarioStart.AddHours(options.MaxHours.Value);
|
||||||
|
if (maxEnd < scenarioEnd)
|
||||||
|
scenarioEnd = maxEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scenarioPatient = BuildPatient(patient, admission);
|
||||||
|
var encounter = BuildEncounter(stay, admission);
|
||||||
|
var events = BuildEvents(stay, admission, scenarioStart, scenarioEnd, options, warnings);
|
||||||
|
var meta = BuildMeta(stay, admission, patient, scenarioStart, scenarioEnd, events);
|
||||||
|
|
||||||
|
var scenario = new ScenarioFile(meta, scenarioPatient, encounter, events, ExpectedOutcomes: null);
|
||||||
|
return (scenario, warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ScenarioPatient BuildPatient(MimicPatient patient, MimicAdmission admission)
|
||||||
|
{
|
||||||
|
var birthYear = DateTime.UtcNow.Year - patient.AnchorAge;
|
||||||
|
var dob = new DateTime(birthYear, 7, 1);
|
||||||
|
|
||||||
|
return new ScenarioPatient(
|
||||||
|
FirstName: $"MIMIC-{patient.SubjectId}",
|
||||||
|
LastName: $"S{admission.HadmId}",
|
||||||
|
DateOfBirth: dob.ToString("yyyy-MM-dd"),
|
||||||
|
Gender: patient.Gender == "F" ? "Female" : "Male");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ScenarioEncounter BuildEncounter(MimicIcuStay stay, MimicAdmission admission)
|
||||||
|
{
|
||||||
|
return new ScenarioEncounter(
|
||||||
|
Department: MimicCareUnitMap.ToVigilCareDepartment(stay.FirstCareUnit),
|
||||||
|
EncounterType: MimicCareUnitMap.ToVigilCareEncounterType(admission.AdmissionType),
|
||||||
|
AttendingPhysician: "MIMIC-Physician",
|
||||||
|
RoomBed: $"ICU-{stay.StayId % 100:D2}",
|
||||||
|
AdmissionReason: $"MIMIC-IV admission ({admission.AdmissionType}, from {admission.AdmissionLocation ?? "unknown"})");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ScenarioEvent> BuildEvents(
|
||||||
|
MimicIcuStay stay, MimicAdmission admission,
|
||||||
|
DateTime scenarioStart, DateTime scenarioEnd,
|
||||||
|
MimicGenerateOptions options, List<string> warnings)
|
||||||
|
{
|
||||||
|
var rawObs = CollectObservations(stay, admission, scenarioStart, scenarioEnd, options);
|
||||||
|
var deduplicated = DeduplicateBloodPressure(rawObs);
|
||||||
|
var events = new List<ScenarioEvent>();
|
||||||
|
|
||||||
|
foreach (var obs in deduplicated)
|
||||||
|
{
|
||||||
|
var offsetMinutes = Math.Round((obs.ChartTime - scenarioStart).TotalMinutes);
|
||||||
|
if (offsetMinutes < 0) offsetMinutes = 0;
|
||||||
|
|
||||||
|
var data = JsonSerializer.SerializeToElement(new
|
||||||
|
{
|
||||||
|
code = obs.Code,
|
||||||
|
value = obs.Value,
|
||||||
|
unit = obs.Unit,
|
||||||
|
source = obs.Source
|
||||||
|
}, SerializerOptions);
|
||||||
|
|
||||||
|
events.Add(new ScenarioEvent(offsetMinutes, "observation", data, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.IncludeMedications)
|
||||||
|
{
|
||||||
|
var meds = CollectMedications(admission, scenarioStart, scenarioEnd, warnings);
|
||||||
|
events.AddRange(meds);
|
||||||
|
}
|
||||||
|
|
||||||
|
events = events.OrderBy(e => e.OffsetMinutes).ToList();
|
||||||
|
events = EnforceClusterLimit(events, warnings);
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<RawObservation> CollectObservations(
|
||||||
|
MimicIcuStay stay, MimicAdmission admission,
|
||||||
|
DateTime scenarioStart, DateTime scenarioEnd,
|
||||||
|
MimicGenerateOptions options)
|
||||||
|
{
|
||||||
|
var observations = new List<RawObservation>();
|
||||||
|
|
||||||
|
foreach (var ce in _loader.StreamChartEvents(stay.StayId))
|
||||||
|
{
|
||||||
|
if (ce.ChartTime < scenarioStart || ce.ChartTime > scenarioEnd) continue;
|
||||||
|
if (!MimicItemMap.TryMapChartEvent(ce.ItemId, out var mapping)) continue;
|
||||||
|
if (ce.ValueNum is null) continue;
|
||||||
|
|
||||||
|
var value = MimicItemMap.ConvertValue(ce.ValueNum.Value, mapping);
|
||||||
|
observations.Add(new RawObservation(
|
||||||
|
ce.ChartTime, mapping.Code, value, mapping.Unit, mapping.Source, mapping.Priority));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.IncludeLabs)
|
||||||
|
{
|
||||||
|
var labCodes = new HashSet<(DateTime time, string code)>(
|
||||||
|
observations.Select(o => (o.ChartTime, o.Code)));
|
||||||
|
|
||||||
|
foreach (var le in _loader.StreamLabEvents(admission.HadmId, scenarioStart, scenarioEnd))
|
||||||
|
{
|
||||||
|
if (!MimicItemMap.TryMapLabEvent(le.ItemId, out var mapping)) continue;
|
||||||
|
if (le.ValueNum is null) continue;
|
||||||
|
|
||||||
|
if (labCodes.Contains((le.ChartTime, mapping.Code)))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
observations.Add(new RawObservation(
|
||||||
|
le.ChartTime, mapping.Code, le.ValueNum.Value, mapping.Unit, "Lab", 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return observations.OrderBy(o => o.ChartTime).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<RawObservation> DeduplicateBloodPressure(List<RawObservation> observations)
|
||||||
|
{
|
||||||
|
var bpGroups = observations
|
||||||
|
.Where(o => o.Code is "SYSTOLIC_BP" or "DIASTOLIC_BP")
|
||||||
|
.GroupBy(o => (Time: RoundToMinute(o.ChartTime), o.Code));
|
||||||
|
|
||||||
|
var removals = new HashSet<RawObservation>();
|
||||||
|
foreach (var group in bpGroups)
|
||||||
|
{
|
||||||
|
var items = group.ToList();
|
||||||
|
if (items.Count <= 1) continue;
|
||||||
|
|
||||||
|
var hasPrimary = items.Any(i => i.Priority == 0);
|
||||||
|
if (hasPrimary)
|
||||||
|
{
|
||||||
|
foreach (var fallback in items.Where(i => i.Priority > 0))
|
||||||
|
removals.Add(fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return removals.Count > 0
|
||||||
|
? observations.Where(o => !removals.Contains(o)).ToList()
|
||||||
|
: observations;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ScenarioEvent> CollectMedications(
|
||||||
|
MimicAdmission admission, DateTime scenarioStart, DateTime scenarioEnd,
|
||||||
|
List<string> warnings)
|
||||||
|
{
|
||||||
|
var events = new List<ScenarioEvent>();
|
||||||
|
var count = 0;
|
||||||
|
var skipped = 0;
|
||||||
|
|
||||||
|
foreach (var rx in _loader.StreamPrescriptions(admission.HadmId, scenarioStart, scenarioEnd))
|
||||||
|
{
|
||||||
|
var dose = ParseDose(rx.DoseValRx);
|
||||||
|
if (dose is null || string.IsNullOrWhiteSpace(rx.DoseUnitRx)
|
||||||
|
|| string.IsNullOrWhiteSpace(rx.Route))
|
||||||
|
{
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var offsetMinutes = Math.Round((rx.StartTime - scenarioStart).TotalMinutes);
|
||||||
|
if (offsetMinutes < 0) offsetMinutes = 0;
|
||||||
|
|
||||||
|
var data = JsonSerializer.SerializeToElement(new
|
||||||
|
{
|
||||||
|
drugName = rx.Drug,
|
||||||
|
dose = dose.Value,
|
||||||
|
doseUnit = rx.DoseUnitRx,
|
||||||
|
route = rx.Route,
|
||||||
|
administeredBy = "MIMIC-RN"
|
||||||
|
}, SerializerOptions);
|
||||||
|
|
||||||
|
events.Add(new ScenarioEvent(offsetMinutes, "medication", data, null));
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipped > 0)
|
||||||
|
warnings.Add($"Skipped {skipped} prescriptions with missing dose/unit/route data");
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ScenarioEvent> EnforceClusterLimit(
|
||||||
|
List<ScenarioEvent> events, List<string> warnings)
|
||||||
|
{
|
||||||
|
var result = new List<ScenarioEvent>();
|
||||||
|
var clusters = events.GroupBy(e => e.OffsetMinutes).OrderBy(g => g.Key).ToList();
|
||||||
|
var spillover = new List<(double offset, ScenarioEvent evt)>();
|
||||||
|
|
||||||
|
foreach (var cluster in clusters)
|
||||||
|
{
|
||||||
|
var obsInCluster = cluster.Where(e => e.Type == "observation").ToList();
|
||||||
|
var otherInCluster = cluster.Where(e => e.Type != "observation").ToList();
|
||||||
|
|
||||||
|
// Add any spillover from previous clusters at this offset
|
||||||
|
var spilled = spillover.Where(s => s.offset == cluster.Key).Select(s => s.evt).ToList();
|
||||||
|
spillover.RemoveAll(s => s.offset == cluster.Key);
|
||||||
|
obsInCluster.AddRange(spilled);
|
||||||
|
|
||||||
|
if (obsInCluster.Count > 10)
|
||||||
|
{
|
||||||
|
var sorted = obsInCluster
|
||||||
|
.OrderBy(e => GetObservationPriority(e))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var keep = sorted.Take(10).ToList();
|
||||||
|
var overflow = sorted.Skip(10).ToList();
|
||||||
|
|
||||||
|
warnings.Add(
|
||||||
|
$"Offset {cluster.Key}: split {obsInCluster.Count} observations " +
|
||||||
|
$"(moved {overflow.Count} to offset {cluster.Key + 1})");
|
||||||
|
|
||||||
|
foreach (var evt in overflow)
|
||||||
|
spillover.Add((cluster.Key + 1,
|
||||||
|
new ScenarioEvent(cluster.Key + 1, evt.Type, evt.Data, evt.Note)));
|
||||||
|
|
||||||
|
obsInCluster = keep;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.AddRange(obsInCluster);
|
||||||
|
result.AddRange(otherInCluster);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle any remaining spillover
|
||||||
|
foreach (var (offset, evt) in spillover.OrderBy(s => s.offset))
|
||||||
|
result.Add(evt);
|
||||||
|
|
||||||
|
return result.OrderBy(e => e.OffsetMinutes).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetObservationPriority(ScenarioEvent evt)
|
||||||
|
{
|
||||||
|
var code = evt.Data.TryGetProperty("code", out var codeProp)
|
||||||
|
? codeProp.GetString() : null;
|
||||||
|
if (code is null) return 999;
|
||||||
|
var idx = Array.IndexOf(ObservationPriority, code);
|
||||||
|
return idx >= 0 ? idx : 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ScenarioMeta BuildMeta(
|
||||||
|
MimicIcuStay stay, MimicAdmission admission, MimicPatient patient,
|
||||||
|
DateTime scenarioStart, DateTime scenarioEnd,
|
||||||
|
List<ScenarioEvent> events)
|
||||||
|
{
|
||||||
|
var durationMinutes = (int)(scenarioEnd - scenarioStart).TotalMinutes;
|
||||||
|
var obsCount = events.Count(e => e.Type == "observation");
|
||||||
|
var medCount = events.Count(e => e.Type == "medication");
|
||||||
|
|
||||||
|
var description =
|
||||||
|
$"Real de-identified MIMIC-IV data. " +
|
||||||
|
$"Subject {patient.SubjectId}, stay {stay.StayId}. " +
|
||||||
|
$"{(patient.Gender == "F" ? "Female" : "Male")}, age ~{patient.AnchorAge}. " +
|
||||||
|
$"ICU LOS: {stay.Los:F1} days. Care unit: {stay.FirstCareUnit}. " +
|
||||||
|
$"{obsCount} observations, {medCount} medications. " +
|
||||||
|
(admission.HospitalExpireFlag == 1
|
||||||
|
? "Patient expired during hospitalization."
|
||||||
|
: $"Discharged to {admission.DischargeLocation ?? "unknown"}.");
|
||||||
|
|
||||||
|
return new ScenarioMeta(
|
||||||
|
Id: $"mimic-s{stay.StayId}",
|
||||||
|
Name: $"MIMIC-IV — {stay.FirstCareUnit} ({patient.Gender}, ~{patient.AnchorAge}y)",
|
||||||
|
Description: description,
|
||||||
|
DurationMinutes: durationMinutes,
|
||||||
|
Tags: MimicCareUnitMap.GetTags(stay.FirstCareUnit, admission.HospitalExpireFlag));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal? ParseDose(string? doseValRx)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(doseValRx)) return null;
|
||||||
|
|
||||||
|
var val = doseValRx.Trim();
|
||||||
|
var dashIdx = val.IndexOf('-');
|
||||||
|
if (dashIdx > 0) val = val[..dashIdx];
|
||||||
|
|
||||||
|
return decimal.TryParse(val, System.Globalization.NumberStyles.Any,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var d) && d > 0
|
||||||
|
? d
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime RoundToMinute(DateTime dt)
|
||||||
|
=> new(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0);
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
private record RawObservation(
|
||||||
|
DateTime ChartTime, string Code, decimal Value,
|
||||||
|
string Unit, string Source, int Priority);
|
||||||
|
}
|
||||||
@@ -6,5 +6,7 @@ rootCommand.AddCommand(ReplayCommand.Create());
|
|||||||
rootCommand.AddCommand(ReplayAllCommand.Create());
|
rootCommand.AddCommand(ReplayAllCommand.Create());
|
||||||
rootCommand.AddCommand(ValidateCommand.Create());
|
rootCommand.AddCommand(ValidateCommand.Create());
|
||||||
rootCommand.AddCommand(DryRunCommand.Create());
|
rootCommand.AddCommand(DryRunCommand.Create());
|
||||||
|
rootCommand.AddCommand(MimicListCommand.Create());
|
||||||
|
rootCommand.AddCommand(MimicGenerateCommand.Create());
|
||||||
|
|
||||||
return await rootCommand.InvokeAsync(args);
|
return await rootCommand.InvokeAsync(args);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,276 @@
|
|||||||
|
subject_id,hadm_id,admittime,dischtime,deathtime,admission_type,admit_provider_id,admission_location,discharge_location,insurance,language,marital_status,race,edregtime,edouttime,hospital_expire_flag
|
||||||
|
10004235,24181354,2196-02-24 14:38:00,2196-03-04 14:02:00,,URGENT,P03YMR,TRANSFER FROM HOSPITAL,SKILLED NURSING FACILITY,Medicaid,ENGLISH,SINGLE,BLACK/CAPE VERDEAN,2196-02-24 12:15:00,2196-02-24 17:07:00,0
|
||||||
|
10009628,25926192,2153-09-17 17:08:00,2153-09-25 13:20:00,,URGENT,P41R5N,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Medicaid,?,MARRIED,HISPANIC/LATINO - PUERTO RICAN,,,0
|
||||||
|
10018081,23983182,2134-08-18 02:02:00,2134-08-23 19:35:00,,URGENT,P233F6,TRANSFER FROM HOSPITAL,SKILLED NURSING FACILITY,Medicare,ENGLISH,MARRIED,WHITE,2134-08-17 16:24:00,2134-08-18 03:15:00,0
|
||||||
|
10006053,22942076,2111-11-13 23:39:00,2111-11-15 17:20:00,2111-11-15 17:20:00,URGENT,P38TI6,TRANSFER FROM HOSPITAL,DIED,Medicaid,ENGLISH,,UNKNOWN,,,1
|
||||||
|
10031404,21606243,2113-08-04 18:46:00,2113-08-06 20:57:00,,URGENT,P07HDB,TRANSFER FROM HOSPITAL,HOME,Other,ENGLISH,WIDOWED,WHITE,,,0
|
||||||
|
10005817,20626031,2132-12-12 01:43:00,2132-12-20 15:04:00,,URGENT,P41R5N,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Medicare,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10019385,20297618,2180-02-15 20:28:00,2180-02-25 13:45:00,,URGENT,P536JC,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Other,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10002495,24982426,2141-05-22 20:17:00,2141-05-29 17:41:00,,URGENT,P79SJ2,TRANSFER FROM HOSPITAL,SKILLED NURSING FACILITY,Medicare,ENGLISH,MARRIED,UNKNOWN,,,0
|
||||||
|
10038081,20755971,2115-09-27 20:40:00,2115-10-12 00:00:00,2115-10-12 22:20:00,URGENT,P48GIG,TRANSFER FROM HOSPITAL,DIED,Other,?,SINGLE,UNKNOWN,,,1
|
||||||
|
10019917,22585261,2182-01-07 23:25:00,2182-01-10 16:52:00,,URGENT,P3529J,TRANSFER FROM HOSPITAL,HOME,Other,ENGLISH,SINGLE,OTHER,,,0
|
||||||
|
10037861,24256866,2115-10-09 20:28:00,2115-10-18 16:50:00,,URGENT,P64TGC,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Medicare,ENGLISH,MARRIED,UNKNOWN,,,0
|
||||||
|
10026354,24547356,2119-10-26 07:11:00,2119-11-06 12:30:00,,URGENT,P77BSD,TRANSFER FROM HOSPITAL,SKILLED NURSING FACILITY,Other,?,,WHITE,2119-10-26 06:00:00,2119-10-26 06:37:00,0
|
||||||
|
10015860,26352758,2192-09-24 09:19:00,2192-09-28 19:52:00,,URGENT,P54RLA,TRANSFER FROM SKILLED NURSING FACILITY,SKILLED NURSING FACILITY,Other,ENGLISH,SINGLE,WHITE,2192-09-23 22:43:00,2192-09-24 09:38:00,0
|
||||||
|
10020740,23831430,2150-03-11 15:34:00,2150-04-25 13:50:00,,URGENT,P7554I,TRANSFER FROM HOSPITAL,SKILLED NURSING FACILITY,Other,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10012552,27089790,2140-03-22 17:18:00,2140-03-30 14:10:00,,URGENT,P47E1G,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Medicare,ENGLISH,MARRIED,UNKNOWN,,,0
|
||||||
|
10014354,28335091,2147-04-26 16:44:00,2147-04-29 15:30:00,,URGENT,P79SJ2,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10014354,27487226,2148-06-30 01:09:00,2148-07-13 19:35:00,,URGENT,P44WVR,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,2148-06-29 21:06:00,2148-06-30 02:27:00,0
|
||||||
|
10039708,25864431,2142-03-26 06:08:00,2142-04-11 21:00:00,,URGENT,P81KFM,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2142-03-25 22:22:00,2142-03-26 08:08:00,0
|
||||||
|
10000032,22595853,2180-05-06 22:23:00,2180-05-07 17:15:00,,URGENT,P874LG,TRANSFER FROM HOSPITAL,HOME,Other,ENGLISH,WIDOWED,WHITE,2180-05-06 19:17:00,2180-05-06 23:30:00,0
|
||||||
|
10020786,23488445,2189-06-09 12:45:00,2189-06-13 17:20:00,,URGENT,P031HZ,TRANSFER FROM HOSPITAL,HOME,Medicare,ENGLISH,WIDOWED,WHITE,,,0
|
||||||
|
10027445,27488741,2145-12-08 19:47:00,2145-12-19 14:36:00,,URGENT,P38RSS,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Other,ENGLISH,WIDOWED,WHITE,,,0
|
||||||
|
10027445,29163082,2142-08-27 21:05:00,2142-09-05 17:33:00,,URGENT,P509SB,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Other,ENGLISH,WIDOWED,WHITE,,,0
|
||||||
|
10015931,22130791,2177-03-24 21:47:00,2177-03-29 14:15:00,2177-03-29 14:15:00,URGENT,P031HZ,TRANSFER FROM HOSPITAL,DIED,Medicare,ENGLISH,MARRIED,WHITE,,,1
|
||||||
|
10036156,28019404,2157-07-01 04:52:00,2157-07-03 15:08:00,,URGENT,P94V16,TRANSFER FROM HOSPITAL,HOME,Other,ENGLISH,,UNKNOWN,,,0
|
||||||
|
10021312,25020332,2113-08-16 00:32:00,2113-08-18 17:35:00,,URGENT,P31A9M,TRANSFER FROM HOSPITAL,HOME,Other,ENGLISH,,UNKNOWN,,,0
|
||||||
|
10021312,28829452,2113-09-12 14:42:00,2113-09-20 18:40:00,,URGENT,P72G4H,TRANSFER FROM HOSPITAL,ACUTE HOSPITAL,Other,ENGLISH,,UNKNOWN,,,0
|
||||||
|
10003400,23559586,2137-08-04 00:07:00,2137-09-02 17:05:00,2137-09-02 17:05:00,URGENT,P99U21,TRANSFER FROM HOSPITAL,DIED,Medicare,ENGLISH,MARRIED,BLACK/AFRICAN AMERICAN,,,1
|
||||||
|
10003400,20214994,2137-02-24 10:00:00,2137-03-19 15:45:00,,URGENT,P60ZCO,TRANSFER FROM SKILLED NURSING FACILITY,CHRONIC/LONG TERM ACUTE CARE,Medicare,ENGLISH,MARRIED,BLACK/AFRICAN AMERICAN,,,0
|
||||||
|
10023117,24244087,2174-06-07 23:25:00,2174-06-12 15:55:00,,URGENT,P878WT,TRANSFER FROM HOSPITAL,HOME,Medicare,ENGLISH,WIDOWED,WHITE,2174-06-07 16:24:00,2174-06-08 01:02:00,0
|
||||||
|
10038992,22797747,2185-11-02 18:26:00,2185-11-08 16:22:00,,URGENT,P534S8,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Medicare,ENGLISH,MARRIED,UNKNOWN,,,0
|
||||||
|
10037975,27617929,2185-01-17 19:11:00,2185-01-22 14:25:00,2185-01-22 14:25:00,URGENT,P17MK7,TRANSFER FROM HOSPITAL,DIED,Medicare,ENGLISH,MARRIED,UNKNOWN,,,1
|
||||||
|
10035185,22580999,2120-05-12 12:53:00,2120-05-17 16:00:00,,URGENT,P41R5N,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Other,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10002930,28301173,2197-04-08 19:37:00,2197-04-15 12:01:00,,URGENT,P3763P,INTERNAL TRANSFER TO OR FROM PSYCH,HOME,Medicare,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,,,0
|
||||||
|
10002930,22733922,2198-04-22 16:17:00,2198-05-04 13:20:00,,URGENT,P3763P,INTERNAL TRANSFER TO OR FROM PSYCH,HOME,Medicare,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,,,0
|
||||||
|
10007795,28477357,2136-04-10 20:33:00,2136-05-02 16:35:00,,URGENT,P172D4,TRANSFER FROM HOSPITAL,CHRONIC/LONG TERM ACUTE CARE,Medicare,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10007795,25135483,2136-05-04 20:20:00,2136-05-12 17:12:00,,URGENT,P99LA7,TRANSFER FROM HOSPITAL,REHAB,Medicare,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10004733,27411876,2174-12-04 11:28:00,2174-12-27 14:00:00,,URGENT,P48KFD,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Medicaid,ENGLISH,SINGLE,UNKNOWN,,,0
|
||||||
|
10021118,24490144,2161-11-15 20:10:00,2161-11-23 16:00:00,,URGENT,P96FGV,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Other,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10006580,24159665,2137-08-10 11:00:00,2137-08-15 13:25:00,,ELECTIVE,P76K54,PHYSICIAN REFERRAL,HOME,Medicaid,?,MARRIED,HISPANIC/LATINO - SALVADORAN,,,0
|
||||||
|
10023771,20044587,2113-08-25 07:15:00,2113-08-30 14:15:00,,ELECTIVE,P47E1G,PHYSICIAN REFERRAL,SKILLED NURSING FACILITY,Medicare,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10005348,29496232,2128-09-05 08:30:00,2128-09-12 16:55:00,,ELECTIVE,P786Y5,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10019172,24997044,2118-11-15 14:00:00,2118-11-21 18:31:00,,ELECTIVE,P41R5N,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,,UNABLE TO OBTAIN,,,0
|
||||||
|
10019172,21540783,2118-10-08 14:00:00,2118-10-11 20:15:00,,ELECTIVE,P41R5N,PHYSICIAN REFERRAL,HOME,Other,ENGLISH,,UNABLE TO OBTAIN,,,0
|
||||||
|
10021487,20429160,2117-07-16 07:15:00,2117-07-25 12:34:00,,ELECTIVE,P96Y5O,PHYSICIAN REFERRAL,HOME,Other,ENGLISH,DIVORCED,WHITE,,,0
|
||||||
|
10013049,22675517,2114-06-20 10:15:00,2114-06-24 16:45:00,,ELECTIVE,P41R5N,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10035631,20385771,2112-12-04 00:00:00,2112-12-27 16:24:00,,ELECTIVE,P45GUA,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10035631,24912093,2112-10-22 00:00:00,2112-10-28 12:16:00,,ELECTIVE,P45GUA,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,MARRIED,UNKNOWN,,,0
|
||||||
|
10019003,26226543,2155-10-17 18:01:00,2155-11-03 18:00:00,,ELECTIVE,P7280F,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,WIDOWED,WHITE,,,0
|
||||||
|
10019003,28003918,2148-12-21 07:15:00,2148-12-24 17:10:00,,ELECTIVE,P80J89,PHYSICIAN REFERRAL,HOME,Medicare,ENGLISH,WIDOWED,WHITE,,,0
|
||||||
|
10009035,28324362,2161-04-27 07:15:00,2161-05-01 13:45:00,,ELECTIVE,P41R5N,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10040025,25933959,2147-12-29 19:36:00,2148-01-09 17:38:00,,ELECTIVE,P43BTJ,PHYSICIAN REFERRAL,SKILLED NURSING FACILITY,Other,ENGLISH,DIVORCED,WHITE,,,0
|
||||||
|
10014729,23300884,2125-03-19 16:58:00,2125-03-28 13:37:00,,EW EMER.,P76K54,EMERGENCY ROOM,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE - OTHER EUROPEAN,2125-03-19 12:36:00,2125-03-19 18:45:00,0
|
||||||
|
10026255,20437651,2200-09-17 22:53:00,2200-09-29 18:25:00,,EW EMER.,P233F6,EMERGENCY ROOM,HOME,Other,ENGLISH,MARRIED,WHITE,2200-09-17 18:38:00,2200-09-18 00:57:00,0
|
||||||
|
10026255,22059910,2201-07-07 18:15:00,2201-07-13 23:27:00,2201-07-13 23:27:00,EW EMER.,P48KFD,EMERGENCY ROOM,DIED,Other,ENGLISH,MARRIED,WHITE,2201-07-07 12:31:00,2201-07-07 19:40:00,1
|
||||||
|
10007058,22954658,2167-11-07 19:05:00,2167-11-11 14:23:00,,EW EMER.,P76K54,EMERGENCY ROOM,HOME,Other,ENGLISH,MARRIED,WHITE,2167-11-07 17:57:00,2167-11-07 20:22:00,0
|
||||||
|
10018845,21101111,2184-10-08 02:28:00,2184-10-11 17:00:00,,EW EMER.,P34SFE,EMERGENCY ROOM,REHAB,Other,ENGLISH,MARRIED,WHITE,2184-10-07 22:35:00,2184-10-08 04:09:00,0
|
||||||
|
10020640,27984218,2153-02-13 00:22:00,2153-02-20 13:52:00,,EW EMER.,P7554I,EMERGENCY ROOM,SKILLED NURSING FACILITY,Other,ENGLISH,WIDOWED,WHITE,2153-02-12 21:59:00,2153-02-13 01:38:00,0
|
||||||
|
10018081,25973915,2134-09-06 15:57:00,2134-09-18 15:50:00,,EW EMER.,P13ZRJ,EMERGENCY ROOM,CHRONIC/LONG TERM ACUTE CARE,Medicare,ENGLISH,MARRIED,WHITE,2134-09-05 16:14:00,2134-09-06 16:49:00,0
|
||||||
|
10018081,21027282,2133-12-18 16:58:00,2134-01-12 11:00:00,,EW EMER.,P19SWB,EMERGENCY ROOM,SKILLED NURSING FACILITY,Medicare,ENGLISH,MARRIED,WHITE,2133-12-18 12:09:00,2133-12-18 17:10:00,0
|
||||||
|
10021666,22756440,2172-03-12 23:47:00,2172-03-23 15:40:00,,EW EMER.,P99698,EMERGENCY ROOM,CHRONIC/LONG TERM ACUTE CARE,Medicare,ENGLISH,MARRIED,WHITE,2172-03-12 20:52:00,2172-03-13 01:46:00,0
|
||||||
|
10015272,27993466,2137-06-12 18:36:00,2137-06-18 15:45:00,,EW EMER.,P3529J,EMERGENCY ROOM,HOME HEALTH CARE,Medicare,ENGLISH,,WHITE,2137-06-12 16:54:00,2137-06-12 20:37:00,0
|
||||||
|
10016810,20973395,2185-06-16 01:31:00,2185-06-21 15:55:00,,EW EMER.,P3638G,EMERGENCY ROOM,HOME,Other,ENGLISH,,UNKNOWN,2185-06-15 23:08:00,2185-06-16 02:16:00,0
|
||||||
|
10021938,23112364,2181-10-13 01:48:00,2181-10-14 17:40:00,,EW EMER.,P3529J,EMERGENCY ROOM,HOME,Medicare,ENGLISH,SINGLE,WHITE,2181-10-12 20:17:00,2181-10-13 02:52:00,0
|
||||||
|
10021938,27154822,2181-10-25 10:44:00,2181-10-27 15:30:00,,EW EMER.,P99BGS,EMERGENCY ROOM,HOME,Medicare,ENGLISH,SINGLE,WHITE,2181-10-25 09:23:00,2181-10-25 11:35:00,0
|
||||||
|
10001725,25563031,2110-04-11 15:08:00,2110-04-14 15:00:00,,EW EMER.,P35SU0,PACU,HOME,Other,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10001217,24597018,2157-11-18 22:56:00,2157-11-25 18:00:00,,EW EMER.,P4645A,EMERGENCY ROOM,HOME HEALTH CARE,Other,?,MARRIED,WHITE,2157-11-18 17:38:00,2157-11-19 01:24:00,0
|
||||||
|
10023239,29295881,2137-06-19 17:35:00,2137-06-22 14:57:00,,EW EMER.,P44WVR,EMERGENCY ROOM,HOME,Other,ENGLISH,SINGLE,WHITE,2137-06-19 15:05:00,2137-06-19 19:09:00,0
|
||||||
|
10023239,21759936,2140-10-03 09:06:00,2140-10-08 15:28:00,,EW EMER.,P031HZ,TRANSFER FROM HOSPITAL,HOME,Other,ENGLISH,MARRIED,WHITE,2140-10-03 06:20:00,2140-10-03 11:04:00,0
|
||||||
|
10020944,29974575,2131-02-27 15:34:00,2131-03-13 17:01:00,,EW EMER.,P27588,EMERGENCY ROOM,SKILLED NURSING FACILITY,Medicare,ENGLISH,,UNKNOWN,2131-02-27 13:16:00,2131-02-27 16:40:00,0
|
||||||
|
10027602,28166872,2201-10-30 12:05:00,2201-11-20 14:45:00,,EW EMER.,P47SIK,EMERGENCY ROOM,REHAB,Other,ENGLISH,SINGLE,WHITE,2201-10-30 10:48:00,2201-10-30 12:25:00,0
|
||||||
|
10032725,25177949,2143-02-17 14:20:00,2143-03-16 17:15:00,,EW EMER.,P39043,EMERGENCY ROOM,CHRONIC/LONG TERM ACUTE CARE,Other,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2143-02-17 07:19:00,2143-02-17 15:18:00,0
|
||||||
|
10032725,20611640,2143-03-22 04:59:00,2143-03-25 13:00:00,,EW EMER.,P7554I,EMERGENCY ROOM,HOSPICE,Other,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2143-03-22 01:19:00,2143-03-22 06:42:00,0
|
||||||
|
10005348,25239799,2130-10-26 17:03:00,2130-11-02 16:00:00,,EW EMER.,P38RSS,PROCEDURE SITE,SKILLED NURSING FACILITY,Other,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10037861,24540843,2117-03-14 16:34:00,2117-03-24 00:01:00,2117-03-24 23:08:00,EW EMER.,P01LRQ,EMERGENCY ROOM,DIED,Medicare,ENGLISH,MARRIED,UNKNOWN,2117-03-14 15:19:00,2117-03-14 17:53:00,1
|
||||||
|
10010471,29842315,2155-12-02 19:36:00,2155-12-07 15:30:00,2155-12-07 15:30:00,EW EMER.,P04X8Y,EMERGENCY ROOM,DIED,Medicare,ENGLISH,WIDOWED,WHITE,2155-12-02 16:00:00,2155-12-02 20:33:00,1
|
||||||
|
10005866,27167814,2148-03-10 16:16:00,2148-03-21 18:30:00,,EW EMER.,P28OI9,EMERGENCY ROOM,SKILLED NURSING FACILITY,Medicaid,ENGLISH,SINGLE,PORTUGUESE,2148-03-10 04:46:00,2148-03-10 11:27:00,0
|
||||||
|
10005866,26134779,2149-09-13 07:36:00,2149-09-19 18:00:00,,EW EMER.,P95QHH,EMERGENCY ROOM,HOME,Medicaid,ENGLISH,SINGLE,PORTUGUESE,2149-09-12 15:31:00,2149-09-13 09:02:00,0
|
||||||
|
10005866,26158160,2146-06-06 00:50:00,2146-06-09 16:45:00,,EW EMER.,P94RT6,EMERGENCY ROOM,HOME,Medicaid,ENGLISH,SINGLE,PORTUGUESE,2146-06-05 22:26:00,2146-06-06 01:45:00,0
|
||||||
|
10024043,24717014,2117-04-11 20:46:00,2117-04-16 18:55:00,,EW EMER.,P5487F,EMERGENCY ROOM,HOME HEALTH CARE,Medicare,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2117-04-11 17:23:00,2117-04-11 22:05:00,0
|
||||||
|
10021487,28998349,2116-12-03 00:23:00,2116-12-28 13:19:00,,EW EMER.,P61T4I,EMERGENCY ROOM,HOME,Other,ENGLISH,MARRIED,WHITE,2116-12-02 22:57:00,2116-12-03 01:02:00,0
|
||||||
|
10021487,21928381,2117-12-03 17:07:00,2117-12-06 17:30:00,,EW EMER.,P96Y5O,PROCEDURE SITE,HOME,Other,ENGLISH,DIVORCED,WHITE,,,0
|
||||||
|
10021487,26321862,2117-01-28 00:19:00,2117-02-05 15:40:00,,EW EMER.,P82B0E,EMERGENCY ROOM,HOME,Other,ENGLISH,MARRIED,WHITE,2117-01-27 18:19:00,2117-01-28 01:23:00,0
|
||||||
|
10021487,27112038,2117-10-25 22:22:00,2117-10-29 14:40:00,,EW EMER.,P233F6,EMERGENCY ROOM,HOME,Other,ENGLISH,DIVORCED,WHITE,2117-10-25 15:49:00,2117-10-25 22:53:00,0
|
||||||
|
10015860,24698912,2192-05-12 07:42:00,2192-05-27 18:50:00,,EW EMER.,P03YMR,PHYSICIAN REFERRAL,SKILLED NURSING FACILITY,Other,ENGLISH,SINGLE,WHITE,2192-05-11 16:26:00,2192-05-12 09:31:00,0
|
||||||
|
10015860,28236161,2187-09-15 18:49:00,2187-09-19 14:50:00,,EW EMER.,P62A9I,EMERGENCY ROOM,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,2187-09-15 14:14:00,2187-09-15 20:49:00,0
|
||||||
|
10015860,20790339,2189-05-23 02:14:00,2189-05-25 14:45:00,,EW EMER.,P00P3O,EMERGENCY ROOM,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,2189-05-22 23:18:00,2189-05-23 03:38:00,0
|
||||||
|
10015860,25085565,2186-09-15 16:12:00,2186-09-29 18:05:00,,EW EMER.,P56SP9,EMERGENCY ROOM,REHAB,Other,ENGLISH,SINGLE,WHITE,2186-09-15 12:56:00,2186-09-15 17:15:00,0
|
||||||
|
10015860,20854119,2188-08-06 00:49:00,2188-08-12 17:49:00,,EW EMER.,P29CGZ,EMERGENCY ROOM,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,2188-08-05 20:36:00,2188-08-06 03:24:00,0
|
||||||
|
10018423,29366372,2167-05-03 21:24:00,2167-05-11 12:57:00,,EW EMER.,P204LI,EMERGENCY ROOM,HOME HEALTH CARE,Other,ENGLISH,MARRIED,WHITE,2167-05-03 15:18:00,2167-05-03 22:50:00,0
|
||||||
|
10007928,20338077,2129-04-05 22:56:00,2129-04-11 17:25:00,,EW EMER.,P825SO,EMERGENCY ROOM,HOME,Other,ENGLISH,DIVORCED,WHITE,2129-04-05 21:18:00,2129-04-06 00:25:00,0
|
||||||
|
10014078,25809882,2166-08-21 23:09:00,2166-08-26 14:48:00,,EW EMER.,P64JK6,EMERGENCY ROOM,HOME HEALTH CARE,Medicaid,ENGLISH,,UNABLE TO OBTAIN,2166-08-21 21:39:00,2166-08-22 00:36:00,0
|
||||||
|
10020740,25826145,2150-06-03 20:12:00,2150-06-07 15:05:00,,EW EMER.,P80QIV,PACU,SKILLED NURSING FACILITY,Other,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10008454,20291550,2110-11-30 06:31:00,2110-12-10 15:53:00,,EW EMER.,P77BSD,EMERGENCY ROOM,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,2110-11-30 04:45:00,2110-11-30 08:03:00,0
|
||||||
|
10022281,29642388,2125-06-17 04:11:00,2125-06-19 15:25:00,,EW EMER.,P76K54,EMERGENCY ROOM,HOME,Other,ENGLISH,MARRIED,OTHER,2125-06-16 20:32:00,2125-06-17 05:14:00,0
|
||||||
|
10014354,24357615,2150-05-09 16:09:00,2150-05-10 15:59:00,,EW EMER.,P15PLY,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,2150-05-08 22:30:00,2150-05-09 19:09:00,0
|
||||||
|
10014354,22741225,2146-10-08 23:47:00,2146-10-12 18:20:00,,EW EMER.,P5487F,EMERGENCY ROOM,HOME HEALTH CARE,Other,ENGLISH,MARRIED,WHITE,2146-10-08 21:27:00,2146-10-09 01:08:00,0
|
||||||
|
10014354,26013492,2147-11-14 22:12:00,2147-11-16 15:00:00,,EW EMER.,P424IO,EMERGENCY ROOM,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,2147-11-14 18:22:00,2147-11-14 23:09:00,0
|
||||||
|
10014354,29757856,2150-04-10 02:40:00,2150-04-15 18:00:00,,EW EMER.,P08904,WALK-IN/SELF REFERRAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,2150-04-09 19:50:00,2150-04-10 03:46:00,0
|
||||||
|
10038933,25129047,2148-09-10 12:09:00,2148-09-23 12:18:00,,EW EMER.,P47KFL,EMERGENCY ROOM,REHAB,Other,ENGLISH,SINGLE,WHITE,2148-09-10 09:23:00,2148-09-10 13:19:00,0
|
||||||
|
10016150,29374560,2142-05-10 15:05:00,2142-05-15 19:49:00,,EW EMER.,P1037P,PROCEDURE SITE,REHAB,Medicare,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10022041,28909879,2187-05-18 17:08:00,2187-05-23 17:20:00,,EW EMER.,P09IS0,EMERGENCY ROOM,HOME HEALTH CARE,Medicare,ENGLISH,MARRIED,OTHER,2187-05-18 14:59:00,2187-05-18 18:39:00,0
|
||||||
|
10039708,20572787,2138-10-30 23:30:00,2138-11-06 23:30:00,,EW EMER.,P39SWK,EMERGENCY ROOM,AGAINST ADVICE,Other,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2138-10-30 17:27:00,2138-10-31 00:19:00,0
|
||||||
|
10039708,28258130,2140-01-23 16:19:00,2140-02-26 18:15:00,,EW EMER.,P44WVR,EMERGENCY ROOM,CHRONIC/LONG TERM ACUTE CARE,Other,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2140-01-23 12:35:00,2140-01-23 18:08:00,0
|
||||||
|
10000032,22841357,2180-06-26 18:27:00,2180-06-27 18:49:00,,EW EMER.,P09Q6Y,EMERGENCY ROOM,HOME,Medicaid,ENGLISH,WIDOWED,WHITE,2180-06-26 15:54:00,2180-06-26 21:31:00,0
|
||||||
|
10000032,25742920,2180-08-05 23:44:00,2180-08-07 17:50:00,,EW EMER.,P60CC5,EMERGENCY ROOM,HOSPICE,Medicaid,ENGLISH,WIDOWED,WHITE,2180-08-05 20:58:00,2180-08-06 01:44:00,0
|
||||||
|
10000032,29079034,2180-07-23 12:35:00,2180-07-25 17:55:00,,EW EMER.,P30KEH,EMERGENCY ROOM,HOME,Medicaid,ENGLISH,WIDOWED,WHITE,2180-07-23 05:54:00,2180-07-23 14:00:00,0
|
||||||
|
10026406,25260176,2129-01-03 15:55:00,2129-01-05 14:10:00,,EW EMER.,P51MA2,EMERGENCY ROOM,AGAINST ADVICE,Other,ENGLISH,DIVORCED,WHITE,2129-01-02 23:41:00,2129-01-03 18:33:00,0
|
||||||
|
10037928,22326517,2177-12-21 20:47:00,2177-12-23 17:45:00,,EW EMER.,P51MA2,EMERGENCY ROOM,HOME HEALTH CARE,Medicare,?,WIDOWED,HISPANIC/LATINO - CUBAN,2177-12-21 16:49:00,2177-12-21 22:26:00,0
|
||||||
|
10037928,22490490,2177-07-14 16:55:00,2177-07-24 13:33:00,,EW EMER.,P44WVR,EMERGENCY ROOM,HOME HEALTH CARE,Medicare,?,WIDOWED,HISPANIC/LATINO - CUBAN,2177-07-14 14:52:00,2177-07-14 20:38:00,0
|
||||||
|
10037928,20192635,2177-09-04 12:05:00,2177-09-07 16:10:00,,EW EMER.,P072C5,EMERGENCY ROOM,HOME HEALTH CARE,Medicare,?,WIDOWED,HISPANIC/LATINO - CUBAN,2177-09-04 06:29:00,2177-09-04 14:44:00,0
|
||||||
|
10037928,29802992,2179-07-25 00:06:00,2179-07-28 15:54:00,,EW EMER.,P450WM,EMERGENCY ROOM,HOME HEALTH CARE,Medicare,?,WIDOWED,HISPANIC/LATINO - CUBAN,2179-07-24 18:21:00,2179-07-25 01:17:00,0
|
||||||
|
10037928,23721604,2179-03-27 18:27:00,2179-04-04 19:40:00,,EW EMER.,P80GYA,EMERGENCY ROOM,HOME HEALTH CARE,Medicare,?,WIDOWED,HISPANIC/LATINO - CUBAN,2179-03-27 14:15:00,2179-03-27 19:48:00,0
|
||||||
|
10037928,24225421,2178-09-28 23:05:00,2178-10-02 17:13:00,,EW EMER.,P30AMB,EMERGENCY ROOM,HOME,Medicare,?,WIDOWED,HISPANIC/LATINO - CUBAN,2178-09-28 20:29:00,2178-09-29 00:43:00,0
|
||||||
|
10037928,24656677,2178-12-21 05:30:00,2178-12-26 18:35:00,,EW EMER.,P30SZF,EMERGENCY ROOM,HOME HEALTH CARE,Medicare,?,WIDOWED,HISPANIC/LATINO - CUBAN,2178-12-21 03:17:00,2178-12-21 08:27:00,0
|
||||||
|
10029484,20764029,2160-11-08 04:16:00,2160-11-11 11:40:00,,EW EMER.,P38TI6,EMERGENCY ROOM,HOME,Other,ENGLISH,SINGLE,WHITE,2160-11-07 19:15:00,2160-11-08 05:23:00,0
|
||||||
|
10027445,26275841,2142-07-31 00:32:00,2142-08-09 17:30:00,,EW EMER.,P3529J,EMERGENCY ROOM,HOME HEALTH CARE,Other,ENGLISH,WIDOWED,WHITE,2142-07-30 23:33:00,2142-07-31 01:41:00,0
|
||||||
|
10012853,22539296,2176-06-06 18:09:00,2176-06-08 18:30:00,,EW EMER.,P172D4,EMERGENCY ROOM,HOME,Medicare,ENGLISH,WIDOWED,BLACK/AFRICAN AMERICAN,2176-06-06 13:33:00,2176-06-06 20:18:00,0
|
||||||
|
10012853,26369609,2175-04-05 15:36:00,2175-04-10 16:55:00,,EW EMER.,P623U2,EMERGENCY ROOM,HOME,Other,ENGLISH,WIDOWED,BLACK/AFRICAN AMERICAN,2175-04-05 06:22:00,2175-04-05 17:10:00,0
|
||||||
|
10012853,27882036,2176-11-25 21:28:00,2176-12-03 15:24:00,,EW EMER.,P450WM,EMERGENCY ROOM,SKILLED NURSING FACILITY,Medicare,ENGLISH,WIDOWED,BLACK/AFRICAN AMERICAN,2176-11-25 18:34:00,2176-11-25 23:51:00,0
|
||||||
|
10019003,29279905,2153-03-27 23:25:00,2153-04-07 16:20:00,,EW EMER.,P60F78,EMERGENCY ROOM,HOME HEALTH CARE,Other,ENGLISH,WIDOWED,WHITE,2153-03-27 21:28:00,2153-03-28 02:21:00,0
|
||||||
|
10019003,21457723,2155-07-10 17:48:00,2155-07-18 16:59:00,,EW EMER.,P44WVR,TRANSFER FROM HOSPITAL,HOME,Other,ENGLISH,WIDOWED,WHITE,2155-07-10 12:46:00,2155-07-10 19:03:00,0
|
||||||
|
10019003,27525946,2153-04-12 19:07:00,2153-04-20 17:09:00,,EW EMER.,P56B4N,EMERGENCY ROOM,HOME,Other,ENGLISH,WIDOWED,WHITE,2153-04-12 13:03:00,2153-04-12 21:40:00,0
|
||||||
|
10019003,26703331,2155-06-10 23:09:00,2155-06-15 16:30:00,,EW EMER.,P203BW,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Other,ENGLISH,WIDOWED,WHITE,2155-06-10 20:37:00,2155-06-11 01:53:00,0
|
||||||
|
10019777,27738145,2187-02-10 18:57:00,2187-02-27 13:22:00,,EW EMER.,P08BV8,EMERGENCY ROOM,HOSPICE,Other,ENGLISH,DIVORCED,WHITE,2187-02-10 15:11:00,2187-02-10 20:34:00,0
|
||||||
|
10018501,28479513,2141-07-30 22:34:00,2141-08-05 18:06:00,,EW EMER.,P99698,EMERGENCY ROOM,SKILLED NURSING FACILITY,Medicare,ENGLISH,,WHITE,2141-07-30 18:53:00,2141-07-31 00:01:00,0
|
||||||
|
10004422,21255400,2111-01-15 14:55:00,2111-01-25 15:00:00,,EW EMER.,P1037P,PROCEDURE SITE,HOME HEALTH CARE,Medicare,ENGLISH,WIDOWED,WHITE,,,0
|
||||||
|
10003400,27296885,2136-12-31 21:40:00,2137-01-03 17:05:00,,EW EMER.,P14622,EMERGENCY ROOM,SKILLED NURSING FACILITY,Medicare,ENGLISH,MARRIED,BLACK/AFRICAN AMERICAN,2136-12-31 13:41:00,2137-01-01 00:35:00,0
|
||||||
|
10003400,29483621,2136-11-04 20:43:00,2136-11-12 17:40:00,,EW EMER.,P37QNF,EMERGENCY ROOM,SKILLED NURSING FACILITY,Medicare,ENGLISH,MARRIED,BLACK/AFRICAN AMERICAN,2136-11-04 16:08:00,2136-11-04 22:12:00,0
|
||||||
|
10003400,22390287,2137-02-07 19:42:00,2137-02-18 18:30:00,,EW EMER.,P42DF5,EMERGENCY ROOM,SKILLED NURSING FACILITY,Medicare,ENGLISH,MARRIED,BLACK/AFRICAN AMERICAN,2137-02-07 13:06:00,2137-02-07 21:44:00,0
|
||||||
|
10003400,26090619,2134-06-06 02:25:00,2134-06-07 15:05:00,,EW EMER.,P77SO2,EMERGENCY ROOM,HOME HEALTH CARE,Medicare,ENGLISH,MARRIED,BLACK/AFRICAN AMERICAN,2134-06-05 21:42:00,2134-06-06 03:44:00,0
|
||||||
|
10003400,26467376,2136-12-09 14:44:00,2136-12-15 16:00:00,,EW EMER.,P14622,EMERGENCY ROOM,SKILLED NURSING FACILITY,Medicare,ENGLISH,MARRIED,BLACK/AFRICAN AMERICAN,2136-12-09 13:16:00,2136-12-09 18:45:00,0
|
||||||
|
10023117,28872262,2171-11-07 21:37:00,2171-11-22 15:30:00,,EW EMER.,P89ZCW,EMERGENCY ROOM,HOME HEALTH CARE,Medicare,ENGLISH,WIDOWED,WHITE,2171-11-07 17:40:00,2171-11-07 22:50:00,0
|
||||||
|
10023117,29858644,2173-04-16 22:15:00,2173-04-20 16:40:00,,EW EMER.,P23URY,EMERGENCY ROOM,HOME,Medicare,ENGLISH,WIDOWED,WHITE,2173-04-16 17:00:00,2173-04-16 23:18:00,0
|
||||||
|
10017492,27417763,2116-06-26 18:25:00,2116-07-05 08:05:00,2116-07-05 08:05:00,EW EMER.,P3417E,EMERGENCY ROOM,DIED,Medicaid,?,SINGLE,PATIENT DECLINED TO ANSWER,2116-06-26 14:29:00,2116-06-26 21:46:00,1
|
||||||
|
10017492,27672872,2114-03-19 20:05:00,2114-04-02 18:30:00,,EW EMER.,P68RKH,EMERGENCY ROOM,HOME HEALTH CARE,Other,?,SINGLE,PATIENT DECLINED TO ANSWER,2114-03-19 15:57:00,2114-03-19 21:38:00,0
|
||||||
|
10038999,29026789,2132-05-17 23:32:00,2132-05-23 13:01:00,,EW EMER.,P64JOR,TRANSFER FROM HOSPITAL,REHAB,Medicare,ENGLISH,SINGLE,WHITE,2132-05-17 19:56:00,2132-05-18 01:36:00,0
|
||||||
|
10040025,25172300,2145-07-03 23:46:00,2145-07-05 13:57:00,,EW EMER.,P73Y1N,EMERGENCY ROOM,HOME,Other,ENGLISH,DIVORCED,WHITE,2145-07-03 20:02:00,2145-07-04 10:58:00,0
|
||||||
|
10040025,25384176,2145-10-19 23:48:00,2145-10-24 14:20:00,,EW EMER.,P1904G,EMERGENCY ROOM,HOME,Other,ENGLISH,DIVORCED,WHITE,2145-10-19 19:23:00,2145-10-20 00:54:00,0
|
||||||
|
10040025,27876215,2147-11-09 08:02:00,2147-11-14 18:53:00,,EW EMER.,P059D0,EMERGENCY ROOM,SKILLED NURSING FACILITY,Other,ENGLISH,DIVORCED,WHITE,2147-11-09 04:17:00,2147-11-09 10:56:00,0
|
||||||
|
10040025,27553957,2145-07-24 19:00:00,2145-07-31 14:12:00,,EW EMER.,P38RSS,EMERGENCY ROOM,HOME,Other,ENGLISH,DIVORCED,WHITE,2145-07-24 11:30:00,2145-07-24 20:32:00,0
|
||||||
|
10009049,22995465,2174-05-26 08:21:00,2174-05-31 14:15:00,,EW EMER.,P3529J,EMERGENCY ROOM,HOME,Other,ENGLISH,MARRIED,WHITE,2174-05-26 04:20:00,2174-05-26 09:18:00,0
|
||||||
|
10016742,29281842,2178-07-03 21:13:00,2178-07-08 20:20:00,,EW EMER.,P031HZ,EMERGENCY ROOM,SKILLED NURSING FACILITY,Medicaid,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2178-07-03 17:39:00,2178-07-03 22:45:00,0
|
||||||
|
10004457,28723315,2141-08-12 16:02:00,2141-08-13 17:47:00,,EW EMER.,P46WR5,EMERGENCY ROOM,HOME,Medicare,ENGLISH,SINGLE,WHITE,2141-08-12 12:08:00,2141-08-12 17:20:00,0
|
||||||
|
10029291,22205327,2123-02-20 01:59:00,2123-03-10 15:30:00,,EW EMER.,P44UQI,EMERGENCY ROOM,HOME HEALTH CARE,Medicare,ENGLISH,DIVORCED,WHITE,2123-02-19 23:57:00,2123-02-20 04:13:00,0
|
||||||
|
10004720,22081550,2186-11-12 18:01:00,2186-11-17 18:30:00,2186-11-17 18:30:00,EW EMER.,P3529J,INFORMATION NOT AVAILABLE,DIED,Medicare,ENGLISH,SINGLE,WHITE,2186-11-12 16:09:00,2186-11-12 19:55:00,1
|
||||||
|
10019568,28710730,2120-01-30 21:07:00,2120-02-02 15:40:00,,EW EMER.,P80LEI,EMERGENCY ROOM,HOME,Medicare,ENGLISH,SINGLE,WHITE,2120-01-30 19:44:00,2120-01-30 22:51:00,0
|
||||||
|
10002930,25922998,2198-04-17 19:38:00,2198-04-22 16:02:00,,EW EMER.,P30KEH,EMERGENCY ROOM,PSYCH FACILITY,Medicare,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2198-04-17 11:42:00,2198-04-17 21:24:00,0
|
||||||
|
10002930,25696644,2196-04-14 12:25:00,2196-04-17 15:28:00,,EW EMER.,P43A1R,EMERGENCY ROOM,PSYCH FACILITY,Medicare,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2196-04-14 05:55:00,2196-04-14 13:40:00,0
|
||||||
|
10007795,22051341,2136-09-22 20:51:00,2136-09-24 14:20:00,,EW EMER.,P99LA7,EMERGENCY ROOM,HOME,Medicare,ENGLISH,SINGLE,WHITE,2136-09-22 17:07:00,2136-09-22 22:30:00,0
|
||||||
|
10007795,20285402,2136-08-04 22:16:00,2136-08-11 19:20:00,,EW EMER.,P74QAJ,EMERGENCY ROOM,HOME HEALTH CARE,Medicare,ENGLISH,SINGLE,WHITE,2136-08-04 16:26:00,2136-08-05 00:17:00,0
|
||||||
|
10010867,22429197,2147-12-30 08:40:00,2148-01-11 17:55:00,,EW EMER.,P3417E,EMERGENCY ROOM,REHAB,Other,ENGLISH,SINGLE,WHITE - BRAZILIAN,2147-12-30 06:45:00,2147-12-30 09:33:00,0
|
||||||
|
10025463,24470193,2137-10-08 21:20:00,2137-10-09 15:30:00,2137-10-09 15:30:00,EW EMER.,P01LRQ,EMERGENCY ROOM,DIED,Other,ENGLISH,MARRIED,WHITE,2137-10-08 18:16:00,2137-10-08 20:44:00,1
|
||||||
|
10002428,28662225,2156-04-12 14:16:00,2156-04-29 16:26:00,,EW EMER.,P64TOH,EMERGENCY ROOM,SKILLED NURSING FACILITY,Medicare,ENGLISH,WIDOWED,WHITE,2156-04-12 09:56:00,2156-04-12 17:11:00,0
|
||||||
|
10002428,20321825,2156-04-30 20:35:00,2156-05-03 16:36:00,,EW EMER.,P825SO,EMERGENCY ROOM,CHRONIC/LONG TERM ACUTE CARE,Medicare,ENGLISH,WIDOWED,WHITE,2156-04-30 18:30:00,2156-04-30 21:53:00,0
|
||||||
|
10002428,23473524,2156-05-11 14:49:00,2156-05-22 14:16:00,,EW EMER.,P3529J,EMERGENCY ROOM,CHRONIC/LONG TERM ACUTE CARE,Medicare,ENGLISH,WIDOWED,WHITE,2156-05-11 11:29:00,2156-05-11 16:53:00,0
|
||||||
|
10007818,22987108,2146-06-10 16:37:00,2146-07-12 00:00:00,2146-07-12 20:50:00,DIRECT EMER.,P48GIG,PHYSICIAN REFERRAL,DIED,Medicare,ENGLISH,MARRIED,WHITE,,,1
|
||||||
|
10004235,22187210,2196-06-20 21:11:00,2196-06-22 13:30:00,,DIRECT EMER.,P98TAU,PHYSICIAN REFERRAL,HOME HEALTH CARE,Medicaid,ENGLISH,SINGLE,BLACK/CAPE VERDEAN,,,0
|
||||||
|
10001217,27703517,2157-12-18 16:58:00,2157-12-24 14:55:00,,DIRECT EMER.,P99698,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,?,MARRIED,WHITE,,,0
|
||||||
|
10021487,27660781,2117-03-03 15:59:00,2117-03-27 16:40:00,,DIRECT EMER.,P82B0E,CLINIC REFERRAL,HOME,Other,ENGLISH,DIVORCED,WHITE,,,0
|
||||||
|
10015860,28613200,2188-03-29 14:14:00,2188-04-02 16:30:00,,DIRECT EMER.,P62A9I,CLINIC REFERRAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10031757,28477280,2137-10-12 22:43:00,2137-10-24 17:30:00,,DIRECT EMER.,P94RT6,CLINIC REFERRAL,HOSPICE,Other,ENGLISH,DIVORCED,WHITE,,,0
|
||||||
|
10014354,27562275,2148-07-18 02:31:00,2148-07-20 17:47:00,,DIRECT EMER.,P90LIK,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,2148-07-17 20:36:00,2148-07-18 04:46:00,0
|
||||||
|
10035631,27496788,2113-08-26 17:07:00,2113-08-29 15:18:00,,DIRECT EMER.,P45GUA,CLINIC REFERRAL,HOME,Other,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10035631,29462354,2112-09-17 19:13:00,2112-10-17 01:41:00,,DIRECT EMER.,P45GUA,PHYSICIAN REFERRAL,HOME,Other,ENGLISH,MARRIED,UNKNOWN,2112-09-17 18:46:00,2112-09-17 19:50:00,0
|
||||||
|
10035631,22732862,2112-11-10 15:55:00,2112-11-20 16:20:00,,DIRECT EMER.,P11CH5,CLINIC REFERRAL,HOME,Other,ENGLISH,MARRIED,UNKNOWN,,,0
|
||||||
|
10035631,21599196,2116-02-13 11:17:00,2116-02-15 17:09:00,,DIRECT EMER.,P15Q8N,PHYSICIAN REFERRAL,HOME,Other,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10019003,26529390,2155-05-17 00:02:00,2155-05-19 18:27:00,,DIRECT EMER.,P73PF1,PHYSICIAN REFERRAL,HOME,Other,ENGLISH,WIDOWED,WHITE,2155-05-17 21:03:00,2155-05-18 03:03:00,0
|
||||||
|
10023117,28887654,2174-12-16 13:25:00,2174-12-20 10:27:00,,DIRECT EMER.,P94CCF,PHYSICIAN REFERRAL,HOME,Medicare,ENGLISH,WIDOWED,WHITE,,,0
|
||||||
|
10002930,23688993,2193-08-05 11:45:00,2193-08-11 09:25:00,,DIRECT EMER.,P356OC,PHYSICIAN REFERRAL,HOME,Medicare,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2193-08-05 05:04:00,2193-08-05 12:00:00,0
|
||||||
|
10025463,27327816,2136-10-31 07:15:00,2136-11-02 16:50:00,,DIRECT EMER.,P38W5P,PHYSICIAN REFERRAL,HOME,Other,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10023239,28984460,2140-10-22 22:04:00,2140-10-23 17:01:00,,EU OBSERVATION,P76VMW,PHYSICIAN REFERRAL,,Other,ENGLISH,MARRIED,WHITE,2140-10-22 18:05:00,2140-10-23 17:01:00,0
|
||||||
|
10024043,25561728,2117-02-03 20:10:00,2117-02-04 11:53:00,,EU OBSERVATION,P19QFH,EMERGENCY ROOM,,Medicare,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2117-02-03 17:13:00,2117-02-04 11:53:00,0
|
||||||
|
10015860,27670224,2187-12-14 15:48:00,2187-12-16 14:38:00,,EU OBSERVATION,P25XDB,EMERGENCY ROOM,,Other,ENGLISH,SINGLE,WHITE,2187-12-14 12:49:00,2187-12-14 17:48:00,0
|
||||||
|
10015860,22607171,2190-05-15 08:16:00,2190-05-16 15:44:00,,EU OBSERVATION,P73Y1N,EMERGENCY ROOM,,Other,ENGLISH,SINGLE,WHITE,2190-05-14 17:11:00,2190-05-15 10:00:00,0
|
||||||
|
10015860,22416954,2193-05-03 22:45:00,2193-05-05 02:13:00,,EU OBSERVATION,P21E4S,TRANSFER FROM HOSPITAL,,Other,ENGLISH,SINGLE,WHITE,2193-05-03 19:12:00,2193-05-05 02:13:00,0
|
||||||
|
10020740,23143086,2151-01-15 15:25:00,2151-01-16 02:38:00,,EU OBSERVATION,P607HY,EMERGENCY ROOM,,Other,ENGLISH,SINGLE,WHITE,2151-01-15 11:47:00,2151-01-16 02:38:00,0
|
||||||
|
10020740,23199774,2150-09-15 14:09:00,2150-09-15 17:09:00,,EU OBSERVATION,P30KYN,EMERGENCY ROOM,,Other,ENGLISH,SINGLE,WHITE,2150-09-15 09:31:00,2150-09-15 17:09:00,0
|
||||||
|
10014354,24096336,2149-06-19 22:54:00,2149-06-20 13:48:00,,EU OBSERVATION,P83K3D,PHYSICIAN REFERRAL,,Other,ENGLISH,SINGLE,WHITE,2149-06-19 17:01:00,2149-06-20 13:48:00,0
|
||||||
|
10014354,26722126,2146-11-09 01:53:00,2146-11-09 13:13:00,,EU OBSERVATION,P187JB,EMERGENCY ROOM,,Other,ENGLISH,MARRIED,WHITE,2146-11-08 21:24:00,2146-11-09 13:13:00,0
|
||||||
|
10014354,20900955,2149-03-04 23:14:00,2149-03-05 14:59:00,,EU OBSERVATION,P63AD6,WALK-IN/SELF REFERRAL,,Other,ENGLISH,SINGLE,WHITE,2149-03-04 20:24:00,2149-03-05 14:59:00,0
|
||||||
|
10014354,22502504,2147-09-12 05:06:00,2147-09-12 19:00:00,,EU OBSERVATION,P04X8Y,EMERGENCY ROOM,,Other,ENGLISH,SINGLE,WHITE,2147-09-12 02:01:00,2147-09-12 06:37:00,0
|
||||||
|
10014354,26173805,2149-09-17 22:54:00,2149-09-18 10:45:00,,EU OBSERVATION,P57NY9,WALK-IN/SELF REFERRAL,,Other,ENGLISH,SINGLE,WHITE,2149-09-17 09:08:00,2149-09-17 23:55:00,0
|
||||||
|
10014354,27494880,2147-06-04 00:42:00,2147-06-04 15:00:00,,EU OBSERVATION,P79SJ2,EMERGENCY ROOM,,Other,ENGLISH,SINGLE,WHITE,2147-06-03 22:39:00,2147-06-04 09:54:00,0
|
||||||
|
10039708,27504040,2142-07-06 09:08:00,2142-07-07 16:40:00,,EU OBSERVATION,P94RT6,PHYSICIAN REFERRAL,,Other,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2142-07-06 07:21:00,2142-07-06 08:27:00,0
|
||||||
|
10026406,25166559,2133-03-01 19:30:00,2133-03-04 17:05:00,,EU OBSERVATION,P42K8Q,WALK-IN/SELF REFERRAL,,Other,ENGLISH,DIVORCED,WHITE,2133-03-01 16:42:00,2133-03-04 17:05:00,0
|
||||||
|
10037928,22228639,2183-08-04 04:04:00,2183-08-04 16:07:00,,EU OBSERVATION,P850UN,PHYSICIAN REFERRAL,,Medicare,?,WIDOWED,HISPANIC/LATINO - CUBAN,2183-08-03 19:27:00,2183-08-04 16:07:00,0
|
||||||
|
10037928,28543425,2175-10-26 01:31:00,2175-10-26 12:48:00,,EU OBSERVATION,P40IUG,EMERGENCY ROOM,,Medicare,?,SINGLE,HISPANIC OR LATINO,2175-10-25 19:55:00,2175-10-26 12:48:00,0
|
||||||
|
10012853,22896692,2176-08-11 15:17:00,2176-08-11 17:35:00,,EU OBSERVATION,P76VMW,EMERGENCY ROOM,,Medicare,ENGLISH,WIDOWED,BLACK/AFRICAN AMERICAN,2176-08-11 08:36:00,2176-08-11 17:35:00,0
|
||||||
|
10039997,21390688,2135-11-07 02:42:00,2135-11-07 06:27:00,,EU OBSERVATION,P03N1K,EMERGENCY ROOM,,Medicare,ENGLISH,WIDOWED,BLACK/AFRICAN AMERICAN,2135-11-06 21:44:00,2135-11-07 06:27:00,0
|
||||||
|
10020306,26332470,2133-05-05 05:43:00,2133-05-05 15:37:00,,EU OBSERVATION,P45LON,CLINIC REFERRAL,,Medicare,ENGLISH,WIDOWED,BLACK/AFRICAN AMERICAN,2133-05-04 20:52:00,2133-05-05 15:37:00,0
|
||||||
|
10002930,25282382,2197-04-17 02:01:00,2197-04-17 09:48:00,,EU OBSERVATION,P33NK4,EMERGENCY ROOM,,Medicare,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2197-04-16 22:57:00,2197-04-17 09:48:00,0
|
||||||
|
10002930,28697806,2200-06-05 05:43:00,2200-06-05 10:26:00,,EU OBSERVATION,P68U3G,PHYSICIAN REFERRAL,,Medicare,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2200-06-04 16:38:00,2200-06-05 10:26:00,0
|
||||||
|
10002930,20846853,2201-02-12 16:58:00,2201-02-13 11:11:00,,EU OBSERVATION,P2358X,PHYSICIAN REFERRAL,,Medicare,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2201-02-12 15:11:00,2201-02-13 11:11:00,0
|
||||||
|
10002930,22380825,2193-08-05 06:18:00,2193-08-05 11:44:00,,EU OBSERVATION,P38XXV,EMERGENCY ROOM,,Medicare,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2193-08-05 05:04:00,2193-08-05 12:00:00,0
|
||||||
|
10002930,23720373,2199-02-17 21:45:00,2199-02-19 13:38:00,,EU OBSERVATION,P59HPG,EMERGENCY ROOM,,Medicare,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2199-02-17 14:32:00,2199-02-19 13:38:00,0
|
||||||
|
10002930,20282368,2201-03-23 19:15:00,2201-03-26 14:24:00,,EU OBSERVATION,P850UN,PHYSICIAN REFERRAL,,Medicare,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2201-03-23 12:04:00,2201-03-26 14:24:00,0
|
||||||
|
10002930,28477649,2197-04-07 06:56:00,2197-04-08 19:37:00,,EU OBSERVATION,P11RCJ,EMERGENCY ROOM,,Medicare,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2197-04-07 03:20:00,2197-04-08 20:14:00,0
|
||||||
|
10002428,28676446,2157-07-16 04:09:00,2157-07-18 16:49:00,,EU OBSERVATION,P08S8F,EMERGENCY ROOM,,Medicare,ENGLISH,WIDOWED,WHITE,2157-07-16 01:50:00,2157-07-16 09:51:00,0
|
||||||
|
10002428,25797028,2155-07-14 19:15:00,2155-07-15 18:37:00,,EU OBSERVATION,P64TOH,EMERGENCY ROOM,,Medicare,ENGLISH,WIDOWED,WHITE,2155-07-14 16:58:00,2155-07-14 20:04:00,0
|
||||||
|
10002428,26549334,2160-07-15 23:37:00,2160-07-16 18:49:00,,EU OBSERVATION,P607HY,EMERGENCY ROOM,,Medicare,ENGLISH,WIDOWED,WHITE,2160-07-15 17:34:00,2160-07-16 18:49:00,0
|
||||||
|
10018328,26706939,2154-02-05 21:58:00,2154-02-09 15:15:00,,OBSERVATION ADMIT,P233F6,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Other,ENGLISH,DIVORCED,WHITE,2154-02-05 17:09:00,2154-02-05 22:54:00,0
|
||||||
|
10005817,28661809,2135-01-03 21:54:00,2135-01-19 18:36:00,2135-01-19 18:36:00,OBSERVATION ADMIT,P7554I,TRANSFER FROM HOSPITAL,DIED,Medicare,ENGLISH,MARRIED,WHITE,,,1
|
||||||
|
10019385,20611796,2180-03-04 01:16:00,2180-03-06 18:32:00,,OBSERVATION ADMIT,P95P90,EMERGENCY ROOM,HOME HEALTH CARE,Other,ENGLISH,MARRIED,WHITE,2180-03-03 20:26:00,2180-03-04 02:24:00,0
|
||||||
|
10020187,24104168,2169-01-15 04:04:00,2169-01-24 17:20:00,,OBSERVATION ADMIT,P898NM,EMERGENCY ROOM,HOME HEALTH CARE,Other,?,MARRIED,HISPANIC/LATINO - SALVADORAN,2169-01-14 23:26:00,2169-01-15 04:56:00,0
|
||||||
|
10010471,21322534,2155-05-08 17:05:00,2155-05-10 18:55:00,,OBSERVATION ADMIT,P89T1L,EMERGENCY ROOM,HOME,Medicare,ENGLISH,WIDOWED,WHITE,2155-05-08 13:48:00,2155-05-08 18:16:00,0
|
||||||
|
10005866,22589518,2149-02-11 21:49:00,2149-02-14 16:45:00,,OBSERVATION ADMIT,P874LG,EMERGENCY ROOM,HOME,Medicaid,ENGLISH,SINGLE,PORTUGUESE,2149-02-11 08:52:00,2149-02-11 23:09:00,0
|
||||||
|
10005866,21636229,2149-09-20 14:30:00,2149-09-26 15:05:00,,OBSERVATION ADMIT,P96Y5O,EMERGENCY ROOM,HOME,Medicaid,ENGLISH,SINGLE,PORTUGUESE,2149-09-20 05:50:00,2149-09-20 15:53:00,0
|
||||||
|
10005866,23514107,2149-06-20 19:27:00,2149-06-25 15:55:00,,OBSERVATION ADMIT,P28OI9,EMERGENCY ROOM,HOME,Medicaid,ENGLISH,SINGLE,PORTUGUESE,2149-06-20 10:20:00,2149-06-20 20:59:00,0
|
||||||
|
10005866,20364112,2149-10-01 18:56:00,2149-10-25 18:50:00,,OBSERVATION ADMIT,P94RT6,PHYSICIAN REFERRAL,SKILLED NURSING FACILITY,Medicaid,ENGLISH,SINGLE,PORTUGUESE,2149-10-01 02:07:00,2149-10-01 15:59:00,0
|
||||||
|
10015860,25103777,2192-07-31 16:05:00,2192-08-06 16:37:00,,OBSERVATION ADMIT,P104SU,TRANSFER FROM SKILLED NURSING FACILITY,SKILLED NURSING FACILITY,Other,ENGLISH,SINGLE,WHITE,2192-07-31 11:00:00,2192-07-31 17:10:00,0
|
||||||
|
10015860,22413744,2191-01-15 01:55:00,2191-01-30 17:09:00,,OBSERVATION ADMIT,P172D4,EMERGENCY ROOM,SKILLED NURSING FACILITY,Other,ENGLISH,SINGLE,WHITE,2191-01-14 21:18:00,2191-01-15 03:20:00,0
|
||||||
|
10015860,28196804,2193-11-23 19:15:00,2193-11-27 21:58:00,,OBSERVATION ADMIT,P54UVQ,WALK-IN/SELF REFERRAL,SKILLED NURSING FACILITY,Other,ENGLISH,SINGLE,WHITE,2193-11-23 12:38:00,2193-11-23 20:59:00,0
|
||||||
|
10014354,22508257,2148-05-10 23:29:00,2148-05-20 14:30:00,,OBSERVATION ADMIT,P57B0F,EMERGENCY ROOM,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,2148-05-10 20:25:00,2148-05-11 01:22:00,0
|
||||||
|
10014354,29600294,2148-08-14 22:57:00,2148-08-18 21:12:00,,OBSERVATION ADMIT,P723L9,PHYSICIAN REFERRAL,AGAINST ADVICE,Other,ENGLISH,SINGLE,WHITE,2148-08-14 16:32:00,2148-08-15 00:27:00,0
|
||||||
|
10014354,26228185,2150-04-30 20:19:00,2150-05-07 14:10:00,,OBSERVATION ADMIT,P17YHV,TRANSFER FROM HOSPITAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,2150-04-30 14:26:00,2150-04-30 21:54:00,0
|
||||||
|
10014354,24980601,2150-02-04 20:12:00,2150-02-08 14:10:00,,OBSERVATION ADMIT,P42VJP,PHYSICIAN REFERRAL,AGAINST ADVICE,Other,ENGLISH,SINGLE,WHITE,2150-02-04 14:50:00,2150-02-04 22:44:00,0
|
||||||
|
10014354,26486158,2148-08-22 15:18:00,2148-09-08 12:00:00,,OBSERVATION ADMIT,P17YHV,CLINIC REFERRAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10014354,29780751,2147-11-26 00:39:00,2147-11-30 16:54:00,,OBSERVATION ADMIT,P21X87,EMERGENCY ROOM,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,2147-11-25 19:39:00,2147-11-26 01:30:00,0
|
||||||
|
10014354,23132022,2148-06-24 15:22:00,2148-06-28 13:54:00,,OBSERVATION ADMIT,P259GB,PHYSICIAN REFERRAL,HOME,Other,ENGLISH,SINGLE,WHITE,2148-06-24 09:22:00,2148-06-24 16:12:00,0
|
||||||
|
10005909,20199380,2144-10-28 23:20:00,2144-11-02 15:23:00,,OBSERVATION ADMIT,P43BTJ,EMERGENCY ROOM,HOME,Other,ENGLISH,MARRIED,WHITE,2144-10-28 18:29:00,2144-10-29 00:10:00,0
|
||||||
|
10039708,26793610,2140-09-25 04:17:00,2140-09-26 17:40:00,,OBSERVATION ADMIT,P91LM4,EMERGENCY ROOM,HOME,Other,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2140-09-24 21:17:00,2140-09-25 05:57:00,0
|
||||||
|
10039708,29488258,2144-01-19 12:07:00,2144-01-21 21:20:00,,OBSERVATION ADMIT,P8785Z,WALK-IN/SELF REFERRAL,HOME,Other,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2144-01-18 11:53:00,2144-01-19 13:59:00,0
|
||||||
|
10039708,24928679,2143-09-19 18:36:00,2143-09-22 23:00:00,,OBSERVATION ADMIT,P8785Z,CLINIC REFERRAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2143-09-19 10:57:00,2143-09-19 20:57:00,0
|
||||||
|
10039708,23819016,2140-06-18 00:22:00,2140-06-22 17:40:00,,OBSERVATION ADMIT,P48KFD,EMERGENCY ROOM,HOME,Other,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2140-06-17 17:47:00,2140-06-18 01:41:00,0
|
||||||
|
10039708,20093566,2143-09-26 18:24:00,2143-09-30 20:00:00,,OBSERVATION ADMIT,P04ZFH,CLINIC REFERRAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2143-09-26 08:16:00,2143-09-26 19:52:00,0
|
||||||
|
10037928,24885579,2182-04-29 04:29:00,2182-05-03 17:20:00,,OBSERVATION ADMIT,P3417E,WALK-IN/SELF REFERRAL,HOME,Medicare,?,WIDOWED,HISPANIC/LATINO - CUBAN,2182-04-28 17:25:00,2182-04-29 07:49:00,0
|
||||||
|
10035631,21476294,2115-11-08 13:54:00,2115-12-08 17:31:00,,OBSERVATION ADMIT,P45GUA,EMERGENCY ROOM,HOME,Other,ENGLISH,MARRIED,WHITE,2115-11-08 12:02:00,2115-11-08 15:43:00,0
|
||||||
|
10035631,29276678,2116-02-27 20:55:00,2116-03-12 07:45:00,2116-03-12 07:45:00,OBSERVATION ADMIT,P17YHV,EMERGENCY ROOM,DIED,Other,ENGLISH,MARRIED,WHITE,2116-02-27 15:33:00,2116-02-27 22:03:00,1
|
||||||
|
10015931,24420677,2176-12-16 23:31:00,2176-12-31 17:35:00,,OBSERVATION ADMIT,P49NLZ,EMERGENCY ROOM,HOME HEALTH CARE,Medicare,ENGLISH,MARRIED,WHITE,2176-12-16 18:24:00,2176-12-17 01:07:00,0
|
||||||
|
10015931,28157142,2176-11-14 18:02:00,2176-11-27 13:30:00,,OBSERVATION ADMIT,P20N5X,EMERGENCY ROOM,SKILLED NURSING FACILITY,Medicare,ENGLISH,MARRIED,WHITE,2176-11-14 02:27:00,2176-11-14 19:51:00,0
|
||||||
|
10019003,25508812,2155-05-22 21:46:00,2155-05-30 03:30:00,,OBSERVATION ADMIT,P88A32,PHYSICIAN REFERRAL,HOME,Other,ENGLISH,WIDOWED,WHITE,,,0
|
||||||
|
10039997,22484749,2137-03-15 22:08:00,2137-03-18 16:34:00,,OBSERVATION ADMIT,P70N8P,PHYSICIAN REFERRAL,REHAB,Medicare,ENGLISH,WIDOWED,BLACK/AFRICAN AMERICAN,2137-03-15 15:27:00,2137-03-16 00:11:00,0
|
||||||
|
10023117,21607814,2175-07-06 15:57:00,2175-07-20 00:00:00,2175-07-20 22:50:00,OBSERVATION ADMIT,P49NLZ,EMERGENCY ROOM,DIED,Medicare,ENGLISH,WIDOWED,WHITE,2175-07-06 14:02:00,2175-07-06 17:41:00,1
|
||||||
|
10023117,21133938,2175-03-20 23:29:00,2175-03-29 16:00:00,,OBSERVATION ADMIT,P66Z67,TRANSFER FROM HOSPITAL,HOME,Medicare,ENGLISH,WIDOWED,WHITE,,,0
|
||||||
|
10020306,23052851,2135-01-15 20:55:00,2135-02-07 17:50:00,,OBSERVATION ADMIT,P54UVQ,PHYSICIAN REFERRAL,SKILLED NURSING FACILITY,Other,ENGLISH,WIDOWED,BLACK/AFRICAN AMERICAN,2135-01-15 16:12:00,2135-01-15 22:38:00,0
|
||||||
|
10038999,27189241,2131-05-22 21:49:00,2131-06-04 13:43:00,,OBSERVATION ADMIT,P44UQI,EMERGENCY ROOM,HOME,Medicare,ENGLISH,SINGLE,WHITE,2131-05-22 20:33:00,2131-05-22 21:28:00,0
|
||||||
|
10040025,21791856,2147-06-16 22:00:00,2147-06-22 16:15:00,,OBSERVATION ADMIT,P7634X,EMERGENCY ROOM,HOME HEALTH CARE,Other,ENGLISH,DIVORCED,WHITE,2147-06-16 18:58:00,2147-06-16 23:06:00,0
|
||||||
|
10040025,22251969,2147-08-03 02:58:00,2147-08-06 16:50:00,,OBSERVATION ADMIT,P85BWS,EMERGENCY ROOM,HOME,Other,ENGLISH,DIVORCED,WHITE,2147-08-02 18:14:00,2147-08-03 02:41:00,0
|
||||||
|
10040025,27259207,2147-12-04 20:48:00,2147-12-18 16:43:00,,OBSERVATION ADMIT,P43BTJ,EMERGENCY ROOM,SKILLED NURSING FACILITY,Other,ENGLISH,DIVORCED,WHITE,2147-12-04 13:11:00,2147-12-05 02:28:00,0
|
||||||
|
10040025,27996267,2148-01-23 12:18:00,2148-02-04 20:51:00,,OBSERVATION ADMIT,P10WWR,TRANSFER FROM SKILLED NURSING FACILITY,HOSPICE,Other,ENGLISH,DIVORCED,WHITE,2148-01-22 14:47:00,2148-01-23 09:42:00,0
|
||||||
|
10016742,28506150,2178-07-13 05:40:00,2178-07-16 02:00:00,,OBSERVATION ADMIT,P031HZ,EMERGENCY ROOM,SKILLED NURSING FACILITY,Medicaid,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2178-07-13 04:37:00,2178-07-13 08:16:00,0
|
||||||
|
10016742,27568122,2178-07-22 07:19:00,2178-07-25 16:30:00,,OBSERVATION ADMIT,P513EK,EMERGENCY ROOM,CHRONIC/LONG TERM ACUTE CARE,Medicaid,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2178-07-22 05:41:00,2178-07-22 08:19:00,0
|
||||||
|
10010867,22196214,2148-03-07 23:30:00,2148-03-13 15:50:00,,OBSERVATION ADMIT,P233F6,EMERGENCY ROOM,REHAB,Other,ENGLISH,DIVORCED,WHITE - BRAZILIAN,2148-03-07 18:21:00,2148-03-08 00:50:00,0
|
||||||
|
10010867,22950920,2148-01-25 22:58:00,2148-01-30 11:23:00,,OBSERVATION ADMIT,P39FGY,EMERGENCY ROOM,REHAB,Other,ENGLISH,DIVORCED,WHITE - BRAZILIAN,2148-01-25 18:46:00,2148-01-26 01:27:00,0
|
||||||
|
10002428,28295257,2160-04-14 12:30:00,2160-04-18 16:00:00,,OBSERVATION ADMIT,P18I28,EMERGENCY ROOM,SKILLED NURSING FACILITY,Medicare,ENGLISH,WIDOWED,WHITE,2160-04-14 09:01:00,2160-04-14 14:28:00,0
|
||||||
|
10005348,29176490,2129-05-22 16:00:00,2129-05-23 11:30:00,,DIRECT OBSERVATION,P132L1,PHYSICIAN REFERRAL,,Other,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10020740,29820177,2150-07-09 22:09:00,2150-07-12 18:00:00,,DIRECT OBSERVATION,P1942H,CLINIC REFERRAL,,Other,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10039708,22999601,2142-05-15 17:14:00,2142-05-15 18:21:00,,DIRECT OBSERVATION,P42K8Q,PHYSICIAN REFERRAL,,Other,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,2142-05-15 10:47:00,2142-05-15 18:21:00,0
|
||||||
|
10012853,20457729,2177-11-03 09:30:00,2177-11-04 15:06:00,,DIRECT OBSERVATION,P05BRR,PHYSICIAN REFERRAL,,Medicare,ENGLISH,WIDOWED,BLACK/AFRICAN AMERICAN,,,0
|
||||||
|
10012853,26739864,2177-02-11 07:15:00,2177-02-12 16:20:00,,DIRECT OBSERVATION,P05BRR,PHYSICIAN REFERRAL,,Medicare,ENGLISH,WIDOWED,BLACK/AFRICAN AMERICAN,,,0
|
||||||
|
10004457,25559382,2148-09-14 14:19:00,2148-09-15 12:45:00,,DIRECT OBSERVATION,P466EI,PHYSICIAN REFERRAL,,Medicare,ENGLISH,DIVORCED,WHITE,,,0
|
||||||
|
10007795,27962747,2136-07-24 17:15:00,2136-07-27 14:27:00,,DIRECT OBSERVATION,P99LA7,PHYSICIAN REFERRAL,,Medicare,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10035631,29654498,2113-07-17 17:15:00,2113-07-18 14:55:00,,AMBULATORY OBSERVATION,P48P6U,PACU,,Other,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10020306,28778757,2129-10-29 08:00:00,2129-10-30 13:20:00,,AMBULATORY OBSERVATION,P75JMU,PACU,,Medicare,ENGLISH,SINGLE,BLACK/AFRICAN AMERICAN,,,0
|
||||||
|
10040025,27125816,2143-03-18 12:34:00,2143-03-19 12:00:00,,AMBULATORY OBSERVATION,P623U2,PROCEDURE SITE,,Other,ENGLISH,DIVORCED,WHITE,,,0
|
||||||
|
10004457,21039249,2140-09-17 13:41:00,2140-09-18 12:40:00,,AMBULATORY OBSERVATION,P99YFT,PROCEDURE SITE,,Medicare,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10004457,21216581,2143-03-09 11:10:00,2143-03-10 11:35:00,,AMBULATORY OBSERVATION,P89ZCW,PROCEDURE SITE,,Medicare,ENGLISH,DIVORCED,WHITE,,,0
|
||||||
|
10011398,27505812,2146-12-15 07:15:00,2146-12-19 13:37:00,,SURGICAL SAME DAY ADMISSION,P47E1G,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,MARRIED,HISPANIC OR LATINO,,,0
|
||||||
|
10014729,28889419,2125-02-27 07:15:00,2125-03-06 14:25:00,,SURGICAL SAME DAY ADMISSION,P17BJ5,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE - OTHER EUROPEAN,,,0
|
||||||
|
10004235,25970245,2196-06-14 08:30:00,2196-06-19 14:54:00,,SURGICAL SAME DAY ADMISSION,P96Y5O,PHYSICIAN REFERRAL,HOME HEALTH CARE,Medicaid,ENGLISH,SINGLE,BLACK/CAPE VERDEAN,,,0
|
||||||
|
10039831,26924951,2115-12-28 07:15:00,2116-01-02 14:34:00,,SURGICAL SAME DAY ADMISSION,P82B0E,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,MARRIED,UNABLE TO OBTAIN,,,0
|
||||||
|
10018328,23786647,2154-04-24 03:15:00,2154-05-03 14:00:00,,SURGICAL SAME DAY ADMISSION,P898NM,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,DIVORCED,WHITE,,,0
|
||||||
|
10018081,28861356,2134-08-02 07:15:00,2134-08-13 17:34:00,,SURGICAL SAME DAY ADMISSION,P3417E,PHYSICIAN REFERRAL,SKILLED NURSING FACILITY,Medicare,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10027602,21477991,2201-12-11 12:00:00,2201-12-17 13:45:00,,SURGICAL SAME DAY ADMISSION,P898NM,PHYSICIAN REFERRAL,REHAB,Other,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10020187,26842957,2170-02-24 00:00:00,2170-02-25 15:00:00,,SURGICAL SAME DAY ADMISSION,P898NM,PHYSICIAN REFERRAL,HOME,Other,?,MARRIED,HISPANIC/LATINO - SALVADORAN,,,0
|
||||||
|
10003046,26048429,2154-01-02 07:15:00,2154-01-09 11:53:00,,SURGICAL SAME DAY ADMISSION,P2720E,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10022017,22342963,2189-09-10 00:00:00,2189-09-16 15:00:00,,SURGICAL SAME DAY ADMISSION,P47E1G,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10025612,23403708,2125-09-25 07:15:00,2125-10-03 12:24:00,,SURGICAL SAME DAY ADMISSION,P786Y5,PHYSICIAN REFERRAL,HOME HEALTH CARE,Medicare,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10039997,24294180,2134-09-07 12:00:00,2134-09-11 13:10:00,,SURGICAL SAME DAY ADMISSION,P47SIK,PHYSICIAN REFERRAL,HOME,Medicare,ENGLISH,MARRIED,BLACK/AFRICAN AMERICAN,,,0
|
||||||
|
10023117,29839885,2170-10-08 07:15:00,2170-10-09 16:30:00,,SURGICAL SAME DAY ADMISSION,P89ZCW,PHYSICIAN REFERRAL,HOME HEALTH CARE,Medicare,ENGLISH,WIDOWED,WHITE,,,0
|
||||||
|
10038992,24745425,2187-07-29 01:05:00,2187-08-03 17:02:00,,SURGICAL SAME DAY ADMISSION,P41R5N,PHYSICIAN REFERRAL,SKILLED NURSING FACILITY,Medicare,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10008287,22168393,2145-09-28 01:17:00,2145-10-02 13:35:00,,SURGICAL SAME DAY ADMISSION,P898NM,PHYSICIAN REFERRAL,HOME HEALTH CARE,Other,ENGLISH,SINGLE,WHITE,,,0
|
||||||
|
10022880,27708593,2177-03-12 07:15:00,2177-03-19 14:25:00,,SURGICAL SAME DAY ADMISSION,P99698,PHYSICIAN REFERRAL,HOME,Medicare,ENGLISH,MARRIED,WHITE,,,0
|
||||||
|
10004457,23251352,2141-12-17 11:00:00,2141-12-21 15:56:00,,SURGICAL SAME DAY ADMISSION,P41R5N,PHYSICIAN REFERRAL,REHAB,Medicare,ENGLISH,SINGLE,OTHER,,,0
|
||||||
|
10004457,28108313,2147-12-19 00:00:00,2147-12-21 16:10:00,,SURGICAL SAME DAY ADMISSION,P10WWR,PHYSICIAN REFERRAL,SKILLED NURSING FACILITY,Medicare,ENGLISH,DIVORCED,WHITE,,,0
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+35836
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,141 @@
|
|||||||
|
subject_id,hadm_id,stay_id,first_careunit,last_careunit,intime,outtime,los
|
||||||
|
10018328,23786647,31269608,Neuro Stepdown,Neuro Stepdown,2154-04-24 23:03:44,2154-05-02 15:55:21,7.7025115740740739
|
||||||
|
10020187,24104168,37509585,Neuro Surgical Intensive Care Unit (Neuro SICU),Neuro Stepdown,2169-01-15 04:56:00,2169-01-20 15:47:50,5.4526620370370367
|
||||||
|
10020187,26842957,32554129,Neuro Intermediate,Neuro Intermediate,2170-02-24 18:18:46,2170-02-25 15:15:26,0.87268518518518523
|
||||||
|
10012853,27882036,31338022,Trauma SICU (TSICU),Trauma SICU (TSICU),2176-11-26 02:34:49,2176-11-29 20:58:54,3.766724537037037
|
||||||
|
10020740,25826145,32145159,Trauma SICU (TSICU),Trauma SICU (TSICU),2150-06-03 20:12:32,2150-06-04 21:05:58,1.0371064814814817
|
||||||
|
10039708,23819016,38559363,Trauma SICU (TSICU),Trauma SICU (TSICU),2140-06-18 01:41:00,2140-06-19 21:47:16,1.8376851851851852
|
||||||
|
10020306,23052851,38540883,Trauma SICU (TSICU),Trauma SICU (TSICU),2135-01-21 17:01:57,2135-01-24 21:47:45,3.1984722222222222
|
||||||
|
10019568,28710730,30876334,Trauma SICU (TSICU),Trauma SICU (TSICU),2120-01-30 22:51:00,2120-01-31 18:25:12,0.81541666666666668
|
||||||
|
10018081,28861356,38333427,Trauma SICU (TSICU),Trauma SICU (TSICU),2134-08-05 14:53:33,2134-08-07 17:32:43,2.1105324074074074
|
||||||
|
10018081,21027282,37293400,Trauma SICU (TSICU),Trauma SICU (TSICU),2133-12-18 17:10:00,2134-01-01 14:44:53,13.899224537037037
|
||||||
|
10010867,22429197,39880770,Trauma SICU (TSICU),Trauma SICU (TSICU),2147-12-30 09:33:00,2148-01-08 18:14:21,9.3620486111111116
|
||||||
|
10021487,28998349,38197705,Trauma SICU (TSICU),Trauma SICU (TSICU),2116-12-03 01:02:00,2116-12-18 17:34:03,15.688923611111109
|
||||||
|
10003046,26048429,35514836,Trauma SICU (TSICU),Trauma SICU (TSICU),2154-01-02 15:57:15,2154-01-04 15:19:56,1.9740856481481481
|
||||||
|
10017492,27417763,36035031,Trauma SICU (TSICU),Trauma SICU (TSICU),2116-06-27 17:35:34,2116-06-27 20:26:18,0.1185648148148148
|
||||||
|
10017492,27417763,39543480,Trauma SICU (TSICU),Trauma SICU (TSICU),2116-06-26 20:35:09,2116-06-27 15:44:27,0.79812499999999986
|
||||||
|
10018501,28479513,35446858,Trauma SICU (TSICU),Trauma SICU (TSICU),2141-07-31 00:01:00,2141-08-01 22:36:21,1.9412152777777776
|
||||||
|
10008454,20291550,31959184,Trauma SICU (TSICU),Trauma SICU (TSICU),2110-11-30 17:11:36,2110-12-05 16:48:24,4.983888888888889
|
||||||
|
10026354,24547356,36091287,Trauma SICU (TSICU),Trauma SICU (TSICU),2119-10-26 08:33:32,2119-10-27 17:50:50,1.387013888888889
|
||||||
|
10029291,22205327,36059427,Coronary Care Unit (CCU),Coronary Care Unit (CCU),2123-02-20 04:13:00,2123-02-26 12:03:56,6.3270370370370363
|
||||||
|
10027445,29163082,36084484,Coronary Care Unit (CCU),Coronary Care Unit (CCU),2142-08-30 13:20:56,2142-08-31 18:19:36,1.2074074074074075
|
||||||
|
10010471,29842315,32119961,Coronary Care Unit (CCU),Coronary Care Unit (CCU),2155-12-02 20:33:00,2155-12-07 18:19:18,4.9071527777777781
|
||||||
|
10023117,21607814,30955999,Coronary Care Unit (CCU),Coronary Care Unit (CCU),2175-07-06 17:41:00,2175-07-19 22:50:08,13.214675925925926
|
||||||
|
10002495,24982426,36753294,Coronary Care Unit (CCU),Coronary Care Unit (CCU),2141-05-22 20:18:01,2141-05-27 22:24:02,5.0875115740740737
|
||||||
|
10031404,21606243,35544374,Coronary Care Unit (CCU),Coronary Care Unit (CCU),2113-08-04 18:47:42,2113-08-05 23:45:02,1.2064814814814815
|
||||||
|
10017492,27417763,36871784,Coronary Care Unit (CCU),Coronary Care Unit (CCU),2116-07-05 06:50:16,2116-07-05 13:06:59,0.26160879629629624
|
||||||
|
10026255,22059910,31248398,Coronary Care Unit (CCU),Coronary Care Unit (CCU),2201-07-07 19:40:00,2201-07-08 15:43:15,0.83559027777777783
|
||||||
|
10015931,28157142,39544395,Coronary Care Unit (CCU),Coronary Care Unit (CCU),2176-11-19 14:24:52,2176-11-20 19:18:13,1.2037152777777778
|
||||||
|
10038999,27189241,39711498,Coronary Care Unit (CCU),Coronary Care Unit (CCU),2131-05-22 21:50:33,2131-05-31 17:55:04,8.8364699074074071
|
||||||
|
10029291,22205327,35146796,Coronary Care Unit (CCU),Coronary Care Unit (CCU),2123-02-26 12:12:32,2123-03-04 23:36:14,6.4747916666666674
|
||||||
|
10023117,21133938,38554095,Medical Intensive Care Unit (MICU),Coronary Care Unit (CCU),2175-03-21 03:20:53,2175-03-27 17:52:59,6.605625
|
||||||
|
10023117,28872262,30057454,Cardiac Vascular Intensive Care Unit (CVICU),Coronary Care Unit (CCU),2171-11-14 10:06:41,2171-11-18 20:49:43,4.4465509259259264
|
||||||
|
10014354,27487226,38017367,Neuro Surgical Intensive Care Unit (Neuro SICU),Coronary Care Unit (CCU),2148-07-07 15:48:09,2148-07-10 18:25:51,3.1095138888888889
|
||||||
|
10003400,23559586,38383343,Coronary Care Unit (CCU),Medical Intensive Care Unit (MICU),2137-08-17 17:36:37,2137-09-02 19:17:11,16.069837962962961
|
||||||
|
10004235,24181354,34100191,Coronary Care Unit (CCU),Medical Intensive Care Unit (MICU),2196-02-24 17:07:00,2196-02-29 15:58:02,4.9521064814814819
|
||||||
|
10002930,25696644,37049133,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2196-04-14 13:40:00,2196-04-15 16:54:44,1.1352314814814815
|
||||||
|
10002428,20321825,34807493,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2156-04-30 21:53:00,2156-05-02 22:27:20,2.0238425925925925
|
||||||
|
10007928,20338077,35128235,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2129-04-06 00:25:00,2129-04-08 21:02:55,2.8596643518518516
|
||||||
|
10020944,29974575,30757476,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2131-02-27 16:40:00,2131-03-08 18:30:38,9.0768287037037041
|
||||||
|
10002428,28662225,38875437,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2156-04-19 18:11:19,2156-04-26 18:58:41,7.0328935185185193
|
||||||
|
10026406,25260176,30864406,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2129-01-05 02:37:19,2129-01-05 14:11:03,0.48175925925925928
|
||||||
|
10020740,23831430,35889503,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2150-03-25 18:22:56,2150-03-28 22:20:47,3.1651736111111113
|
||||||
|
10036156,28019404,38587181,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2157-07-01 04:52:37,2157-07-02 14:18:55,1.3932638888888889
|
||||||
|
10006053,22942076,34617352,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2111-11-14 00:19:12,2111-11-15 18:21:10,1.7513657407407408
|
||||||
|
10006053,22942076,32895909,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2111-11-13 23:40:00,2111-11-14 00:14:10,0.02372685185185185
|
||||||
|
10000032,29079034,39553978,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2180-07-23 14:00:00,2180-07-23 23:50:47,0.4102662037037037
|
||||||
|
10019777,27738145,34578020,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2187-02-10 20:34:00,2187-02-17 23:38:57,7.1284375
|
||||||
|
10021312,28829452,37507305,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2113-09-14 10:19:17,2113-09-15 12:43:00,1.0998032407407408
|
||||||
|
10007818,22987108,32359580,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2146-06-22 11:46:29,2146-07-13 00:27:47,20.528680555555557
|
||||||
|
10020740,23831430,31077365,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2150-03-30 07:57:10,2150-04-04 10:58:43,5.1260763888888894
|
||||||
|
10021938,23112364,39492446,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2181-10-13 02:52:00,2181-10-14 18:03:28,1.6329629629629629
|
||||||
|
10016742,28506150,32314488,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2178-07-13 08:16:00,2178-07-16 14:41:31,3.2677199074074075
|
||||||
|
10019917,22585261,34324099,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2182-01-07 23:26:32,2182-01-09 07:09:31,1.3215162037037038
|
||||||
|
10037975,27617929,39061571,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2185-01-17 19:12:12,2185-01-22 16:16:52,4.8782407407407407
|
||||||
|
10002428,28662225,33987268,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2156-04-12 16:24:18,2156-04-17 15:57:08,4.9811342592592593
|
||||||
|
10015931,24420677,38137964,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2176-12-22 16:24:33,2176-12-23 20:14:53,1.1599537037037038
|
||||||
|
10021938,27154822,33083787,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2181-10-25 11:35:00,2181-10-26 20:53:57,1.3881597222222224
|
||||||
|
10027445,26275841,34499716,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2142-07-31 01:41:00,2142-08-03 21:05:58,3.8090046296296296
|
||||||
|
10015860,25085565,32496174,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2186-09-15 17:15:00,2186-09-16 11:17:40,0.75185185185185188
|
||||||
|
10016742,27568122,30425410,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2178-07-22 08:19:00,2178-07-25 16:42:43,3.3498032407407403
|
||||||
|
10029484,20764029,35396193,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2160-11-08 05:23:00,2160-11-08 21:04:55,0.65410879629629626
|
||||||
|
10038081,20755971,38430513,Medical Intensive Care Unit (MICU),Medical Intensive Care Unit (MICU),2115-10-09 10:15:25,2115-10-13 03:01:17,3.6985185185185188
|
||||||
|
10004720,22081550,35009126,Surgical Intensive Care Unit (SICU),Medical Intensive Care Unit (MICU),2186-11-12 19:55:00,2186-11-17 21:15:55,5.05619212962963
|
||||||
|
10002428,23473524,35479615,Surgical Intensive Care Unit (SICU),Medical Intensive Care Unit (MICU),2156-05-11 14:49:34,2156-05-22 14:16:46,10.977222222222222
|
||||||
|
10005866,20364112,34170353,Trauma SICU (TSICU),Surgical Intensive Care Unit (SICU),2149-10-02 12:48:08,2149-10-04 17:48:36,2.2086574074074075
|
||||||
|
10026255,22059910,38229329,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2201-07-10 10:10:47,2201-07-11 13:48:02,1.1508680555555555
|
||||||
|
10008287,22168393,33348260,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2145-09-28 20:59:43,2145-09-30 00:34:15,1.1489814814814816
|
||||||
|
10001217,24597018,37067082,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2157-11-20 19:18:02,2157-11-21 22:08:00,1.1180324074074075
|
||||||
|
10022041,28909879,30913302,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2187-05-18 18:39:00,2187-05-20 16:04:02,1.8923842592592592
|
||||||
|
10016810,20973395,35436337,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2185-06-16 02:16:00,2185-06-18 14:00:02,2.488912037037037
|
||||||
|
10024043,24717014,32374504,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2117-04-11 22:05:00,2117-04-14 14:36:11,2.6883217592592596
|
||||||
|
10018845,21101111,36427705,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2184-10-08 04:09:00,2184-10-09 15:55:53,1.4908912037037034
|
||||||
|
10014078,25809882,38907302,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2166-08-22 00:36:00,2166-08-24 13:12:44,2.5255092592592594
|
||||||
|
10016150,29374560,33652203,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2142-05-10 16:39:15,2142-05-10 20:23:49,0.15594907407407407
|
||||||
|
10021487,27660781,35065627,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2117-03-07 23:06:21,2117-03-09 18:01:57,1.7886111111111109
|
||||||
|
10015860,24698912,36734659,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2192-05-12 09:31:00,2192-05-13 00:55:45,0.6421875
|
||||||
|
10027602,21477991,32453351,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2201-12-11 20:11:52,2201-12-13 18:29:00,1.9285648148148147
|
||||||
|
10039831,26924951,39142259,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2115-12-28 19:36:43,2115-12-30 16:31:48,1.8715856481481483
|
||||||
|
10025463,24470193,38275267,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2137-10-09 02:51:25,2137-10-09 17:32:37,0.61194444444444451
|
||||||
|
10039997,24294180,36893762,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2134-09-07 18:03:58,2134-09-08 22:11:05,1.1716087962962962
|
||||||
|
10016742,29281842,37057036,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2178-07-03 22:45:00,2178-07-08 20:34:44,4.9095370370370377
|
||||||
|
10039708,24928679,37323533,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2143-09-19 19:40:55,2143-09-20 16:29:51,0.86731481481481476
|
||||||
|
10025612,23403708,32587226,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2125-09-25 13:23:24,2125-09-30 18:54:33,5.2299652777777776
|
||||||
|
10031757,28477280,33244906,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2137-10-15 17:29:21,2137-10-17 22:16:51,2.1996527777777777
|
||||||
|
10001217,27703517,34592300,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2157-12-19 15:42:24,2157-12-20 14:27:41,0.94811342592592585
|
||||||
|
10040025,27996267,36107367,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2148-01-24 04:50:17,2148-01-30 17:45:09,6.5381018518518523
|
||||||
|
10038933,25129047,32166508,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2148-09-10 13:19:00,2148-09-15 21:50:29,5.35519675925926
|
||||||
|
10014354,22741225,37200209,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2146-10-09 01:08:00,2146-10-09 21:20:50,0.84224537037037039
|
||||||
|
10021666,22756440,35475449,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2172-03-13 01:46:00,2172-03-13 16:33:34,0.6163657407407408
|
||||||
|
10022880,27708593,39623478,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2177-03-15 05:49:26,2177-03-15 22:47:09,0.70674768518518516
|
||||||
|
10027602,28166872,32391858,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2201-10-30 12:25:00,2201-11-12 18:37:10,13.258449074074074
|
||||||
|
10031757,28477280,30458995,Surgical Intensive Care Unit (SICU),Surgical Intensive Care Unit (SICU),2137-10-12 22:44:57,2137-10-14 17:08:34,1.766400462962963
|
||||||
|
10019172,24997044,32283063,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2118-11-16 09:38:22,2118-11-19 20:34:51,3.455891203703704
|
||||||
|
10035185,22580999,39084876,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2120-05-13 14:27:34,2120-05-14 16:28:21,1.0838773148148149
|
||||||
|
10013049,22675517,35679826,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2114-06-20 09:32:58,2114-06-21 13:47:44,1.1769212962962963
|
||||||
|
10022017,22342963,39497668,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2189-09-10 10:05:24,2189-09-14 21:27:28,4.4736574074074076
|
||||||
|
10009049,22995465,35636875,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2174-05-26 09:18:00,2174-05-27 14:31:12,1.2175
|
||||||
|
10004457,23251352,31494479,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2141-12-17 10:24:25,2141-12-18 14:16:17,1.1610185185185184
|
||||||
|
10038992,24745425,37127068,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2187-07-29 09:41:11,2187-07-30 13:42:51,1.1678240740740742
|
||||||
|
10004422,21255400,32155744,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2111-01-17 09:44:50,2111-01-23 18:18:46,6.3568981481481472
|
||||||
|
10007058,22954658,32506122,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2167-11-07 20:22:00,2167-11-09 20:55:04,2.0229629629629629
|
||||||
|
10006580,24159665,38329661,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2137-08-10 16:23:10,2137-08-11 13:56:56,0.89844907407407415
|
||||||
|
10005909,20199380,36496303,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2144-10-29 23:09:03,2144-11-02 15:24:29,3.6773842592592594
|
||||||
|
10011398,27505812,37648963,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2146-12-15 09:54:58,2146-12-16 10:53:06,1.0403703703703704
|
||||||
|
10018423,29366372,30665396,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2167-05-05 12:54:06,2167-05-06 18:40:10,1.240324074074074
|
||||||
|
10009035,28324362,38507547,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2161-04-27 10:38:12,2161-04-28 15:06:17,1.1861689814814815
|
||||||
|
10023771,20044587,33177122,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2113-08-25 09:32:41,2113-08-27 16:27:53,2.2883333333333331
|
||||||
|
10022281,29642388,30585761,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2125-06-17 04:12:54,2125-06-18 14:55:55,1.4465393518518521
|
||||||
|
10005348,25239799,34629895,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2130-10-27 12:06:00,2130-10-29 12:05:02,1.9993287037037037
|
||||||
|
10012552,27089790,33383124,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2140-03-25 14:37:26,2140-03-28 18:25:54,3.1586574074074072
|
||||||
|
10019385,20297618,39268883,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2180-02-21 08:34:06,2180-02-22 16:05:14,1.3132870370370371
|
||||||
|
10014729,28889419,33558396,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2125-02-27 10:03:08,2125-03-01 21:21:37,2.4711689814814815
|
||||||
|
10009628,25926192,35258379,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2153-09-19 09:54:49,2153-09-21 16:39:06,2.2807523148148148
|
||||||
|
10005817,20626031,32604416,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2132-12-15 09:29:01,2132-12-17 18:06:07,2.3590972222222222
|
||||||
|
10021118,24490144,36558922,Cardiac Vascular Intensive Care Unit (CVICU),Cardiac Vascular Intensive Care Unit (CVICU),2161-11-19 10:04:04,2161-11-20 21:45:42,1.4872453703703703
|
||||||
|
10037861,24540843,34531557,Neuro Surgical Intensive Care Unit (Neuro SICU),Neuro Surgical Intensive Care Unit (Neuro SICU),2117-03-14 16:34:58,2117-03-25 02:35:08,10.416782407407407
|
||||||
|
10005817,28661809,31316840,Medical Intensive Care Unit (MICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2135-01-03 21:55:32,2135-01-19 21:16:23,15.972812499999998
|
||||||
|
10015272,27993466,37267577,Cardiac Vascular Intensive Care Unit (CVICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2137-06-12 18:37:22,2137-06-14 20:25:41,2.0752199074074071
|
||||||
|
10019003,29279905,34107647,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2153-03-28 02:21:00,2153-03-31 16:59:04,3.6097685185185182
|
||||||
|
10037928,22490490,31552399,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2177-07-14 20:38:00,2177-07-15 16:08:36,0.81291666666666662
|
||||||
|
10039708,28258130,33281088,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2140-01-23 18:08:00,2140-02-08 22:28:20,16.180787037037035
|
||||||
|
10002930,25922998,35629889,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2198-04-17 21:24:00,2198-04-18 13:41:43,0.67896990740740737
|
||||||
|
10014354,27487226,34600477,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2148-06-30 02:27:00,2148-07-01 20:58:50,1.7721064814814815
|
||||||
|
10019003,21457723,35727289,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2155-07-10 17:48:57,2155-07-13 15:04:42,2.8859375000000003
|
||||||
|
10014354,29600294,39864867,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2148-08-16 08:57:26,2148-08-17 14:45:17,1.2415625
|
||||||
|
10004733,27411876,39635619,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2174-12-04 11:28:24,2174-12-12 20:03:01,8.3573726851851848
|
||||||
|
10007795,28477357,31921355,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2136-04-22 18:01:13,2136-04-23 19:13:58,1.0505208333333333
|
||||||
|
10020640,27984218,30849778,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2153-02-13 01:38:00,2153-02-15 15:00:57,2.5576041666666667
|
||||||
|
10020740,23831430,35044342,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2150-03-19 04:41:33,2150-03-23 20:49:33,4.6722222222222225
|
||||||
|
10023239,29295881,33846653,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2137-06-19 19:09:00,2137-06-22 14:57:32,2.8253703703703703
|
||||||
|
10023239,21759936,35024147,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2140-10-03 09:07:56,2140-10-05 19:31:27,2.4329976851851853
|
||||||
|
10001725,25563031,31205490,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2110-04-11 15:52:22,2110-04-12 23:59:56,1.3385879629629629
|
||||||
|
10003400,23559586,34577403,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2137-08-10 19:54:51,2137-08-13 17:54:54,2.9167013888888889
|
||||||
|
10019003,27525946,35214014,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2153-04-13 19:45:30,2153-04-16 21:15:15,3.0623263888888892
|
||||||
|
10035631,29276678,30932571,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2116-02-28 18:43:20,2116-03-10 06:35:04,10.494259259259259
|
||||||
|
10003400,20214994,32128372,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2137-02-25 23:37:19,2137-03-10 21:29:36,12.91130787037037
|
||||||
|
10020786,23488445,33683112,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2189-06-09 12:46:30,2189-06-10 22:58:09,1.4247569444444446
|
||||||
|
10020740,23831430,35026312,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2150-03-11 15:34:56,2150-03-19 02:17:47,7.4464236111111113
|
||||||
|
10032725,20611640,30101877,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2143-03-22 06:42:00,2143-03-25 15:05:33,3.3496875
|
||||||
|
10037928,24656677,39804682,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2178-12-21 06:05:18,2178-12-22 02:16:08,0.8408564814814814
|
||||||
|
10015931,22130791,37093652,Medical/Surgical Intensive Care Unit (MICU/SICU),Medical/Surgical Intensive Care Unit (MICU/SICU),2177-03-24 21:48:07,2177-03-29 18:03:36,4.8440856481481482
|
||||||
|
+107728
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
|||||||
|
subject_id,gender,anchor_age,anchor_year,anchor_year_group,dod
|
||||||
|
10014729,F,21,2125,2011 - 2013,
|
||||||
|
10003400,F,72,2134,2011 - 2013,2137-09-02
|
||||||
|
10002428,F,80,2155,2011 - 2013,
|
||||||
|
10032725,F,38,2143,2011 - 2013,2143-03-30
|
||||||
|
10027445,F,48,2142,2011 - 2013,2146-02-09
|
||||||
|
10037928,F,78,2175,2011 - 2013,
|
||||||
|
10001725,F,46,2110,2011 - 2013,
|
||||||
|
10040025,F,64,2143,2011 - 2013,2148-02-07
|
||||||
|
10008454,F,26,2110,2011 - 2013,
|
||||||
|
10020640,F,91,2153,2011 - 2013,2154-02-04
|
||||||
|
10014078,F,60,2166,2011 - 2013,
|
||||||
|
10007795,F,53,2136,2011 - 2013,
|
||||||
|
10001217,F,55,2157,2011 - 2013,
|
||||||
|
10019172,F,63,2118,2011 - 2013,
|
||||||
|
10039831,F,57,2115,2011 - 2013,
|
||||||
|
10007928,F,59,2129,2011 - 2013,
|
||||||
|
10019003,F,65,2148,2011 - 2013,2155-12-03
|
||||||
|
10002930,F,48,2193,2011 - 2013,2201-12-24
|
||||||
|
10010471,F,89,2155,2014 - 2016,2155-12-07
|
||||||
|
10027602,F,71,2201,2014 - 2016,
|
||||||
|
10031757,F,67,2137,2014 - 2016,2137-10-31
|
||||||
|
10016742,F,58,2178,2014 - 2016,
|
||||||
|
10039997,F,67,2134,2014 - 2016,
|
||||||
|
10012853,F,91,2175,2014 - 2016,
|
||||||
|
10000032,F,52,2180,2014 - 2016,2180-09-09
|
||||||
|
10031404,F,82,2113,2014 - 2016,
|
||||||
|
10029291,F,50,2123,2014 - 2016,
|
||||||
|
10005909,F,40,2144,2014 - 2016,
|
||||||
|
10039708,F,46,2138,2014 - 2016,
|
||||||
|
10019568,F,59,2120,2014 - 2016,
|
||||||
|
10020187,F,63,2169,2014 - 2016,
|
||||||
|
10016810,F,66,2185,2014 - 2016,
|
||||||
|
10038081,F,63,2115,2014 - 2016,2115-10-12
|
||||||
|
10021312,F,55,2113,2014 - 2016,
|
||||||
|
10006580,F,63,2137,2014 - 2016,
|
||||||
|
10010867,F,28,2147,2014 - 2016,
|
||||||
|
10020786,F,86,2189,2014 - 2016,
|
||||||
|
10020306,F,74,2129,2014 - 2016,
|
||||||
|
10023239,F,29,2137,2014 - 2016,
|
||||||
|
10018328,F,83,2154,2014 - 2016,
|
||||||
|
10008287,F,43,2145,2014 - 2016,
|
||||||
|
10036156,F,88,2157,2014 - 2016,
|
||||||
|
10015272,F,78,2137,2014 - 2016,
|
||||||
|
10022281,M,84,2125,2011 - 2013,
|
||||||
|
10035631,M,63,2112,2011 - 2013,2116-03-12
|
||||||
|
10024043,M,67,2117,2011 - 2013,2117-06-26
|
||||||
|
10025612,M,82,2125,2011 - 2013,
|
||||||
|
10003046,M,64,2154,2011 - 2013,
|
||||||
|
10021666,M,87,2172,2011 - 2013,2172-04-19
|
||||||
|
10018423,M,37,2162,2011 - 2013,
|
||||||
|
10025463,M,66,2136,2011 - 2013,2137-10-09
|
||||||
|
10017492,M,84,2114,2011 - 2013,2116-07-05
|
||||||
|
10004422,M,78,2111,2011 - 2013,
|
||||||
|
10011398,M,67,2146,2011 - 2013,
|
||||||
|
10021938,M,65,2181,2011 - 2013,2182-10-16
|
||||||
|
10009628,M,58,2153,2011 - 2013,
|
||||||
|
10004457,M,65,2140,2011 - 2013,
|
||||||
|
10015860,M,53,2186,2011 - 2013,
|
||||||
|
10021487,M,43,2116,2011 - 2013,
|
||||||
|
10013049,M,52,2114,2011 - 2013,
|
||||||
|
10026255,M,66,2200,2011 - 2013,2201-07-13
|
||||||
|
10023771,M,70,2113,2011 - 2013,
|
||||||
|
10023117,M,53,2170,2011 - 2013,2175-07-20
|
||||||
|
10022041,M,64,2187,2011 - 2013,
|
||||||
|
10005348,M,76,2128,2011 - 2013,
|
||||||
|
10009035,M,28,2161,2011 - 2013,
|
||||||
|
10018081,M,79,2133,2011 - 2013,2134-10-28
|
||||||
|
10038933,M,34,2148,2011 - 2013,
|
||||||
|
10037975,M,60,2185,2014 - 2016,2185-01-22
|
||||||
|
10035185,M,70,2120,2014 - 2016,
|
||||||
|
10014354,M,60,2146,2014 - 2016,
|
||||||
|
10022880,M,66,2177,2014 - 2016,
|
||||||
|
10004720,M,61,2183,2014 - 2016,2186-11-17
|
||||||
|
10019385,M,44,2180,2014 - 2016,
|
||||||
|
10029484,M,64,2160,2014 - 2016,
|
||||||
|
10037861,M,77,2115,2014 - 2016,2117-03-24
|
||||||
|
10026406,M,45,2129,2014 - 2016,
|
||||||
|
10007818,M,69,2146,2014 - 2016,2146-07-12
|
||||||
|
10020740,M,56,2150,2014 - 2016,
|
||||||
|
10038999,M,45,2131,2014 - 2016,
|
||||||
|
10004235,M,47,2196,2014 - 2016,
|
||||||
|
10022017,M,59,2189,2014 - 2016,
|
||||||
|
10002495,M,81,2141,2014 - 2016,
|
||||||
|
10018845,M,91,2184,2014 - 2016,2184-11-22
|
||||||
|
10020944,M,72,2131,2014 - 2016,2131-04-28
|
||||||
|
10016150,M,69,2142,2014 - 2016,
|
||||||
|
10005866,M,57,2146,2014 - 2016,2149-11-21
|
||||||
|
10026354,M,34,2119,2014 - 2016,
|
||||||
|
10019917,M,44,2182,2014 - 2016,
|
||||||
|
10019777,M,51,2187,2014 - 2016,2187-04-29
|
||||||
|
10005817,M,66,2132,2014 - 2016,2135-01-19
|
||||||
|
10009049,M,56,2174,2014 - 2016,
|
||||||
|
10015931,M,87,2176,2014 - 2016,2177-03-29
|
||||||
|
10006053,M,52,2111,2014 - 2016,2111-11-15
|
||||||
|
10012552,M,78,2140,2014 - 2016,
|
||||||
|
10004733,M,51,2174,2014 - 2016,
|
||||||
|
10021118,M,62,2161,2014 - 2016,
|
||||||
|
10018501,M,83,2141,2014 - 2016,
|
||||||
|
10007058,M,48,2167,2014 - 2016,
|
||||||
|
10038992,M,70,2185,2014 - 2016,
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,268 @@
|
|||||||
|
# Guide 16: PHI Encryption with Data Protection API
|
||||||
|
|
||||||
|
## What is PHI and Why Encrypt It?
|
||||||
|
|
||||||
|
**PHI** stands for **Protected Health Information** — any data that can identify a patient and relates to their health. This includes names, dates of birth, medical record numbers, allergies, and emergency contact details. Healthcare regulations (HIPAA in the US, GDPR in the EU) require that PHI be **encrypted at rest** — meaning even if someone steals the database files or a backup, they can't read the patient data without the encryption key.
|
||||||
|
|
||||||
|
**Encryption at rest** protects against scenarios like:
|
||||||
|
- A database backup is accidentally uploaded to a public S3 bucket
|
||||||
|
- A disgruntled employee copies the database files
|
||||||
|
- The database server's hard drive is stolen or improperly disposed of
|
||||||
|
|
||||||
|
Without encryption, the database stores plaintext: `first_name = "Sarah"`. With encryption: `first_name = "CfDJ8Nrq7...long encrypted string..."`. The application decrypts transparently when reading, and encrypts transparently when writing.
|
||||||
|
|
||||||
|
## What is the Data Protection API?
|
||||||
|
|
||||||
|
**.NET's Data Protection API (DPAPI)** is a built-in framework for encrypting and decrypting data. It manages encryption keys, handles key rotation, and provides a simple `Protect()`/`Unprotect()` interface. You don't need to pick cipher algorithms or manage IVs manually — DPAPI handles the cryptographic details.
|
||||||
|
|
||||||
|
Key concepts:
|
||||||
|
- **`IDataProtectionProvider`**: The factory that creates protectors. Registered with DI at startup.
|
||||||
|
- **`IDataProtector`**: An instance tied to a specific **purpose string**. A protector created with purpose `"VigilCare.PatientPhi.v1"` can only decrypt data that was encrypted with the same purpose. This prevents accidentally decrypting data meant for a different part of the application.
|
||||||
|
- **Key ring**: DPAPI stores encryption keys on disk (configurable path). Keys are automatically rotated and expired on a schedule. Old keys are kept so previously encrypted data can still be decrypted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How PHI Encryption Works in This Project
|
||||||
|
|
||||||
|
```
|
||||||
|
Application reads patient.FirstName
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
EF Core Value Converter
|
||||||
|
│ calls crypto.Decrypt(ciphertext)
|
||||||
|
▼
|
||||||
|
PhiEncryptionService.Decrypt()
|
||||||
|
│ calls _protector.Unprotect(ciphertext)
|
||||||
|
▼
|
||||||
|
Returns "Sarah" to the application
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Application writes patient.FirstName = "Sarah"
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
EF Core Value Converter
|
||||||
|
│ calls crypto.Encrypt("Sarah")
|
||||||
|
▼
|
||||||
|
PhiEncryptionService.Encrypt()
|
||||||
|
│ calls _protector.Protect("Sarah")
|
||||||
|
▼
|
||||||
|
Stores "CfDJ8Nrq7..." in PostgreSQL
|
||||||
|
```
|
||||||
|
|
||||||
|
The application code never sees ciphertext — it works with plaintext strings as usual. The encryption and decryption happen inside EF Core value converters (see Guide 4), invisible to the rest of the codebase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### PhiEncryptionOptions
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PhiEncryptionOptions
|
||||||
|
{
|
||||||
|
public const string Section = "PhiEncryption";
|
||||||
|
public string ProtectorPurpose { get; set; } = "VigilCare.PatientPhi.v1";
|
||||||
|
public string SearchTokenKey { get; set; } = null!;
|
||||||
|
public bool LogListAccess { get; set; } = true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Setting | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `ProtectorPurpose` | The purpose string for the data protector. Changing this creates a new encryption scope — old data can't be decrypted with a new purpose without migration. |
|
||||||
|
| `SearchTokenKey` | HMAC key for generating searchable name tokens (explained below). Separate from the encryption key. |
|
||||||
|
| `LogListAccess` | Whether to log PHI access for list/search operations (compliance auditing). |
|
||||||
|
|
||||||
|
### appsettings.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"PhiEncryption": {
|
||||||
|
"ProtectorPurpose": "VigilCare.PatientPhi.v1",
|
||||||
|
"SearchTokenKey": "DEV-ONLY-HMAC-KEY-REPLACE-IN-PRODUCTION-32bytes!!",
|
||||||
|
"LogListAccess": true
|
||||||
|
},
|
||||||
|
"DataProtection": {
|
||||||
|
"KeyPath": "./data-protection-keys"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registration in Program.cs
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddDataProtection()
|
||||||
|
.PersistKeysToFileSystem(new DirectoryInfo(
|
||||||
|
builder.Configuration["DataProtection:KeyPath"]
|
||||||
|
?? "./data-protection-keys"))
|
||||||
|
.SetApplicationName("VigilCareClinical");
|
||||||
|
```
|
||||||
|
|
||||||
|
`PersistKeysToFileSystem` stores the encryption keys in a local directory. In production, you'd use `PersistKeysToAzureBlobStorage()` or `PersistKeysToStackExchangeRedis()` so keys survive container restarts. `SetApplicationName` ensures all instances of the application share the same key ring.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The PhiEncryptionService
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PhiEncryptionService : IPhiEncryptionService
|
||||||
|
{
|
||||||
|
private readonly IDataProtector _protector;
|
||||||
|
private readonly byte[] _searchKey;
|
||||||
|
|
||||||
|
public PhiEncryptionService(
|
||||||
|
IDataProtectionProvider provider,
|
||||||
|
IOptions<PhiEncryptionOptions> options)
|
||||||
|
{
|
||||||
|
var opts = options.Value;
|
||||||
|
_protector = provider.CreateProtector(opts.ProtectorPurpose);
|
||||||
|
_searchKey = /* derived from SearchTokenKey */;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Encrypt(string plaintext)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(plaintext)) return plaintext;
|
||||||
|
return _protector.Protect(plaintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Decrypt(string ciphertext)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(ciphertext)) return ciphertext;
|
||||||
|
if (!IsEncrypted(ciphertext)) return ciphertext; // plaintext rows still readable
|
||||||
|
return _protector.Unprotect(ciphertext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsEncrypted(string value) =>
|
||||||
|
value.StartsWith("CfDJ8", StringComparison.Ordinal) || value.Length > 50;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**The `IsEncrypted` check**: During the migration from plaintext to encrypted data, some rows may still contain plaintext. The `Decrypt` method checks if the value looks encrypted (DPAPI-encrypted values start with `"CfDJ8"` and are much longer than typical names). If not, it returns the value as-is. This lets the application work correctly with a partially-migrated database.
|
||||||
|
|
||||||
|
### How EF Core Uses the Service
|
||||||
|
|
||||||
|
The `PatientPhiConverterConfigurator` (from Guide 4) wires the service into EF Core value converters:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
entity.Property(p => p.FirstName)
|
||||||
|
.HasConversion(
|
||||||
|
v => crypto.Encrypt(v), // called on every INSERT/UPDATE
|
||||||
|
v => crypto.Decrypt(v)); // called on every SELECT
|
||||||
|
|
||||||
|
entity.Property(p => p.DateOfBirth)
|
||||||
|
.HasConversion(
|
||||||
|
v => crypto.Encrypt(v.ToString("yyyy-MM-dd")),
|
||||||
|
v => DateOnly.Parse(crypto.Decrypt(v)));
|
||||||
|
```
|
||||||
|
|
||||||
|
Encrypted columns: `first_name`, `last_name`, `date_of_birth`, `allergies`, `emergency_contact_name`, `emergency_contact_phone`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Search Problem: HMAC Name Tokens
|
||||||
|
|
||||||
|
**The problem**: If names are encrypted, you can't search for patients by name. SQL `WHERE first_name LIKE '%Sarah%'` doesn't work on ciphertext because each encryption of "Sarah" produces a different ciphertext (DPAPI uses random IVs).
|
||||||
|
|
||||||
|
**The solution**: Store a deterministic, one-way hash of the name in a separate column (`name_search_token`). To search, hash the query the same way and compare hashes.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public string ComputeNameSearchToken(string firstName, string lastName)
|
||||||
|
{
|
||||||
|
var normalized = $"{firstName.Trim().ToLowerInvariant()}|{lastName.Trim().ToLowerInvariant()}";
|
||||||
|
using var hmac = new HMACSHA256(_searchKey);
|
||||||
|
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(normalized));
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**What is HMAC?** HMAC (Hash-based Message Authentication Code) is a keyed hash function. Unlike plain SHA-256 (where anyone can compute the same hash), HMAC requires a secret key. Without the key, an attacker who has the hash can't reverse it to find the name, and can't compute hashes for other names to test against.
|
||||||
|
|
||||||
|
**Why not just use SHA-256?** Plain SHA-256 is vulnerable to rainbow table attacks — an attacker precomputes hashes for common names ("Sarah Smith" → hash, "John Doe" → hash) and compares them against the stored hashes. HMAC with a secret key makes this impossible because the attacker would need the key to compute valid hashes.
|
||||||
|
|
||||||
|
The search flow:
|
||||||
|
1. User searches for "Sarah Smith"
|
||||||
|
2. Application computes `HMACSHA256("sarah|smith")` → `"a1b2c3d4..."`
|
||||||
|
3. SQL query: `WHERE name_search_token = 'a1b2c3d4...'`
|
||||||
|
4. Matching rows are returned, and EF Core's value converter decrypts the actual names
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PHI Access Logging
|
||||||
|
|
||||||
|
Every access to patient data is logged for compliance auditing:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PhiAccessLogService : IPhiAccessLogService
|
||||||
|
{
|
||||||
|
public async Task LogViewAsync(Guid patientId, string resourcePath) =>
|
||||||
|
await WriteAsync(PhiAccessType.View, patientId, resourcePath);
|
||||||
|
|
||||||
|
public async Task LogListAsync(string resourcePath, int resultCount,
|
||||||
|
string? searchQuery = null)
|
||||||
|
{
|
||||||
|
var accessType = string.IsNullOrWhiteSpace(searchQuery)
|
||||||
|
? PhiAccessType.List
|
||||||
|
: PhiAccessType.Search;
|
||||||
|
await WriteAsync(accessType, null, resourcePath, resultCount, searchQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WriteAsync(PhiAccessType accessType, Guid? patientId, ...)
|
||||||
|
{
|
||||||
|
_db.PhiAccessLogs.Add(new PhiAccessLog
|
||||||
|
{
|
||||||
|
AccessType = accessType,
|
||||||
|
PatientId = patientId,
|
||||||
|
UserId = _currentUser.UserId!.Value,
|
||||||
|
UserDisplayName = _currentUser.DisplayName,
|
||||||
|
ResourcePath = resourcePath,
|
||||||
|
SearchQueryHash = searchQuery is null ? null : HashQuery(searchQuery),
|
||||||
|
IpAddress = _currentUser.IpAddress,
|
||||||
|
CorrelationId = /* from middleware */,
|
||||||
|
AccessedAt = DateTimeOffset.UtcNow
|
||||||
|
});
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
_metrics.PhiAccessLogsTotal.WithLabels(accessType.ToDbString()).Inc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The log records who accessed what, when, from where, and what they searched for. Search queries are hashed (not stored in plaintext) to avoid storing potentially sensitive search terms.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## One-Time Migration: EncryptPhiCommand
|
||||||
|
|
||||||
|
For existing databases with plaintext patient data, a CLI command encrypts all rows in place:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class EncryptPhiCommand
|
||||||
|
{
|
||||||
|
public static async Task RunAsync(IServiceProvider services)
|
||||||
|
{
|
||||||
|
var patients = await db.Patients.ToListAsync();
|
||||||
|
foreach (var p in patients)
|
||||||
|
{
|
||||||
|
p.NameSearchToken = crypto.ComputeNameSearchToken(p.FirstName, p.LastName);
|
||||||
|
}
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
Console.WriteLine($"Encrypted {patients.Count} patient records.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run via: `dotnet run -- encrypt-phi`
|
||||||
|
|
||||||
|
The EF Core value converters handle the actual encryption — loading each patient triggers `Decrypt` (which passes plaintext through via `IsEncrypted` check), and saving triggers `Encrypt` (which encrypts the now-plaintext values). The command also computes `NameSearchToken` for every patient to enable encrypted name search.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Takeaways
|
||||||
|
|
||||||
|
- **Encryption at rest protects against data breaches** — even if the database is stolen, patient data is unreadable without the encryption keys
|
||||||
|
- **EF Core value converters make encryption transparent** — application code works with plaintext; encryption/decryption happens automatically on every read and write
|
||||||
|
- **DPAPI handles key management** — key generation, rotation, and storage are built-in. You don't manage cryptographic primitives directly.
|
||||||
|
- **Searchable encryption uses HMAC tokens** — deterministic, keyed hashes enable exact-match search on encrypted columns without decrypting every row
|
||||||
|
- **PHI access logging creates an audit trail** — every view, search, and modification of patient data is recorded with user identity, timestamp, and IP address
|
||||||
|
- **The purpose string isolates encryption scopes** — data encrypted with `"VigilCare.PatientPhi.v1"` can only be decrypted with the same purpose, preventing cross-contamination between different parts of the application
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
# Guide 17: Password Hashing with BCrypt
|
||||||
|
|
||||||
|
## What is Password Hashing?
|
||||||
|
|
||||||
|
When a user creates an account with the password `"DemoNurse1!"`, the application must store something that lets it verify the password later — but it should **never store the password itself**. If the database is compromised, plaintext passwords would be immediately usable by the attacker.
|
||||||
|
|
||||||
|
**Hashing** converts a password into a fixed-length string of random-looking characters using a one-way mathematical function. "One-way" means you can compute the hash from the password, but you can't compute the password from the hash:
|
||||||
|
|
||||||
|
```
|
||||||
|
"DemoNurse1!" → hash() → "$2a$12$xK7W3M...long hash string..."
|
||||||
|
```
|
||||||
|
|
||||||
|
When the user logs in, you hash the submitted password and compare it to the stored hash. If they match, the password is correct — without ever storing the actual password.
|
||||||
|
|
||||||
|
## Why BCrypt Specifically?
|
||||||
|
|
||||||
|
Not all hash functions are created equal. General-purpose hash functions like SHA-256 are designed to be **fast** — billions of hashes per second on modern hardware. That's a problem for passwords: an attacker who steals the hashed passwords can try billions of guesses per second.
|
||||||
|
|
||||||
|
**BCrypt** is specifically designed for password hashing with two key properties:
|
||||||
|
|
||||||
|
1. **It's intentionally slow**: BCrypt has a configurable "cost factor" (also called "work factor") that controls how many iterations the algorithm performs. Cost factor 12 (the default) means 2^12 = 4,096 iterations, making each hash take ~250ms. Fast enough that a single login is imperceptible, but an attacker trying 1 million passwords would need ~70 hours.
|
||||||
|
|
||||||
|
2. **It includes a built-in salt**: A **salt** is a random value mixed into the hash. Without a salt, two users with the same password would have the same hash — an attacker could build a precomputed table (a "rainbow table") of common passwords and their hashes, then look up matches instantly. BCrypt generates a random salt for each password and embeds it in the output, so identical passwords produce different hashes.
|
||||||
|
|
||||||
|
A BCrypt hash looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
$2a$12$xK7W3MqQ5Z6Y8B9A0C1D2EfGhIjKlMnOpQrStUvWxYz0123456789Ab
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ └── The hash itself
|
||||||
|
│ │ └── The salt (22 chars)
|
||||||
|
│ └── Cost factor (12 = 2^12 iterations)
|
||||||
|
└── Algorithm version
|
||||||
|
```
|
||||||
|
|
||||||
|
The salt and cost factor are stored right in the hash string, so you don't need a separate column for them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How BCrypt is Used in This Project
|
||||||
|
|
||||||
|
### NuGet Package
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
|
```
|
||||||
|
|
||||||
|
`BCrypt.Net-Next` is a .NET implementation of the BCrypt algorithm. It provides two key methods: `HashPassword()` and `Verify()`.
|
||||||
|
|
||||||
|
### Hashing on Account Creation
|
||||||
|
|
||||||
|
When a new user is created, the plaintext password is hashed before storage:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<ClinicalUserResponse> CreateAsync(CreateUserRequest req)
|
||||||
|
{
|
||||||
|
// Validate the password meets minimum requirements
|
||||||
|
ValidatePassword(req.Password);
|
||||||
|
|
||||||
|
var user = new ClinicalUser
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Username = req.Username.Trim(),
|
||||||
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword(req.Password), // hash here
|
||||||
|
DisplayName = req.DisplayName.Trim(),
|
||||||
|
Role = role,
|
||||||
|
IsActive = true,
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
};
|
||||||
|
|
||||||
|
_db.ClinicalUsers.Add(user);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return Map(user);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`HashPassword(req.Password)` generates a random salt, applies BCrypt with the default cost factor (12), and returns the full hash string. Each call with the same password produces a different hash (because the salt is random).
|
||||||
|
|
||||||
|
### Password Validation Rules
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static void ValidatePassword(string password)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(password))
|
||||||
|
throw new ValidationException("Password is required.", "PASSWORD_REQUIRED");
|
||||||
|
if (password.Length < 8)
|
||||||
|
throw new ValidationException(
|
||||||
|
"Password must be at least 8 characters.", "PASSWORD_TOO_SHORT");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The minimum length of 8 characters is a baseline. In production, you'd typically also require uppercase, lowercase, digits, and special characters — but the BCrypt hash itself doesn't care about password complexity.
|
||||||
|
|
||||||
|
### Verification on Login
|
||||||
|
|
||||||
|
When a user logs in, the submitted password is verified against the stored hash:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<LoginResponse> LoginAsync(LoginRequest req)
|
||||||
|
{
|
||||||
|
var user = await _db.ClinicalUsers
|
||||||
|
.FirstOrDefaultAsync(u => u.Username == req.Username && u.IsActive);
|
||||||
|
|
||||||
|
if (user is null || !BCrypt.Net.BCrypt.Verify(req.Password, user.PasswordHash))
|
||||||
|
throw new ValidationException(
|
||||||
|
"Invalid username or password.", "INVALID_CREDENTIALS");
|
||||||
|
|
||||||
|
// ... generate JWT token
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**How does `Verify` work?**
|
||||||
|
1. Extract the salt and cost factor from the stored hash string
|
||||||
|
2. Hash the submitted password using the same salt and cost factor
|
||||||
|
3. Compare the result to the stored hash
|
||||||
|
4. If they match, the password is correct
|
||||||
|
|
||||||
|
**Security note**: The error message says "Invalid username or password" — it does not distinguish between "user not found" and "wrong password." This prevents an attacker from enumerating valid usernames by observing different error messages.
|
||||||
|
|
||||||
|
### Seed Data (Development Only)
|
||||||
|
|
||||||
|
The user seeder creates demo accounts with hashed passwords:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
new ClinicalUser
|
||||||
|
{
|
||||||
|
Username = "nurse.demo",
|
||||||
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword("DemoNurse1!"),
|
||||||
|
DisplayName = "Demo Nurse",
|
||||||
|
Role = ClinicalRole.Nurse,
|
||||||
|
},
|
||||||
|
new ClinicalUser
|
||||||
|
{
|
||||||
|
Username = "physician.demo",
|
||||||
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword("DemoPhysician1!"),
|
||||||
|
DisplayName = "Dr. Demo Physician",
|
||||||
|
Role = ClinicalRole.Physician,
|
||||||
|
},
|
||||||
|
new ClinicalUser
|
||||||
|
{
|
||||||
|
Username = "admin.demo",
|
||||||
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword("DemoAdmin1!"),
|
||||||
|
DisplayName = "Demo Admin",
|
||||||
|
Role = ClinicalRole.Admin,
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Even in development, passwords are never stored in plaintext in the database. The passwords themselves (`"DemoNurse1!"` etc.) appear in the seeder code, but they're only used during the initial seeding and don't persist as plaintext anywhere.
|
||||||
|
|
||||||
|
### Database Schema
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Property(u => u.PasswordHash).HasColumnName("password_hash")
|
||||||
|
.HasMaxLength(500).IsRequired();
|
||||||
|
```
|
||||||
|
|
||||||
|
`HasMaxLength(500)` accommodates the BCrypt hash string (typically ~60 characters) with room for future algorithm changes that might produce longer hashes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cost Factor Considerations
|
||||||
|
|
||||||
|
The default cost factor of 12 is a good balance for 2024-era hardware:
|
||||||
|
|
||||||
|
| Cost Factor | Iterations | Approximate Time | Use Case |
|
||||||
|
|-------------|-----------|------------------|----------|
|
||||||
|
| 10 | 1,024 | ~65ms | Minimum for production |
|
||||||
|
| 11 | 2,048 | ~130ms | Reasonable for high-traffic APIs |
|
||||||
|
| **12** | **4,096** | **~250ms** | **Default — good balance** |
|
||||||
|
| 13 | 8,192 | ~500ms | More security, but login feels slower |
|
||||||
|
| 14 | 16,384 | ~1s | High-security environments |
|
||||||
|
|
||||||
|
The cost factor should be increased over time as hardware gets faster. What takes 250ms today might take 25ms in 10 years. The industry recommendation: choose the highest cost factor that keeps login time under ~500ms for your hardware.
|
||||||
|
|
||||||
|
You can customize the cost factor:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
BCrypt.Net.BCrypt.HashPassword(password, workFactor: 13);
|
||||||
|
```
|
||||||
|
|
||||||
|
Existing hashes with a lower cost factor continue to verify correctly — BCrypt reads the cost factor from the hash string.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What BCrypt Does NOT Protect Against
|
||||||
|
|
||||||
|
- **Weak passwords**: BCrypt slows down brute-force attacks, but "password123" will still be cracked quickly. Enforce password complexity rules at the application level.
|
||||||
|
- **Phishing**: If a user gives their password to an attacker directly, hashing doesn't help.
|
||||||
|
- **Memory dumps**: While the application is running, the plaintext password exists briefly in memory (during the Verify call). In extremely sensitive environments, you'd use secure memory handling.
|
||||||
|
- **Credential stuffing**: If a user reuses their password from another breached site, BCrypt can't help. Multi-factor authentication (MFA) addresses this.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Takeaways
|
||||||
|
|
||||||
|
- **Never store plaintext passwords** — always hash them before writing to the database. There is no valid reason to store or log a user's actual password.
|
||||||
|
- **BCrypt is purposefully slow** — the cost factor makes brute-force attacks impractical while keeping legitimate logins fast
|
||||||
|
- **Each hash includes its own salt** — even identical passwords produce different hashes, defeating rainbow table attacks
|
||||||
|
- **The cost factor is embedded in the hash** — you can increase the cost factor for new passwords without invalidating existing ones
|
||||||
|
- **Give generic error messages** — "Invalid username or password" prevents username enumeration. Never reveal whether the username or the password was wrong.
|
||||||
|
- **BCrypt handles the hard parts** — salt generation, iteration count, and comparison are all managed by the library. You call `HashPassword()` and `Verify()` — nothing else.
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
# Guide 18: REST API Design & Conventions
|
||||||
|
|
||||||
|
## What is a REST API?
|
||||||
|
|
||||||
|
A **REST API** (Representational State Transfer) is the most common way to build web APIs. It uses standard HTTP methods and URLs to perform operations on resources. If you've ever used a URL like `GET /api/patients/123`, you've interacted with a REST API.
|
||||||
|
|
||||||
|
Key principles:
|
||||||
|
- **Resources** are the "nouns" — patients, encounters, observations, alerts. Each resource has a URL (called an endpoint).
|
||||||
|
- **HTTP methods** are the "verbs" — GET (read), POST (create), PUT (replace), PATCH (partial update), DELETE (remove).
|
||||||
|
- **Status codes** tell the caller what happened — 200 (success), 201 (created), 400 (bad request), 404 (not found), 500 (server error).
|
||||||
|
- **Stateless** — each request contains all the information the server needs. The server doesn't remember previous requests (that's what JWT tokens are for — they carry identity in every request).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## URL Conventions
|
||||||
|
|
||||||
|
### Resource Naming
|
||||||
|
|
||||||
|
URLs use lowercase, plural nouns with hyphens between words:
|
||||||
|
|
||||||
|
```
|
||||||
|
/api/v1/encounters — collection of encounters
|
||||||
|
/api/v1/encounters/{encounterId} — one encounter
|
||||||
|
/api/v1/encounters/{encounterId}/observations — observations within an encounter
|
||||||
|
/api/v1/alert-thresholds — hyphenated multi-word resource
|
||||||
|
/api/v1/sepsis-bundles/{bundleId}/elements — nested sub-resource
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why plural?** `GET /api/v1/encounters` returns a list, and `GET /api/v1/encounters/123` returns one item from that list. Using the plural form for both keeps URLs consistent.
|
||||||
|
|
||||||
|
**Why `v1`?** Version prefixing lets you introduce breaking changes in a `v2` without breaking existing clients.
|
||||||
|
|
||||||
|
### Controller Declaration
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/v1/encounters")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
[Authorize]
|
||||||
|
public class EncountersController : ControllerBase
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`[ApiController]`** enables automatic model validation, `[FromBody]` inference, and `ProblemDetails` error responses
|
||||||
|
- **`[Route("api/v1/encounters")]`** sets the base URL for all actions in this controller
|
||||||
|
- **`[Produces("application/json")]`** declares that all responses are JSON
|
||||||
|
- **`[Authorize]`** requires authentication for all actions (overridable per-action)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Response Envelope
|
||||||
|
|
||||||
|
Every API response uses a consistent wrapper called `ApiResponse<T>`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ApiResponse<T>(bool Success, int StatusCode, T? Data, ApiError? Error)
|
||||||
|
{
|
||||||
|
public static ApiResponse<T> Ok(T data) =>
|
||||||
|
new(true, 200, data, null);
|
||||||
|
|
||||||
|
public static ApiResponse<T> Created(T data) =>
|
||||||
|
new(true, 201, data, null);
|
||||||
|
|
||||||
|
public static ApiResponse<T> Fail(int statusCode, string message, string code) =>
|
||||||
|
new(false, statusCode, default, new ApiError(message, code));
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ApiError(string Message, string Code);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why an envelope?** Without a wrapper, successful and error responses have completely different shapes, making it harder for clients to parse. With the envelope, every response has the same top-level structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
// Success
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"statusCode": 200,
|
||||||
|
"data": { "id": "...", "status": "Active", ... },
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"statusCode": 400,
|
||||||
|
"data": null,
|
||||||
|
"error": {
|
||||||
|
"message": "Invalid status filter.",
|
||||||
|
"code": "INVALID_STATUS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `code` field (like `"INVALID_STATUS"`) is a machine-readable error identifier. The `message` is human-readable. Clients can switch on the code without parsing the message string, which may change or be localized.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HTTP Methods and Status Codes
|
||||||
|
|
||||||
|
### GET — Read (never modifies data)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[HttpGet]
|
||||||
|
[AuthorizePermission(ClinicalPermissions.EncountersRead)]
|
||||||
|
public async Task<IActionResult> List(
|
||||||
|
[FromQuery] string? status,
|
||||||
|
[FromQuery] string? department,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 20)
|
||||||
|
{
|
||||||
|
var result = await _encounters.ListAsync(parsedStatus, parsedDepartment, page, pageSize);
|
||||||
|
return Ok(ApiResponse<object>.Ok(new
|
||||||
|
{
|
||||||
|
items = result.Items,
|
||||||
|
page = result.Page,
|
||||||
|
pageSize = result.PageSize,
|
||||||
|
totalCount = result.TotalCount,
|
||||||
|
totalPages = result.TotalPages
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Returns **200 OK** on success
|
||||||
|
- Filter parameters go in query strings (`?status=ACTIVE&department=ICU`)
|
||||||
|
- Pagination parameters are also query strings (`?page=1&pageSize=20`)
|
||||||
|
|
||||||
|
### POST — Create a new resource
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[HttpPost]
|
||||||
|
[AuthorizePermission(ClinicalPermissions.ThresholdsWrite)]
|
||||||
|
public async Task<IActionResult> Create([FromBody] AlertThresholdRequest req)
|
||||||
|
{
|
||||||
|
var threshold = await _thresholds.CreateAsync(req);
|
||||||
|
return StatusCode(201, ApiResponse<AlertThreshold>.Created(threshold));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Request body is JSON (`[FromBody]`)
|
||||||
|
- Returns **201 Created** on success (not 200 — 201 is semantically correct for creation)
|
||||||
|
- Returns **409 Conflict** if the resource already exists
|
||||||
|
|
||||||
|
### POST for Ingest (Batch Operations)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[HttpPost]
|
||||||
|
[AuthorizePermission(ClinicalPermissions.ObservationsIngest)]
|
||||||
|
public async Task<IActionResult> Ingest(Guid encounterId, [FromBody] BatchIngestRequest req)
|
||||||
|
{
|
||||||
|
if (req.Observations.Count == 0)
|
||||||
|
return BadRequest(ApiResponse<object>.Fail(400,
|
||||||
|
"At least one observation is required.", "EMPTY_BATCH"));
|
||||||
|
|
||||||
|
if (req.Observations.Count > 10)
|
||||||
|
return BadRequest(ApiResponse<object>.Fail(400,
|
||||||
|
"Batch size cannot exceed 10 observations.", "BATCH_TOO_LARGE"));
|
||||||
|
|
||||||
|
// Process each observation...
|
||||||
|
|
||||||
|
return StatusCode(201, ApiResponse<object>.Created(results));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Batch endpoints accept arrays but enforce limits to prevent abuse or accidental huge payloads.
|
||||||
|
|
||||||
|
### PATCH — Partial update
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[HttpPatch("{alertId:guid}/acknowledge")]
|
||||||
|
[AuthorizePermission(ClinicalPermissions.AlertsAcknowledge)]
|
||||||
|
public async Task<IActionResult> Acknowledge(Guid alertId, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
- PATCH means "modify part of the resource" (only the fields you send are changed)
|
||||||
|
- Returns **200 OK** with the updated resource
|
||||||
|
- Returns **404 Not Found** if the resource doesn't exist
|
||||||
|
|
||||||
|
### PUT — Full replace
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[HttpPut("{id:guid}")]
|
||||||
|
[AuthorizePermission(ClinicalPermissions.ThresholdsWrite)]
|
||||||
|
public async Task<IActionResult> Update(Guid id, [FromBody] AlertThresholdRequest req)
|
||||||
|
```
|
||||||
|
|
||||||
|
- PUT means "replace the entire resource with this new version"
|
||||||
|
- Returns **200 OK** with the updated resource
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The `ExceptionHandlerMiddleware` (from Guide 3) catches all exceptions and converts them to consistent API responses. The controller code throws domain exceptions, and the middleware translates them:
|
||||||
|
|
||||||
|
| Exception | HTTP Status | When Used |
|
||||||
|
|-----------|-------------|-----------|
|
||||||
|
| `NotFoundException` | 404 Not Found | Resource doesn't exist |
|
||||||
|
| `BadRequestException` | 400 Bad Request | Invalid input that FluentValidation didn't catch |
|
||||||
|
| `ValidationException` | 422 Unprocessable Entity | Business rule violation |
|
||||||
|
| `ConflictException` | 409 Conflict | Duplicate resource (e.g., duplicate MRN) |
|
||||||
|
| `Exception` (unhandled) | 500 Internal Server Error | Unexpected bugs |
|
||||||
|
|
||||||
|
Client errors (4xx) are logged as **Warning** — they're expected. Server errors (5xx) are logged as **Error** with the full stack trace.
|
||||||
|
|
||||||
|
The 500 response never leaks exception details to the client:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"statusCode": 500,
|
||||||
|
"data": null,
|
||||||
|
"error": {
|
||||||
|
"message": "An unexpected error occurred",
|
||||||
|
"code": "INTERNAL_ERROR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pagination: Offset-Based vs Cursor-Based
|
||||||
|
|
||||||
|
This project uses two pagination strategies depending on the use case.
|
||||||
|
|
||||||
|
### Offset-Based (Page Number)
|
||||||
|
|
||||||
|
For encounter lists where the client needs "page 3 of 10":
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/encounters?page=2&pageSize=20
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [...],
|
||||||
|
"page": 2,
|
||||||
|
"pageSize": 20,
|
||||||
|
"totalCount": 157,
|
||||||
|
"totalPages": 8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**How it works**: `OFFSET (page - 1) * pageSize LIMIT pageSize`. Simple but has a known limitation — if data is inserted between page requests, items can be duplicated or skipped.
|
||||||
|
|
||||||
|
### Cursor-Based (Keyset Pagination)
|
||||||
|
|
||||||
|
For observation history where data is frequently appended:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/encounters/{id}/observations?limit=20
|
||||||
|
GET /api/v1/encounters/{id}/observations?limit=20&cursor=eyJ0Ijoi...
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [...],
|
||||||
|
"nextCursor": "eyJ0IjoiMjAyNi0wNi0yNFQxNDoyMzowMFoiLCJpIjoiYWJjLTEyMyJ9",
|
||||||
|
"hasMore": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**What is a cursor?** An opaque token that encodes the position of the last item. The server decodes it to construct a `WHERE` clause that fetches the next batch:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<CursorPage<Observation>> GetHistoryAsync(
|
||||||
|
Guid encounterId, string? code,
|
||||||
|
DateTimeOffset? from, DateTimeOffset? to,
|
||||||
|
int limit, string? cursorToken)
|
||||||
|
{
|
||||||
|
var cursor = ObservationCursor.Decode(cursorToken);
|
||||||
|
|
||||||
|
var query = _db.Observations
|
||||||
|
.Where(o => o.EncounterId == encounterId);
|
||||||
|
|
||||||
|
if (cursor is not null)
|
||||||
|
{
|
||||||
|
// Keyset condition: ORDER BY recorded_at DESC, id DESC
|
||||||
|
// "Give me rows AFTER this position"
|
||||||
|
query = query.Where(o =>
|
||||||
|
o.RecordedAt < cursor.RecordedAt ||
|
||||||
|
(o.RecordedAt == cursor.RecordedAt && o.Id.CompareTo(cursor.Id) < 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = await query
|
||||||
|
.OrderByDescending(o => o.RecordedAt)
|
||||||
|
.ThenByDescending(o => o.Id)
|
||||||
|
.Take(limit + 1) // fetch one extra to detect "has more"
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var hasMore = items.Count > limit;
|
||||||
|
if (hasMore) items.RemoveAt(limit);
|
||||||
|
|
||||||
|
var nextCursor = hasMore
|
||||||
|
? new ObservationCursor(items[^1].RecordedAt, items[^1].Id).Encode()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return new CursorPage<Observation>(items, nextCursor, hasMore);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why cursor-based for observations?** Observations are append-only and ordered by timestamp. Offset pagination (`OFFSET 100`) gets slower as the offset grows (PostgreSQL must scan and skip 100 rows). Keyset pagination (`WHERE recorded_at < '2026-06-24T14:23:00Z'`) uses the index directly and performs consistently regardless of how deep you paginate.
|
||||||
|
|
||||||
|
**The `Take(limit + 1)` trick**: Fetch one more item than requested. If you get `limit + 1` items, there's a next page — remove the extra item and return `hasMore: true`. If you get `limit` or fewer, there's no next page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Idempotency
|
||||||
|
|
||||||
|
**What is idempotency?** An operation is idempotent if performing it multiple times produces the same result as performing it once. `GET` is naturally idempotent (reading doesn't change anything). `POST` is not (creating a resource twice creates two resources).
|
||||||
|
|
||||||
|
For observation ingest, the API supports an optional `Idempotency-Key` header:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/encounters/{id}/observations
|
||||||
|
Idempotency-Key: device-123-reading-456
|
||||||
|
|
||||||
|
{ "observationCode": "HEART_RATE", "value": 82, ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
If the same idempotency key is sent twice, the second request returns the original observation instead of creating a duplicate. This is essential for device integrations where network retries are common — a device might send the same reading twice if it doesn't receive an acknowledgment.
|
||||||
|
|
||||||
|
The implementation uses a partial unique index in PostgreSQL (see Guide 4) — only non-null idempotency keys are checked for uniqueness.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Route Parameter Constraints
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[HttpGet("{id:guid}")]
|
||||||
|
public async Task<IActionResult> Get(Guid id)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `:guid` constraint means ASP.NET Core only matches this route if the `{id}` segment is a valid GUID. A request to `/api/v1/encounters/not-a-guid` returns 404 instead of reaching the controller and failing during GUID parsing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Swagger / OpenAPI Documentation
|
||||||
|
|
||||||
|
The API is self-documenting via Swagger:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI(options =>
|
||||||
|
{
|
||||||
|
options.SwaggerEndpoint("/swagger/v1/swagger.json", "VigilCare Clinical API v1");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Available at `http://localhost:5270/swagger/ui` during development. XML documentation comments on controller actions (the `<summary>` blocks) appear in the Swagger UI, making it easy for frontend developers to understand each endpoint without reading the C# code.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>
|
||||||
|
/// Returns cursor-paginated observation history for an encounter.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="encounterId">Encounter id.</param>
|
||||||
|
/// <param name="code">Optional observation code filter.</param>
|
||||||
|
/// <param name="limit">Maximum items per page.</param>
|
||||||
|
/// <param name="cursor">Opaque cursor from a previous page.</param>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> History(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
`[ProducesResponseType]` tells Swagger which response shapes are possible, generating accurate API documentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Takeaways
|
||||||
|
|
||||||
|
- **Consistent envelope** (`ApiResponse<T>`) makes every response predictable — clients always know where to find the data, error message, and status code
|
||||||
|
- **Machine-readable error codes** (like `"INVALID_STATUS"`) let clients handle errors programmatically without parsing human-readable messages
|
||||||
|
- **Use the right HTTP method** — GET reads, POST creates, PATCH updates partially, PUT replaces fully. This isn't just convention; proxies, caches, and browsers treat these differently.
|
||||||
|
- **Use the right status code** — 201 for creation, 409 for conflicts, 422 for validation failures. Don't use 200 for everything.
|
||||||
|
- **Cursor pagination for append-only data** — performs consistently regardless of dataset size, handles concurrent inserts correctly
|
||||||
|
- **Offset pagination for browsable lists** — simpler for UI that needs "page X of Y" navigation
|
||||||
|
- **Idempotency keys prevent duplicate resources** — essential for unreliable networks where requests may be retried
|
||||||
|
- **Swagger documents the API automatically** — XML comments on controllers become interactive API documentation
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
# Guide 19: FHIR R4 Integration Facade
|
||||||
|
|
||||||
|
## What is FHIR?
|
||||||
|
|
||||||
|
**FHIR** (Fast Healthcare Interoperability Resources, pronounced "fire") is a standard for exchanging healthcare data between systems. If your hospital has an EHR (Electronic Health Record) like Epic or Cerner, a lab system, a pharmacy system, and a monitoring platform, they all need to share patient data. FHIR defines a common language for this — standard data formats (called **resources**) and standard ways to send them (RESTful HTTP endpoints).
|
||||||
|
|
||||||
|
Key concepts:
|
||||||
|
|
||||||
|
- **Resource**: A structured JSON object representing a healthcare concept. Examples: `Patient`, `Encounter`, `Observation`, `MedicationAdministration`. Each resource type has a defined set of fields and data types.
|
||||||
|
- **FHIR R4**: The fourth major release of the FHIR standard (Release 4). R4 is the current normative version used by most healthcare systems.
|
||||||
|
- **Identifier**: A system+value pair that identifies a resource in an external system. For example, `system: "http://hospital.example/mrn"`, `value: "MRN-001"`. A patient might have different identifiers in different systems — FHIR uses these to match records across systems.
|
||||||
|
- **Coding**: A code from a standard terminology. `system: "http://loinc.org"`, `code: "8867-4"` means "Heart rate" in the LOINC vocabulary. `system: "http://snomed.info/sct"`, `code: "364075005"` also means "Heart rate" in SNOMED CT.
|
||||||
|
- **Bundle**: A collection of resources sent together. A "transaction Bundle" is like a database transaction — all entries succeed or all fail.
|
||||||
|
- **CapabilityStatement**: A resource that describes what a FHIR server can do — which resource types it supports and which operations (create, read, search).
|
||||||
|
- **OperationOutcome**: A FHIR-standard error response. Instead of returning `{"error": "..."}`, FHIR servers return a structured `OperationOutcome` resource with severity, issue type, and diagnostic details.
|
||||||
|
|
||||||
|
**What is a "facade"?** This project doesn't implement a full FHIR server. It implements a **facade** — a thin translation layer that accepts FHIR-formatted requests, maps them to the internal data model, and returns FHIR-formatted responses. The internal database schema is not FHIR-native; the facade translates between the two worlds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why FHIR in This Project?
|
||||||
|
|
||||||
|
Hospital integration engines (like Mirth Connect or Rhapsody) speak FHIR. They send ADT (Admit/Discharge/Transfer) messages with Patient and Encounter resources, vital signs as Observation resources, and medication records as MedicationAdministration resources. By implementing FHIR endpoints, VigilCareClinical can receive data from any FHIR-capable system without custom integration code for each one.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
Hospital Systems VigilCareClinical
|
||||||
|
┌──────────────┐ ┌──────────────────────────────┐
|
||||||
|
│ EHR (Epic) │ │ FHIR Ingest Controller │
|
||||||
|
│ │ POST /fhir/R4/ │ │ │
|
||||||
|
│ Mirth Connect│ Patient │ ▼ │
|
||||||
|
│ (integration │ ────────────────► │ PatientFhirMapper │
|
||||||
|
│ engine) │ │ │ FHIR Patient → internal │
|
||||||
|
│ │ POST /fhir/R4/ │ ▼ │
|
||||||
|
│ Lab System │ Observation │ PatientService │
|
||||||
|
│ │ ────────────────► │ │ save to PostgreSQL │
|
||||||
|
│ Pharmacy │ │ ▼ │
|
||||||
|
│ │ POST /fhir/R4/ │ FhirMapper (reverse) │
|
||||||
|
│ │ Bundle │ │ internal → FHIR response│
|
||||||
|
│ │ ────────────────► │ ▼ │
|
||||||
|
│ │ │ Return FHIR JSON │
|
||||||
|
└──────────────┘ └──────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Supported Resources and Interactions
|
||||||
|
|
||||||
|
The `CapabilityStatement` (available at `GET /fhir/R4/metadata`, no auth required) declares:
|
||||||
|
|
||||||
|
| Resource | Create | Read | Search |
|
||||||
|
|----------|--------|------|--------|
|
||||||
|
| Patient | ✓ | ✓ | ✓ (by identifier) |
|
||||||
|
| Encounter | ✓ | ✓ | ✓ (by patient, status) |
|
||||||
|
| Observation | ✓ | | |
|
||||||
|
| MedicationAdministration | ✓ | | |
|
||||||
|
| Bundle (transaction) | ✓ | | |
|
||||||
|
|
||||||
|
All FHIR endpoints use `application/fhir+json` as the content type (not `application/json`), following the FHIR specification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LOINC Code Mapping
|
||||||
|
|
||||||
|
FHIR Observations use standardized codes (LOINC, SNOMED CT) to identify what's being measured. The internal data model uses simpler codes like `"HEART_RATE"`. The `LoincCodeMapper` translates between them:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class LoincCodeMapper
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, LoincMapping> _map = new()
|
||||||
|
{
|
||||||
|
["8867-4"] = new("HEART_RATE", "/min"),
|
||||||
|
["8310-5"] = new("TEMP_C", "Cel", AllowFahrenheit: true),
|
||||||
|
["2823-3"] = new("POTASSIUM_MEQ_L", "mmol/L"),
|
||||||
|
["2708-6"] = new("SPO2", "%"),
|
||||||
|
["9279-1"] = new("RESP_RATE", "/min"),
|
||||||
|
["6690-2"] = new("WBC_K_UL", "10*3/uL"),
|
||||||
|
["8480-6"] = new("SYSTOLIC_BP", "mm[Hg]"),
|
||||||
|
["8462-4"] = new("DIASTOLIC_BP", "mm[Hg]"),
|
||||||
|
["2524-7"] = new("LACTATE_MMOL_L", "mmol/L"),
|
||||||
|
["777-3"] = new("PLATELET_K_UL", "10*3/uL"),
|
||||||
|
["1975-2"] = new("BILIRUBIN_MG_DL", "mg/dL"),
|
||||||
|
["2160-0"] = new("CREATININE_MG_DL", "mg/dL"),
|
||||||
|
["2703-7"] = new("PAO2_MMHG", "mm[Hg]"),
|
||||||
|
["80288-7"] = new("GCS_EYE", "{score}"),
|
||||||
|
["80289-5"] = new("GCS_VERBAL", "{score}"),
|
||||||
|
["80290-3"] = new("GCS_MOTOR", "{score}"),
|
||||||
|
// ... 19 mappings total
|
||||||
|
};
|
||||||
|
|
||||||
|
// SNOMED CT fallbacks for systems that don't use LOINC
|
||||||
|
private static readonly Dictionary<string, LoincMapping> _snomedMap = new()
|
||||||
|
{
|
||||||
|
["364075005"] = new("HEART_RATE", "/min"),
|
||||||
|
["431314004"] = new("SPO2", "%"),
|
||||||
|
["86290005"] = new("RESP_RATE", "/min"),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool TryMap(string system, string code, out LoincMapping mapping)
|
||||||
|
{
|
||||||
|
if (system.Contains("loinc") && _map.TryGetValue(code, out mapping!))
|
||||||
|
return true;
|
||||||
|
if (system.Contains("snomed") && _snomedMap.TryGetValue(code, out mapping!))
|
||||||
|
return true;
|
||||||
|
mapping = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When a FHIR Observation arrives with code `system: "http://loinc.org"`, `code: "8867-4"`, the mapper translates it to internal code `"HEART_RATE"` with expected unit `"/min"`.
|
||||||
|
|
||||||
|
### Unit Conversion
|
||||||
|
|
||||||
|
Some hospitals send temperatures in Fahrenheit. The `FhirUnitConverter` handles this:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static (decimal Value, string Unit) Normalize(
|
||||||
|
string internalCode, decimal value, string? fhirUnit, bool allowFahrenheit)
|
||||||
|
{
|
||||||
|
if (internalCode == "TEMP_C" && allowFahrenheit &&
|
||||||
|
(fhirUnit == "[degF]" || fhirUnit == "degF"))
|
||||||
|
{
|
||||||
|
var celsius = (value - 32m) * 5m / 9m;
|
||||||
|
return (Math.Round(celsius, 2), "Cel");
|
||||||
|
}
|
||||||
|
return (value, fhirUnit ?? "1");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The internal system always stores temperature in Celsius. If a FHIR Observation arrives in Fahrenheit, it's converted transparently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Observation Mapper
|
||||||
|
|
||||||
|
The `ObservationFhirMapper` handles the most complex mapping because FHIR Observations can have multiple formats:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<IReadOnlyList<MappedObservation>> ToIngestRequestsAsync(
|
||||||
|
Hl7.Fhir.Model.Observation fhir)
|
||||||
|
{
|
||||||
|
var encounterId = await _refs.ResolveEncounterReferenceAsync(fhir.Encounter);
|
||||||
|
var source = MapSource(fhir); // vital-signs → Device, laboratory → Lab
|
||||||
|
var recordedAt = fhir.Effective.ToUtcDateTimeOffset() ?? DateTimeOffset.UtcNow;
|
||||||
|
var idempotencyKey = fhir.Identifier?.FirstOrDefault()?.Value ?? fhir.Id;
|
||||||
|
|
||||||
|
// FHIR Observations can be single-value or multi-component
|
||||||
|
if (fhir.Component?.Count > 0)
|
||||||
|
{
|
||||||
|
// Multi-component (e.g., blood pressure with systolic + diastolic)
|
||||||
|
foreach (var component in fhir.Component)
|
||||||
|
results.AddRange(MapSingleCoding(component.Code, component.Value, ...));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Single value (e.g., heart rate)
|
||||||
|
results.AddRange(MapSingleCoding(fhir.Code, fhir.Value, ...));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A FHIR blood pressure Observation has two **components** (systolic and diastolic). The mapper produces two internal observations from one FHIR resource.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Transaction Bundle Processing
|
||||||
|
|
||||||
|
A transaction Bundle groups multiple resources into one atomic operation — like a database transaction. The `FhirBundleProcessor` handles this:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<Bundle> ProcessTransactionAsync(Bundle transaction)
|
||||||
|
{
|
||||||
|
var response = new Bundle { Type = Bundle.BundleType.TransactionResponse };
|
||||||
|
|
||||||
|
// Sort entries: Patient first, then Encounter, then everything else
|
||||||
|
var entries = transaction.Entry
|
||||||
|
.OrderBy(e => Priority(e.Resource))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await using var tx = await _db.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var location = resource switch
|
||||||
|
{
|
||||||
|
Patient p => await ProcessPatientAsync(p),
|
||||||
|
Encounter e => await ProcessEncounterAsync(e),
|
||||||
|
Observation o => await ProcessObservationAsync(o),
|
||||||
|
MedicationAdministration m => await ProcessMedAsync(m),
|
||||||
|
_ => throw new FhirMappingException("Unsupported resource type")
|
||||||
|
};
|
||||||
|
|
||||||
|
response.Entry.Add(new Bundle.EntryComponent
|
||||||
|
{
|
||||||
|
Response = new Bundle.ResponseComponent
|
||||||
|
{ Status = "201 Created", Location = location }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await tx.RollbackAsync(); // all-or-nothing
|
||||||
|
response.Entry.Add(/* error entry */);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.CommitAsync();
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why sort by priority?** An Encounter references a Patient, and an Observation references an Encounter. If the Bundle contains all three, the Patient must be created first (so the Encounter can reference it), then the Encounter (so the Observation can reference it). The `Priority()` function ensures this ordering.
|
||||||
|
|
||||||
|
**All-or-nothing**: If any entry fails, the entire transaction rolls back — no partially-created data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FHIR Error Responses: OperationOutcome
|
||||||
|
|
||||||
|
FHIR has its own error format. Instead of the project's standard `ApiResponse<T>` envelope, FHIR endpoints return `OperationOutcome` resources:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class FhirOperationOutcomeBuilder
|
||||||
|
{
|
||||||
|
public static OperationOutcome FromException(Exception ex) => ex switch
|
||||||
|
{
|
||||||
|
FhirMappingException fme => Create(fme.HttpStatus, fme.FhirIssueCode, fme.Message),
|
||||||
|
NotFoundException => Create(404, "not-found", ex.Message),
|
||||||
|
ValidationException => Create(422, "invalid", ex.Message),
|
||||||
|
ConflictException => Create(409, "conflict", ex.Message),
|
||||||
|
_ => Create(500, "exception", "An unexpected error occurred.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `FhirExceptionFilter` catches exceptions on `/fhir/*` paths and converts them to `OperationOutcome` responses with `Content-Type: application/fhir+json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"resourceType": "OperationOutcome",
|
||||||
|
"issue": [{
|
||||||
|
"severity": "warning",
|
||||||
|
"code": "not-found",
|
||||||
|
"diagnostics": "Patient not found."
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## External Identifier Resolution
|
||||||
|
|
||||||
|
When a FHIR Encounter references a Patient as `"subject": {"reference": "Patient/MRN-001"}`, the system needs to find the internal UUID for that patient. The `ExternalResourceIdentifier` table maps between external identifiers (hospital MRNs, visit numbers) and internal UUIDs:
|
||||||
|
|
||||||
|
| resource_type | system | value | internal_id |
|
||||||
|
|--------------|--------|-------|-------------|
|
||||||
|
| Patient | `http://hospital.example/mrn` | MRN-001 | `3fa85f64-...` |
|
||||||
|
| Encounter | `http://hospital.example/visit` | VISIT-100 | `7e4b2a1f-...` |
|
||||||
|
|
||||||
|
This mapping is created when a resource is first ingested and used for all subsequent references. It's what makes the FHIR endpoints **idempotent** — sending the same Patient twice (same identifier) updates the existing record instead of creating a duplicate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication for FHIR Endpoints
|
||||||
|
|
||||||
|
FHIR endpoints accept two authentication methods (see Guide 15):
|
||||||
|
|
||||||
|
1. **JWT token** — for admin users accessing via the dashboard or Swagger
|
||||||
|
2. **API key** — for integration engines like Mirth Connect
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /fhir/R4/Observation
|
||||||
|
X-Api-Key: dev-integration-key-change-in-production
|
||||||
|
Content-Type: application/fhir+json
|
||||||
|
```
|
||||||
|
|
||||||
|
The FHIR API key creates an identity with the `Integration` role, which has permissions for `fhir:ingest`, `fhir:read`, `patients:write`, `encounters:write`, `observations:ingest`, and `medications:write`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Takeaways
|
||||||
|
|
||||||
|
- **FHIR is the healthcare data standard** — it defines how to represent patients, encounters, observations, and medications as JSON resources with standard coding systems
|
||||||
|
- **A facade translates, not stores** — the internal database uses its own schema; the FHIR layer maps between FHIR resources and internal entities
|
||||||
|
- **LOINC and SNOMED CT codes are translated to internal codes** — `"8867-4"` (LOINC) becomes `"HEART_RATE"` internally; SNOMED fallbacks handle systems that don't use LOINC
|
||||||
|
- **Transaction Bundles are atomic** — Patient → Encounter → Observation ordering is enforced, and any failure rolls back the entire bundle
|
||||||
|
- **OperationOutcome is the FHIR error format** — FHIR endpoints return structured error resources instead of the project's standard API envelope
|
||||||
|
- **External identifiers enable idempotency** — the same resource sent twice (same identifier) updates rather than duplicates
|
||||||
|
- **Unit conversion happens transparently** — Fahrenheit temperatures are converted to Celsius during mapping so the internal system works with a single unit
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
# Guide 20: OpenAPI / Swagger Documentation
|
||||||
|
|
||||||
|
## What is OpenAPI and Swagger?
|
||||||
|
|
||||||
|
**OpenAPI** (formerly called Swagger Specification) is a standard format for describing REST APIs. An OpenAPI specification is a JSON or YAML file that lists every endpoint, its parameters, request/response shapes, authentication requirements, and error codes. Think of it as a machine-readable instruction manual for your API.
|
||||||
|
|
||||||
|
**Swagger** is a set of tools that work with OpenAPI specifications:
|
||||||
|
- **Swagger UI**: A web-based interactive API explorer. Developers can browse endpoints, see parameter descriptions, and make live test requests — all from the browser, without writing any code.
|
||||||
|
- **Swashbuckle**: A .NET library that automatically generates the OpenAPI specification from your controller code and XML documentation comments, and hosts Swagger UI.
|
||||||
|
|
||||||
|
**Why does this matter?** Without API documentation, frontend developers need to read the backend C# code (or ask the backend developer) to understand how to call each endpoint. With Swagger UI, they open a browser, see every endpoint, and can test them immediately. The documentation stays in sync with the code automatically because it's generated from the code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup in Program.cs
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen(options =>
|
||||||
|
{
|
||||||
|
// Basic API information
|
||||||
|
options.SwaggerDoc("v1", new OpenApiInfo
|
||||||
|
{
|
||||||
|
Title = "VigilCare Clinical API",
|
||||||
|
Version = "v1",
|
||||||
|
Description = "Clinical monitoring and alerting platform API"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tell Swagger UI that the API requires a JWT Bearer token
|
||||||
|
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Name = "Authorization",
|
||||||
|
Type = SecuritySchemeType.Http,
|
||||||
|
Scheme = "bearer",
|
||||||
|
BearerFormat = "JWT",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Description = "Enter your JWT token"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply the Bearer requirement to all endpoints by default
|
||||||
|
options.AddSecurityRequirement(document => new OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
{
|
||||||
|
new OpenApiSecuritySchemeReference("Bearer", document),
|
||||||
|
new List<string>()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Include XML documentation comments from the compiled assembly
|
||||||
|
var xmlPath = Path.Combine(AppContext.BaseDirectory,
|
||||||
|
$"{Assembly.GetExecutingAssembly().GetName().Name}.xml");
|
||||||
|
options.IncludeXmlComments(xmlPath);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**What does each part do?**
|
||||||
|
|
||||||
|
| Setting | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `AddEndpointsApiExplorer()` | Enables the metadata extraction that Swashbuckle needs to discover your endpoints |
|
||||||
|
| `SwaggerDoc("v1", ...)` | Names the API spec "v1" with a title and description shown at the top of Swagger UI |
|
||||||
|
| `AddSecurityDefinition("Bearer", ...)` | Adds an "Authorize" button to Swagger UI where developers can paste their JWT token |
|
||||||
|
| `AddSecurityRequirement(...)` | Shows a lock icon on every endpoint, indicating authentication is required |
|
||||||
|
| `IncludeXmlComments(xmlPath)` | Reads the `///` XML doc comments from your C# code and displays them as endpoint descriptions |
|
||||||
|
|
||||||
|
### Enabling Swagger UI
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
app.UseSwagger(); // Serves the OpenAPI spec at /swagger/v1/swagger.json
|
||||||
|
app.UseSwaggerUI(options =>
|
||||||
|
{
|
||||||
|
options.SwaggerEndpoint("/swagger/v1/swagger.json", "VigilCare Clinical API v1");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Available at: `http://localhost:5270/swagger/ui`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Writing Good Swagger Documentation
|
||||||
|
|
||||||
|
Swagger UI shows two things about each endpoint: information from your C# code attributes, and information from XML documentation comments.
|
||||||
|
|
||||||
|
### Controller and Action Attributes
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/v1/alert-thresholds")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
[Authorize]
|
||||||
|
public class AlertThresholdsController : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpPost]
|
||||||
|
[AuthorizePermission(ClinicalPermissions.ThresholdsWrite)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<AlertThreshold>), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status409Conflict)]
|
||||||
|
public async Task<IActionResult> Create([FromBody] AlertThresholdRequest req)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`[Produces("application/json")]`** tells Swagger the response format
|
||||||
|
- **`[ProducesResponseType(...)]`** documents which status codes the endpoint can return and what the response body looks like. Swagger UI shows these as expandable response examples.
|
||||||
|
- **`[FromBody]`** tells Swagger the request body schema comes from `AlertThresholdRequest`
|
||||||
|
|
||||||
|
### XML Documentation Comments
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new alert threshold for an observation code.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="req">Threshold bounds and display metadata.</param>
|
||||||
|
/// <returns>The created threshold.</returns>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Create([FromBody] AlertThresholdRequest req)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `<summary>` appears as the endpoint description in Swagger UI. The `<param>` tags describe individual parameters. The `<returns>` tag describes the response.
|
||||||
|
|
||||||
|
**How does this work?** When you build a C# project with `<GenerateDocumentationFile>true</GenerateDocumentationFile>` in the `.csproj` file, the compiler creates an XML file containing all `///` comments. Swashbuckle reads this XML file at runtime and merges the comments into the OpenAPI specification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Swagger UI Shows
|
||||||
|
|
||||||
|
When you open `http://localhost:5270/swagger/ui`, you see:
|
||||||
|
|
||||||
|
1. **API title and description** from `SwaggerDoc()`
|
||||||
|
2. **Grouped endpoints** organized by controller (Encounters, Observations, Alert Thresholds, FHIR, etc.)
|
||||||
|
3. **For each endpoint**:
|
||||||
|
- HTTP method and URL (e.g., `POST /api/v1/encounters/{encounterId}/observations`)
|
||||||
|
- Summary from `<summary>` XML comment
|
||||||
|
- Parameter descriptions from `<param>` XML comments
|
||||||
|
- Request body schema (auto-generated from the C# request class)
|
||||||
|
- Response schemas for each status code (from `[ProducesResponseType]`)
|
||||||
|
4. **"Authorize" button** — paste a JWT token to authenticate all subsequent requests
|
||||||
|
5. **"Try it out" button** — fill in parameters and execute real requests against the running API
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Scheme in Swagger UI
|
||||||
|
|
||||||
|
The security definition adds an "Authorize" button at the top of Swagger UI:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Name = "Authorization",
|
||||||
|
Type = SecuritySchemeType.Http,
|
||||||
|
Scheme = "bearer",
|
||||||
|
BearerFormat = "JWT",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Description = "Enter your JWT token"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**How to use it:**
|
||||||
|
1. Call `POST /api/v1/auth/login` with username/password to get a token
|
||||||
|
2. Click "Authorize" in Swagger UI
|
||||||
|
3. Paste the token (without the "Bearer " prefix — Swagger adds it automatically)
|
||||||
|
4. Click "Authorize" — all subsequent requests include the `Authorization: Bearer <token>` header
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Generated OpenAPI Specification
|
||||||
|
|
||||||
|
The raw specification is available at `/swagger/v1/swagger.json`. It's a standard OpenAPI 3.0 document that can be consumed by:
|
||||||
|
|
||||||
|
- **Code generators**: Generate API client libraries for TypeScript, Python, Java, etc. using tools like `openapi-generator` or `nswag`
|
||||||
|
- **Testing tools**: Import into Postman, Insomnia, or Bruno for manual testing
|
||||||
|
- **Documentation platforms**: Host on ReadMe, Stoplight, or Redocly for public documentation
|
||||||
|
- **Contract testing**: Validate that the API implementation matches the specification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Takeaways
|
||||||
|
|
||||||
|
- **Swagger UI makes your API self-documenting** — developers can explore and test endpoints from a browser without reading C# code
|
||||||
|
- **Write `<summary>` comments on every controller action** — they become the endpoint descriptions in Swagger UI. Without them, endpoints are listed without any explanation.
|
||||||
|
- **Use `[ProducesResponseType]` for every status code** — this documents the possible responses and their shapes, making it clear what success and error responses look like
|
||||||
|
- **The security definition enables authenticated testing** — developers can paste a JWT token in the UI and test protected endpoints without using curl or Postman
|
||||||
|
- **The OpenAPI spec is a machine-readable contract** — frontend teams can generate TypeScript API clients from it, ensuring type-safe API calls without manual typing
|
||||||
|
- **Documentation stays in sync with code** — because it's generated from the actual controller code and attributes, it can never go stale (unlike a manually-written Wiki page)
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
# Guide 21: Input Validation with FluentValidation
|
||||||
|
|
||||||
|
## What is Input Validation?
|
||||||
|
|
||||||
|
Every time your API receives data from the outside world — a request body, a query parameter, a header — you need to check that the data is valid before processing it. Does the observation code exist? Is the value within a plausible range? Is the required field present? Without validation, invalid data can corrupt your database, crash your application, or produce nonsensical clinical alerts.
|
||||||
|
|
||||||
|
**Input validation** is the practice of checking incoming data at the system boundary (where your application meets the outside world) and rejecting anything that doesn't meet the rules. It's your first line of defense.
|
||||||
|
|
||||||
|
## What is FluentValidation?
|
||||||
|
|
||||||
|
**FluentValidation** is a .NET library that lets you define validation rules using a readable, method-chain syntax (a "fluent" API). Instead of writing validation logic inside your controllers with lots of `if` statements, you create a dedicated validator class for each request type:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Without FluentValidation — verbose, mixed with controller logic
|
||||||
|
if (string.IsNullOrEmpty(req.ObservationCode))
|
||||||
|
return BadRequest("ObservationCode is required");
|
||||||
|
if (req.ObservationCode.Length > 50)
|
||||||
|
return BadRequest("ObservationCode must be 50 chars or fewer");
|
||||||
|
if (req.RecordedAt > DateTimeOffset.UtcNow.AddMinutes(5))
|
||||||
|
return BadRequest("RecordedAt cannot be in the future");
|
||||||
|
|
||||||
|
// With FluentValidation — clean, separate class
|
||||||
|
public class IngestObservationRequestValidator
|
||||||
|
: AbstractValidator<IngestObservationRequest>
|
||||||
|
{
|
||||||
|
public IngestObservationRequestValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.ObservationCode).NotEmpty().MaximumLength(50);
|
||||||
|
RuleFor(x => x.Unit).NotEmpty().MaximumLength(20);
|
||||||
|
RuleFor(x => x.RecordedAt)
|
||||||
|
.LessThanOrEqualTo(DateTimeOffset.UtcNow.AddMinutes(5))
|
||||||
|
.WithMessage("RecordedAt cannot be more than 5 minutes in the future.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The FluentValidation version is more readable, testable (you can unit test the validator), and separated from the controller logic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How FluentValidation Works
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP Request arrives
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌───────────────┐ model binding ┌──────────────────┐
|
||||||
|
│ ASP.NET Core │ ──────────────────► │ Request DTO │
|
||||||
|
│ deserializes │ (JSON → C# obj) │ (e.g. Ingest │
|
||||||
|
│ request body │ │ ObservationReq) │
|
||||||
|
└───────────────┘ └────────┬─────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐
|
||||||
|
│ FluentValidation │
|
||||||
|
│ auto-validates │
|
||||||
|
│ (finds matching │
|
||||||
|
│ validator class) │
|
||||||
|
└────────┬─────────┘
|
||||||
|
│
|
||||||
|
┌───────────────┴──────────────┐
|
||||||
|
│ │
|
||||||
|
Valid? Invalid?
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌────────────────┐ ┌─────────────────┐
|
||||||
|
│ Controller │ │ 400 Bad Request │
|
||||||
|
│ action runs │ │ with field-level │
|
||||||
|
│ │ │ error details │
|
||||||
|
└────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### NuGet Package
|
||||||
|
|
||||||
|
FluentValidation is already referenced through the ASP.NET Core integration package. No explicit package reference is needed if you're using the meta-package.
|
||||||
|
|
||||||
|
### Registration in Program.cs
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddFluentValidationAutoValidation();
|
||||||
|
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
|
||||||
|
```
|
||||||
|
|
||||||
|
**What do these two lines do?**
|
||||||
|
|
||||||
|
1. **`AddFluentValidationAutoValidation()`**: Hooks FluentValidation into ASP.NET Core's model binding pipeline. When a request arrives, ASP.NET automatically finds the matching validator and runs it _before_ your controller action executes. If validation fails, the action never runs — a 400 Bad Request response is returned immediately.
|
||||||
|
|
||||||
|
2. **`AddValidatorsFromAssemblyContaining<Program>()`**: Scans the assembly (the compiled project) for all classes that extend `AbstractValidator<T>` and registers them with dependency injection. This means you don't need to register each validator manually — just create a new validator class and it's automatically discovered.
|
||||||
|
|
||||||
|
### Custom Error Response
|
||||||
|
|
||||||
|
When validation fails, ASP.NET Core calls the `InvalidModelStateResponseFactory` to build the error response:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Services.Configure<ApiBehaviorOptions>(options =>
|
||||||
|
{
|
||||||
|
options.InvalidModelStateResponseFactory = context =>
|
||||||
|
{
|
||||||
|
var errors = context.ModelState
|
||||||
|
.Where(e => e.Value!.Errors.Count > 0)
|
||||||
|
.Select(e => new { field = e.Key, errors = e.Value!.Errors.Select(x => x.ErrorMessage) })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new BadRequestObjectResult(
|
||||||
|
ApiResponse<object>.Fail(400, "Validation failed.", "VALIDATION_ERROR"));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures validation errors use the same `ApiResponse<T>` envelope as all other API responses.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Writing Validators
|
||||||
|
|
||||||
|
### Basic Rules
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class IngestObservationRequestValidator
|
||||||
|
: AbstractValidator<IngestObservationRequest>
|
||||||
|
{
|
||||||
|
public IngestObservationRequestValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.ObservationCode).NotEmpty().MaximumLength(50);
|
||||||
|
RuleFor(x => x.Unit).NotEmpty().MaximumLength(20);
|
||||||
|
RuleFor(x => x.RecordedAt)
|
||||||
|
.LessThanOrEqualTo(DateTimeOffset.UtcNow.AddMinutes(5))
|
||||||
|
.WithMessage("RecordedAt cannot be more than 5 minutes in the future.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**What does each rule do?**
|
||||||
|
|
||||||
|
| Rule | What It Checks | Error If |
|
||||||
|
|------|---------------|----------|
|
||||||
|
| `.NotEmpty()` | String is not null, not empty, not whitespace | `""`, `null`, `" "` |
|
||||||
|
| `.MaximumLength(50)` | String is at most 50 characters | `"A" * 51` |
|
||||||
|
| `.GreaterThan(0)` | Number is positive | `0`, `-1` |
|
||||||
|
| `.LessThanOrEqualTo(...)` | Value is at or before a limit | Timestamp in the future |
|
||||||
|
| `.WithMessage(...)` | Custom error message (default messages are generic) | — |
|
||||||
|
| `.When(...)` | Only apply this rule if a condition is true | — |
|
||||||
|
|
||||||
|
### Cross-Field Validation
|
||||||
|
|
||||||
|
Sometimes a rule depends on multiple fields. Alert thresholds must be in order: CriticalLow < WarningLow < WarningHigh < CriticalHigh:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class AlertThresholdRequestValidator
|
||||||
|
: AbstractValidator<AlertThresholdRequest>
|
||||||
|
{
|
||||||
|
public AlertThresholdRequestValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.ObservationCode).NotEmpty().MaximumLength(50);
|
||||||
|
RuleFor(x => x.DisplayName).NotEmpty().MaximumLength(200);
|
||||||
|
RuleFor(x => x.Unit).NotEmpty().MaximumLength(20);
|
||||||
|
|
||||||
|
// Cross-field: threshold ordering
|
||||||
|
RuleFor(x => x)
|
||||||
|
.Must(x => !x.CriticalLow.HasValue || !x.WarningLow.HasValue
|
||||||
|
|| x.CriticalLow < x.WarningLow)
|
||||||
|
.WithMessage("CriticalLow must be less than WarningLow.");
|
||||||
|
|
||||||
|
RuleFor(x => x)
|
||||||
|
.Must(x => !x.WarningLow.HasValue || !x.WarningHigh.HasValue
|
||||||
|
|| x.WarningLow < x.WarningHigh)
|
||||||
|
.WithMessage("WarningLow must be less than WarningHigh.");
|
||||||
|
|
||||||
|
RuleFor(x => x)
|
||||||
|
.Must(x => !x.WarningHigh.HasValue || !x.CriticalHigh.HasValue
|
||||||
|
|| x.WarningHigh < x.CriticalHigh)
|
||||||
|
.WithMessage("WarningHigh must be less than CriticalHigh.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `.Must()` rule takes a lambda that returns `true` (valid) or `false` (invalid). Using `RuleFor(x => x)` applies the rule to the entire object rather than a single property, which is necessary for cross-field checks.
|
||||||
|
|
||||||
|
The `!x.CriticalLow.HasValue ||` guard makes the comparison optional — if a bound is null, the ordering check is skipped.
|
||||||
|
|
||||||
|
### Conditional Rules
|
||||||
|
|
||||||
|
Sometimes a rule only applies if another field has a value:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class CreateMedicationAdministrationRequestValidator
|
||||||
|
: AbstractValidator<CreateMedicationAdministrationRequest>
|
||||||
|
{
|
||||||
|
public CreateMedicationAdministrationRequestValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.DrugName).NotEmpty().MaximumLength(200);
|
||||||
|
RuleFor(x => x.Dose).GreaterThan(0);
|
||||||
|
RuleFor(x => x.DoseUnit).NotEmpty().MaximumLength(20);
|
||||||
|
RuleFor(x => x.Route).NotEmpty().MaximumLength(50);
|
||||||
|
RuleFor(x => x.AdministeredBy).NotEmpty().MaximumLength(100);
|
||||||
|
|
||||||
|
RuleFor(x => x.AdministeredAt)
|
||||||
|
.LessThanOrEqualTo(DateTimeOffset.UtcNow.AddMinutes(5))
|
||||||
|
.When(x => x.AdministeredAt.HasValue)
|
||||||
|
.WithMessage("AdministeredAt cannot be in the future.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`.When(x => x.AdministeredAt.HasValue)` means: "only check if `AdministeredAt` is in the future when it's actually provided." If `AdministeredAt` is null (meaning 'use current time'), the rule is skipped entirely.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validators in This Project
|
||||||
|
|
||||||
|
| Validator | Request Type | Key Rules |
|
||||||
|
|-----------|-------------|-----------|
|
||||||
|
| `IngestObservationRequestValidator` | `IngestObservationRequest` | Code required, unit required, timestamp not in future |
|
||||||
|
| `AlertThresholdRequestValidator` | `AlertThresholdRequest` | Code+name+unit required, threshold ordering (CritLow < WarnLow < WarnHigh < CritHigh) |
|
||||||
|
| `LoginRequestValidator` | `LoginRequest` | Username and password required |
|
||||||
|
| `CreateMedicationAdministrationRequestValidator` | `CreateMedicationAdministrationRequest` | Drug name, dose > 0, unit, route, administered-by required |
|
||||||
|
| `TransitionStatusRequestValidator` | `TransitionStatusRequest` | New status required |
|
||||||
|
| `TransitionOrderStatusRequestValidator` | `TransitionOrderStatusRequest` | New status required |
|
||||||
|
| `AcknowledgeAlertRequestValidator` | `AcknowledgeAlertRequest` | Acknowledged-by required |
|
||||||
|
| `CreateOrderRequestValidator` | `CreateOrderRequest` | Description, type required |
|
||||||
|
| `RecordOrderResultRequestValidator` | `RecordOrderResultRequest` | Result summary required |
|
||||||
|
| `UpdatePatientRequestValidator` | `UpdatePatientRequest` | Name length limits |
|
||||||
|
| `CreateSiteRequestValidator` | `CreateSiteRequest` | Site code and name required |
|
||||||
|
| `RegisterGatewayRequestValidator` | `RegisterGatewayRequest` | Gateway code, site ID, department required |
|
||||||
|
| `GatewayHeartbeatRequestValidator` | `GatewayHeartbeatRequest` | Buffer depth >= 0 |
|
||||||
|
| `SubmitAlertFeedbackRequestValidator` | `SubmitAlertFeedbackRequest` | Rating required, valid rating values |
|
||||||
|
| `FhirPatientUpsertRequestValidator` | `FhirPatientUpsertRequest` | First name, last name, gender required |
|
||||||
|
| `FhirEncounterUpsertRequestValidator` | `FhirEncounterUpsertRequest` | Patient ID, department required |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation vs Domain Rules
|
||||||
|
|
||||||
|
FluentValidation handles **input validation** — basic format and constraint checks at the API boundary. More complex **business rules** are enforced in the service layer:
|
||||||
|
|
||||||
|
| Check | Where | Example |
|
||||||
|
|-------|-------|---------|
|
||||||
|
| "Is the field present?" | Validator | `RuleFor(x => x.DrugName).NotEmpty()` |
|
||||||
|
| "Is the value in the right range?" | Validator | `RuleFor(x => x.Dose).GreaterThan(0)` |
|
||||||
|
| "Is the timestamp format valid?" | Validator | `RuleFor(x => x.RecordedAt).LessThanOrEqualTo(...)` |
|
||||||
|
| "Does the encounter exist?" | Service layer | `_db.Encounters.FindAsync(encounterId)` |
|
||||||
|
| "Is the encounter active?" | Service layer | `if (encounter.Status != Active) throw ...` |
|
||||||
|
| "Is the observation code plausible?" | Service layer | `PlausibilityValidator.IsPlausible(code, value)` |
|
||||||
|
| "Would this create a duplicate?" | Database | Partial unique index on `idempotency_key` |
|
||||||
|
|
||||||
|
The rule of thumb: validators check **format** (shape, size, type). Services check **meaning** (does this entity exist? is this transition valid?). The database checks **integrity** (uniqueness, foreign keys, check constraints). Together, these three layers form a defense-in-depth strategy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Takeaways
|
||||||
|
|
||||||
|
- **Validate at the boundary** — check input as soon as it enters your system, before any database queries or business logic run
|
||||||
|
- **Separate validators from controllers** — each request type gets its own validator class, keeping controllers clean and validators testable
|
||||||
|
- **Auto-validation means zero boilerplate** — `AddFluentValidationAutoValidation()` + `AddValidatorsFromAssemblyContaining<>()` makes validation automatic. Create a validator class; it's discovered and applied without any additional wiring.
|
||||||
|
- **Cross-field rules use `.Must()`** — when a rule depends on multiple fields (like threshold ordering), apply the rule to the entire object
|
||||||
|
- **Conditional rules use `.When()`** — skip a rule when the field is null or when a condition isn't met
|
||||||
|
- **Validators are not the only defense** — they catch format issues. Service-layer logic catches business rule violations. Database constraints catch integrity violations. All three layers matter.
|
||||||
+130
-1
@@ -18,6 +18,7 @@ Think of it as a flight simulator, but for clinical decision support.
|
|||||||
8. [Creating Your Own Scenarios](#8-creating-your-own-scenarios)
|
8. [Creating Your Own Scenarios](#8-creating-your-own-scenarios)
|
||||||
9. [Troubleshooting](#9-troubleshooting)
|
9. [Troubleshooting](#9-troubleshooting)
|
||||||
10. [Ward Outage Scenario (Climate Resilience)](#10-ward-outage-scenario-climate-resilience)
|
10. [Ward Outage Scenario (Climate Resilience)](#10-ward-outage-scenario-climate-resilience)
|
||||||
|
11. [MIMIC-IV Real Patient Data](#11-mimic-iv-real-patient-data)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ That's it! Read on for more detail.
|
|||||||
|
|
||||||
## 3. Available Commands
|
## 3. Available Commands
|
||||||
|
|
||||||
The simulator has four commands. All are run with `dotnet run --project VigilCare.Simulator -- <command>`.
|
The simulator has six commands. All are run with `dotnet run --project VigilCare.Simulator -- <command>`.
|
||||||
|
|
||||||
### replay -- Run a scenario against the API
|
### replay -- Run a scenario against the API
|
||||||
|
|
||||||
@@ -137,6 +138,62 @@ dotnet run --project VigilCare.Simulator -- dry-run \
|
|||||||
VigilCare.Simulator/Scenarios/List/cardiac-arrest-post-mi-01.json
|
VigilCare.Simulator/Scenarios/List/cardiac-arrest-post-mi-01.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### mimic-list -- Browse available MIMIC-IV patients and ICU stays
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run --project VigilCare.Simulator -- mimic-list <mimic-data-dir> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
Displays a table of all ICU stays in the MIMIC-IV dataset with patient demographics, care unit, length of stay, and outcome. Does **not** contact the API.
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `--subject-id <int>` | Filter to a specific patient |
|
||||||
|
| `--stay-id <int>` | Filter to a specific ICU stay |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run --project VigilCare.Simulator -- mimic-list docs/MIMIC-IV/
|
||||||
|
```
|
||||||
|
|
||||||
|
### mimic-generate -- Generate a scenario from real MIMIC-IV data
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run --project VigilCare.Simulator -- mimic-generate <mimic-data-dir> --stay-id <int> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
Reads MIMIC-IV CSV files and generates a VigilCare scenario JSON from a specific ICU stay. The generated file is fully compatible with `replay`, `validate`, and `dry-run`. Does **not** contact the API.
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `--stay-id <int>` | (required) | ICU stay ID to generate scenario for |
|
||||||
|
| `--max-hours <int>` | full stay | Limit scenario duration (real ICU stays can be days/weeks) |
|
||||||
|
| `--no-medications` | off | Exclude medication events |
|
||||||
|
| `--no-labs` | off | Exclude lab observations |
|
||||||
|
| `--output <path>` | `Scenarios/List/mimic-s{stayId}.json` | Custom output file path |
|
||||||
|
| `--validate` | off | Run scenario validation after generation |
|
||||||
|
|
||||||
|
**Example -- generate a 24-hour CVICU scenario:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run --project VigilCare.Simulator -- mimic-generate docs/MIMIC-IV/ \
|
||||||
|
--stay-id 32604416 --max-hours 24 --validate
|
||||||
|
```
|
||||||
|
|
||||||
|
Then replay it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run --project VigilCare.Simulator -- replay \
|
||||||
|
VigilCare.Simulator/Scenarios/List/mimic-s32604416.json --speed 0 --poll
|
||||||
|
```
|
||||||
|
|
||||||
|
See [MIMIC-IV Real Patient Data](#11-mimic-iv-real-patient-data) for the full guide.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Understanding Scenarios
|
## 4. Understanding Scenarios
|
||||||
@@ -489,3 +546,75 @@ The `--gateway` flag targets `http://localhost:5081` automatically. `--encounter
|
|||||||
- Critical potassium alert created on gateway at T+45 min while central is down
|
- Critical potassium alert created on gateway at T+45 min while central is down
|
||||||
- Ack recorded locally at T+50 min with `acknowledged_by` = `RN-Wu`
|
- Ack recorded locally at T+50 min with `acknowledged_by` = `RN-Wu`
|
||||||
- After reconnect: central has observations, alert, and ack; no duplicate paging logs
|
- After reconnect: central has observations, alert, and ack; no duplicate paging logs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. MIMIC-IV Real Patient Data
|
||||||
|
|
||||||
|
The simulator can generate scenarios from **real de-identified ICU data** from MIT's MIMIC-IV dataset (PhysioNet). This bridges the gap from synthetic scenarios to actual clinical records — a critical validation step for scoring engines and alert logic.
|
||||||
|
|
||||||
|
The MIMIC-IV CSV files in `docs/MIMIC-IV/` contain 100 patients, 140 ICU stays, 668K chart events, and 107K lab events. The generator reads these files and produces standard scenario JSON files that replay through VigilCare's full scoring pipeline (NEWS2, SOFA, GCS, qSOFA, trend detection, alerting).
|
||||||
|
|
||||||
|
### Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Browse available ICU stays
|
||||||
|
dotnet run --project VigilCare.Simulator -- mimic-list docs/MIMIC-IV/
|
||||||
|
|
||||||
|
# 2. Generate a 24-hour scenario from a CVICU patient
|
||||||
|
dotnet run --project VigilCare.Simulator -- mimic-generate docs/MIMIC-IV/ \
|
||||||
|
--stay-id 32604416 --max-hours 24 --validate
|
||||||
|
|
||||||
|
# 3. Preview the timeline
|
||||||
|
dotnet run --project VigilCare.Simulator -- dry-run \
|
||||||
|
VigilCare.Simulator/Scenarios/List/mimic-s32604416.json
|
||||||
|
|
||||||
|
# 4. Replay against the live API
|
||||||
|
dotnet run --project VigilCare.Simulator -- replay \
|
||||||
|
VigilCare.Simulator/Scenarios/List/mimic-s32604416.json --speed 0 --poll
|
||||||
|
```
|
||||||
|
|
||||||
|
### What gets generated
|
||||||
|
|
||||||
|
The generator maps MIMIC-IV data to VigilCare observations:
|
||||||
|
|
||||||
|
| MIMIC Source | VigilCare Codes |
|
||||||
|
|---|---|
|
||||||
|
| **Vital signs** (chartevents) | HEART_RATE, RESP_RATE, SYSTOLIC_BP, DIASTOLIC_BP, TEMP_C, SPO2, FIO2_PCT |
|
||||||
|
| **GCS** (chartevents, text labels) | GCS_EYE, GCS_VERBAL, GCS_MOTOR |
|
||||||
|
| **Labs** (labevents) | CREATININE_MG_DL, PLATELET_K_UL, BILIRUBIN_MG_DL, LACTATE_MMOL_L, WBC_K_UL, POTASSIUM_MEQ_L, GLUCOSE_MG_DL, PAO2_MMHG |
|
||||||
|
| **Prescriptions** | Medication events (drug name, dose, route) |
|
||||||
|
|
||||||
|
The generated scenario has no `expectedOutcomes` — these are exploratory replays. The alerts and scores that fire are the real output, driven by actual patient trajectories.
|
||||||
|
|
||||||
|
### Useful options
|
||||||
|
|
||||||
|
| Option | Effect |
|
||||||
|
|--------|--------|
|
||||||
|
| `--max-hours 24` | Cap the scenario at 24 hours (real ICU stays can be weeks) |
|
||||||
|
| `--no-medications` | Exclude medication events for a cleaner vitals-only replay |
|
||||||
|
| `--no-labs` | Exclude lab observations (vitals + GCS only) |
|
||||||
|
| `--validate` | Run `ScenarioValidator` after generation and print results |
|
||||||
|
| `--output my-file.json` | Write to a custom path instead of `Scenarios/List/` |
|
||||||
|
|
||||||
|
### Data handling notes
|
||||||
|
|
||||||
|
- **GCS:** MIMIC stores GCS as text labels ("Obeys Commands", "To Speech", etc.). The generator reads the numeric `valuenum` column; if missing, falls back to a text-to-numeric lookup dictionary.
|
||||||
|
- **Temperature:** Fahrenheit readings (item 223761) are converted to Celsius. Celsius readings (item 223762) pass through.
|
||||||
|
- **Blood pressure:** When both non-invasive (NBP) and arterial (ABP) readings exist at the same time, the generator keeps non-invasive and discards arterial. ABP is used only when no NBP is available.
|
||||||
|
- **Cluster limits:** The VigilCare API accepts at most 10 observations per batch. When a MIMIC timestamp has more than 10 readings, the generator keeps vital signs first and spills labs to the next minute offset.
|
||||||
|
- **Patient identity:** De-identified names (`MIMIC-{subjectId}` / `S{hadmId}`). Date of birth approximated from `anchor_age` and `anchor_year`.
|
||||||
|
|
||||||
|
### Finding interesting patients
|
||||||
|
|
||||||
|
Use `mimic-list` with filters to find specific cases:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find all stays for a specific patient
|
||||||
|
dotnet run --project VigilCare.Simulator -- mimic-list docs/MIMIC-IV/ --subject-id 10005817
|
||||||
|
|
||||||
|
# Look for a specific stay
|
||||||
|
dotnet run --project VigilCare.Simulator -- mimic-list docs/MIMIC-IV/ --stay-id 32604416
|
||||||
|
```
|
||||||
|
|
||||||
|
The table shows care unit, length of stay, and whether the patient expired -- useful for finding clinically interesting trajectories to replay.
|
||||||
|
|||||||
Reference in New Issue
Block a user