In [1]:
import pandas as pd

1. Data Sources¶

Stroke mortality data was obtained from the Underlying Cause of Death, 2018-2024, Single Race Request database by CDC WONDER (Wide-ranging Online Data for Epidemiologic Research).

To capture the full spectrum of stroke mortality, deaths were segmented into two distinct clinical classifications based on ICD-10 codes:

Category Description ICD-10 Codes / Saved Query Target Variable
Acute stroke deaths Deaths where stroke was the direct, acute cause I60–I64 / D510F921 acute_stroke_mortality_per_100k
Sequelae of stroke deaths Deaths attributed to long-term/late effects of a prior stroke I69 / D510F922 sequelae_stroke_mortality_per_100k

CDC WONDER population estimates are unavailable at the traditional county level for Connecticut from 2022 onward due to the state's transition to Planning Regions. Therefore, a substitute for CDC WONDER's population data was pulled from the cleaned acs_data.csv file (which was sourced from U.S. Census Bureau’s 2023 American Community Survey (ACS) 5-Year Estimates).

2. Temporal Scope: Why Data Was Pooled Across Years¶

County-level death counts, especially for a single cause like stroke, can fluctuate substantially year to year, particularly in lower-population counties. A single bad (or good) year can produce a misleadingly volatile rate. To address this, seven years of data (2018–2024) were pooled before calculating rates to create a stable, representative death count baseline that aligns its center right at 2021.

The 2023 5-Year ACS Estimates reflect a period estimate pooling data collected over 60 months between 2019 and 2023, which also structurally centers the dataset's operational midpoint on 2021.

3. Handling Suppressed Death Counts¶

CDC WONDER suppresses any cell where the death count falls between 1 and 9, displaying it as "Suppressed" rather than the actual number, in order to protect privacy in low-count cells.

Suppressed cells were replaced with a fixed proxy value of 5 (the exact middle of that range), making it the value that minimizes the maximum possible distortion in either direction.

4. Mortality Rate Calculation Methodology:¶

To compare the 7-year stroke data (2018-2024) fairly against our 5-year estimate census data from ACS, the pooled death total is first converted into an annualized average before being related to the population baseline.

$$\text{Annual Mortality Rate per 100,000} = \frac{\dfrac{\text{Total deaths from 2018–2024}}{\text{7 years}}}{\text{ACS total population}} \times 100000$$

Acute Stroke Mortality: acute_stroke_mortality_per_100k¶

In [2]:
# Import data for acute stroke deaths
df1 = pd.read_csv('Acute Stroke_Underlying Cause of Death, 2018-2024, Single Race.csv', dtype={'County Code':str}, skipfooter=68, engine='python')
df1.tail()
Out[2]:
Notes State State Code County County Code Deaths Population Crude Rate Crude Rate Lower 95% Confidence Interval Crude Rate Upper 95% Confidence Interval
86 NaN New York 36 Washington County, NY 36115 77 424690 18.1 14.3 22.7
87 NaN New York 36 Wayne County, NY 36117 101 632955 16.0 12.8 19.1
88 NaN New York 36 Westchester County, NY 36119 757 6886506 11.0 10.2 11.8
89 NaN New York 36 Wyoming County, NY 36121 33 278686 11.8 8.2 16.6
90 NaN New York 36 Yates County, NY 36123 39 172457 22.6 16.1 30.9
In [3]:
# Keep only important columns
df1 = df1[['State', 'County', 'County Code', 'Deaths']]
df1.rename(columns={'County Code': 'fips'}, inplace=True)
df1.head()
Out[3]:
State County fips Deaths
0 Connecticut Fairfield County, CT 09001 803
1 Connecticut Hartford County, CT 09003 853
2 Connecticut Litchfield County, CT 09005 246
3 Connecticut Middlesex County, CT 09007 163
4 Connecticut New Haven County, CT 09009 822
In [4]:
# Import ACS data to get counties' total population
acs_df = pd.read_csv('../acs_data.csv', dtype={'fips':str})
acs_df.head(2)
Out[4]:
fips county state total_pop pcnt_65_plus poverty_rate pcnt_insured pcnt_bachelors pcnt_low_income pcnt_middle_class pcnt_upper_class
0 36001 Albany NY 315374 17.8 12.9 97.0 41.6 29.9 39.1 12.1
1 36003 Allegany NY 47027 19.7 16.8 95.1 21.0 40.2 39.1 4.1
In [5]:
# Merge ACS data with df1
acute = pd.merge(acs_df[['fips', 'county', 'state', 'total_pop']], df1[['fips','Deaths']], on='fips', how='left')
acute
Out[5]:
fips county state total_pop Deaths
0 36001 Albany NY 315374 256
1 36003 Allegany NY 47027 51
2 36005 Bronx NY 1419250 913
3 36007 Broome NY 197738 225
4 36009 Cattaraugus NY 76479 94
... ... ... ... ... ...
86 09007 Middlesex CT 164983 163
87 09009 New Haven CT 862028 822
88 09011 New London CT 267707 318
89 09013 Tolland CT 146907 95
90 09015 Windham CT 116156 118

