Hospital / Emergency Services Data¶

Loading Hospital Data: Filtering to NY/NJ/CT¶

In [3]:
import pandas as pd
import requests

hospitals = pd.read_csv(
    "Hospital_General_Information.csv"
)
ct_towns = pd.read_csv("../../reference/ct_crosswalk/ct_town_crosswalk.csv")

hospitals = hospitals[
    hospitals["State"].isin(["NY", "NJ", "CT"])
]

Getting FIPS Code¶

In [5]:
fips_df = pd.read_csv("../ny_nj_ct_fips.csv")
In [6]:
hospitals.columns = hospitals.columns.str.lower()
fips_df.columns = fips_df.columns.str.lower()
In [7]:
hospitals["county"] = hospitals["county/parish"].str.upper().str.strip()
hospitals["state"] = hospitals["state"].str.upper().str.strip()

fips_df["county"] = fips_df["county"].str.upper().str.strip()
fips_df["state"] = fips_df["state"].str.upper().str.strip()
In [8]:
fips_df["county"] = (
    fips_df["county"]
    .str.upper()
    .str.replace(" COUNTY", "", regex=False)
    .str.strip()
)
In [9]:
fips_df["fips"] = fips_df["fips"].astype(str).str.zfill(5)
In [10]:
hospitals["state"] = hospitals["state"].str.upper().str.strip()
fips_df["state"] = fips_df["state"].str.upper().str.strip()

Merging Hospitals Dataset with FIPS Dataframe -> Creating FIPS Column¶

In [12]:
hospitals_geo = hospitals.merge(
    fips_df,
    on=["state", "county"],
    how="left"
)

Getting Hospital Count Number By County¶

In [14]:
hospital_counts = hospitals_geo.groupby("fips").size().reset_index(name="hospital_count")
In [15]:
hospital_counts
Out[15]:
fips hospital_count
0 09001 8
1 09003 8
2 09005 2
3 09007 3
4 09009 9
... ... ...
80 36113 1
81 36117 1
82 36119 12
83 36121 1
84 36123 1

85 rows × 2 columns

Finding Connecticut County Population and Calculating Hospitals/100k¶

In [17]:
ct_towns
Out[17]:
town_name town_fips_2020 county_fips county_name town_fips_2022 region_fips region_name
0 Andover 901301080 9013 Tolland County 911001080 9110 Capitol Planning Region
1 Ansonia 900901220 9009 New Haven County 914001220 9140 Naugatuck Valley Planning Region
2 Ashford 901501430 9015 Windham County 915001430 9150 Northeastern Connecticut Planning Region
3 Avon 900302060 9003 Hartford County 911002060 9110 Capitol Planning Region
4 Barkhamsted 900502760 9005 Litchfield County 916002760 9160 Northwest Hills Planning Region
... ... ... ... ... ... ... ...
164 Windsor Locks 900387070 9003 Hartford County 911087070 9110 Capitol Planning Region
165 Wolcott 900987560 9009 New Haven County 914087560 9140 Naugatuck Valley Planning Region
166 Woodbridge 900987700 9009 New Haven County 917087700 9170 South Central Connecticut Planning Region
167 Woodbury 900587910 9005 Litchfield County 914087910 9140 Naugatuck Valley Planning Region
168 Woodstock 901588190 9015 Windham County 915088190 9150 Northeastern Connecticut Planning Region

169 rows × 7 columns

In [18]:
# Load the population data
pop_df = pd.read_csv('../ct_towns_pop2023.csv')

# Standardize town names for merging (uppercase to match ct_towns)
pop_df['town_name'] = pop_df['town_name'].str.upper()

# Merge with ct_towns to get county info
merged = ct_towns[['town_name', 'county_name']].merge(
    pop_df,
    on='town_name',
    how='left'
)

# Group by county and sum populations
county_pop = merged.groupby('county_name')['pop_2023'].sum().reset_index()
county_pop.columns = ['county_name', 'total_pop_2023']
county_pop = county_pop.sort_values('total_pop_2023', ascending=False)

print(county_pop)
         county_name  total_pop_2023
