Analyzing FHIR Data in a Tabular Format With Python

Last updated: May 20, 2025
Roles: Informaticist
Learning objectives
  1. Understand the high-level approaches for converting FHIR-formatted data into tabular for analysis in Python.
  2. Learn how the FHIR-PYrate library facilitates requesting data from a FHIR server, and creating tidy tabular data tables.

Data analysis approaches in Python often use Pandas DataFrames to store tabular data. There are two primary approaches to loading FHIR-formatted data into Pandas DataFrames:

  1. Writing Python code to manually convert FHIR instances in JSON format into DataFrames.

    This does not require any special skills beyond data manipulation in Python, but in practice can be laborious (especially with large number of data elements) and prone to bugs.

  2. Using a purpose-built library like FHIR-PYrate to automatically convert FHIR instances into DataFrames.

    It is recommended to try this approach first, and only fall back to (1) if needed.

To use FHIR-PYrate, you will need a Python 3 runtime with FHIR-PYrate and Pandas installed.

1 FHIR testing server

The examples in this module use a FHIR testing server populated with Synthea data in FHIR R4 format via public HAPI Test Server operated by HAPI FHIR.

The endpoint for this testing server is:

https://hapi.fhir.org/baseR4

However, any FHIR server loaded with testing data can be used. See Standing up a FHIR Testing Server for instructions to set up your own test server.

The code blocks in the following section show sample output immediately after. This is similar to the code cells and results in a Jupyter notebook.

2 Retrieving FHIR data

Once your environment is set up, you can run the following Python code to retrieve instances of the Patient resource from a test server:

# Load dependencies
from fhir_pyrate import Pirate
import pandas as pd

# Instantiate a Pirate object using the FHIR-PYrate library to query a test FHIR server
search = Pirate(
    auth=None,
    base_url="https://hapi.fhir.org/baseR4",
    print_request_url=True,
)

# Use the whimsically named `steal_bundles()` method to instantiate a search interaction
#
# For more information, see https://github.com/UMEssen/FHIR-PYrate/#pirate
bundles = search.steal_bundles(
    resource_type="Patient",
    request_params={
        "_count": 10,  # Get 10 instances per page
        "identifier": "https://github.com/synthetichealth/synthea|",
    },
    num_pages=1,  # Get 1 page (so a total of 10 instances)
)

# Execute the search and convert to a Pandas DataFrame
df = search.bundles_to_dataframe(bundles)

