Plotting Surgical Demand With Trilliant Health’s Forecasting API

Jamie Supica
The Trilliant Health Tech Blog
6 min readSep 25, 2023
Image by Midjourney — a colorful happy line drawing of a doctor seen from behind holding a set of keys looking at large magic 8 ball with the doctor offset to the side

Trilliant Health recently announced that the Basic tier of their National Demand Forecast will be available at no cost. For a higher-level overview of what the forecast does and how it fits into the current state of healthcare analytics, see this article.

In this article, the assumption is you’re intrigued enough to start playing with the data.

The code blocks below are meant to be pasted into the notebook of your choice (Jupyter, Colab, etc.). Functions defined early on are referenced in later code blocks, so make sure you get it all. In the examples below, we’re going to:

  • View the available forecast parameters for the Surgical care discipline and compare projected volumes for multiple service lines
  • Look up patient demographic parameters, narrow a forecast to a specific patient demographic, and chart the Interquartile Range (IQR).

Now, let’s get started…

Set up an Account

You’ll need to sign up for Trilliant Health’s Demand Forecast National Basic API and get a key to make calls. You can either go through their website or visit the developer portal directly to sign up.

Once you’re signed up and in the portal, there’s documentation on setting up authentication and how to interpret the data, but I’d highly recommend the tutorial in this article, which walks you through authentication, making test calls, and interpreting data in the JSON response.

Once you’re set up and have your API key, we can start making calls to the endpoint!

Comparing Surgical Service Lines

First, we’ll pull some of the serviceLines that comprise the Surgical care discipline. Start by importing the necessary libraries.

import requests
import pandas as pd
import plotly.graph_objs as go

And create functions to call the API and to pretty-print the response.

# Set values to appear in headers.  
# These will be the same for all endpoints we call.
accept = "application/json"
api_key = "your API key" # replace this with your own API key!
user_agent = "" # leave this blank

HEADERS = {"accept": accept, "apiKey": api_key, "user-agent": user_agent}


def get_api_result(url: str) -> dict:
response = requests.get(url, headers=HEADERS)
response.raise_for_status()
return response.json()


def pprint_result(result: dict) -> None:
from pprint import pprint

pprint(result, sort_dicts=False)

Next, we’ll view parameters specific to the surgical demand forecast at /demand-forecast-criteria-options/surgical.

# Making a call to return surgical specific options
surgical_url = (
f"https://api.trillianthealth.com/v1/demand-forecast-criteria-options/surgical"
)

pprint_result(get_api_result(surgical_url))

The response will be all of the serviceLines available in the surgical discipline and their subgroupings: prcedureSubType.

