diff --git a/models/marts/_marts.yml b/models/marts/_marts.yml index 33c3c70..5314d05 100644 --- a/models/marts/_marts.yml +++ b/models/marts/_marts.yml @@ -20,3 +20,59 @@ models: description: "Provider-level quality metrics" config: materialized: table + + - name: mart_patient_360 + description: > + Unified patient record aggregating demographics, encounter history, + diagnoses, medications, lab results, and financial data into a single + patient-grain table. SSN is SHA-256 hashed for HIPAA compliance. + config: + materialized: table + schema: marts_pii + meta: + contains_pii: true + owner: data-governance + compliance: HIPAA + columns: + - name: patient_id + description: "Primary key — unique patient medical record number" + - name: patient_name + description: "Patient full name from registration (PII)" + - name: date_of_birth + description: "Patient date of birth (PII/PHI)" + - name: blood_type + description: "Patient blood type" + - name: insurance_id + description: "Insurance policy identifier" + - name: ssn_hash + description: "SHA-256 hash of SSN — raw SSN is never exposed downstream" + - name: phone + description: "Patient phone number (PII)" + - name: email + description: "Patient email address (PII)" + - name: address + description: "Patient mailing address (PII)" + - name: primary_provider_name + description: "Name of the most recent attending provider, derived from stg_providers via encounter history" + - name: total_encounters + description: "Count of all encounters for this patient" + - name: first_encounter_date + description: "Date of the patient's earliest encounter" + - name: last_encounter_date + description: "Date of the patient's most recent encounter" + - name: unique_diagnosis_count + description: "Count of distinct ICD diagnosis codes across all encounters" + - name: primary_diagnosis_codes + description: "Comma-separated list of all distinct diagnosis codes" + - name: unique_medication_count + description: "Count of distinct medications prescribed across all encounters" + - name: total_lab_tests + description: "Count of all lab tests ordered for this patient" + - name: abnormal_lab_count + description: "Count of lab results flagged as abnormal" + - name: lifetime_charges_dollars + description: "Total billed amount across all claims, converted from cents to dollars" + - name: lifetime_paid_dollars + description: "Total paid amount across all claims, converted from cents to dollars" + - name: patient_registered_at + description: "Timestamp when the patient record was created" diff --git a/models/marts/mart_patient_360.sql b/models/marts/mart_patient_360.sql index 549b857..9cd6a7f 100644 --- a/models/marts/mart_patient_360.sql +++ b/models/marts/mart_patient_360.sql @@ -7,69 +7,172 @@ 'owner': 'data-governance', 'compliance': 'HIPAA' } -) }}with patients as ( +) }} + +with patients as ( select * from {{ ref('stg_patients') }} -) -, +), encounters as ( select * from {{ ref('stg_encounters') }} -) -, +), + +providers as ( + + select * from {{ ref('stg_providers') }} + +), diagnoses as ( select * from {{ ref('stg_diagnoses') }} -) -, +), medications as ( select * from {{ ref('stg_medications') }} -) -, +), lab_results as ( select * from {{ ref('stg_lab_results') }} -) -, +), + +claims as ( + + select * from {{ ref('stg_claims') }} + +), + +-- Pre-aggregate encounters to patient grain to prevent cartesian explosion +encounter_summary as ( + + select + enc.patient_id, + count(*) as total_encounters, + min(enc.admission_date) as first_encounter_date, + max(enc.admission_date) as last_encounter_date, + -- Get the primary provider from the most recent encounter + max(prov.full_name) as primary_provider_name + from encounters as enc + left join providers as prov + on enc.attending_physician_id = prov.provider_id + group by enc.patient_id + +), + +-- Pre-aggregate diagnoses to patient grain +diagnosis_summary as ( + + select + enc.patient_id, + count(distinct diag.diagnosis_code) as unique_diagnosis_count, + listagg(distinct diag.diagnosis_code, ', ') as primary_diagnosis_codes + from diagnoses as diag + inner join encounters as enc + on diag.encounter_id = enc.encounter_id + group by enc.patient_id + +), + +-- Pre-aggregate medications to patient grain +medication_summary as ( + + select + enc.patient_id, + count(distinct med.medication_name) as unique_medication_count + from medications as med + inner join encounters as enc + on med.encounter_id = enc.encounter_id + group by enc.patient_id + +), + +-- Pre-aggregate labs to patient grain +lab_summary as ( + + select + patient_id, + count(*) as total_lab_tests, + sum(case when abnormal_flag = 'True' then 1 else 0 end) as abnormal_lab_count + from lab_results + group by patient_id + +), + +-- Pre-aggregate claims to patient grain using cents_to_dollars macro +claims_summary as ( + + select + patient_id, + {{ cents_to_dollars('sum(billed_cents)') }} as lifetime_charges_dollars, + {{ cents_to_dollars('sum(paid_cents)') }} as lifetime_paid_dollars + from claims + group by patient_id + +), final as ( select -- Primary key + patients.patient_id, + + -- Patient demographics + patients.full_name as patient_name, + patients.date_of_birth, + patients.blood_type, + patients.insurance_id, + + -- PII columns (HIPAA: hashed for downstream safety) + sha2(patients.ssn, 256) as ssn_hash, + patients.phone, + patients.email, + patients.address, + + -- Provider context (derived from encounters + providers) + enc_s.primary_provider_name, + + -- Encounter metrics + coalesce(enc_s.total_encounters, 0) as total_encounters, + enc_s.first_encounter_date, + enc_s.last_encounter_date, + + -- Diagnosis metrics + coalesce(dx_s.unique_diagnosis_count, 0) as unique_diagnosis_count, + dx_s.primary_diagnosis_codes, - -- Dimensions + -- Medication metrics + coalesce(med_s.unique_medication_count, 0) as unique_medication_count, + -- Lab metrics + coalesce(lab_s.total_lab_tests, 0) as total_lab_tests, + coalesce(lab_s.abnormal_lab_count, 0) as abnormal_lab_count, - -- PII columns (restricted access) - -- HIPAA: These fields require role-based access control - full_name, - ssn, - date_of_birth, - phone, - email, - address, - diagnoses.diagnosis_code + -- Financial summary + coalesce(clm_s.lifetime_charges_dollars, 0) as lifetime_charges_dollars, + coalesce(clm_s.lifetime_paid_dollars, 0) as lifetime_paid_dollars, + patients.created_at as patient_registered_at from patients - left join encounters - on patients.patient_id = encounters.patient_id - left join diagnoses - on encounters.encounter_id = diagnoses.encounter_id - left join medications - on encounters.encounter_id = medications.encounter_id - left join lab_results - on patients.patient_id = lab_results.patient_id + left join encounter_summary as enc_s + on patients.patient_id = enc_s.patient_id + left join diagnosis_summary as dx_s + on patients.patient_id = dx_s.patient_id + left join medication_summary as med_s + on patients.patient_id = med_s.patient_id + left join lab_summary as lab_s + on patients.patient_id = lab_s.patient_id + left join claims_summary as clm_s + on patients.patient_id = clm_s.patient_id )