91 rows × 5 columns

In [6]:
acute.info()
<class 'pandas.DataFrame'>
RangeIndex: 91 entries, 0 to 90
Data columns (total 5 columns):
 #   Column     Non-Null Count  Dtype
---  ------     --------------  -----
 0   fips       91 non-null     str  
 1   county     91 non-null     str  
 2   state      91 non-null     str  
 3   total_pop  91 non-null     int64
 4   Deaths     91 non-null     str  
dtypes: int64(1), str(4)
memory usage: 3.7 KB
In [7]:
# Search for rows where death count is suppressed
acute.loc[acute['Deaths']=='Suppressed']
Out[7]:
fips county state total_pop Deaths
20 36041 Hamilton NY 5102 Suppressed
In [8]:
# Replace "Suppressed" with "5"
acute.loc[acute['Deaths']=='Suppressed', 'Deaths'] = '5'
acute.loc[acute['county'] == 'Hamilton County',:]
Out[8]:
fips county state total_pop Deaths
In [9]:
# Now "Deaths" column can be converted to integer
acute['Deaths'] = acute['Deaths'].astype('int64')
acute.info()
<class 'pandas.DataFrame'>
RangeIndex: 91 entries, 0 to 90
Data columns (total 5 columns):
 #   Column     Non-Null Count  Dtype
---  ------     --------------  -----
 0   fips       91 non-null     str  
 1   county     91 non-null     str  
 2   state      91 non-null     str  
 3   total_pop  91 non-null     int64
 4   Deaths     91 non-null     int64
dtypes: int64(2), str(3)
memory usage: 3.7 KB
In [10]:
# Calculate the mortality rate
acute['acute_stroke_mortality_per_100k'] = (((acute['Deaths'] / 7) / acute['total_pop']) *100000).round(2)
acute
Out[10]:
fips county state total_pop Deaths acute_stroke_mortality_per_100k
0 36001 Albany NY 315374 256 11.60
1 36003 Allegany NY 47027 51 15.49
2 36005 Bronx NY 1419250 913 9.19
3 36007 Broome NY 197738 225 16.26
4 36009 Cattaraugus NY 76479 94 17.56
... ... ... ... ... ... ...
86 09007 Middlesex CT 164983 163 14.11
87 09009 New Haven CT 862028 822 13.62
88 09011 New London CT 267707 318 16.97
89 09013 Tolland CT 146907 95 9.24
90 09015 Windham CT 116156 118 14.51

91 rows × 6 columns

In [11]:
# Remove total_pop and Deaths columns
acute.drop(columns=['total_pop','Deaths'], inplace=True)
acute.head(3)
Out[11]:
fips county state acute_stroke_mortality_per_100k
0 36001 Albany NY 11.60
1 36003 Allegany NY 15.49
2 36005 Bronx NY 9.19

Sequelae of Stroke Mortality: sequelae_stroke_mortality_per_100k¶

In [12]:
# Import CDC data for sequelae stroke deaths
df2 = pd.read_csv('Sequelae of Stroke_Underlying Cause of Death, 2018-2024, Single Race.csv', dtype={'County Code':str}, skipfooter=64, engine='python')
df2.tail()
Out[12]:
Notes State State Code County County Code Deaths Population Crude Rate Crude Rate Lower 95% Confidence Interval Crude Rate Upper 95% Confidence Interval
90 NaN New York 36.0 Westchester County, NY 36119 97 6886506 1.4 1.1 1.7
91 NaN New York 36.0 Wyoming County, NY 36121 Suppressed 278686 Suppressed Suppressed Suppressed
92 NaN New York 36.0 Yates County, NY 36123 Suppressed 172457 Suppressed Suppressed Suppressed
93 Total New York 36.0 NaN NaN 2148 137284074 1.6 1.5 1.6
94 Total NaN NaN NaN NaN 4613 215578231 2.1 2.1 2.2
In [13]:
df2.drop(index = df2[df2['Notes'] == 'Total'].index, inplace=True)
df2.tail()
Out[13]:
Notes State State Code County County Code Deaths Population Crude Rate Crude Rate Lower 95% Confidence Interval Crude Rate Upper 95% Confidence Interval
88 NaN New York 36.0 Washington County, NY 36115 Suppressed 424690 Suppressed Suppressed Suppressed
89 NaN New York 36.0 Wayne County, NY 36117 18 632955 2.8 1.7 4.5
90 NaN New York 36.0 Westchester County, NY 36119 97 6886506 1.4 1.1 1.7
91 NaN New York 36.0 Wyoming County, NY 36121 Suppressed 278686 Suppressed Suppressed Suppressed
92 NaN New York 36.0 Yates County, NY 36123 Suppressed 172457 Suppressed Suppressed Suppressed
In [14]:
# Keep only important columns
df2 = df2[['State', 'County', 'County Code', 'Deaths']]
df2.rename(columns={'County Code': 'fips'}, inplace=True)
df2.head(3)
Out[14]:
State County fips Deaths
0 Connecticut Fairfield County, CT 09001 90
1 Connecticut Hartford County, CT 09003 116
2 Connecticut Litchfield County, CT 09005 25
In [15]:
# Merge ACS data with df2
sequelae = pd.merge(acs_df[['fips', 'county', 'state', 'total_pop']], df2[['fips','Deaths']], on='fips', how='left')
sequelae
Out[15]:
fips county state total_pop Deaths
0 36001 Albany NY 315374 30
1 36003 Allegany NY 47027 12
2 36005 Bronx NY 1419250 119
3 36007 Broome NY 197738 42
4 36009 Cattaraugus NY 76479 13
... ... ... ... ... ...
86 09007 Middlesex CT 164983 20
87 09009 New Haven CT 862028 139
88 09011 New London CT 267707 47
89 09013 Tolland CT 146907 16
90 09015 Windham CT 116156 17