{
"serviceLines": [
{
"name": "Endocrine System",
"procedureSubTypes": [
"Adrenal and Pituitary Procedures",
"Other Endocrine Nutritional and Metabolic O.R. Procedures",
"Thyroid Parathyroid and Thyroglossal Procedures"
]
},
{
"name": "ENT",
"procedureSubTypes": [
"Mouth Procedures",
"Other Ear Nose Mouth and Throat O.R. Procedures",
"Salivary Gland Procedures",
"Sinus and Mastoid Procedures"
]
},
{
"name": "Heart/Vascular",
"procedureSubTypes": [
"Arterial Procedures",
"Cardiac Assist Procedures",
"Cardiac Catheterization",
"Cardiac Electrophysiology",
...
]
},
...

While this is the complete list of procedure groupings, the open access National Basic version is limited to forecasts at the serviceLine level, so let’s return only the Surgical serviceLines:

# Return just the service lines
surgical_url = (
f"https://api.trillianthealth.com/v1/demand-forecast-criteria-options/surgical"
)

for i, j in get_api_result(surgical_url).items():
for k in j:
print(k["name"])

Ahh, that’s better.

Digestive System
Endocrine System
ENT
Eye/Ocular
Heart/Vascular
Hemic/Lymphatic
Integumentary System
Interventional Pain Management
Male Reproductive System
Neuro/Spine
OB/GYN
Orthopedic
Other
Respiratory System
Urinary System

To plot projections for some of these serviceLines, we create a function that accepts care_discipline, geographical_location, and service_line. and outputs a line plot of all three.

care_discipline will be “surgical”, although you could use this function to call one of the other endpoints like medical, primary-care, or emergency.

geographical_location will always be “nationwide” in the open access version, but state, Core-Based Statistical Area (CBSA), and ZIP code level forecasts are available in the paid versions.

Now, let’s get to plotting…

# Plot service lines of your choice

# Empty string to hold your coordinate values
traces = []

# Make a list of service lines you'd like to plot
# Then call create_service_line_trace for each
for service_lines in ["Orthopedic", "Heart/Vascular", "Digestive System"]:
traces.append(
create_service_line_trace(
care_discipline="surgical",
geographical_location="nationwide",
service_line=service_lines,
)
)

# Plot your results
fig = go.Figure(data=traces)

fig.update_layout(
legend_title="Service Lines",
title_text="Average Nationwide Visit Count Projection by Surgical Service Line",
margin=dict(t=120),
plot_bgcolor="white",
xaxis=dict(title="Year"),
yaxis=dict(title="Mean Predicted Visit Count"),
)

fig.update_yaxes(gridcolor="lightgrey")

fig.show()

And voila! A comparison of popular surgical service lines in the US!

Which is great, but let’s get a little more specific.

Plotting IQR for a Specific Demographic

My dad had a heart procedure last year, which has me wondering about the trends for Heart/Vascular procedures for guys his age. But what are my parameter options?

Enter the /v1/demand-forecast-criteria-options/common endpoint. It’s going to tell us what fields and options are available for the fields that are common to all serviceLines, not just surgical.

Using theget_api_result function we defined previously, we return all the common parameters and their enums.

# Making a call to return the common parameter options options
common_url = (
f"https://api.trillianthealth.com/v1/demand-forecast-criteria-options/common"
)

result = get_api_result(common_url)
pprint_result(result)

Which returns.

{'ageGroups': ['0-14', '15-34', '35-49', '50-64', '65-84', '85+'],
'genders': ['MALE', 'FEMALE'],
'careSettings': ['INPATIENT', 'OUTPATIENT']}

Dad is 72, so I’ll limit my results to the following.

  • ageGroups= ‘65–84'
  • gender= ‘MALE’
  • careSetting= ‘INPATIENT’

These get used a little later, so sit on them for now.

Returning and Plotting Data

Part of the power of the forecasting response is that it provides a distribution of possible outcomes instead of a single value which is almost guaranteed to be wrong.

Because we can’t actually predict the future, we’ll plot the median along with the middle 50% of possible values (the IQR). This results in a plot that makes predictions into the future but also acknowledges that there’s a range of possible values you could reasonably expect.

This function accepts each individual segment parameter and outputs a segment graph.

# Create a function that accepts segment parameters and creates an IQR plot
def create_and_graph_iqr(
care_discipline: str,
geographical_location: str,
service_line: str,
care_setting: str,
gender: str,
age_group: str,
):
forecast_url = (
f"https://api.trillianthealth.com/v1/demand-forecast/"
f"{care_discipline}/{geographical_location}?"
f"gender={gender}&"
f"serviceLine={service_line}&"
f"ageGroup={age_group}&"
f"careSetting={care_setting}"
)

# get data from the API for your segment
data = get_api_result(forecast_url)

# Create lists of data to plot
years = []
percentile25 = []
percentile50 = []
percentile75 = []

# Populate the lists from the response data
for year, counts_data in data["projections"].items():
years.append(year)
percentile25.append(counts_data["counts"]["percentile25"])
percentile50.append(counts_data["counts"]["percentile50"])
percentile75.append(counts_data["counts"]["percentile75"])

# Plot the median and IQR using plotly
fig = go.Figure(
[
# Plot the median
go.Scatter(
x=years,
y=percentile50,
line=dict(color="rgb(0,100,80)"),
mode="lines",
name="percentile50",
),
# Plot the shading of the Interquartile range
go.Scatter(
x=years + years[::-1],
y=percentile75 + percentile25[::-1],
fill="toself",
fillcolor="rgba(0,100,80,0.2)",
line=dict(color="rgba(255,255,255,0)"),
hoverinfo="skip",
showlegend=False,
),
# Plot the 25th percentile to enable tooltip
go.Scatter(
x=years,
y=percentile75,
line=dict(color="rgba(255,255,255,0)"),
mode="lines",
showlegend=False,
name="percentile25",
),
# Plot the 75th percentile to enable tooltip
go.Scatter(
x=years,
y=percentile75,
line=dict(color="rgba(255,255,255,0)"),
mode="lines",
showlegend=False,
name="percentile75",
),
]
)
fig.update_layout(
legend_traceorder="reversed",
legend_title="Projected Visit Counts",
title_text= f"Interquartile Range of Visit Count by Year With Filters" \
f"<br><sup><b>geographicalLocation:</b> {geographical_location} | " \
f"<b>ageGroup:</b> {age_group} | " \
f"<b>gender:</b> {gender}<br>" \
f"<b>careSetting:</b> {care_setting} | " \
f"<b>serviceLine:</b> {service_line}</sup>",
margin=dict(t=120),
plot_bgcolor="white",
xaxis=dict(title="Year"),
yaxis=dict(title="Predicted Visit Count"),
)

fig.update_yaxes(gridcolor="lightgrey")

fig.show()

Then, calling the function with the parameters below:

# Call the function and pass it your segment parameters
create_and_graph_iqr(
care_discipline="surgical",
geographical_location="nationwide",
service_line="Heart/Vascular",
care_setting="INPATIENT",
gender="MALE",
age_group="65-84",
)

We get a plot of the median and shading in the IQR!

What Next?

Projections at a national level by patient demographic are useful as a generalization, but healthcare trends are a highly local phenomenon. Using a nationally forecasted incident rate to predict visits at a local level can be misleading. Coronary Bypass operations within the Surgical Heart/Vascular service line will have very different forecasts in Oregon and Mississippi.

The subscription-based versions of Demand Forecast provide a more detailed breakdown of geography, with forecasts calculated at the state, Core-Based Statistical Area (CBSA), and ZIP code level as well as the next level of procedure subcategorizations is available for each serviceLine.

If you’re interested in finding out more about the expanded Demand Forecast products, you can contact Trilliant Health for more information.

--

--