Skip to content

Access to airports can boost connectivity and as a result, increase tourism and enhance regional visibility. This helps with the economic development and business activity of the surrounding regions. It is also critical to have ease of access to airports to support emergency services and disaster response in times of need, especially for regions with significant population density.

In this example, we will use the Spatially Enabled DataFrame (SEDF) from the ArcGIS API for Python along with some spatial analysis concepts and techniques to answer some real-world questions. We will work through an example that seeks to answer the following questions:

  1. Which counties contain airports?
  2. Which counties do not contain airports?
  3. Which of those counties do not have access to airports within 30 miles?

Import necessary packages

import pandas as pd

from arcgis.features import FeatureLayer, GeoAccessor
from arcgis.gis import GIS
from arcgis.geometry import Geometry, distance
from arcgis.geometry.functions import LengthUnits
gis = GIS(profile="your_online_profile")

Having imported necessary packages and connecting to our GIS, we now proceed to read in the data required.

Extract data for Counties of the US as a SEDF

We will access the data for Counties in the US and read it in as a Spatially Enabled DataFrame using the spatial namespace which returns a GeoAccessor object.

counties_url = 'https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/USA_Counties_Generalized_Boundaries/FeatureServer/0'
counties_layer = FeatureLayer(counties_url)
counties_sdf = counties_layer.query(as_df=True, out_sr=4326)
counties_sdf.shape
(3144, 13)

This dataset has information for 3144 counties across the US.

