How To See America’s Staggering Medicare Coverage Gap With Python
How To See America’s Staggering Medicare Coverage Gap With Python
A Python Plotly tutorial using the newly released 2027 CMS Marketplace data
ACA open enrollment for 2027 begins on November 1, 2026.
For Americans in their 50s and early 60s, this is the most consequential coverage decision they will make in their working years leading up to retirement.
Data at Depth is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.
I’ve had this conversation with American friends living abroad — what does it actually cost when I go back home?
The answer for those in one of the 29 FFM (federally facilitated martketplace) states, is ever-increasing. For example, a 60-year-old on a Silver plan in 2027, pays a median of $1,709 per month. That’s $20,508 a year. At 65, Medicare Part B runs roughly $210 per month (this year’s number will be released soon).
The gap between those two numbers (almost $1,500 a month!) lands in the five years before the age of 65.
Let’s look at that gap with some real Python data visualization, using the 2027 CMS Marketplace data (recently made available publicly on the helathare.gov site).
NOTE: The complete notebook file (medicare_gap.ipynb) can be accessed on my GitHub: [HERE]. The datafiles are too large for github so use please use the link provide in “The Dataset” section.
The Dataset
The 2027 rate plans were very quietly released last week by the Centers for Medicare and Medicaid Services — specifically the rate and plan-attributes files from healthcare.gov. This covers 29 federally facilitated marketplace (FFM) states. Other states not in this plan run their own exchanges and are therefore not in this data.
All premiums shown are full unsubsidized rates. There are the full rates of what an early retiree without employer coverage faces.
The two files we need, after downloading and organizing, give us:
rate-puf.csv— individual monthly premiums by state, plan, age, and tobacco statusplan-attributes-puf.csv— plan metadata includingMetalLevelandDentalOnlyPlan
Here is how we load and prepare the data:
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
# Load the rate file (individual premiums)
df_rate = pd.read_csv('rate-puf.csv',
usecols=['BusinessYear', 'StateCode', 'PlanId', 'Tobacco', 'Age', 'IndividualRate'],
low_memory=False)
# Load plan attributes and filter to medical plans only
df_plan = pd.read_csv('plan-attributes-puf.csv',
usecols=['StandardComponentId', 'MetalLevel', 'DentalOnlyPlan'],
low_memory=False)
# Filter: 2027 data, exclude dental and child/family rows
df_rate = df_rate[df_rate['BusinessYear'] == 2027]
df_rate = df_rate[df_rate['Tobacco'].isin(['No Preference', 'Tobacco User/Non-Tobacco User'])]
df_rate = df_rate[~df_rate['Age'].isin(['0-14', 'Family Option'])]
df_plan = df_plan[df_plan['DentalOnlyPlan'] == 'No']
df_plan_lookup = df_plan[['StandardComponentId', 'MetalLevel']].drop_duplicates('StandardComponentId')
# Join on the 14-character standard component ID
df = df_rate.merge(df_plan_lookup,
left_on='PlanId', right_on='StandardComponentId', how='inner')
print(f"Joined rows: {len(df):,} | States: {df['StateCode'].nunique()}")
Running this gives us a clean dataset covering all 29 FFM states, with every plan, age and metal tier we need.
Looking at what we see, each row is one plan’s monthly premium for one age in one state. The MetalLevel column is what we use to isolate Silver, Bronze, Gold, and Platinum plans.
Now let’s start to make sense of this data.
Chart 1: The Premium Ladder
The first thing to show is the trajectory — how Silver plan premiums move across a 20-year age window, from 40 to 60, across all 29 FFM states.
# Silver plans only
df_silver = df[df['MetalLevel'] == 'Silver']
# Median monthly premium at ages 40, 45, 50, 55, 60
ladder_ages = ['40', '45', '50', '55', '60']
df_ladder = df_silver[df_silver['Age'].isin(ladder_ages)].copy()
df_ladder['Age'] = df_ladder['Age'].astype(int)
age_medians = df_ladder.groupby('Age')['IndividualRate'].median().reset_index()
age_medians.columns = ['Age', 'MedianMonthly']
fig = go.Figure()
fig.add_trace(go.Scatter(
x=age_medians['Age'],
y=age_medians['MedianMonthly'],
mode='lines+markers',
fill='tozeroy',
fillcolor='rgba(214, 39, 40, 0.1)',
line=dict(color='#D62728', width=3),
marker=dict(size=10)
))
fig.update_layout(
title="Silver plan premiums rise sharply in your 50s — then Medicare begins",
xaxis=dict(title="Age", tickvals=[40, 45, 50, 55, 60]),
yaxis=dict(title="Monthly premium ($)", tickprefix='$', tickformat=',d')
)
fig.show()
Running this code produces the chart below:
Silver plan median monthly premium by age, ages 40–60, across all 29 FFM states. Full unsubsidized rates, 2027 CMS Marketplace data.
What this chart tells us is a fairly direct story. We can see how the slope starts to steepen after the age of 45. This is truly incessant. Ouch.
The jump from 55 to 60 is from about $1,400 to $1,709. And at 60, Medicare is still five years away!
A 60-year-old on a Silver plan pays roughly eight times what a 65-year-old pays for Medicare Part B.
How many Americans at 60 try to avoid the costs of medical care until they are 65? If I was in that sitation there would certainly be a consideration on taking the risk.
Chart 2: The State Breakdown
The national median hides a lot. Where you live plays a massive role in what you pay per month. Let’s put together a bar chart to show the variance:
# Silver plan, age 60, by state
df_60 = df_silver[df_silver['Age'] == '60']
state_med = df_60.groupby('StateCode')['IndividualRate'].median().reset_index()
state_med.columns = ['State', 'MedianMonthly']
state_med = state_med.sort_values('MedianMonthly').reset_index(drop=True)
national_median = df_60['IndividualRate'].median()
fig = px.bar(
state_med, x='MedianMonthly', y='State',
orientation='h',
labels={'MedianMonthly': 'Median monthly premium ($)', 'State': ''}
)
fig.add_vline(x=national_median, line_dash='dash',
annotation_text=f"National median ${national_median:,.0f}")
fig.update_layout(title="Where you live determines how painful the Medicare gap is")
fig.show()
Running this produces a ranked view of all 29 states from cheapest to most expensive.
Silver plan median monthly premium for a 60-year-old, by state, sorted cheapest to most expensive. All 29 FFM states. Full unsubsidized rate, 2027 CMS Marketplace data.
As a comparison, Iowa and West Virginia are at opposite ends in this dataset. A 60-year-old in each state lives in very different worlds. If I am someon living in West Virginia, paying premiums and approaching 60, moving to Iowa would be something to consider.
Iowa comes in at $1,270 per month. West Virginia comes in at $2,612. That is more than double.
Using the national median of $1,709 as a planning number would be a significant underestimate for anyone in West Virginia, Alaska, or Wyoming.
Chart 3: And It Gets Worse: The Five-Year Cost
A single monthly premium is alread staggering, but the Medicare gap isn’t one month after 60 - it goes for 5 more years when premiums are at their highest. Let’s take a look at these numbers:
# 5-year total cost: monthly median × 60 months
state_med['FiveYearTotal'] = state_med['MedianMonthly'] * 60
fig = px.bar(
state_med.sort_values('FiveYearTotal'),
x='FiveYearTotal', y='State',
orientation='h',
labels={'FiveYearTotal': '5-year total premium cost ($)', 'State': ''}
)
national_5yr = national_median * 60
fig.add_vline(x=national_5yr, line_dash='dash',
annotation_text=f"National median ${national_5yr:,.0f}")
fig.update_layout(title="Five years before Medicare: up to $157K in unsubsidized premiums")
fig.show()
Running this shows what the full window looks like when the months add up.
Total Silver plan premium cost for a 60-year-old over five years (monthly median × 60 months)
The national median across all 29 states is about $103,000 for the five-year window.
West Virginia comes in at nearly $157,000. Iowa sits at $76,000. Again, all else being equal, if I’m 60 living in WV, I’m packing up my truck and heading West.
Anyone above the subsidy threshold — a high earner, a retiree drawing down savings, a returning expat — faces these numbers directly.
Chart 4: Even Bronze Is Expensive at 60
The final chart answers a natural follow-up question - what if someone chooses a cheaper plan? Bronze plans have lower premiums than Silver, but they come with higher deductibles. So how much cheaper are they, actually, for a 60-year-old? Let’s code this one out:
# Metal tier comparison at age 60, national median
tiers = ['Bronze', 'Silver', 'Gold', 'Platinum']
df_60_all = df[df['Age'] == '60']
tier_med = (df_60_all[df_60_all['MetalLevel'].isin(tiers)]
.groupby('MetalLevel')['IndividualRate'].median()
.reindex(tiers).reset_index())
tier_med.columns = ['MetalLevel', 'MedianMonthly']
fig = px.bar(
tier_med, x='MetalLevel', y='MedianMonthly',
color='MetalLevel',
color_discrete_map={'Bronze':'#B5651D','Silver':'#888','Gold':'#C8A200','Platinum':'#4A7BA8'},
labels={'MetalLevel': 'Plan type', 'MedianMonthly': 'Median monthly premium ($)'}
)
fig.update_layout(
title="At 60, even the cheapest plan costs more than most Americans' rent",
showlegend=False
)
fig.show()
This produces a side-by-side view of all four metal tiers at age 60.
National median monthly premium at age 60 by plan type across all 29 FFM states.
Bronze comes in at about $1,200 per month nationally.
That’s not cheap. The US median rent in 2026 is roughly $1,300 per month. At 60 years of age, Bronze coverage is almost the same. This means that just to stay healthy and safe, you need on average 30,000 dollars a year. And this doesn’t include eating.
The key takeaway here is that the “discount” from Bronze versus Silver is meaningful but not transformational — you’re comparing expensive to very expensive, with the cost-sharing tradeoffs landing later when you actually need care.
In Summary…
The numbers in this article have a habit of sitting with you.
When doing retirement calculations, this 5 year gap can really catch people off guard. Most folks have planned for their 401k and for their mortgage but not this gap. I am 57 years old, and at this age retirement is really not that far off.
The five years between early retirement and Medicare eligibility, the years when premiums are at their highest, now require some careful consideration
And with Python tools available, we can clearly visualize what this trend looks like The he area chart fills in and the bar chart sorts the states. We can turn the monthly amounts into a five-year total that you can wrap your mind around.
None of this is analysis that needs a data scientist. All you need to do is find the right CSV file and do a simple join, a groupby, and using the right library, display serious visuals.
NOTE: The complete notebook file (medicare_gap.ipynb) can be accessed on my GitHub: [HERE]. The datafiles are too large for github so use please use the link provide in “The Dataset” section.
Data at Depth is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.
© 2026 John Loewen
Via Dei Cerchi, Rome, Latium, Italy