Austin limited and full purpose census tracts

Author

Kaitlan Wong

Sources

Downloaded the City of Austin jurisdictions shapefile here: https://data.austintexas.gov/City-Government/BOUNDARIES_jurisdictions/3pzb-6mbr

Downloaded Texas census tracts shapefile from TIGER/line here: https://www.census.gov/geographies/mapping-files/time-series/geo/tiger-line-file.html

Notes

I am filtering for only full and limited purpose jurisdictions.

I decided to do this analysis in R because performing an intersecting overlay or spatial join in ArcGIS would have cost 84 credits.

Data Loading and Prep

library(sf)
Linking to GEOS 3.13.1, GDAL 3.11.0, PROJ 9.6.0; sf_use_s2() is TRUE
library(readr)
library(dplyr)

Attaching package: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
# read Austin jurisdictions shapefile

austin_jur <- st_read("BOUNDARIES_jurisdictions_20260205/geo_export_5db1be39-2392-451a-8502-b3021a29112a.shp")
Reading layer `geo_export_5db1be39-2392-451a-8502-b3021a29112a' from data source `C:\Users\kaitl\OneDrive\Documents\Every Texan\R\City of Austin Equity Index\Full and LTD census tracts\BOUNDARIES_jurisdictions_20260205\geo_export_5db1be39-2392-451a-8502-b3021a29112a.shp' 
  using driver `ESRI Shapefile'
Simple feature collection with 352 features and 10 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -98.01504 ymin: 30.04298 xmax: -97.48034 ymax: 30.51685
Geodetic CRS:  WGS 84
# quick check
print(unique(austin_jur$JURISDICTION_LABEL))
NULL
# inspect Austin shapefile
names(austin_jur)
 [1] "objectid"   "jurisdicti" "city_name"  "jurisdic_2" "modified_f"
 [6] "jurisdic_3" "jurisdic_4" "shape_area" "shape_leng" "globalid"  
[11] "geometry"  
head(austin_jur)
Simple feature collection with 6 features and 10 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -97.92222 ymin: 30.22973 xmax: -97.77758 ymax: 30.3428
Geodetic CRS:  WGS 84
  objectid jurisdicti      city_name        jurisdic_2 modified_f
1      312  401472386 CITY OF AUSTIN AUSTIN 2 MILE ETJ        202
2      230  401472355 CITY OF AUSTIN AUSTIN 2 MILE ETJ        202
3      231  401472356 CITY OF AUSTIN AUSTIN 2 MILE ETJ        202
4      258  401472326 CITY OF AUSTIN AUSTIN 2 MILE ETJ        202
5      193         10 CITY OF AUSTIN        AUSTIN LTD        211
6      222  401472320 CITY OF AUSTIN AUSTIN 2 MILE ETJ        202
                       jurisdic_3 jurisdic_4  shape_area shape_leng
1          DISANNEXED PER SB 1844 2 MILE ETJ    7263.191   467.3437
2          DISANNEXED PER SB 1844 2 MILE ETJ   43172.439  1007.8005
3          DISANNEXED PER SB 1844 2 MILE ETJ   61576.410  1022.9614
4          DISANNEXED PER SB 1844 2 MILE ETJ   43322.393   837.6653
5 LIMITED PURPOSE PLANNING ZONING        LTD 7173169.650 16879.2008
6          DISANNEXED PER SB 1844 2 MILE ETJ   67123.084  1494.6134
                              globalid                       geometry
1 2644acde-7560-4f95-9336-d04480bb6ba6 MULTIPOLYGON (((-97.88191 3...
2 b5fef16f-58ac-4f6d-8757-bc27ce043d4c MULTIPOLYGON (((-97.78378 3...
3 eb5ea736-435c-49d0-8318-5c2cb4f49fb7 MULTIPOLYGON (((-97.8187 30...
4 150cc82f-8b67-4924-a6e8-0d0f07d97ddd MULTIPOLYGON (((-97.92192 3...
5 dabb5d59-05a0-4ea2-96a0-a69303af5c83 MULTIPOLYGON (((-97.899 30....
6 4fac79bf-020e-4d9b-b655-dc8574a7852b MULTIPOLYGON (((-97.77912 3...

Here I am filtering to include only limited and full-purpose jurisdictions in Austin.

austin_jur_filtered <- austin_jur %>%
  filter(jurisdic_2 %in% c("AUSTIN LTD", "AUSTIN FULL PURPOSE"))

print(unique(austin_jur_filtered$jurisdic_2))
[1] "AUSTIN LTD"          "AUSTIN FULL PURPOSE"
# read the Texas Census tracts shapefile
tx_census_tracts <- st_read("tl_2025_48_tract/tl_2025_48_tract.shp")
Reading layer `tl_2025_48_tract' from data source 
  `C:\Users\kaitl\OneDrive\Documents\Every Texan\R\City of Austin Equity Index\Full and LTD census tracts\tl_2025_48_tract\tl_2025_48_tract.shp' 
  using driver `ESRI Shapefile'
