AI-Enriched FHIR Healthcare Pipeline
Parse clinical FHIR data, enrich with Claude AI, and visualize in Power BI.
Introduction
30 Second Summary
Healthcare data is notoriously complex. Patient records are buried in deeply nested JSON structures that no dashboard can read directly.
In this project, you will build an end-to-end data pipeline that parses raw clinical data, enriches it with AI-generated risk assessments, loads it into a cloud data warehouse, and visualizes the results in an interactive dashboard.
What You'll Build
An end-to-end healthcare data analytics pipeline using Python, Claude's API, Snowflake, and Power BI Desktop.
By the end of this project, you'll have:
- Python scripts that parse nested FHIR Bundle JSON files into clean CSV tables and enrich patient records with AI-generated risk categories.
- A Snowflake data warehouse with structured clinical tables and analytical SQL queries using JOINs, CTEs, and window functions.
- An interactive Power BI dashboard connected to Snowflake, visualizing patient demographics, condition prevalence, and AI-generated risk distributions.
- Secret Mission: Add a patient encounter timeline visual showing a chronological view of each patient's clinical journey.
Are there any prerequisites?
This project assumes intermediate Python knowledge (working with files, loops, and dictionaries). You'll also need JSON files containing FHIR Bundle data to parse through the pipeline.
Set Up Your Environment
Before you can parse healthcare data and build dashboards, you need a workspace with the right tools installed.
This step gets your project folder, Python packages, cloud accounts, and visualization tool ready so you can focus on building the pipeline in later steps.
In this step, get ready to:
- Install Python packages for AI enrichment and data processing.
- Create Snowflake and Anthropic accounts for data warehousing and AI.
- Install Power BI Desktop for dashboard visualization.
Create your project folder and install packages
Your pipeline needs a dedicated folder to keep scripts, raw data, and processed output organized.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (the backtick key, above Tab on your keyboard).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and its subdirectories by running these commands:
mkdir fhir-pipeline
cd fhir-pipeline
mkdir data output
- Confirm the folders exist by running:
ls
You should see data and output listed in the terminal output.
What are these folders for?
The data/ folder holds your raw FHIR Bundle JSON files. The output/ folder stores the processed CSV files your Python scripts generate.
- Now open this folder in VS Code so the Explorer panel shows your project structure.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the fhir-pipeline folder, then click Open.
Why open the folder?
Opening a folder in VS Code sets it as your workspace. Any new terminal you open will automatically start inside fhir-pipeline.
- Open a fresh terminal by pressing Ctrl+`.
- Create a Python virtual environment by running this command:
macOS
python3 -m venv venv
Windows
python -m venv venv
- Activate the virtual environment:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
You should see (venv) appear at the beginning of your terminal prompt.
What is a virtual environment?
A virtual environment isolates the Python packages you install for this project from other projects on your machine. This prevents version conflicts between different projects.
- Install the required Python packages by running:
pip install anthropic pandas
You should see installation output ending with a success message showing both packages installed.
Not seeing (venv) in your prompt?
Make sure you ran the activation command for your operating system. On macOS, use source venv/bin/activate. On Windows, use venv\Scripts\activate.
If you see a permissions error on Windows, you may need to run Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned in PowerShell first.
Help me activate my Python virtual environment.
Create your Snowflake and Anthropic accounts
Your pipeline stores processed data in Snowflake (a cloud data warehouse) and uses Claude (Anthropic's AI model) to enrich patient records.
- Open your browser and navigate to signup.snowflake.com.
- Fill in the signup form with your name, email, and a password.
- Select Standard for the Snowflake edition.
- Choose a cloud provider and region closest to you.
- Click Get Started.
- Check your email for a verification link and activate your account.
What is Snowflake?
Snowflake is a cloud data warehouse that lets you store and query large datasets using SQL. The 30-day trial includes $400 in free credits and requires no credit card.
- Next, open a new browser tab and navigate to console.anthropic.com.
- Click Sign Up and create your developer account.
- Once signed in, navigate to the API Keys section.
- Click Create Key to generate a new API key.
- Copy the key immediately and save it somewhere safe. You will not be able to see it again.
Keep your API key safe
Never share your API key or commit it to version control. You will paste it into a terminal environment variable in the next step. Do not store it in any project files.
- Back in your VS Code terminal (with the (venv) active), set your API key as an environment variable:
macOS
export ANTHROPIC_API_KEY=your-api-key-here
Windows
$env:ANTHROPIC_API_KEY="your-api-key-here"
- Replace your-api-key-here with the key you just copied from the Anthropic console.
Why an environment variable?
The ANTHROPIC_API_KEY environment variable is read automatically by the Anthropic Python SDK. This keeps your secret out of your code files.
Note that this variable only lasts for the current terminal session. You will need to set it again if you close and reopen the terminal.
Getting an authentication error later?
If your API calls fail with an authentication error, your environment variable may have expired. Re-run the export command above with your key.
Help me troubleshoot my Anthropic API key setup.
Install Power BI Desktop
Power BI Desktop is a free Windows application that connects to data sources and builds interactive dashboards. You will use it to visualize your Snowflake data in the final step.
macOS users
Power BI Desktop is Windows-only. If you are on macOS, you can use Power BI in a Windows virtual machine, or follow along with the Snowflake and Python portions of this project and skip the Power BI step.
- Press the Windows key to open your search bar.
- Type Microsoft Store and press Enter to open it.
- In the Microsoft Store search bar, type Power BI Desktop.
- Click on the Power BI Desktop result from Microsoft Corporation.
- Click Install (or Get) and wait for the download to complete.
The installation takes a few minutes. You do not need to open Power BI Desktop yet.
Microsoft Store not loading?
Make sure you are connected to the internet. If the Store app itself is not responding, try restarting it from the Start menu. You can also download Power BI Desktop directly from the Microsoft Download Center as an alternative.
Help me install Power BI Desktop.
Your environment is ready. Next up, you will write a Python script that parses raw FHIR Bundle JSON files into clean, structured CSV data.
Parse FHIR Bundles
Your environment is ready with Python, your packages, and your FHIR data files waiting in the data/ directory.
The challenge now is that FHIR Bundle JSON files store clinical data in deeply nested structures. Patient names live inside arrays of objects, conditions reference patients through URN strings, and every resource type uses a different schema. You need to untangle all of this into flat, tabular CSV files that a data warehouse can ingest.
In this step, get ready to:
- Create a Python script that loads and groups FHIR Bundle resources by type.
- Write flattening functions that extract key fields into flat rows.
- Run the script and validate the CSV output.
Create the FHIR parsing script
A FHIR Bundle is a JSON container with an entry array. Each entry holds a resource object with a resourceType field (like Patient, Condition, or Encounter). Your script needs to open every JSON file, confirm it is a Bundle, and sort each resource into the correct group.
- Create a new file called parse_fhir.py in your project folder (the same level as your data/ and output/ directories).
- Add the imports and the two loading functions by pasting the following code:
import json
import os
import pandas as pd
from pathlib import Path
def load_bundles(data_dir):
"""Load all FHIR Bundle JSON files from a directory."""
bundles = []
data_path = Path(data_dir)
for json_file in data_path.glob("*.json"):
with open(json_file, "r", encoding="utf-8") as f:
bundle = json.load(f)
if bundle.get("resourceType") == "Bundle":
bundles.append(bundle)
print(f"Loaded {len(bundles)} FHIR Bundles")
return bundles
def extract_resources(bundles):
"""Extract resources from bundles, grouped by resource type."""
resources = {}
for bundle in bundles:
for entry in bundle.get("entry", []):
resource = entry.get("resource", {})
resource_type = resource.get("resourceType")
if resource_type:
if resource_type not in resources:
resources[resource_type] = []
resources[resource_type].append(resource)
for rtype, rlist in resources.items():
print(f" {rtype}: {len(rlist)} resources")
return resources
What does this code do?
load_bundles() scans every .json file in your data/ directory. It only keeps files where resourceType equals "Bundle". This protects you from accidentally loading non-FHIR files.
extract_resources() loops through every entry in every bundle and groups them into a dictionary keyed by resourceType. After this runs, you have separate lists for Patient, Condition, Encounter, Observation, and any other resource types in your data.
- Save parse_fhir.py.
You will not see any output yet. The next substep adds the functions that transform each resource type into flat rows.
Write flattening functions for each resource type
Each FHIR resource type buries useful data at different nesting levels. A Patient's name is inside name[0].given and name[0].family. A Condition's description is at code.coding[0].display. Your flattening functions dig into these paths and pull out the values you need for clean, tabular CSV columns.
- In parse_fhir.py, add these four flattening functions below the extract_resources function:
def flatten_patients(patients):
"""Flatten Patient resources into tabular format."""
rows = []
for p in patients:
name = ""
if p.get("name"):
name_obj = p["name"][0]
given = " ".join(name_obj.get("given", []))
family = name_obj.get("family", "")
name = f"{given} {family}".strip()
city = ""
state = ""
if p.get("address"):
addr = p["address"][0]
city = addr.get("city", "")
state = addr.get("state", "")
rows.append({
"patient_id": p.get("id", ""),
"name": name,
"gender": p.get("gender", ""),
"birth_date": p.get("birthDate", ""),
"city": city,
"state": state,
})
return pd.DataFrame(rows)
def flatten_conditions(conditions):
"""Flatten Condition resources into tabular format."""
rows = []
for c in conditions:
subject_ref = c.get("subject", {}).get("reference", "")
patient_id = subject_ref.replace("urn:uuid:", "").replace("Patient/", "")
code_text = ""
code_value = ""
coding = c.get("code", {}).get("coding", [])
if coding:
code_text = coding[0].get("display", "")
code_value = coding[0].get("code", "")
onset = c.get("onsetDateTime", "")
abatement = c.get("abatementDateTime", "")
rows.append({
"condition_id": c.get("id", ""),
"patient_id": patient_id,
"code": code_value,
"description": code_text,
"onset_date": onset,
"abatement_date": abatement,
})
return pd.DataFrame(rows)
def flatten_encounters(encounters):
"""Flatten Encounter resources into tabular format."""
rows = []
for e in encounters:
subject_ref = e.get("subject", {}).get("reference", "")
patient_id = subject_ref.replace("urn:uuid:", "").replace("Patient/", "")
enc_type = ""
type_list = e.get("type", [])
if type_list and type_list[0].get("coding"):
enc_type = type_list[0]["coding"][0].get("display", "")
period = e.get("period", {})
start = period.get("start", "")
end = period.get("end", "")
enc_class = ""
class_obj = e.get("class", {})
if isinstance(class_obj, dict):
enc_class = class_obj.get("code", "")
rows.append({
"encounter_id": e.get("id", ""),
"patient_id": patient_id,
"encounter_class": enc_class,
"encounter_type": enc_type,
"start_date": start,
"end_date": end,
})
return pd.DataFrame(rows)
def flatten_observations(observations):
"""Flatten Observation resources into tabular format."""
rows = []
for o in observations:
subject_ref = o.get("subject", {}).get("reference", "")
patient_id = subject_ref.replace("urn:uuid:", "").replace("Patient/", "")
code_text = ""
coding = o.get("code", {}).get("coding", [])
if coding:
code_text = coding[0].get("display", "")
value = ""
unit = ""
if o.get("valueQuantity"):
value = str(o["valueQuantity"].get("value", ""))
unit = o["valueQuantity"].get("unit", "")
elif o.get("valueCodeableConcept"):
cc_coding = o["valueCodeableConcept"].get("coding", [])
if cc_coding:
value = cc_coding[0].get("display", "")
rows.append({
"observation_id": o.get("id", ""),
"patient_id": patient_id,
"observation_name": code_text,
"value": value,
"unit": unit,
"effective_date": o.get("effectiveDateTime", ""),
})
return pd.DataFrame(rows)
What does this code do?
Each function follows the same pattern: loop through the resource list, dig into the nested FHIR structure to pull out the fields you need, and append a flat dictionary to a rows list. At the end, pandas converts the list into a DataFrame.
flatten_patients() extracts the patient's full name by joining given names with the family name. It also pulls city and state from the first address entry.
flatten_conditions() resolves the patient reference (stripping the urn:uuid: or Patient/ prefix) and extracts the condition's display name and clinical code. The same reference-resolution pattern appears in flatten_encounters() and flatten_observations().
flatten_observations() handles two value formats: numeric measurements (like blood pressure) come from valueQuantity, while coded values (like a qualitative result) come from valueCodeableConcept.
- Save parse_fhir.py.
You will not see any output yet. The next substep adds the main() function that ties everything together and writes the CSV files.
Run the script and validate the output
The main() function orchestrates the full pipeline: load bundles, extract resources, flatten each type, and write the results to CSV. It also prints row counts so you can verify at a glance.
- Add the main() function at the bottom of parse_fhir.py:
def main():
data_dir = "data"
output_dir = "output"
os.makedirs(output_dir, exist_ok=True)
print("Loading FHIR Bundles...")
bundles = load_bundles(data_dir)
print("\nExtracting resources...")
resources = extract_resources(bundles)
print("\nFlattening resources...")
if "Patient" in resources:
patients_df = flatten_patients(resources["Patient"])
patients_df.to_csv(f"{output_dir}/patients.csv", index=False)
print(f" patients.csv: {len(patients_df)} rows")
if "Condition" in resources:
conditions_df = flatten_conditions(resources["Condition"])
conditions_df.to_csv(f"{output_dir}/conditions.csv", index=False)
print(f" conditions.csv: {len(conditions_df)} rows")
if "Encounter" in resources:
encounters_df = flatten_encounters(resources["Encounter"])
encounters_df.to_csv(f"{output_dir}/encounters.csv", index=False)
print(f" encounters.csv: {len(encounters_df)} rows")
if "Observation" in resources:
observations_df = flatten_observations(resources["Observation"])
observations_df.to_csv(f"{output_dir}/observations.csv", index=False)
print(f" observations.csv: {len(observations_df)} rows")
print("\nDone! CSV files saved to output/ directory.")
if __name__ == "__main__":
main()
What does this code do?
main() checks whether each resource type exists in your data before attempting to flatten it. This means your script works regardless of which resource types your specific FHIR files contain.
os.makedirs(output_dir, exist_ok=True) ensures the output/ directory exists (safe to call even if it already does). Each flattened DataFrame is saved as a CSV with index=False so you get clean column-based output without row numbers.
- Save parse_fhir.py.
- Run the script in your terminal by running:
python parse_fhir.py
- You should see output showing the number of bundles loaded, a breakdown of resource types, and the row counts for each CSV file.
What should I see?
Your terminal output will look something like this (exact numbers depend on your data files):
Loading FHIR Bundles...
Loaded 10 FHIR Bundles
Extracting resources...
Patient: 10 resources
Condition: 42 resources
Encounter: 87 resources
Observation: 312 resources
Flattening resources...
patients.csv: 10 rows
conditions.csv: 42 rows
encounters.csv: 87 rows
observations.csv: 312 rows
Done! CSV files saved to output/ directory.
- Verify the CSV files exist by running:
ls output/
- You should see patients.csv, conditions.csv, encounters.csv, and observations.csv listed.
- Inspect the first few rows of patients.csv to confirm the data looks correct by running:
head -5 output/patients.csv
- You should see a header row with patient_id,name,gender,birth_date,city,state followed by patient data rows.
Not seeing any CSV files?
- Make sure your FHIR JSON files are in the data/ directory and have a .json extension. The script only scans for *.json files.
- Check that each JSON file has "resourceType": "Bundle" at the top level. Files without this field are silently skipped.
- Confirm you are running the script from your project folder (the directory that contains data/ and output/).
Help me debug why parse_fhir.py is not producing CSV output.
✔️ Awesome, I've got everything!
Great. Double check you have saved parse_fhir.py and that all four CSV files are present in your output/ directory.
ⓧ I'd like to double check the full code
Here is the complete parse_fhir.py file for comparison:
import json
import os
import pandas as pd
from pathlib import Path
def load_bundles(data_dir):
"""Load all FHIR Bundle JSON files from a directory."""
bundles = []
data_path = Path(data_dir)
for json_file in data_path.glob("*.json"):
with open(json_file, "r", encoding="utf-8") as f:
bundle = json.load(f)
if bundle.get("resourceType") == "Bundle":
bundles.append(bundle)
print(f"Loaded {len(bundles)} FHIR Bundles")
return bundles
def extract_resources(bundles):
"""Extract resources from bundles, grouped by resource type."""
resources = {}
for bundle in bundles:
for entry in bundle.get("entry", []):
resource = entry.get("resource", {})
resource_type = resource.get("resourceType")
if resource_type:
if resource_type not in resources:
resources[resource_type] = []
resources[resource_type].append(resource)
for rtype, rlist in resources.items():
print(f" {rtype}: {len(rlist)} resources")
return resources
def flatten_patients(patients):
"""Flatten Patient resources into tabular format."""
rows = []
for p in patients:
name = ""
if p.get("name"):
name_obj = p["name"][0]
given = " ".join(name_obj.get("given", []))
family = name_obj.get("family", "")
name = f"{given} {family}".strip()
city = ""
state = ""
if p.get("address"):
addr = p["address"][0]
city = addr.get("city", "")
state = addr.get("state", "")
rows.append({
"patient_id": p.get("id", ""),
"name": name,
"gender": p.get("gender", ""),
"birth_date": p.get("birthDate", ""),
"city": city,
"state": state,
})
return pd.DataFrame(rows)
def flatten_conditions(conditions):
"""Flatten Condition resources into tabular format."""
rows = []
for c in conditions:
subject_ref = c.get("subject", {}).get("reference", "")
patient_id = subject_ref.replace("urn:uuid:", "").replace("Patient/", "")
code_text = ""
code_value = ""
coding = c.get("code", {}).get("coding", [])
if coding:
code_text = coding[0].get("display", "")
code_value = coding[0].get("code", "")
onset = c.get("onsetDateTime", "")
abatement = c.get("abatementDateTime", "")
rows.append({
"condition_id": c.get("id", ""),
"patient_id": patient_id,
"code": code_value,
"description": code_text,
"onset_date": onset,
"abatement_date": abatement,
})
return pd.DataFrame(rows)
def flatten_encounters(encounters):
"""Flatten Encounter resources into tabular format."""
rows = []
for e in encounters:
subject_ref = e.get("subject", {}).get("reference", "")
patient_id = subject_ref.replace("urn:uuid:", "").replace("Patient/", "")
enc_type = ""
type_list = e.get("type", [])
if type_list and type_list[0].get("coding"):
enc_type = type_list[0]["coding"][0].get("display", "")
period = e.get("period", {})
start = period.get("start", "")
end = period.get("end", "")
enc_class = ""
class_obj = e.get("class", {})
if isinstance(class_obj, dict):
enc_class = class_obj.get("code", "")
rows.append({
"encounter_id": e.get("id", ""),
"patient_id": patient_id,
"encounter_class": enc_class,
"encounter_type": enc_type,
"start_date": start,
"end_date": end,
})
return pd.DataFrame(rows)
def flatten_observations(observations):
"""Flatten Observation resources into tabular format."""
rows = []
for o in observations:
subject_ref = o.get("subject", {}).get("reference", "")
patient_id = subject_ref.replace("urn:uuid:", "").replace("Patient/", "")
code_text = ""
coding = o.get("code", {}).get("coding", [])
if coding:
code_text = coding[0].get("display", "")
value = ""
unit = ""
if o.get("valueQuantity"):
value = str(o["valueQuantity"].get("value", ""))
unit = o["valueQuantity"].get("unit", "")
elif o.get("valueCodeableConcept"):
cc_coding = o["valueCodeableConcept"].get("coding", [])
if cc_coding:
value = cc_coding[0].get("display", "")
rows.append({
"observation_id": o.get("id", ""),
"patient_id": patient_id,
"observation_name": code_text,
"value": value,
"unit": unit,
"effective_date": o.get("effectiveDateTime", ""),
})
return pd.DataFrame(rows)
def main():
data_dir = "data"
output_dir = "output"
os.makedirs(output_dir, exist_ok=True)
print("Loading FHIR Bundles...")
bundles = load_bundles(data_dir)
print("\nExtracting resources...")
resources = extract_resources(bundles)
print("\nFlattening resources...")
if "Patient" in resources:
patients_df = flatten_patients(resources["Patient"])
patients_df.to_csv(f"{output_dir}/patients.csv", index=False)
print(f" patients.csv: {len(patients_df)} rows")
if "Condition" in resources:
conditions_df = flatten_conditions(resources["Condition"])
conditions_df.to_csv(f"{output_dir}/conditions.csv", index=False)
print(f" conditions.csv: {len(conditions_df)} rows")
if "Encounter" in resources:
encounters_df = flatten_encounters(resources["Encounter"])
encounters_df.to_csv(f"{output_dir}/encounters.csv", index=False)
print(f" encounters.csv: {len(encounters_df)} rows")
if "Observation" in resources:
observations_df = flatten_observations(resources["Observation"])
observations_df.to_csv(f"{output_dir}/observations.csv", index=False)
print(f" observations.csv: {len(observations_df)} rows")
print("\nDone! CSV files saved to output/ directory.")
if __name__ == "__main__":
main()
Your raw FHIR data is now clean, flat, and ready for analysis. Next up, you will use Claude's API to enrich each patient record with AI-generated risk assessments.
Enrich Patient Data with Claude AI
You now have clean, tabular CSVs representing your patients, conditions, encounters, and observations. The data is structured, but it tells you nothing about risk.
In this step, you'll use Claude's API to read each patient's demographics and condition history, then generate a structured risk assessment. The result is a new CSV that adds intelligence your raw data never had.
In this step, get ready to:
- Create the enrichment script with an API client and prompt builder.
- Call Claude's Messages API to generate risk categories for each patient.
- Run the script and verify the risk category distribution.
Create the enrichment script and initialize the API client
The enrichment script needs to connect to Claude's API and read the CSV files you generated in the previous step. You'll start by setting up the imports and a function that initializes the Anthropic client using your environment variable.
- Create a new file called enrich_with_ai.py in your project folder (the same directory where parse_fhir.py lives).
- Add the imports and client setup by pasting this code:
import os
import json
import pandas as pd
from anthropic import Anthropic
def get_client():
"""Initialize the Anthropic client."""
client = Anthropic(
api_key=os.environ.get("ANTHROPIC_API_KEY"),
)
return client
What does this code do?
The from anthropic import Anthropic line brings in the official SDK you installed earlier.
The get_client() function reads your ANTHROPIC_API_KEY environment variable and returns an authenticated client object. Wrapping this in a function keeps the API setup separate from your data logic.
- Save enrich_with_ai.py.
Build the enrichment function that calls Claude
Each patient in your dataset has a unique combination of demographics and conditions. You need a function that loops through every patient, assembles their clinical profile into a prompt, sends it to Claude, and parses the structured response.
- In enrich_with_ai.py, add the enrich_patients function below get_client():
def enrich_patients(client, patients_df, conditions_df):
"""Use Claude to generate risk categories and summaries for patients."""
enriched_rows = []
for _, patient in patients_df.iterrows():
patient_conditions = conditions_df[
conditions_df["patient_id"] == patient["patient_id"]
]
condition_list = patient_conditions["description"].tolist()
prompt = f"""Analyze this patient's health profile and provide a structured assessment.
Patient: {patient['name']}
Gender: {patient['gender']}
Birth Date: {patient['birth_date']}
Conditions: {', '.join(condition_list) if condition_list else 'None recorded'}
Respond in exactly this JSON format with no other text:
{{
"risk_category": "Low" or "Medium" or "High",
"condition_group": "a short clinical category label",
"summary": "one sentence patient summary"
}}"""
message = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=256,
messages=[{"role": "user", "content": prompt}],
)
response_text = message.content[0].text
try:
assessment = json.loads(response_text)
except json.JSONDecodeError:
assessment = {
"risk_category": "Unknown",
"condition_group": "Uncategorized",
"summary": "Unable to assess",
}
enriched_rows.append({
"patient_id": patient["patient_id"],
"risk_category": assessment.get("risk_category", "Unknown"),
"condition_group": assessment.get("condition_group", "Uncategorized"),
"ai_summary": assessment.get("summary", ""),
})
print(f" Enriched: {patient['name']} -> {assessment.get('risk_category')}")
return pd.DataFrame(enriched_rows)
What does this function do?
- For each patient, it filters the conditions DataFrame to find only that patient's conditions using patient_id.
- It builds an f-string prompt that includes the patient's name, gender, birth date, and full condition list.
- The prompt instructs Claude to respond in strict JSON format with three fields: risk_category, condition_group, and summary.
- The client.messages.create() call sends the prompt to the claude-sonnet-4-5 model and returns a response.
- A try/except block handles cases where Claude's response isn't valid JSON, falling back to safe defaults.
Why use an LLM instead of a rules-based model?
A traditional risk model would require labeled training data, feature engineering, and a separate ML pipeline. An LLM can read unstructured condition descriptions and reason about them in context.
This pattern (structured prompt in, structured JSON out) is how production data pipelines use LLMs for enrichment tasks that would otherwise need a domain expert.
- Save enrich_with_ai.py.
Seeing a red underline on the Anthropic import?
Make sure your virtual environment is activated in VS Code. Check the bottom-left corner of VS Code for the Python interpreter. It should point to your virtual environment, not the system Python.
Help me fix my Anthropic import error in VS Code.
Run the enrichment script and verify results
The final piece is a main() function that orchestrates the full enrichment flow: load the CSVs, call the enrichment function, save results, and print a summary.
- In enrich_with_ai.py, add the main() function and entry point below enrich_patients:
def main():
output_dir = "output"
print("Initializing Claude API client...")
client = get_client()
print("Loading parsed data...")
patients_df = pd.read_csv(f"{output_dir}/patients.csv")
conditions_df = pd.read_csv(f"{output_dir}/conditions.csv")
print(f"\nEnriching {len(patients_df)} patients with AI analysis...")
enrichments_df = enrich_patients(client, patients_df, conditions_df)
enrichments_df.to_csv(f"{output_dir}/ai_enrichments.csv", index=False)
print(f"\nSaved ai_enrichments.csv: {len(enrichments_df)} rows")
print("\nRisk category distribution:")
print(enrichments_df["risk_category"].value_counts().to_string())
print("\nDone! AI enrichment complete.")
if __name__ == "__main__":
main()
What does this code do?
- It reads patients.csv and conditions.csv from your output/ directory.
- It passes both DataFrames into enrich_patients() which loops through every patient and calls Claude.
- The results are saved to ai_enrichments.csv and a risk category distribution is printed so you can verify the output at a glance.
- Save enrich_with_ai.py.
- Run the script in your terminal by running this command:
python enrich_with_ai.py
You should see each patient being enriched one by one, followed by a risk category distribution showing counts for Low, Medium, and High.
How long does this take?
The script processes patients sequentially, making one API call per patient. For 10-50 patients, expect the script to run for 30 seconds to a few minutes depending on API latency.
- Confirm that output/ai_enrichments.csv was created by running:
ls output/
You should see ai_enrichments.csv alongside your existing CSV files.
Seeing an authentication error?
Make sure your ANTHROPIC_API_KEY environment variable is set in the same terminal session where you're running the script. If you opened a new terminal, you'll need to set it again.
Help me troubleshoot my Anthropic API authentication error.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file and that the terminal output shows a risk distribution.
ⓧ I'd like to double check the full code
import os
import json
import pandas as pd
from anthropic import Anthropic
def get_client():
"""Initialize the Anthropic client."""
client = Anthropic(
api_key=os.environ.get("ANTHROPIC_API_KEY"),
)
return client
def enrich_patients(client, patients_df, conditions_df):
"""Use Claude to generate risk categories and summaries for patients."""
enriched_rows = []
for _, patient in patients_df.iterrows():
patient_conditions = conditions_df[
conditions_df["patient_id"] == patient["patient_id"]
]
condition_list = patient_conditions["description"].tolist()
prompt = f"""Analyze this patient's health profile and provide a structured assessment.
Patient: {patient['name']}
Gender: {patient['gender']}
Birth Date: {patient['birth_date']}
Conditions: {', '.join(condition_list) if condition_list else 'None recorded'}
Respond in exactly this JSON format with no other text:
{{
"risk_category": "Low" or "Medium" or "High",
"condition_group": "a short clinical category label",
"summary": "one sentence patient summary"
}}"""
message = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=256,
messages=[{"role": "user", "content": prompt}],
)
response_text = message.content[0].text
try:
assessment = json.loads(response_text)
except json.JSONDecodeError:
assessment = {
"risk_category": "Unknown",
"condition_group": "Uncategorized",
"summary": "Unable to assess",
}
enriched_rows.append({
"patient_id": patient["patient_id"],
"risk_category": assessment.get("risk_category", "Unknown"),
"condition_group": assessment.get("condition_group", "Uncategorized"),
"ai_summary": assessment.get("summary", ""),
})
print(f" Enriched: {patient['name']} -> {assessment.get('risk_category')}")
return pd.DataFrame(enriched_rows)
def main():
output_dir = "output"
print("Initializing Claude API client...")
client = get_client()
print("Loading parsed data...")
patients_df = pd.read_csv(f"{output_dir}/patients.csv")
conditions_df = pd.read_csv(f"{output_dir}/conditions.csv")
print(f"\nEnriching {len(patients_df)} patients with AI analysis...")
enrichments_df = enrich_patients(client, patients_df, conditions_df)
enrichments_df.to_csv(f"{output_dir}/ai_enrichments.csv", index=False)
print(f"\nSaved ai_enrichments.csv: {len(enrichments_df)} rows")
print("\nRisk category distribution:")
print(enrichments_df["risk_category"].value_counts().to_string())
print("\nDone! AI enrichment complete.")
if __name__ == "__main__":
main()
Your patient data now has AI-generated intelligence layered on top. Next up, you'll load everything into Snowflake and write SQL queries to analyze the full dataset.
Load Data into Snowflake and Write SQL Analytics
You have five clean CSV files sitting in your output/ directory. Flat files are great for portability, but they can't answer questions like "which conditions affect the most patients?" or "how do risk categories distribute across gender?" on their own.
A data warehouse gives you the power of SQL to join, aggregate, and slice your data on demand. In this step, you'll load everything into Snowflake, write analytical queries that surface clinical insights, and create a dashboard-ready view that combines all five tables into one flat result set.
In this step, get ready to:
- Create a Snowflake database and table structure for your clinical data.
- Load your CSV files into Snowflake using the built-in data loading wizard.
- Write analytical SQL queries and create a dashboard-ready view for Power BI.
Create the database, schema, and tables in Snowflake
Before you can load data, you need a place to put it. You'll create a database, a schema inside it, and five tables whose columns match your CSV files.
- Sign into your Snowflake trial account at the Snowsight web UI.
- In the navigation menu, select Projects, then Worksheets.
- Click the + button and select SQL Worksheet.
A blank SQL editor opens. You'll paste and run your table-creation statements here.
- Create a new file called snowflake_setup.sql in VS Code (in your project root) for local reference.
- Set up the database and schema by pasting the following SQL into your Snowsight worksheet:
-- Create database and schema
CREATE DATABASE IF NOT EXISTS fhir_analytics;
USE DATABASE fhir_analytics;
CREATE SCHEMA IF NOT EXISTS clinical_data;
USE SCHEMA clinical_data;
- Highlight all four lines and click the Run button (or press Cmd+Return on macOS / Ctrl+Enter on Windows).
What does this SQL do?
CREATE DATABASE IF NOT EXISTS fhir_analytics creates a new database to hold all your clinical data. The IF NOT EXISTS clause means re-running the statement won't cause an error.
USE DATABASE and USE SCHEMA set your active context so that all following statements target the right location.
- You should see a success message in the results pane (e.g. "Statement executed successfully").
- Now paste the table creation statements below your existing SQL in the same worksheet:
-- Create tables
CREATE OR REPLACE TABLE patients (
patient_id VARCHAR,
name VARCHAR,
gender VARCHAR,
birth_date DATE,
city VARCHAR,
state VARCHAR
);
CREATE OR REPLACE TABLE conditions (
condition_id VARCHAR,
patient_id VARCHAR,
code VARCHAR,
description VARCHAR,
onset_date TIMESTAMP_NTZ,
abatement_date TIMESTAMP_NTZ
);
CREATE OR REPLACE TABLE encounters (
encounter_id VARCHAR,
patient_id VARCHAR,
encounter_class VARCHAR,
encounter_type VARCHAR,
start_date TIMESTAMP_NTZ,
end_date TIMESTAMP_NTZ
);
CREATE OR REPLACE TABLE observations (
observation_id VARCHAR,
patient_id VARCHAR,
observation_name VARCHAR,
value VARCHAR,
unit VARCHAR,
effective_date TIMESTAMP_NTZ
);
CREATE OR REPLACE TABLE ai_enrichments (
patient_id VARCHAR,
risk_category VARCHAR,
condition_group VARCHAR,
ai_summary VARCHAR
);
- Highlight the five CREATE OR REPLACE TABLE statements and click Run.
What does this SQL do?
Each CREATE OR REPLACE TABLE defines a table structure that matches the columns in your CSV files. VARCHAR stores text, DATE stores calendar dates, and TIMESTAMP_NTZ stores date-times without timezone info.
The five tables mirror your five CSVs: patients, conditions, encounters, observations, and ai_enrichments.
- Save snowflake_setup.sql in VS Code with all the SQL above for your local records.
- You should see success messages for each table creation in the Snowsight results pane.
Seeing an error about privileges?
Make sure you're using the ACCOUNTADMIN role (the default for trial accounts). Check the role dropdown in the top-left of your worksheet.
help me fix Snowflake permission errors when creating a database
✔️ Awesome, I've got everything!
Great. Double check you've saved snowflake_setup.sql locally in VS Code.
ⓧ I'd like to double check the full code
-- Create database and schema
CREATE DATABASE IF NOT EXISTS fhir_analytics;
USE DATABASE fhir_analytics;
CREATE SCHEMA IF NOT EXISTS clinical_data;
USE SCHEMA clinical_data;
-- Create tables
CREATE OR REPLACE TABLE patients (
patient_id VARCHAR,
name VARCHAR,
gender VARCHAR,
birth_date DATE,
city VARCHAR,
state VARCHAR
);
CREATE OR REPLACE TABLE conditions (
condition_id VARCHAR,
patient_id VARCHAR,
code VARCHAR,
description VARCHAR,
onset_date TIMESTAMP_NTZ,
abatement_date TIMESTAMP_NTZ
);
CREATE OR REPLACE TABLE encounters (
encounter_id VARCHAR,
patient_id VARCHAR,
encounter_class VARCHAR,
encounter_type VARCHAR,
start_date TIMESTAMP_NTZ,
end_date TIMESTAMP_NTZ
);
CREATE OR REPLACE TABLE observations (
observation_id VARCHAR,
patient_id VARCHAR,
observation_name VARCHAR,
value VARCHAR,
unit VARCHAR,
effective_date TIMESTAMP_NTZ
);
CREATE OR REPLACE TABLE ai_enrichments (
patient_id VARCHAR,
risk_category VARCHAR,
condition_group VARCHAR,
ai_summary VARCHAR
);
Load your CSV files into the tables
Your tables exist but are empty. Snowsight has a built-in data loading wizard that lets you upload CSV files directly from your computer into tables without writing any code.
- In your Snowsight worksheet, click Objects in the left panel to open the object explorer.
- Locate the patients table under fhir_analytics > clinical_data.
- Hover over the patients table name, click the more menu (⋮), and select Load Data.
- Select Load files from your computer, then click Select Files.
- Browse to your output/patients.csv file and click Open.
- Click Next through the wizard steps. When prompted for file format, ensure the type is set to CSV and that the header option is enabled (your CSVs have column headers).
- Click Load to finish uploading.
- Click OK to close the wizard.
Why not use Python to load data?
Snowsight's upload wizard handles CSV files up to 50MB without needing a Python connector, ODBC driver, or staging configuration. For a project-sized dataset, this is the fastest path from file to table.
- Repeat the same process for the remaining four CSV files: conditions.csv, encounters.csv, observations.csv, and ai_enrichments.csv. Load each into its matching table.
- Verify the data loaded by running this quick check in your worksheet:
SELECT COUNT(*) FROM patients;
SELECT COUNT(*) FROM conditions;
SELECT COUNT(*) FROM encounters;
SELECT COUNT(*) FROM observations;
SELECT COUNT(*) FROM ai_enrichments;
- Each query should return a row count matching what your Python scripts printed earlier. If any table shows 0, re-run the load wizard for that table.
Table shows 0 rows after loading?
Check that you selected the correct file for the correct table. Also verify the file format was set to CSV with the header row option enabled. If headers were treated as data, you may see garbled values in the first row.
help me troubleshoot why my Snowflake table shows 0 rows after CSV upload
Write analytical SQL queries
With data loaded, you can now ask questions. You'll write five analytical queries that demonstrate aggregations, window functions, multi-table JOINs, and grouping.
- Create a new file called snowflake_analysis.sql in VS Code for local reference.
- In your Snowsight worksheet, clear the editor (or open a new worksheet tab). Paste and run the following context-setting lines first:
USE DATABASE fhir_analytics;
USE SCHEMA clinical_data;
- Start with a patient demographics breakdown and the top 10 conditions by pasting and running this SQL:
-- Patient demographics overview
SELECT
gender,
COUNT(*) AS patient_count,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS percentage
FROM patients
GROUP BY gender
ORDER BY patient_count DESC;
-- Top 10 most common conditions
SELECT
c.description,
COUNT(*) AS occurrence_count,
COUNT(DISTINCT c.patient_id) AS patients_affected
FROM conditions c
GROUP BY c.description
ORDER BY occurrence_count DESC
LIMIT 10;
What does this SQL do?
The demographics query counts patients by gender and calculates each gender's percentage using a window function (SUM(COUNT(*)) OVER ()). This computes the total across all groups without needing a subquery.
The conditions query counts how many times each condition appears and how many distinct patients are affected. LIMIT 10 keeps the output focused on the most prevalent conditions.
- You should see a gender breakdown table and a top-10 conditions list in the results pane.
- Next, paste and run the risk category distribution and the full patient profile JOIN:
-- Risk category distribution from AI enrichment
SELECT
ai.risk_category,
COUNT(*) AS patient_count,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS percentage
FROM ai_enrichments ai
GROUP BY ai.risk_category
ORDER BY patient_count DESC;
-- Full patient profile with condition and encounter counts
SELECT
p.name,
p.gender,
p.birth_date,
ai.risk_category,
ai.condition_group,
COUNT(DISTINCT c.condition_id) AS total_conditions,
COUNT(DISTINCT e.encounter_id) AS total_encounters
FROM patients p
LEFT JOIN ai_enrichments ai ON p.patient_id = ai.patient_id
LEFT JOIN conditions c ON p.patient_id = c.patient_id
LEFT JOIN encounters e ON p.patient_id = e.patient_id
GROUP BY p.name, p.gender, p.birth_date, ai.risk_category, ai.condition_group
ORDER BY total_conditions DESC
LIMIT 20;
-- Encounters by class
SELECT
encounter_class,
COUNT(*) AS encounter_count,
COUNT(DISTINCT patient_id) AS unique_patients
FROM encounters
GROUP BY encounter_class
ORDER BY encounter_count DESC;
What does this SQL do?
The risk distribution query shows how Claude's AI-generated risk categories (Low, Medium, High) are spread across your patient population.
The full profile query is a multi-table JOIN. It connects patients, ai_enrichments, conditions, and encounters using LEFT JOIN so patients appear even if they have no conditions or encounters. COUNT(DISTINCT ...) avoids double-counting when JOINs produce multiple rows per patient.
The encounters-by-class query shows how healthcare visits distribute across categories (ambulatory, emergency, inpatient, etc.).
- Run each query and review the results. You should see risk categories with counts, patient profiles with condition/encounter totals, and encounter class breakdowns.
Queries returning empty results?
Make sure you ran the USE DATABASE fhir_analytics and USE SCHEMA clinical_data lines at the top of your worksheet. Without these, Snowflake won't know which tables to query.
help me debug empty query results in Snowflake
Create the Power BI dashboard view
Power BI works best when it pulls from a single, flat data source. Instead of making Power BI join tables at query time, you'll create a pre-joined view that combines all five tables into one result set.
- Paste and run the following view creation statement in your Snowsight worksheet:
-- Create a view for Power BI consumption
CREATE OR REPLACE VIEW patient_dashboard_view AS
SELECT
p.patient_id,
p.name,
p.gender,
p.birth_date,
p.city,
p.state,
ai.risk_category,
ai.condition_group,
ai.ai_summary,
COUNT(DISTINCT c.condition_id) AS condition_count,
COUNT(DISTINCT e.encounter_id) AS encounter_count
FROM patients p
LEFT JOIN ai_enrichments ai ON p.patient_id = ai.patient_id
LEFT JOIN conditions c ON p.patient_id = c.patient_id
LEFT JOIN encounters e ON p.patient_id = e.patient_id
GROUP BY
p.patient_id, p.name, p.gender, p.birth_date,
p.city, p.state, ai.risk_category, ai.condition_group, ai.ai_summary;
What is a VIEW?
A view is a saved SQL query that acts like a virtual table. Every time you (or Power BI) query patient_dashboard_view, Snowflake runs the underlying JOIN and returns fresh results. This means your dashboard always shows up-to-date data without maintaining a separate copy.
- Verify the view works by running:
SELECT * FROM patient_dashboard_view LIMIT 5;
- You should see rows with patient demographics, risk categories, AI summaries, and condition/encounter counts all in one flat table.
- Save snowflake_analysis.sql in VS Code with all the analytical queries and the view creation statement.
View creation failed?
Check that all five tables were loaded successfully. The view depends on patients, ai_enrichments, conditions, and encounters. Run SELECT COUNT(*) FROM patients to confirm each table has data.
help me fix my Snowflake view creation error
✔️ Awesome, I've got everything!
Great. Double check you've saved snowflake_analysis.sql locally with all five analytical queries and the view creation statement.
ⓧ I'd like to double check the full code
USE DATABASE fhir_analytics;
USE SCHEMA clinical_data;
-- Patient demographics overview
SELECT
gender,
COUNT(*) AS patient_count,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS percentage
FROM patients
GROUP BY gender
ORDER BY patient_count DESC;
-- Top 10 most common conditions
SELECT
c.description,
COUNT(*) AS occurrence_count,
COUNT(DISTINCT c.patient_id) AS patients_affected
FROM conditions c
GROUP BY c.description
ORDER BY occurrence_count DESC
LIMIT 10;
-- Risk category distribution from AI enrichment
SELECT
ai.risk_category,
COUNT(*) AS patient_count,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS percentage
FROM ai_enrichments ai
GROUP BY ai.risk_category
ORDER BY patient_count DESC;
-- Full patient profile with condition and encounter counts
SELECT
p.name,
p.gender,
p.birth_date,
ai.risk_category,
ai.condition_group,
COUNT(DISTINCT c.condition_id) AS total_conditions,
COUNT(DISTINCT e.encounter_id) AS total_encounters
FROM patients p
LEFT JOIN ai_enrichments ai ON p.patient_id = ai.patient_id
LEFT JOIN conditions c ON p.patient_id = c.patient_id
LEFT JOIN encounters e ON p.patient_id = e.patient_id
GROUP BY p.name, p.gender, p.birth_date, ai.risk_category, ai.condition_group
ORDER BY total_conditions DESC
LIMIT 20;
-- Encounters by class
SELECT
encounter_class,
COUNT(*) AS encounter_count,
COUNT(DISTINCT patient_id) AS unique_patients
FROM encounters
GROUP BY encounter_class
ORDER BY encounter_count DESC;
-- Create a view for Power BI consumption
CREATE OR REPLACE VIEW patient_dashboard_view AS
SELECT
p.patient_id,
p.name,
p.gender,
p.birth_date,
p.city,
p.state,
ai.risk_category,
ai.condition_group,
ai.ai_summary,
COUNT(DISTINCT c.condition_id) AS condition_count,
COUNT(DISTINCT e.encounter_id) AS encounter_count
FROM patients p
LEFT JOIN ai_enrichments ai ON p.patient_id = ai.patient_id
LEFT JOIN conditions c ON p.patient_id = c.patient_id
LEFT JOIN encounters e ON p.patient_id = e.patient_id
GROUP BY
p.patient_id, p.name, p.gender, p.birth_date,
p.city, p.state, ai.risk_category, ai.condition_group, ai.ai_summary;
Your Snowflake warehouse is fully loaded and queryable. Next up, you'll connect Power BI Desktop to this view and build an interactive dashboard that brings your clinical data to life.
Build the Power BI Dashboard
Your Snowflake warehouse is loaded with clean, AI-enriched clinical data and a pre-built view ready for consumption.
Now you'll connect Power BI Desktop to Snowflake and build an interactive dashboard that visualizes patient demographics, condition prevalence, and AI-generated risk distributions.
In this step, get ready to:
- Connect Power BI Desktop to your Snowflake warehouse and import the dashboard view.
- Build four visuals showing condition prevalence, risk distribution, patient count, and geographic breakdown.
- Add slicers for interactive filtering and format the report for presentation.
Connect Power BI Desktop to Snowflake
Power BI Desktop has a built-in Snowflake connector that reads directly from your warehouse. You'll point it at your patient_dashboard_view so the dashboard draws from a single, pre-joined data source.
- Press the Windows key to open your search bar.
- Type Power BI Desktop and press Enter to open it.
- In the Home ribbon, click Get Data.
- In the left panel, select Database.
- Select Snowflake from the list.
- Click Connect.
A connection dialog appears asking for your Snowflake details.
- In the Server field, enter your Snowflake account URL. This follows the format orgname-account_name.snowflakecomputing.com and matches what you see in your browser address bar when using Snowsight (without the https:// prefix).
- In the Warehouse field, enter the name of your Snowflake compute warehouse (the one you use for queries in Snowsight).
- Leave the other fields at their defaults and click OK.
- When prompted for credentials, enter your Snowflake username and password.
- Click Connect.
Where do I find my Snowflake account URL?
In Snowsight, look at your browser's address bar. It shows something like https://orgname-account_name.snowflakecomputing.com. Copy the portion after https:// and paste it into the Server field in Power BI.
After connecting, the Navigator window appears. This lets you browse your Snowflake databases and select data to import.
- Expand FHIR_ANALYTICS, then expand CLINICAL_DATA.
- Check the box next to PATIENT_DASHBOARD_VIEW.
- Also check CONDITIONS and PATIENTS (you'll use these for individual visuals).
- Click Load to import the data into Power BI.
Power BI imports the selected tables and view. You should see the field names appear in the Data pane on the right side of the screen.
Connection failing?
Make sure your Snowflake warehouse is not suspended. Return to Snowsight and run a simple query to wake it up. Also verify you typed the full account URL including .snowflakecomputing.com in the Server field.
If you see an authentication error, confirm you're using the same username and password you log in to Snowsight with.
Help me troubleshoot my Snowflake connection in Power BI Desktop
Build the dashboard visuals
With your data loaded, you'll create four visuals that tell the story of your patient population. Each visual answers a different clinical question.
Start with a bar chart showing the most common conditions across all patients.
- Click on an empty area of the report canvas to deselect any existing visual.
- In the Visualizations pane on the right, select the Clustered bar chart icon.
- From the Data pane, expand CONDITIONS.
- Drag DESCRIPTION to the Y-axis field well.
- Drag CONDITION_ID to the X-axis field well. Power BI automatically aggregates it as a count.
- Resize the chart to fill the top-left quarter of the canvas.
What does this visual show?
The bar chart displays condition prevalence. Each bar represents a unique condition (like hypertension or diabetes), and the length shows how many times it appears in your dataset. This mirrors the top-10 query you ran in Snowflake earlier.
Next, add a donut chart to show the distribution of AI-generated risk categories.
- Click an empty area of the canvas.
- In the Visualizations pane, select the Donut chart icon.
- From the Data pane, expand PATIENT_DASHBOARD_VIEW.
- Drag RISK_CATEGORY to the Legend field well.
- Drag PATIENT_ID to the Values field well. It counts patients per category.
- Position the donut chart in the top-right area of the canvas.
Now add a card visual to display the total patient count at a glance.
- Click an empty area of the canvas.
- In the Visualizations pane, select the Card icon.
- Drag PATIENT_ID from PATIENT_DASHBOARD_VIEW into the Fields well.
- Confirm it shows the total number of patients (Power BI defaults to a count).
- Place the card visual above the donut chart or in a prominent spot on the canvas.
Finally, add a table visual to show patients grouped by geographic location.
- Click an empty area of the canvas.
- In the Visualizations pane, select the Table icon.
- Drag STATE from PATIENT_DASHBOARD_VIEW into the Columns well.
- Drag CITY into the Columns well.
- Drag PATIENT_ID into the Columns well. It counts patients per location.
- Position the table in the bottom half of the canvas.
You should now see four visuals on your canvas: a bar chart, a donut chart, a card, and a table.
Visuals showing blank or no data?
Make sure you selected the correct table from the Data pane when dragging fields. PATIENT_DASHBOARD_VIEW fields look similar to PATIENTS fields but come from the joined view.
If fields show as blank, click the visual, then check the Filters pane to confirm no unintended filters are applied.
Help me fix my blank Power BI visuals connected to Snowflake
Add slicers and format the report
Slicers let anyone using your dashboard filter all visuals simultaneously by clicking a value. You'll add two slicers so stakeholders can drill into specific risk categories or genders.
- Click an empty area of the canvas.
- In the Visualizations pane, select the Slicer icon.
- From the Data pane, expand PATIENT_DASHBOARD_VIEW and drag RISK_CATEGORY into the slicer's field well.
- Position the slicer along the left side or top of the canvas.
- Click an empty area of the canvas again.
- Select Slicer from the Visualizations pane.
- Drag GENDER from PATIENT_DASHBOARD_VIEW into the slicer's field well.
- Place this slicer next to the risk category slicer.
Test the slicers by clicking a value in each one. All four visuals should update to show only the filtered subset.
How do slicers work?
Slicers are on-canvas visual filters. When you select a value (like "High" in the risk slicer), Power BI automatically filters every other visual on the page to show only data matching that selection.
Hold Ctrl and click to select multiple values at once.
Finally, format and title the report for presentation.
- Double-click the page title area at the top of the canvas (or insert a text box from the Insert ribbon).
- Type a title like FHIR Patient Analytics Dashboard.
- Select each visual and use the Format pane (paint roller icon) to adjust colors, borders, or titles to your preference.
- Save your report by pressing Ctrl+S.
- Name it something descriptive like FHIR Analytics Dashboard.
Your finished dashboard should have four data visuals, two slicers, and a clear title. Selecting a risk category or gender in the slicers filters the entire page.
Slicers not filtering other visuals?
Make sure both the slicers and the visuals pull from the same table (PATIENT_DASHBOARD_VIEW). If a visual uses a different table that is not related in Power BI's data model, the slicer will not affect it.
Why aren't my Power BI slicers filtering my visuals?
Your healthcare analytics dashboard is complete. You've connected a live data warehouse to an interactive BI tool and built visuals that any stakeholder could use to explore patient risk data.
Secret mission
Patient Encounter Timeline
Your dashboard shows a snapshot of each patient's current state. But clinicians need to see the story. Build a chronological timeline visual that traces each patient's clinical journey from first encounter to latest condition.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Your Snowflake trial has $400 in free credits and expires after 30 days. Anthropic only charges per API call, so no costs accrue while the project is idle.
Cost warning
Snowflake credits are consumed whenever your virtual warehouse is in a running state, even if no queries are executing. If you plan to step away, suspend your warehouse (see the Pause tab) to stop credit consumption immediately.
Resources you used:
- Snowflake fhir_analytics database (tables, views, and loaded CSV data).
- Snowflake virtual warehouse (consumes credits while running).
- Anthropic API key (stored as the ANTHROPIC_API_KEY environment variable).
- Local project folder with Python scripts, SQL files, and CSV output.
- Power BI Desktop file (.pbix).
Keep everything running
No action needed. Choose this if you are still actively building or exploring queries in Snowflake.
- Your Snowflake trial remains active for 30 days with $400 in credits.
- Monitor credit usage in Snowflake by navigating to Admin in the left navigation menu.
- Anthropic charges nothing unless you run enrich_with_ai.py again or make additional API calls.
- Power BI Desktop is free and can remain installed on your machine.
Pause - I'll come back to this later
Suspend your Snowflake warehouse to stop credit consumption while keeping all your data intact.
- In Snowsight, select Compute in the navigation menu, then select Warehouses.
- Find your warehouse, click the ... (ellipsis) menu, and select Suspend.
- Your database, tables, and views remain untouched. Credits only consume when the warehouse is in a running state.
- To resume later, return to Compute > Warehouses, click ... on the suspended warehouse, and select Resume.
- Unset the ANTHROPIC_API_KEY environment variable if you want to prevent accidental API usage.
Delete - I don't want to use this again
Remove all project resources and start fresh. No charges will accrue after the Snowflake trial expires even if you do nothing, but deleting now frees credits for other experiments.
Drop the Snowflake database by running this in a SQL Worksheet:
DROP DATABASE IF EXISTS fhir_analytics;
- This removes all tables, views, and loaded data in one command.
Suspend your warehouse so it stops consuming credits:
- In Snowsight, select Compute > Warehouses, click ... on your warehouse, and select Suspend.
Remove the environment variable holding your API key:
macOS
unset ANTHROPIC_API_KEY
Windows
Remove-Item Env:ANTHROPIC_API_KEY
Delete your local project folder:
macOS
cd ~/Desktop
rm -rf fhir-pipeline
Windows
cd ~\Desktop
Remove-Item -Recurse -Force fhir-pipeline
- This removes parse_fhir.py, enrich_with_ai.py, all SQL files, and the output/ and data/ directories.
- Your Power BI .pbix file is saved inside the project folder, so it is deleted as well.
- The Snowflake trial account expires automatically after 30 days with no further action needed.
That's a Wrap!
That's a Wrap!
Nice work! You've just built a complete healthcare data analytics pipeline that transforms raw FHIR clinical data into AI-enriched, interactive visualizations.
You've learned how to:
- Parse deeply nested FHIR Bundle JSON files into clean, tabular data using Python, and enrich patient records with AI-generated risk assessments using Claude's API.
- Build a Snowflake data warehouse with properly structured tables and analytical SQL queries using JOINs, CTEs, window functions, and views.
- Create a Power BI dashboard connected to Snowflake with interactive slicers and clinical visualizations.
- Secret Mission: Built a patient encounter timeline showing chronological clinical journeys using window functions and a dedicated Power BI visual.
Ready to quiz yourself?