0   Fairfield County             0.0
1    Hartford County             0.0
2  Litchfield County             0.0
3   Middlesex County             0.0
4   New Haven County             0.0
5  New London County             0.0
6     Tolland County             0.0
7     Windham County             0.0
In [19]:
# Check for any towns that didn't match
unmatched = merged[merged['pop_2023'].isna()]['town_name'].tolist()
print("Unmatched towns:", unmatched)
Unmatched towns: ['Andover', 'Ansonia', 'Ashford', 'Avon', 'Barkhamsted', 'Beacon Falls', 'Berlin', 'Bethany', 'Bethel', 'Bethlehem', 'Bloomfield', 'Bolton', 'Bozrah', 'Branford', 'Bridgeport', 'Bridgewater', 'Bristol', 'Brookfield', 'Brooklyn', 'Burlington', 'Canaan', 'Canterbury', 'Canton', 'Chaplin', 'Cheshire', 'Chester', 'Clinton', 'Colchester', 'Colebrook', 'Columbia', 'Cornwall', 'Coventry', 'Cromwell', 'Danbury', 'Darien', 'Deep River', 'Derby', 'Durham', 'East Granby', 'East Haddam', 'East Hampton', 'East Hartford', 'East Haven', 'East Lyme', 'East Windsor', 'Eastford', 'Easton', 'Ellington', 'Enfield', 'Essex', 'Fairfield', 'Farmington', 'Franklin', 'Glastonbury', 'Goshen', 'Granby', 'Greenwich', 'Griswold', 'Groton', 'Guilford', 'Haddam', 'Hamden', 'Hampton', 'Hartford', 'Hartland', 'Harwinton', 'Hebron', 'Kent', 'Killingly', 'Killingworth', 'Lebanon', 'Ledyard', 'Lisbon', 'Litchfield', 'Lyme', 'Madison', 'Manchester', 'Mansfield', 'Marlborough', 'Meriden', 'Middlebury', 'Middlefield', 'Middletown', 'Milford', 'Monroe', 'Montville', 'Morris', 'Naugatuck', 'New Britain', 'New Canaan', 'New Fairfield', 'New Hartford', 'New Haven', 'New London', 'New Milford', 'Newington', 'Newtown', 'Norfolk', 'North Branford', 'North Canaan', 'North Haven', 'North Stonington', 'Norwalk', 'Norwich', 'Old Lyme', 'Old Saybrook', 'Orange', 'Oxford', 'Plainfield', 'Plainville', 'Plymouth', 'Pomfret', 'Portland', 'Preston', 'Prospect', 'Putnam', 'Redding', 'Ridgefield', 'Rocky Hill', 'Roxbury', 'Salem', 'Salisbury', 'Scotland', 'Seymour', 'Sharon', 'Shelton', 'Sherman', 'Simsbury', 'Somers', 'South Windsor', 'Southbury', 'Southington', 'Sprague', 'Stafford', 'Stamford', 'Sterling', 'Stonington', 'Stratford', 'Suffield', 'Thomaston', 'Thompson', 'Tolland', 'Torrington', 'Trumbull', 'Union', 'Vernon', 'Voluntown', 'Wallingford', 'Warren', 'Washington', 'Waterbury', 'Waterford', 'Watertown', 'West Hartford', 'West Haven', 'Westbrook', 'Weston', 'Westport', 'Wethersfield', 'Willington', 'Wilton', 'Winchester', 'Windham', 'Windsor', 'Windsor Locks', 'Wolcott', 'Woodbridge', 'Woodbury', 'Woodstock']
In [20]:
# Normalize both to uppercase for matching
ct_towns_copy = ct_towns.copy()
ct_towns_copy['town_name_upper'] = ct_towns_copy['town_name'].str.upper()
pop_df['town_name_upper'] = pop_df['town_name'].str.upper()

# Merge on the normalized column
merged = ct_towns_copy[['town_name', 'town_name_upper', 'county_name']].merge(
    pop_df[['town_name_upper', 'pop_2023']],
    on='town_name_upper',
    how='left'
)