Simple feature collection with 6896 features and 13 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: -106.6456 ymin: 25.83705 xmax: -93.50804 ymax: 36.5007
Geodetic CRS:  NAD83
# check columns
print(names(tx_census_tracts))
 [1] "STATEFP"  "COUNTYFP" "TRACTCE"  "GEOID"    "GEOIDFQ"  "NAME"    
 [7] "NAMELSAD" "MTFCC"    "FUNCSTAT" "ALAND"    "AWATER"   "INTPTLAT"
[13] "INTPTLON" "geometry"
# rebuild a clean GEOID (I was getting mostly NA values for some reason for GEOID)
tx_census_tracts <- tx_census_tracts %>%
  mutate(
    GEOID_clean = paste0(STATEFP, COUNTYFP, TRACTCE)
  )

# check
head(tx_census_tracts$GEOID_clean)
[1] "48201342001" "48201421101" "48201423301" "48381020400" "48029171926"
[6] "48113017819"
# double check
#unique(census_tracts$GEOID_clean)

# inspect new GEOIDs in Excel
tract_ids <- unique(tx_census_tracts$GEOID_clean)

write.csv(tract_ids, "All_Census_Tract_GEOIDs.csv", row.names = FALSE)

# there are 6,896 tracts in Texas according to this list
# make sure both layers have valid geometry
austin_jur_filtered <- st_make_valid(austin_jur_filtered)
tx_census_tracts <- st_make_valid(tx_census_tracts)
# match coordinate reference systems
if (st_crs(tx_census_tracts) != st_crs(austin_jur_filtered)) {
  tx_census_tracts <- st_transform(tx_census_tracts, st_crs(austin_jur_filtered))
}

Spatial Join

# spatially join tracts to any Austin jurisdiction polygon they intersect
selected_tracts <- st_join(tx_census_tracts, austin_jur_filtered, join = st_intersects)
# keep only those that actually intersect Austin (non-NA jurisdiction label)
selected_tracts <- selected_tracts %>%
  filter(!is.na(jurisdic_2))

Check the Work

# check for NA values
table(is.na(selected_tracts$GEOID_clean))

FALSE 
  383 
head(selected_tracts$GEOID_clean, 20)
 [1] "48453002112" "48453002113" "48453002201" "48453041300" "48453031600"
 [6] "48453032500" "48453045600" "48453045600" "48453043000" "48453043000"
[11] "48453043000" "48453034200" "48453042000" "48453042000" "48453042000"
[16] "48453045100" "48453045100" "48453041400" "48453032100" "48453043100"
# quick check that the selected tracts look reasonable
print(paste("# of tracts intersecting Austin:", nrow(selected_tracts)))
[1] "# of tracts intersecting Austin: 383"
print(paste("# of unique GEOIDs:", length(unique(selected_tracts$GEOID_clean))))
[1] "# of unique GEOIDs: 271"

There are 271 intersecting census tracts, meaning there are 271 tracts in Austin full and limited purpose jurisdictions. This makes sense, as Travis County alone has 291 census tracts, though not all of Travis County is in Austin. There are a total of 493 census tracts in all of Bastrop, Hays, Travis, and Williamson Counties combined.

The reason there are 383 rows, but only 271 distinct tracts, could be because some tracts may touch multiple Austin jurisdiction polygons since Austin’s boundary is split into parts.