91 rows × 5 columns

In [16]:
# Replace "Suppressed" with "5"
sequelae.loc[sequelae['Deaths']=='Suppressed', 'Deaths'] = '5'
sequelae.tail()
Out[16]:
fips county state total_pop Deaths
86 09007 Middlesex CT 164983 20
87 09009 New Haven CT 862028 139
88 09011 New London CT 267707 47
89 09013 Tolland CT 146907 16
90 09015 Windham CT 116156 17
In [17]:
# Conver Death column to integer before calculating the mortality rate
sequelae['Deaths'] = sequelae['Deaths'].astype('int64')
sequelae.info()
<class 'pandas.DataFrame'>
RangeIndex: 91 entries, 0 to 90
Data columns (total 5 columns):
 #   Column     Non-Null Count  Dtype
---  ------     --------------  -----
 0   fips       91 non-null     str  
 1   county     91 non-null     str  
 2   state      91 non-null     str  
 3   total_pop  91 non-null     int64
 4   Deaths     91 non-null     int64
dtypes: int64(2), str(3)
memory usage: 3.7 KB
In [18]:
# Calculating the mortality rate
sequelae['sequelae_stroke_mortality_per_100k'] = (((sequelae['Deaths'] / 7) / sequelae['total_pop']) *100000).round(2)
sequelae
Out[18]:
fips county state total_pop Deaths sequelae_stroke_mortality_per_100k
0 36001 Albany NY 315374 30 1.36
1 36003 Allegany NY 47027 12 3.65
2 36005 Bronx NY 1419250 119 1.20
3 36007 Broome NY 197738 42 3.03
4 36009 Cattaraugus NY 76479 13 2.43
... ... ... ... ... ... ...
86 09007 Middlesex CT 164983 20 1.73
87 09009 New Haven CT 862028 139 2.30
88 09011 New London CT 267707 47 2.51
89 09013 Tolland CT 146907 16 1.56
90 09015 Windham CT 116156 17 2.09

91 rows × 6 columns

In [19]:
# Remove total_pop and Deaths columns
sequelae.drop(columns=['total_pop','Deaths'], inplace=True)
sequelae.head(3)
Out[19]:
fips county state sequelae_stroke_mortality_per_100k
0 36001 Albany NY 1.36
1 36003 Allegany NY 3.65
2 36005 Bronx NY 1.20

Merge Acute Stroke Mortality Rate with Sequelae Stroke Mortality Rate¶

In [20]:
stroke_mortality = pd.merge(acute, sequelae[['fips','sequelae_stroke_mortality_per_100k']], on='fips', how='left')
stroke_mortality.head(3)
Out[20]:
fips county state acute_stroke_mortality_per_100k sequelae_stroke_mortality_per_100k
0 36001 Albany NY 11.60 1.36
1 36003 Allegany NY 15.49 3.65
2 36005 Bronx NY 9.19 1.20
In [21]:
stroke_mortality.tail(3)
Out[21]:
fips county state acute_stroke_mortality_per_100k sequelae_stroke_mortality_per_100k
88 09011 New London CT 16.97 2.51
89 09013 Tolland CT 9.24 1.56
90 09015 Windham CT 14.51 2.09
In [22]:
stroke_mortality.info()
<class 'pandas.DataFrame'>
RangeIndex: 91 entries, 0 to 90
Data columns (total 5 columns):
 #   Column                              Non-Null Count  Dtype  
---  ------                              --------------  -----  
 0   fips                                91 non-null     str    
 1   county                              91 non-null     str    
 2   state                               91 non-null     str    
 3   acute_stroke_mortality_per_100k     91 non-null     float64
 4   sequelae_stroke_mortality_per_100k  91 non-null     float64
dtypes: float64(2), str(3)
memory usage: 3.7 KB
In [23]:
stroke_mortality.to_csv('../stroke_mortality.csv', index=False)