# Check unmatched
unmatched = merged[merged['pop_2023'].isna()]['town_name'].tolist()
print("Unmatched towns:", unmatched)

# Group by county
county_pop = merged.groupby('county_name')['pop_2023'].sum().reset_index()
county_pop.columns = ['county_name', 'total_pop_2023']
county_pop = county_pop.sort_values('total_pop_2023', ascending=False)
print(county_pop)
Unmatched towns: []
         county_name  total_pop_2023
0   Fairfield County          963780
1    Hartford County          898478
4   New Haven County          865717
5  New London County          268518
2  Litchfield County          186551
3   Middlesex County          166110
6     Tolland County          150906
7     Windham County          117116
In [21]:
# Get unique county_fips + county_name from ct_towns
county_fips_map = ct_towns[['county_fips', 'county_name']].drop_duplicates()

# Merge fips into county population df
ct_county_pop = county_pop.merge(
    county_fips_map,
    on='county_name',
    how='left'
)
ct_county_pop.set_index('county_fips', inplace = True)
print(ct_county_pop)
                   county_name  total_pop_2023
county_fips                                   
9001          Fairfield County          963780
9003           Hartford County          898478
9009          New Haven County          865717
9011         New London County          268518
9005         Litchfield County          186551
9007          Middlesex County          166110
9013            Tolland County          150906
9015            Windham County          117116
In [22]:
hospitals_per_100k_all = pd.read_csv('hospitals_per_100k_nj_ny.csv')
In [23]:
hospitals_per_100k_all = hospitals_per_100k_all.merge(
    ct_county_pop[['county_name', 'total_pop_2023']],
    left_on='fips',
    right_index=True,
    how='left'
)

hospitals_per_100k_all['county'] = hospitals_per_100k_all['county'].fillna(hospitals_per_100k_all['county_name'])
hospitals_per_100k_all['population'] = hospitals_per_100k_all['population'].fillna(hospitals_per_100k_all['total_pop_2023'])
hospitals_per_100k_all['hospitals_per_100k'] = hospitals_per_100k_all['hospital_count'] / hospitals_per_100k_all['population'] * 100000

hospitals_per_100k_all.drop(columns=['county_name', 'total_pop_2023'], inplace=True)
print(hospitals_per_100k_all[hospitals_per_100k_all['fips'] < 10000])
    fips      county state  hospital_count  population  hospitals_per_100k
83  9001   FAIRFIELD    CT             8.0    963780.0            0.830065
84  9003    HARTFORD    CT             8.0    898478.0            0.890395
85  9005  LITCHFIELD    CT             2.0    186551.0            1.072093
86  9007   MIDDLESEX    CT             3.0    166110.0            1.806032
87  9009   NEW HAVEN    CT             9.0    865717.0            1.039601
88  9011  NEW LONDON    CT             2.0    268518.0            0.744829
89  9013     TOLLAND    CT             2.0    150906.0            1.325328
90  9015     WINDHAM    CT             2.0    117116.0            1.707709
In [24]:
# Fill NaN state with Connecticut
hospitals_per_100k_all['state'] = hospitals_per_100k_all['state'].fillna('Connecticut')

# Remove ' County' from CT rows only
hospitals_per_100k_all['county'] = hospitals_per_100k_all['county'].str.replace(' County', '', regex=False)
In [25]:
print(hospitals_per_100k_all.tail())
    fips      county state  hospital_count  population  hospitals_per_100k
86  9007   MIDDLESEX    CT             3.0    166110.0            1.806032
87  9009   NEW HAVEN    CT             9.0    865717.0            1.039601
88  9011  NEW LONDON    CT             2.0    268518.0            0.744829
89  9013     TOLLAND    CT             2.0    150906.0            1.325328
90  9015     WINDHAM    CT             2.0    117116.0            1.707709
In [26]:
hospitals_per_100k_all.to_csv('hospitals_per_100k_all.csv', index=False)
In [27]:
hospitals_per_100k_all
Out[27]:
fips county state hospital_count population hospitals_per_100k
0 36001 ALBANY NY 4.0 317590.0 1.259486
1 36003 ALLEGANY NY 2.0 46694.0 4.283206
2 36005 BRONX NY 7.0 1380478.0 0.507071
3 36007 BROOME NY 3.0 195934.0 1.531128
4 36009 CATTARAUGUS NY 1.0 75671.0 1.321510
... ... ... ... ... ... ...
86 9007 MIDDLESEX CT 3.0 166110.0 1.806032
87 9009 NEW HAVEN CT 9.0 865717.0 1.039601
88 9011 NEW LONDON CT 2.0 268518.0 0.744829
89 9013 TOLLAND CT 2.0 150906.0 1.325328
90 9015 WINDHAM CT 2.0 117116.0 1.707709