df.head(5)
https://hapi.fhir.org/baseR4/Patient?_count=10&identifier=https://github.com/synthetichealth/synthea|
Query (Patient):   0%|          | 0/1 [00:00<?, ?it/s]Query (Patient): 100%|██████████| 1/1 [00:00<00:00, 943.81it/s]
resourceType id meta_versionId meta_lastUpdated meta_source meta_tag_0_system meta_tag_0_code text_status text_div extension_0_url ... address_0_country maritalStatus_coding_0_system maritalStatus_coding_0_code maritalStatus_coding_0_display maritalStatus_text multipleBirthBoolean communication_0_language_coding_0_system communication_0_language_coding_0_code communication_0_language_coding_0_display communication_0_language_text
0 Patient ed6a17f3-deb2-4e7a-9548-5d15d2d47c05 1 2026-09-29T01:27:02.704-04:00 #nOZOegRefKiCm14q https://smarthealthit.org/tags synthea-5-2019 generated <div xmlns="http://www.w3.org/1999/xhtml">Gene... http://hl7.org/fhir/us/core/StructureDefinitio... ... US http://terminology.hl7.org/CodeSystem/v3-Marit... M M Custom InteropX mapping rule — not present in ... False urn:ietf:bcp:47 en-US English English
1 Patient 6daf3d7c-1620-4d79-9396-990469306470 1 2026-09-29T01:27:02.927-04:00 #alUkhx0AzSpHeCMc https://smarthealthit.org/tags synthea-5-2019 generated <div xmlns="http://www.w3.org/1999/xhtml">Gene... http://hl7.org/fhir/us/core/StructureDefinitio... ... US http://terminology.hl7.org/CodeSystem/v3-Marit... S Never Married Custom InteropX mapping rule — not present in ... False urn:ietf:bcp:47 en-US English English
2 Patient 2a3d4d46-512f-480f-8eb9-5a11ef3b2176 1 2026-09-29T01:27:03.167-04:00 #q9xI01NGRQykH8dr https://smarthealthit.org/tags synthea-5-2019 generated <div xmlns="http://www.w3.org/1999/xhtml">Gene... http://hl7.org/fhir/us/core/StructureDefinitio... ... US http://terminology.hl7.org/CodeSystem/v3-Marit... S S Custom InteropX mapping rule — not present in ... False urn:ietf:bcp:47 en-US English English
3 Patient 0c15e136-827d-4d70-a5b1-a63bf159b31c 1 2026-09-29T01:27:03.424-04:00 #h8J83cNaK66DggoT https://smarthealthit.org/tags synthea-5-2019 generated <div xmlns="http://www.w3.org/1999/xhtml">Gene... http://hl7.org/fhir/us/core/StructureDefinitio... ... US http://terminology.hl7.org/CodeSystem/v3-Marit... M M Custom InteropX mapping rule — not present in ... False urn:ietf:bcp:47 en-US English English
4 Patient f8cb2d52-c53d-4585-8417-7a1cc1a74377 1 2026-09-29T01:27:03.661-04:00 #RmIHf19qjiFvXJ3S https://smarthealthit.org/tags synthea-5-2019 generated <div xmlns="http://www.w3.org/1999/xhtml">Gene... http://hl7.org/fhir/us/core/StructureDefinitio... ... US http://terminology.hl7.org/CodeSystem/v3-Marit... M M Custom InteropX mapping rule — not present in ... False urn:ietf:bcp:47 pt Portuguese English

5 rows × 94 columns

It is easier to see the contents of this DataFrame by printing out its first row vertically:

# Print the first row of the DataFrame vertically for easier reading.
pd.set_option("display.max_rows", 100)  # Show all rows
df.head(1).T
0
resourceType Patient
id ed6a17f3-deb2-4e7a-9548-5d15d2d47c05
meta_versionId 1
meta_lastUpdated 2026-09-29T01:27:02.704-04:00
meta_source #nOZOegRefKiCm14q
meta_tag_0_system https://smarthealthit.org/tags
meta_tag_0_code synthea-5-2019
text_status generated
text_div <div xmlns="http://www.w3.org/1999/xhtml">Gene...
extension_0_url http://hl7.org/fhir/us/core/StructureDefinitio...
extension_0_extension_0_url ombCategory
extension_0_extension_0_valueCoding_system urn:oid:2.16.840.1.113883.6.238
extension_0_extension_0_valueCoding_code 2054-5
extension_0_extension_0_valueCoding_display Black or African American
extension_0_extension_1_url text
extension_0_extension_1_valueString Black or African American
extension_1_url http://hl7.org/fhir/us/core/StructureDefinitio...
extension_1_extension_0_url ombCategory
extension_1_extension_0_valueCoding_system urn:oid:2.16.840.1.113883.6.238
extension_1_extension_0_valueCoding_code 2186-5
extension_1_extension_0_valueCoding_display Not Hispanic or Latino
extension_1_extension_1_url text
extension_1_extension_1_valueString Not Hispanic or Latino
extension_2_url http://hl7.org/fhir/StructureDefinition/patien...
extension_2_valueString Alana Blick
extension_3_url http://hl7.org/fhir/us/core/StructureDefinitio...
extension_3_valueCode F
extension_4_url http://hl7.org/fhir/StructureDefinition/patien...
extension_4_valueAddress_city Bourne
extension_4_valueAddress_state Massachusetts
extension_4_valueAddress_country US
extension_5_url http://synthetichealth.github.io/synthea/disab...
extension_5_valueDecimal 0.145932
extension_6_url http://synthetichealth.github.io/synthea/quali...
extension_6_valueDecimal 35.854068
identifier_0_system https://github.com/synthetichealth/synthea
identifier_0_value f06c0800-7e56-45e7-85eb-0047c2c5b4b1
identifier_1_type_coding_0_system http://terminology.hl7.org/CodeSystem/v2-0203
identifier_1_type_coding_0_code MR
identifier_1_type_coding_0_display Medical Record Number
identifier_1_type_text Medical Record Number
identifier_1_system http://hospital.smarthealthit.org
identifier_1_value f06c0800-7e56-45e7-85eb-0047c2c5b4b1
identifier_2_type_coding_0_system http://terminology.hl7.org/CodeSystem/v2-0203
identifier_2_type_coding_0_code SS
identifier_2_type_coding_0_display Social Security Number
identifier_2_type_text Social Security Number
identifier_2_system http://hl7.org/fhir/sid/us-ssn
identifier_2_value 999-17-9471
identifier_3_type_coding_0_system http://terminology.hl7.org/CodeSystem/v2-0203
identifier_3_type_coding_0_code DL
identifier_3_type_coding_0_display Driver's License
identifier_3_type_text Driver's License
identifier_3_system urn:oid:2.16.840.1.113883.4.3.25
identifier_3_value S99913919
identifier_4_type_coding_0_system http://terminology.hl7.org/CodeSystem/v2-0203
identifier_4_type_coding_0_code PPN
identifier_4_type_coding_0_display Passport Number
identifier_4_type_text Passport Number
identifier_4_system http://standardhealthrecord.org/fhir/Structure...
identifier_4_value X55105329X
name_0_use official
name_0_family Johnston
name_0_given_0 Paola
name_0_prefix_0 Mrs.
name_1_use maiden
name_1_family Schinner
name_1_given_0 Paola
name_1_prefix_0 Mrs.
telecom_0_system phone
telecom_0_value 555-573-4647
telecom_0_use home
gender female
birthDate 1924-12-30
deceasedDateTime 1962-02-22T02:28:09+00:00
address_0_extension_0_url http://hl7.org/fhir/StructureDefinition/geoloc...
address_0_extension_0_extension_0_url latitude
address_0_extension_0_extension_0_valueDecimal 42.33196
address_0_extension_0_extension_1_url longitude
address_0_extension_0_extension_1_valueDecimal -71.020173
address_0_line_0 742 Howe Spur Apt 91
address_0_city Boston
address_0_state Massachusetts
address_0_postalCode 02108
address_0_country US
maritalStatus_coding_0_system http://terminology.hl7.org/CodeSystem/v3-Marit...
maritalStatus_coding_0_code M
maritalStatus_coding_0_display M
maritalStatus_text Custom InteropX mapping rule — not present in ...
multipleBirthBoolean False
communication_0_language_coding_0_system urn:ietf:bcp:47
communication_0_language_coding_0_code en-US
communication_0_language_coding_0_display English
communication_0_language_text English

If you look at the output above, you can see FHIR-PYrate collapsed the hierarchical FHIR data structure into DataFrame columns. FHIR-PYrate does this by taking an element from the FHIR-formatted data like Patient.identifier[0].value and converting to an underscore-delimited column name like identifier_0_value. (Note that Patient.identifier has multiple values in the FHIR data, so there are multiple identifier_N_... columns in the DataFrame.)

3 Selecting specific columns

Usually not every single value from a FHIR instance is needed for analysis. There are two ways to get a more concise DataFrame:

  1. Use the approach above to load all elements into a DataFrame, remove the unneeded columns, and rename the remaining columns as needed. The process_function capability in FHIR-PYrate allows you to integrate this approach into the bundles_to_dataframe() method call.
  2. Use FHIRPath to select specific elements and map them onto column names.