# quick view of unique GEOIDs
print(sort(unique(selected_tracts$GEOID_clean))[1:20])
 [1] "48021950303" "48209010912" "48209010923" "48453000101" "48453000102"
 [6] "48453000203" "48453000204" "48453000205" "48453000206" "48453000302"
[11] "48453000304" "48453000305" "48453000307" "48453000308" "48453000309"
[16] "48453000401" "48453000402" "48453000500" "48453000601" "48453000605"
# to save only distinct GEOIDs with no duplicates
selected_unique <- selected_tracts %>%
  distinct(GEOID_clean, .keep_all = TRUE)

Save Final Files

# save the intersected tracts shapefile
st_write(selected_tracts, "Austin_LTD_FULL_Tracts.shp")
Warning in abbreviate_shapefile_names(obj): Field names abbreviated for ESRI
Shapefile driver
Writing layer `Austin_LTD_FULL_Tracts' to data source 
  `Austin_LTD_FULL_Tracts.shp' using driver `ESRI Shapefile'
Writing 383 features with 24 fields and geometry type Polygon.
# Also export a CSV list of tract GEOIDs and names
tract_list <- selected_tracts %>%
  st_drop_geometry() %>%
  arrange(GEOID_clean)

write_csv(tract_list, "Austin_LTD_FULL_Tract_List.csv")

###################################################################################

# files without duplicate GEOIDs
st_write(selected_unique, "Austin_LTD_FULL_Tracts_Unique.shp")
Warning in abbreviate_shapefile_names(obj): Field names abbreviated for ESRI
Shapefile driver
Writing layer `Austin_LTD_FULL_Tracts_Unique' to data source 
  `Austin_LTD_FULL_Tracts_Unique.shp' using driver `ESRI Shapefile'
Writing 271 features with 24 fields and geometry type Polygon.
unique_tract_list <- selected_unique %>%
  st_drop_geometry() %>%
  arrange(GEOID_clean)

write_csv(unique_tract_list, "Austin_LTD_FULL_Tract_List_Unique.csv")

Check against Coda’s tracts

Checking to see if I still get 271 tracts when I join my shapefile to Coda’s shapefile. Coda’s shapefile contains all census tracts in Bastrop, Hays, Travis, and Williamson counties.

# read CRG’s shapefile
CRG_tracts <- st_read("CRG_Census_Tracts_Final_All_Counties_Shape/MergedFeatures.shp")
Reading layer `MergedFeatures' from data source 
  `C:\Users\kaitl\OneDrive\Documents\Every Texan\R\City of Austin Equity Index\Full and LTD census tracts\CRG_Census_Tracts_Final_All_Counties_Shape\MergedFeatures.shp' 
  using driver `ESRI Shapefile'
Simple feature collection with 492 features and 13 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: -10942440 ymin: 3471769 xmax: -10800710 ymax: 3620342
Projected CRS: WGS 84 / Pseudo-Mercator
names(CRG_tracts)
 [1] "STATE_ABBR" "STATE_FIPS" "COUNTY_FIP" "STCOFIPS"   "TRACT_FIPS"
 [6] "FIPS"       "POPULATION" "POP_SQMI"   "SQMI"       "POPULATI_1"
[11] "POP20_SQMI" "Shape__Are" "Shape__Len" "geometry"  
# reconstruct clean GEOID
CRG_tracts <- CRG_tracts %>%
  mutate(GEOID_clean = paste0(STATE_FIPS, COUNTY_FIP, TRACT_FIPS))

head(CRG_tracts$GEOID_clean)
[1] "48453001910" "48453002105" "48453035900" "48453000305" "48453000608"
[6] "48453033300"
# check which of my 271 unique GEOIDs are in Coda's file
common_geo <- intersect(unique(selected_unique$GEOID_clean),
                        unique(CRG_tracts$GEOID_clean))

setdiff(unique(selected_unique$GEOID_clean), unique(CRG_tracts$GEOID_clean)) # not in CRG's file
character(0)
length(CRG_tracts$GEOID_clean)
[1] 492
# how many unique GEOID's in Coda's file
length(unique(CRG_tracts$GEOID_clean))
[1] 492
length(common_geo)
[1] 271

Verified that all 271 tracts are in Coda’s file.