91 rows × 6 columns

PCP per 100k¶

In [29]:
#Get Primary Care Physican data from County Health Rankings
df_raw = pd.read_excel('County_Health_Rankings_NY.xlsx', 
                        sheet_name='Ranked Measure Data', header=None)

cols = [0, 1, 2, 114]
pcp_df = df_raw.iloc[2:, cols].copy()

pcp_df.columns = ['FIPS', 'State', 'County', 
                   '# Primary Care Physicians']

pcp_df = pcp_df.iloc[1:]
pcp_df = pcp_df.reset_index(drop=True)
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[29], line 2
      1 #Get Primary Care Physican data from County Health Rankings
----> 2 df_raw = pd.read_excel('County_Health_Rankings_NY.xlsx', 
      3                         sheet_name='Ranked Measure Data', header=None)
      5 cols = [0, 1, 2, 114]
      6 pcp_df = df_raw.iloc[2:, cols].copy()

File ~\anaconda3\Lib\site-packages\pandas\io\excel\_base.py:495, in read_excel(io, sheet_name, header, names, index_col, usecols, dtype, engine, converters, true_values, false_values, skiprows, nrows, na_values, keep_default_na, na_filter, verbose, parse_dates, date_parser, date_format, thousands, decimal, comment, skipfooter, storage_options, dtype_backend, engine_kwargs)
    493 if not isinstance(io, ExcelFile):
    494     should_close = True
--> 495     io = ExcelFile(
    496         io,
    497         storage_options=storage_options,
    498         engine=engine,
    499         engine_kwargs=engine_kwargs,
    500     )
    501 elif engine and engine != io.engine:
    502     raise ValueError(
    503         "Engine should not be specified when passing "
    504         "an ExcelFile - ExcelFile already has the engine set"
    505     )

File ~\anaconda3\Lib\site-packages\pandas\io\excel\_base.py:1550, in ExcelFile.__init__(self, path_or_buffer, engine, storage_options, engine_kwargs)
   1548     ext = "xls"
   1549 else:
-> 1550     ext = inspect_excel_format(
   1551         content_or_path=path_or_buffer, storage_options=storage_options
   1552     )
   1553     if ext is None:
   1554         raise ValueError(
   1555             "Excel file format cannot be determined, you must specify "
   1556             "an engine manually."
   1557         )

File ~\anaconda3\Lib\site-packages\pandas\io\excel\_base.py:1402, in inspect_excel_format(content_or_path, storage_options)
   1399 if isinstance(content_or_path, bytes):
   1400     content_or_path = BytesIO(content_or_path)
-> 1402 with get_handle(
   1403     content_or_path, "rb", storage_options=storage_options, is_text=False
   1404 ) as handle:
   1405     stream = handle.handle
   1406     stream.seek(0)

File ~\anaconda3\Lib\site-packages\pandas\io\common.py:882, in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)
    873         handle = open(
    874             handle,
    875             ioargs.mode,
   (...)
    878             newline="",
    879         )
    880     else:
    881         # Binary mode
--> 882         handle = open(handle, ioargs.mode)
    883     handles.append(handle)
    885 # Convert BytesIO or file objects passed with an encoding

FileNotFoundError: [Errno 2] No such file or directory: 'County_Health_Rankings_NY.xlsx'
In [ ]:
pcp_df.head()
In [ ]:
df_raw = pd.read_excel('County_Health_Rankings_CT.xlsx', 
                        sheet_name='Ranked Measure Data', header=None)