The second approach is typically more concise. For example, to generate a DataFrame like this…

id gender date_of_birth marital_status
… … … …

…you could use the following code:

# Instantiate and perform the FHIR search interaction in a single function call
df = search.steal_bundles_to_dataframe(
    resource_type="Patient",
    request_params={
        "_count": 10,  # Get 10 instances per page
        "identifier": "https://github.com/synthetichealth/synthea|",
    },
    num_pages=1,  # Get 1 page (so a total of 10 instances)
    fhir_paths=[
        ("id", "identifier[0].value"),
        ("gender", "gender"),
        ("date_of_birth", "birthDate"),
        ("marital_status", "maritalStatus.coding[0].code"),
    ],
)
df
https://hapi.fhir.org/baseR4/Patient?_count=10&identifier=https://github.com/synthetichealth/synthea|
Query & Build DF (Patient):   0%|          | 0/1 [00:00<?, ?it/s]Query & Build DF (Patient): 100%|██████████| 1/1 [00:00<00:00, 382.34it/s]
id gender date_of_birth marital_status
0 f06c0800-7e56-45e7-85eb-0047c2c5b4b1 female 1924-12-30 M
1 e916a083-8ee6-4df8-acbe-b711fa791253 female 2015-04-04 S
2 f4673866-81fb-411a-8486-0c27f2a5b480 male 1975-10-12 S
3 0f30cacb-3348-4baf-8946-cd2175a750b3 female 1926-06-06 M
4 7b5bea3e-de83-4c43-a89c-5078e923c3db female 1981-03-26 M
5 fd04234a-e56d-4362-88a5-bbaa47addb6b female 1950-11-17 M
6 645f1b3d-ee47-4e5b-8e06-a49f23ff6f52 female 1964-06-22 S
7 100ddb60-fb28-4663-a8f2-af93cdc4294e female 1973-05-14 M
8 59339691-0270-4c4a-a34d-338e3008b9c3 male 1996-06-24 S
9 211c3874-3973-4b95-b4ee-3be2e1e3f50e female 1944-04-27 M

While FHIRPath can be quite complex, its use in FHIR-PYrate is often straight forward. Nested elements are separated with ., and elements with multiple sub-values are identified by [N] where N is an integer starting at 0. The element paths can typically be constructed by loading all elements into a DataFrame and then manually deriving the FHIRPaths from the column names, or by looking at the hierarchy resource pages in the FHIR specification (see FHIR Resources in Depth for more information on reading the FHIR specification).

4 Elements with multiple sub-values

There are multiple identifier[N].value values for each instance of Patient in this dataset.