counties_sdf.head()
OBJECTIDNAMESTATE_NAMESTATE_FIPSFIPSSQMIPOPULATIONPOP_SQMISTATE_ABBRCOUNTY_FIPSShape__AreaShape__LengthSHAPE
02Grant CountyNorth Dakota38380371643.57142923011.4ND0370.5040683.413506{"rings": [[[-102.003385812939, 46.05284759773...
13Griggs CountyNorth Dakota3838039720.62523063.2ND0390.2230341.949037{"rings": [[[-97.9616729885937, 47.24493801330...
24Hettinger CountyNorth Dakota38380411131.36363624892.2ND0410.3427472.691898{"rings": [[[-102.003371825849, 46.20580311678...
35Kidder CountyNorth Dakota38380431408.23529423941.7ND0430.4378062.719487{"rings": [[[-100.088490419176, 46.63570178753...
47Logan CountyNorth Dakota3838047987.36842118761.9ND0470.3090112.454735{"rings": [[[-99.0440801101036, 46.28338272232...

Extract Spatially Enabled DataFrames for Airports across the US

The data for airports in the US is available across 3 different layers based on the scale of the airport.

  1. The first layer, extracted as SEDF airports1, comprises of airports with capacity of 1,000,000 or more.
  2. The second layer, extracted as SEDF airports2, comprises of airports with capacity of 100,000 - 999,999.
  3. The third layer, extracted as SEDF airports3, comprises of airports with capacity of less than 100,000.

Once we extract data for all 3 SEDFs, we then combine them to create airports that we will use going forward.

airports1_url = 'https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_Airports_by_scale/FeatureServer/1'
airports1 = FeatureLayer(airports1_url).query(as_df=True, out_sr=4326)

airports2_url = 'https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_Airports_by_scale/FeatureServer/2'
airports2 = FeatureLayer(airports2_url).query(as_df=True, out_sr=4326)

airports3_url = 'https://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/USA_Airports_by_scale/FeatureServer/3'
airports3 = FeatureLayer(airports3_url).query(as_df=True, out_sr=4326)

airports = pd.concat([airports1, airports2, airports3], ignore_index=True, sort=False)
airports.head()
OBJECTIDFAA_IDNAMEFACILITYCITYCOUNTYSTATEOWNERELEV_FEETINTLTOWERARRIVALSDEPARTURESENPLANEMENPASSENGERSSHAPE
029ANCTed Stevens Anchorage IntlAirportAnchorageAnchorageAKPublic151NY18519.018524.02291115.01970743.0{"x": -149.9981944442182, "y": 61.174083333778...
1761BHMBirmingham-Shuttlesworth IntlAirportBirminghamJeffersonALPublic650NY16741.016738.01332942.01205588.0{"x": -86.75230555515719, "y": 33.563888889160...
21179LITBill And Hillary Clinton National/Adams FieldAirportLittle RockPulaskiARPublic266NY15475.015472.01054815.0955228.0{"x": -92.22477777747241, "y": 34.729444444105...
31503PHXPhoenix Sky Harbor IntlAirportPhoenixMaricopaAZPublic1135NY177027.0177030.019156669.015944437.0{"x": -112.01158333360394, "y": 33.43427777749...
41593TUSTucson IntlAirportTucsonPimaAZPublic2643YY19494.019494.01569388.01315515.0{"x": -110.94102777810946, "y": 32.11608333317...

Restrict the airports to 'Public' airports

If we observe values of the OWNER attribute of the airports data below, we notice that some of the airports are privately owned and some are dedicated for military use. We want to limit our airports data to include only public_airports that can be accessed by the general public.

airports['OWNER'].value_counts()
OWNER
Public       772
Air Force     50
Private       24
Navy          14
Army          13
Name: count, dtype: Int64
public_airports = airports[airports['OWNER']=='Public']
public_airports.shape
(772, 16)

As we see above, the US has 772 airports accessible to the public.

1. Which counties have public airports within?

This leads us to the first of our 3 questions: Which counties have public airports within?

We spatially join the public airports with the counties to see which airports intersect counties and visualize the results on a map.

spatially_joined = public_airports.spatial.join(counties_sdf, how='left', op='intersects')
counties_with_airports = counties_sdf[counties_sdf['COUNTY_FIPS'].isin(list((spatially_joined['COUNTY_FIPS']).unique()))]
counties_with_airports.shape
(2781, 13)

This gives us 2781 counties (of the 3144 total counties) which have airports within. This shows us that most counties have public airports within, however there are a few missing gaps in the south, central part of the country, Midwest and south east that indicate counties without airports.

map1 = gis.map('USA')
counties_with_airports.spatial.plot(map1)
map1.zoom_to_layer(counties_with_airports)
map1

We can also print out the names of states that are served by airports in their counties.

print(counties_with_airports['STATE_NAME'].unique())
<StringArray>
[        'North Dakota',             'Oklahoma',               'Oregon',
         'South Dakota',               'Hawaii',                'Idaho',
               'Kansas',               'Alaska',           'California',
             'Colorado',              'Georgia',             'Missouri',
              'Montana',             'Nebraska',               'Nevada',
           'New Mexico',             'Kentucky',             'Michigan',
          'Mississippi',              'Wyoming',          'Connecticut',
                'Texas',                 'Utah',             'Virginia',
           'Washington',                 'Ohio',         'Pennsylvania',
       'South Carolina',            'Tennessee',             'Illinois',
              'Indiana',                 'Iowa',              'Alabama',
             'Arkansas',              'Florida',       'North Carolina',
            'Louisiana',                'Maine',        'Massachusetts',
            'Minnesota',        'West Virginia',            'Wisconsin',
             'New York',             'Maryland',              'Vermont',
        'New Hampshire',              'Arizona',         'Rhode Island',
           'New Jersey',             'Delaware', 'District of Columbia']
Length: 51, dtype: string

2. Which counties are underserved by access to airports, despite average or greater population density?

This leads us to find which of these missing counties do not have access to airports despite average or greater population density?

For this we use the enrich_layer analysis method to enrich the counties with population data from the most recent year (2025) using the TOTPOP_CY variable.

Enrich counties with population data from 2025

from arcgis.features.analysis import enrich_layer

Before we enrich our existing counties layer, let's delete any existing layer having the same title and then generate a layer with updated data.

title = 'enriched_counties_2025'

existing_enriched_item = gis.content.search(query='title:'+title)[0]
# delete it if it exists
if existing_enriched_item:
    existing_enriched_item.delete()
enriched_counties = enrich_layer(counties_layer, analysis_variables=['TOTPOP_CY'], output_name=title)
enriched_counties
enriched_counties_2025

Feature Layer Collection by MMajumdar_geosaurus
Last Modified: March 05, 2026
0 comments, 4 views
enriched_df = FeatureLayer(enriched_counties.url+'/0').query(as_df=True, out_sr=4326)

Calculating population density for each county

We now calculate the population density for each of these counties using the population data and the area (SQMI field), and the median population density for the entire country.

enriched_df['pop_density'] = enriched_df['TOTPOP_CY'] / enriched_df['SQMI']
enriched_df['pop_density'].head()
0    51.558841
1     1.324555
2     3.073721
3     2.113379
4     1.638931
Name: pop_density, dtype: Float64

Calculating median population density

median_pop_density = enriched_df['pop_density'].median()
float(median_pop_density)
44.903376023126796

Finding counties that DO NOT have airports and have average or greater population density

We use this to to write a query below to find which of the counties that lack airports also have population density greater than or equal to the median. This leaves us with 163 counties, aligning with what we saw in the missing gaps of the previous map.

counties_without_airports = enriched_df[~enriched_df['COUNTY_FIPS'].isin(list((spatially_joined['COUNTY_FIPS']).unique())) & (enriched_df['pop_density']>=median_pop_density)]
counties_without_airports.shape
(163, 20)
map2 = gis.map('USA')
counties_without_airports.spatial.plot(map2)
map2.zoom_to_layer(counties_without_airports)
map2

We also print out the names of states with counties that lack airports within.

print(counties_without_airports['STATE_NAME'].unique())
<StringArray>
[          'Ohio',       'Oklahoma',        'Alabama',       'Colorado',
        'Georgia',       'Missouri',         'Nevada',       'New York',
 'North Carolina',       'Kentucky',       'Maryland',       'Michigan',
      'Minnesota',      'Tennessee',    'Connecticut',          'Texas',
       'Virginia',           'Iowa',         'Kansas',       'Illinois',
        'Indiana']
Length: 21, dtype: string

3. Which counties are underserved by access to airports within 30 miles?

However, if these counties do not have airports, are they at least within 30 miles of one? To determine this, we first calculate centroids for our counties and then compute a spatial index for each public airport.

#Generate centroids for counties
def get_x(value):
    '''
    Extract x co-ordinate from the centroid column
    '''
    return value[0]

def get_y(value):
    '''
    Extract y co-ordinate from the centroid column
    '''
    return value[1]

counties_without_airports['centroid'] = counties_without_airports['SHAPE'].geom.centroid
counties_without_airports['centroid_x'] = counties_without_airports['centroid'].apply(get_x)
counties_without_airports['centroid_y'] = counties_without_airports['centroid'].apply(get_y)

A spatial index can be thought of as an index on the SHAPE column. This can be used to quickly locate and search for features and also perform many selection or identification tasks.

#Generate spatial index for airports
sindex_airports = public_airports.spatial.sindex(stype='rtree')

We now write this method below to loop through each county and find its nearest airport using the spatial index for a quick search. We then use the distance() Geometry method to find the distance between the centroid of the county and the closest airport in miles. We extract this data in the form of a distance matrix that gives us the distance between a county and its nearest airport, along with the population density.

counties_without_airport_access = []
for row in counties_without_airports[['centroid_x', 'centroid_y', 'SHAPE', 'pop_density']].to_records():
    row = list(row)
    g = row[3]
    source_idx = row[0]
    latlong = (row[1], row[2])
    #Finding nearest airport to each county
    r = [i for i in sindex_airports._index.nearest(latlong, num_results=1)]
    r = list(set(r))
    centroid = Geometry({"x": row[1], "y":row[2], "spatialReference":{'wkid':4326}})
    #Find distance between county and closest airport
    dists = [distance(spatial_ref='4326', geometry1 = centroid, geometry2=public_airports['SHAPE'][i], distance_unit=LengthUnits.SURVEYMILE, geodesic=True) for i in r]
    row = row + r + dists
    counties_without_airport_access.append(row)
df_dist_matrix = pd.DataFrame(data=counties_without_airport_access, columns=['county_idx', 'centroid_x', 'centroid_y', 
                                                                'SHAPE', 'pop_density', 'airport_idx','dist'])
df_dist_matrix.head()
county_idxcentroid_xcentroid_ySHAPEpop_densityairport_idxdist
097-83.16571741.532653{'rings': [[[-83.408963087, 41.497789379], [-8...121.039060739{'distance': 33.49273640493922}
1115-83.36780440.297096{'rings': [[[-83.499565192, 40.1136267840001],...164.26944361{'distance': 32.60488307378162}
2121-84.57604741.561072{'rings': [[[-84.791897201, 41.4278994090001],...86.147014739{'distance': 39.85057292486108}
3185-96.67837234.735751{'rings': [[[-96.824187087, 34.515547381], [-9...53.25305463{'distance': 69.18399832721137}
4268-85.79606532.869278{'rings': [[[-85.589628589, 32.7313466890001],...52.713994414{'distance': 39.07221261130234}
def get_distance(x):
    '''
    Fetch distance value from the distance dictionary
    '''
    return x['distance']

df_dist_matrix['distance'] = df_dist_matrix['dist'].apply(get_distance)
df_dist_matrix_filter = df_dist_matrix[(df_dist_matrix['pop_density'] >= median_pop_density) & (df_dist_matrix['distance'] > 30)]
df_dist_matrix_filter.shape
(77, 8)

We now find which of the previous 163 counties also has their closest airport more than 30 miles away. This leaves us with 77 airports.

We now retrieve the SHAPE for these counties from our previous SEDF to then visualize the results on a map.

counties_away_from_airports = counties_without_airports[counties_without_airports['SHAPE'].isin(list((df_dist_matrix_filter['SHAPE']).unique()))]
counties_away_from_airports.shape
(77, 23)

The map below shows us that a few counties we saw initially in the Midwest and the central part of the country are no longer on this map, indicating that they are within 30 miles of the closest airport.

map3 = gis.map('USA')
counties_away_from_airports.spatial.plot(map3)
map3.zoom_to_layer(counties_away_from_airports)
map3

We can also see state names for these counties. The following list of states can be used to plan efforts which advocate for policy changes that benefit the underserved communities with access to airports through future infrastructure development.

counties_away_from_airports['STATE_NAME'].unique()
array(['Alabama', 'Colorado', 'Georgia', 'Indiana', 'Iowa', 'Kentucky',
       'Michigan', 'Mississippi', 'Missouri', 'New York',
       'North Carolina', 'Ohio', 'Oklahoma', 'Tennessee', 'Texas',
       'Virginia'], dtype=object)

This sample notebook shows us how simple tools can be used from the ArcGIS API for Python to explore and address some critical questions.

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.