cols = [0, 1, 2, 114]
pcp_df2= df_raw.iloc[2:, cols].copy()

pcp_df2.columns = ['FIPS', 'State', 'County', 
                   '# Primary Care Physicians']

pcp_df2 = pcp_df2.iloc[1:]
pcp_df2 = pcp_df2.reset_index(drop=True)
In [ ]:
pcp_df2.head()
In [ ]:
df_raw = pd.read_excel('County_Health_Rankings_NJ.xlsx', 
                        sheet_name='Ranked Measure Data', header=None)

cols = [0, 1, 2, 114]
pcp_df3= df_raw.iloc[2:, cols].copy()

pcp_df3.columns = ['FIPS', 'State', 'County', 
                   '# Primary Care Physicians']
pcp_df3 = pcp_df3.iloc[1:]
pcp_df3 = pcp_df3.reset_index(drop=True)
In [ ]:
pcp_df3.head()
In [ ]:
pcp_all = pd.concat([pcp_df2, pcp_df3, pcp_df], join = 'inner')
pcp_all = pcp_all.rename(columns={'FIPS' :'fips'})
pcp_all.set_index('fips', inplace = True)
pcp_all.head(20)
In [ ]:
import requests

url = "https://api.census.gov/data/2023/acs/acs1"

API_KEY = "3d1a8efb010ab94526aed9bb9b1b8ba58c722e3d"

states = ["09", "34", "36"]  # CT, NJ, NY

all_data = []

for s in states:
    params = {
        "get": "NAME,B01003_001E",
        "for": "county:*",
        "in": f"state:{s}",
        "key": API_KEY
    }

    r = requests.get(url, params=params)

    print("\nSTATE:", s)
    print("STATUS:", r.status_code)
    print("CONTENT (first 300 chars):")
    print(r.text[:300])
    print("-" * 50)
In [ ]:
pop = pd.read_csv("co-est2025-alldata.csv", encoding = "latin1")
In [ ]:
print(pop.columns.tolist())
In [ ]:
# Filtering for NY-NJ-CT
pop = pop[
    pop["STATE"].isin([9, 34, 36])
]
In [ ]:
pop = pop[
    pop["COUNTY"] > 0
]
In [ ]:
pop["fips"] = (
    pop["STATE"].astype(str).str.zfill(2)
    + pop["COUNTY"].astype(str).str.zfill(3)
)
In [ ]:
pop = pop[[
    "fips",
    "STNAME",
    "CTYNAME",
    "POPESTIMATE2023"
]]
In [ ]:
# Renaming columns
pop = pop.rename(columns={
    "STNAME": "state",
    "CTYNAME": "county",
    "POPESTIMATE2023": "population"
})
In [ ]:
# Check data
print(pop.shape)
print(pop.head())
In [ ]:
pop = pop[pop['state'] != 'Connecticut']
In [ ]:
pop.head()
In [ ]:
ct_county_pop.head()
In [ ]:
ct_county_pop = ct_county_pop.rename(columns={'county_name': 'county', 'total_pop_2023':'population'})
ct_county_pop.index.name = 'fips'
ct_county_pop.head()
In [ ]:
ct_county_pop['state'] = 'Connecticut'
pop['fips'] = pop['fips'].astype(int)
pop.set_index('fips', inplace = True)
In [ ]:
pop.head()
In [ ]:
pop_new = pd.concat([ct_county_pop, pop])
In [ ]:
pop_new.head(20)
In [ ]:
pop_new.index = pop_new.index.astype(int)
pcp_all.index =pcp_all.index.astype(int)

merged = pcp_all.merge(pop_new[['population']], left_on='fips', right_index=True, how='left')

merged['pcp_per_100k'] = merged['# Primary Care Physicians'] / merged['population'] * 100000
In [ ]:
merged.head(20)
In [ ]:
merged[merged['pcp_per_100k'] < 30]
In [ ]:
#merged.to_csv('pcp_per_100k_all.csv', index=False)

Stroke Centers Per 100k¶

Importing and Cleaning NY¶