# Instantiate and perform the FHIR search interaction in a single function call
df = search.steal_bundles_to_dataframe(
    resource_type="Patient",
    request_params={
        "_count": 10,  # Get 10 instances per page
        "identifier": "https://github.com/synthetichealth/synthea|",
    },
    num_pages=1,  # Get 1 page (so a total of 10 instances)
    fhir_paths=[("id", "identifier[0].value"), ("identifiers", "identifier.value")],
)
df
https://hapi.fhir.org/baseR4/Patient?_count=10&identifier=https://github.com/synthetichealth/synthea|
Query & Build DF (Patient):   0%|          | 0/1 [00:00<?, ?it/s]Query & Build DF (Patient): 100%|██████████| 1/1 [00:00<00:00, 493.56it/s]
id identifiers
0 f06c0800-7e56-45e7-85eb-0047c2c5b4b1 [f06c0800-7e56-45e7-85eb-0047c2c5b4b1, f06c080...
1 e916a083-8ee6-4df8-acbe-b711fa791253 [e916a083-8ee6-4df8-acbe-b711fa791253, e916a08...
2 f4673866-81fb-411a-8486-0c27f2a5b480 [f4673866-81fb-411a-8486-0c27f2a5b480, f467386...
3 0f30cacb-3348-4baf-8946-cd2175a750b3 [0f30cacb-3348-4baf-8946-cd2175a750b3, 0f30cac...
4 7b5bea3e-de83-4c43-a89c-5078e923c3db [7b5bea3e-de83-4c43-a89c-5078e923c3db, 7b5bea3...
5 fd04234a-e56d-4362-88a5-bbaa47addb6b [fd04234a-e56d-4362-88a5-bbaa47addb6b, fd04234...
6 645f1b3d-ee47-4e5b-8e06-a49f23ff6f52 [645f1b3d-ee47-4e5b-8e06-a49f23ff6f52, 645f1b3...
7 100ddb60-fb28-4663-a8f2-af93cdc4294e [100ddb60-fb28-4663-a8f2-af93cdc4294e, 100ddb6...
8 59339691-0270-4c4a-a34d-338e3008b9c3 [59339691-0270-4c4a-a34d-338e3008b9c3, 5933969...
9 211c3874-3973-4b95-b4ee-3be2e1e3f50e [211c3874-3973-4b95-b4ee-3be2e1e3f50e, 211c387...

To convert to separate columns, you can do the following:

df.join(pd.DataFrame(df.pop("identifiers").values.tolist()).add_prefix("identifier_"))
id identifier_0 identifier_1 identifier_2 identifier_3 identifier_4
0 f06c0800-7e56-45e7-85eb-0047c2c5b4b1 f06c0800-7e56-45e7-85eb-0047c2c5b4b1 f06c0800-7e56-45e7-85eb-0047c2c5b4b1 999-17-9471 S99913919 X55105329X
1 e916a083-8ee6-4df8-acbe-b711fa791253 e916a083-8ee6-4df8-acbe-b711fa791253 e916a083-8ee6-4df8-acbe-b711fa791253 999-55-7132 None None
2 f4673866-81fb-411a-8486-0c27f2a5b480 f4673866-81fb-411a-8486-0c27f2a5b480 f4673866-81fb-411a-8486-0c27f2a5b480 999-38-5099 S99949959 X8357956X
3 0f30cacb-3348-4baf-8946-cd2175a750b3 0f30cacb-3348-4baf-8946-cd2175a750b3 0f30cacb-3348-4baf-8946-cd2175a750b3 999-90-5099 S99948652 X74228791X
4 7b5bea3e-de83-4c43-a89c-5078e923c3db 7b5bea3e-de83-4c43-a89c-5078e923c3db 7b5bea3e-de83-4c43-a89c-5078e923c3db 999-42-8046 S99996382 X6379437X
5 fd04234a-e56d-4362-88a5-bbaa47addb6b fd04234a-e56d-4362-88a5-bbaa47addb6b fd04234a-e56d-4362-88a5-bbaa47addb6b 999-20-3930 S99943973 X26825917X
6 645f1b3d-ee47-4e5b-8e06-a49f23ff6f52 645f1b3d-ee47-4e5b-8e06-a49f23ff6f52 645f1b3d-ee47-4e5b-8e06-a49f23ff6f52 999-14-7250 S99982592 X19873402X
7 100ddb60-fb28-4663-a8f2-af93cdc4294e 100ddb60-fb28-4663-a8f2-af93cdc4294e 100ddb60-fb28-4663-a8f2-af93cdc4294e 999-59-1085 S99955242 X66237126X
8 59339691-0270-4c4a-a34d-338e3008b9c3 59339691-0270-4c4a-a34d-338e3008b9c3 59339691-0270-4c4a-a34d-338e3008b9c3 999-97-9742 S99981315 X49422373X
9 211c3874-3973-4b95-b4ee-3be2e1e3f50e 211c3874-3973-4b95-b4ee-3be2e1e3f50e 211c3874-3973-4b95-b4ee-3be2e1e3f50e 999-25-1173 S99966255 X23836392X

This will give you separate identifier_0, identifier_1, … columns for each Patient.identifier[N] value.