In [ ]:
from bs4 import BeautifulSoup


headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
}

url = "https://www.health.ny.gov/diseases/cardiovascular/stroke/designation/stroke_designated_centers.htm"
response = requests.get(url, headers=headers)
response.raise_for_status()

soup = BeautifulSoup(response.text, "lxml")
table = soup.find("table")

headers_row = [th.get_text(strip=True) for th in table.find_all("th")]

rows = []
for tr in table.find_all("tr")[1:]:  # skip header row
    cells = [td.get_text(strip=True) for td in tr.find_all("td")]
    if len(cells) == len(headers_row):  # skip the "no centers in county" placeholder row
        rows.append(cells)

df = pd.DataFrame(rows, columns=headers_row)
print(df.shape)
print(df.head())
In [ ]:
#Cleaning table
df = df.iloc[1:]
df = df.drop(columns=['PFI', 'Address', 'EMS Region'])
df.head(20)
In [ ]:
#Finding # stroke centers by county 
stroke_centers_by_county = (
    df.groupby("County")["Hospital Name"]
    .count()
    .reset_index()
    .rename(columns={"Hospital Name": "Total Stroke Centers"})
    .sort_values("Total Stroke Centers", ascending=False)
    .reset_index(drop=True)
)
In [ ]:
stroke_centers_by_county.head(20)
In [ ]:
#Finding # of each type of stroke center in each county
stroke_centers_by_county_detailed = (
    df.groupby(["County", "NYSDOH Level of Designation"])["Hospital Name"]
    .count()
    .unstack(fill_value=0)
    .reset_index()
)
stroke_centers_by_county_detailed["Total Stroke Centers"] = stroke_centers_by_county_detailed.iloc[:, 1:].sum(axis=1)
stroke_centers_by_county_detailed.columns.name = None
In [ ]:
stroke_centers_by_county_detailed.head(20)

Importing and cleaning CT¶

In [ ]:
from io import StringIO

url = "https://portal.ct.gov/dph/emergency-medical-services/ems/certified-stroke-centers"
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}

response = requests.get(url, headers=headers)
response.raise_for_status()

tables = pd.read_html(StringIO(response.text))
print(f"{len(tables)} tables found")
for i, t in enumerate(tables):
    print(f"Table {i}: {t.shape}")

ct_df = max(tables, key=lambda t: t.shape[0])
print(ct_df.shape)
ct_df.head()
In [ ]:
ct_df = tables[2].copy()
ct_df.columns = ct_df.iloc[0]        # set first row as header
ct_df = ct_df.drop(0).reset_index(drop=True)  # drop that row
In [ ]:
ct_df.head(20)
In [ ]:
ct_df = ct_df.drop(columns=['DATE OF CERTIFICATION', 'CERTIFYING ORGANIZATION'])
In [ ]:
ct_df.head()
In [ ]:
ct_df.iloc[:, 1] = ct_df.iloc[:, 1].str.split("CONTACT:").str[0].str.strip()
In [ ]:
ct_df.head()
In [ ]:
ct_latlong = pd.read_csv('ct_stroke_centers_geocoded.csv')
ct_latlong.head()
In [ ]:
len(ct_df)
In [ ]:
!conda install -c conda-forge geopandas -y
In [ ]:
import requests

def get_county(lat, lon, vintage=419):
    url = "https://geocoding.geo.census.gov/geocoder/geographies/coordinates"
    params = {
        "x": lon,
        "y": lat,
        "benchmark": "Public_AR_Current",
        "vintage": vintage,
        "format": "json"
    }
    r = requests.get(url, params=params)
    result = r.json()
    try:
        county_info = result["result"]["geographies"]["Counties"][0]
        return county_info["NAME"], county_info["GEOID"]
    except (KeyError, IndexError):
        return None, None

ct_latlong[["County", "County_FIPS"]] = ct_latlong.apply(
    lambda row: pd.Series(get_county(row["latitude"], row["longitude"])),
    axis=1
)
In [ ]:
ct_df_with_county.columns
In [ ]:
ct_df_with_county.head()
In [ ]: