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.

Revised Analysis (Exclude Biasing Tracts)

After talking to Coda, we realized some tracts are biasing the analysis, as they barely fall partially within the bounds of Austin. This revised analysis excludes tracts that are less than 10% inside the Austin boundaries.

# union Austin boundary into a single polygon
austin_union <- st_union(austin_jur_filtered)

# project both Austin union and unique tracts into an equal-area CRS
# using EPSG:3083 (Texas Centric Albers) — same as Coda used
austin_a <- st_transform(austin_union, 3083)
tracts_a <- st_transform(selected_unique, 3083)

# intersection pieces representing portion of each tract inside Austin
inside_pieces <- st_intersection(
  tracts_a %>% select(GEOID_clean, COUNTYFP),
  st_sf(geometry = austin_a)
)
Warning: attribute variables are assumed to be spatially constant throughout
all geometries
# compute overlap share (area percent inside Austin)
overlap_share <- inside_pieces %>%
  mutate(piece_area = as.numeric(st_area(.))) %>%
  st_drop_geometry() %>%
  group_by(GEOID_clean) %>%
  summarise(in_austin_area = sum(piece_area), .groups = "drop") %>%
  left_join(
    tracts_a %>%
      mutate(tract_area = as.numeric(st_area(.))) %>%
      st_drop_geometry() %>%
      select(GEOID_clean, tract_area),
    by = "GEOID_clean"
  ) %>%
  mutate(
    pct_in_austin = 100 * in_austin_area / tract_area
  )

# attach percent overlap back on
selected_strict <- selected_unique %>%
  left_join(overlap_share %>% select(GEOID_clean, pct_in_austin),
            by = "GEOID_clean")
# exclude tracts with less than 10% of area inside Austin
selected_10pct <- selected_strict %>%
  filter(pct_in_austin >= 10)

# check how many are excluded and how many remain
n_before <- nrow(selected_unique)
n_after <- nrow(selected_10pct)
n_excluded <- n_before - n_after

print(paste("Original unique tracts:", n_before))
[1] "Original unique tracts: 271"
print(paste("Tracts after excluding <10% overlaps:", n_after))
[1] "Tracts after excluding <10% overlaps: 250"
print(paste("Number of tracts excluded:", n_excluded))
[1] "Number of tracts excluded: 21"
# ensure 21 were excluded
if (n_excluded >= 20 & n_excluded <= 22) {
  print("exclusion test passed: 21 tracts removed")
} else {
  warning("you got something different from coda!!!!")
}
[1] "exclusion test passed: 21 tracts removed"
# save new outputs

# save the 10% filtered shapefile
st_write(selected_10pct, "Austin_LTD_FULL_Tracts_10pct_Intersect.shp")
Warning in abbreviate_shapefile_names(obj): Field names abbreviated for ESRI
Shapefile driver
Writing layer `Austin_LTD_FULL_Tracts_10pct_Intersect' to data source 
  `Austin_LTD_FULL_Tracts_10pct_Intersect.shp' using driver `ESRI Shapefile'
Writing 250 features with 25 fields and geometry type Polygon.
# export csv of the remaining tracts
strict_tract_list <- selected_10pct %>%
  st_drop_geometry() %>%
  select(GEOID_clean, NAME, NAMELSAD, pct_in_austin) %>%
  arrange(GEOID_clean)

write_csv(strict_tract_list, "Austin_LTD_FULL_Tract_List_10pct.csv")
# identify excluded tracts (those with < 10% area in Austin)
excluded_tracts <- selected_strict %>%
  filter(pct_in_austin < 10)

# check count
print(paste("Excluded tracts:", nrow(excluded_tracts)))
[1] "Excluded tracts: 21"
# export excluded tracts to CSV
excluded_tracts_list <- excluded_tracts %>%
  st_drop_geometry() %>% 
  arrange(pct_in_austin)

write_csv(excluded_tracts_list, "KW_Excluded_Less_Than_10pct.csv")

Exploring the Map of Tracts

# creating a map to highlight Williamson & Hays Tracts
# I need a better idea of where these tracts are located in relation to Travis County tracts. For our evictions indicator, we only have Travis County data. We may need to take averages of surrounding Travis County tracts to use for Williamson and Hays.

library(tmap)
Warning: package 'tmap' was built under R version 4.5.2
# static mode for Quarto embedding
tmap_mode("plot")
ℹ tmap modes "plot" - "view"
ℹ toggle with `tmap::ttm()`
# classify counties
selected_10pct_map <- selected_10pct %>%
  mutate(
    county_name = case_when(
      COUNTYFP == "453" ~ "Travis",
      COUNTYFP == "491" ~ "Williamson",
      COUNTYFP == "209" ~ "Hays",
      TRUE ~ "Other"
    )
  )

# # keep relevant counties
# selected_10pct_map <- selected_10pct_map %>%
#   filter(county_name %in% c("Travis", "Williamson", "Hays"))

# build map
final_map <- tm_shape(austin_union) +
  tm_borders(col = "black", lwd = 2) +

  tm_shape(selected_10pct_map) +
  tm_fill(
    col = "county_name",
    palette = c(
      "Travis" = "lightgray",
      "Williamson" = "#E41A1C",
      "Hays" = "#377EB8"
    ),
    alpha = 0.7,
    title = "County"
  ) +

  tm_borders(col = "white", lwd = 0.4) +


  tm_layout(
    main.title = "Austin Census Tracts (≥10% in City)\nWilliamson & Hays Highlighted",
    legend.outside = TRUE,
    main.title.size = 1.1
  )

── tmap v3 code detected ───────────────────────────────────────────────────────
[v3->v4] `tm_tm_polygons()`: migrate the argument(s) related to the scale of
the visual variable `fill` namely 'palette' (rename to 'values') to fill.scale
= tm_scale(<HERE>).[v3->v4] `tm_polygons()`: use 'fill' for the fill color of polygons/symbols
(instead of 'col'), and 'col' for the outlines (instead of 'border.col').[v3->v4] `tm_polygons()`: use `fill_alpha` instead of `alpha`.[v3->v4] `tm_polygons()`: migrate the argument(s) related to the legend of the
visual variable `fill` namely 'title' to 'fill.legend = tm_legend(<HERE>)'[v3->v4] `tm_layout()`: use `tm_title()` instead of `tm_layout(main.title = )`
final_map

# save map as PNG
tmap_save(
  final_map,
  filename = "Austin_full_limited.png",
  width = 10,
  height = 8,
  units = "in",
  dpi = 300
)
Map saved to C:\Users\kaitl\OneDrive\Documents\Every Texan\R\City of Austin Equity Index\Full and LTD census tracts\Austin_full_limited.png
Resolution: 3000 by 2400 pixels
Size: 10 by 8 inches (300 dpi)

Calculating Eviction Averages for Williamson and Hays Tracts

library(sf)
library(dplyr)
library(readr)
library(stringr)
library(FNN)
Warning: package 'FNN' was built under R version 4.5.2
##### 1. Load eviction data

evictions_raw <- read_csv(
  "evictions_travis_tract_basta_CLEANED.csv",
  col_types = cols(.default = "c")
)

##### 2. Clean eviction rate + GEOID

evictions_raw <- evictions_raw %>%
  mutate(

    # parse numbers safely
    eviction_clean = readr::parse_number(`Eviction filing rate`),

    # normalize GEOID
    GEOID_clean = str_pad(
      str_extract(GEOID, "\\d+"),
      width = 11,
      pad = "0"
    )
  )
Warning: There was 1 warning in `mutate()`.
ℹ In argument: `eviction_clean = readr::parse_number(`Eviction filing rate`)`.
Caused by warning:
! 20 parsing failures.
row col expected actual
  2  -- a number      -
 26  -- a number      -
 33  -- a number      -
 35  -- a number      -
 79  -- a number      -
... ... ........ ......
See problems(...) for more details.
##### 3. Keep only usable rows

evictions_clean <- evictions_raw %>%
  filter(
    !is.na(GEOID_clean),
    !is.na(eviction_clean)
  ) %>%
  select(GEOID_clean, eviction_clean)


##### 4. Join to Austin tracts

tracts_with_evictions <- selected_10pct %>%
  left_join(evictions_clean, by = "GEOID_clean")


##### 5. Split Travis vs non-Travis

evict_col <- "eviction_clean"

travis_tracts <- tracts_with_evictions %>%
  filter(COUNTYFP == "453")

non_travis_tracts <- tracts_with_evictions %>%
  filter(COUNTYFP != "453")


# keep only Travis tracts with real data
travis_valid <- travis_tracts %>%
  filter(!is.na(.data[[evict_col]]))


##### 6. Find touching neighbors

touch_neighbors <- st_touches(
  non_travis_tracts,
  travis_tracts
)


##### 7. Compute centroids in projected CRS (for distance)

non_proj    <- st_transform(non_travis_tracts, 3083)
travis_proj <- st_transform(travis_valid, 3083)

non_cent    <- st_centroid(non_proj)
Warning: st_centroid assumes attributes are constant over geometries
travis_cent <- st_centroid(travis_proj)
Warning: st_centroid assumes attributes are constant over geometries
non_coords    <- st_coordinates(non_cent)
travis_coords <- st_coordinates(travis_cent)


##### 8. k-nearest neighbors (only valid data)

k <- 3

knn <- get.knnx(
  data  = travis_coords,
  query = non_coords,
  k     = k
)

nearest_k <- knn$nn.index


##### 9. Hybrid averaging function

get_hybrid_avg <- function(i) {

  ## touching neighbors
  touch_ids <- touch_neighbors[[i]]

  if (length(touch_ids) > 0) {

    vals <- travis_tracts[[evict_col]][touch_ids]

    vals <- vals[!is.na(vals)]

    if (length(vals) > 0) {
      return(mean(vals))
    }
  }


  ## nearest valid neighbors
  nn_ids <- nearest_k[i, ]

  vals2 <- travis_valid[[evict_col]][nn_ids]

  vals2 <- vals2[!is.na(vals2)]

  if (length(vals2) > 0) {
    return(mean(vals2))
  }


  ## nNo data available
  return(NA_real_)
}


##### 10. Apply function

non_travis_tracts$neighbor_eviction_avg <-
  sapply(seq_len(nrow(non_travis_tracts)), get_hybrid_avg)


##### 11. Combine back with Travis

final_eviction_proxy <- bind_rows(

  travis_tracts %>%
    mutate(
      neighbor_eviction_avg = .data[[evict_col]]
    ),

  non_travis_tracts
)


##### 12. Quality checks

cat(
  "Missing values:",
  sum(is.na(final_eviction_proxy$neighbor_eviction_avg)),
  "\n"
)
Missing values: 8 
final_eviction_proxy %>%
  count(COUNTYFP, is.na(neighbor_eviction_avg))
Simple feature collection with 4 features and 3 fields
Geometry type: GEOMETRY
Dimension:     XY
Bounding box:  xmin: -98.0104 ymin: 30.08853 xmax: -97.45043 ymax: 30.52177
Geodetic CRS:  WGS 84
  COUNTYFP is.na(neighbor_eviction_avg)   n                       geometry
1      209                        FALSE   1 POLYGON ((-98.01009 30.1316...
2      453                        FALSE 225 POLYGON ((-97.83473 30.2966...
3      453                         TRUE   8 MULTIPOLYGON (((-97.6419 30...
4      491                        FALSE  16 POLYGON ((-97.73813 30.5059...
##### 13. Export for composite index

final_eviction_table <- final_eviction_proxy %>%
  st_drop_geometry() %>%
  select(
    GEOID_clean,
    COUNTYFP,
    all_of(evict_col),
    neighbor_eviction_avg
  ) %>%
  arrange(GEOID_clean)


write_csv(
  final_eviction_table,
  "Austin_Evictions_Hybrid_Spatial_Proxy.csv"
)
# INSPECT TOUCHING TRAVIS NEIGHBORS + EVICTION RATES

library(purrr)
library(tidyr)

# Build table of touching relationships
touch_table <- map_df(
  seq_along(touch_neighbors),
  function(i) {

    non_id <- non_travis_tracts$GEOID_clean[i]

    travis_ids <- touch_neighbors[[i]]

    # skip if no touching neighbors
    if (length(travis_ids) == 0) return(NULL)

    tibble(
      non_travis_geoid = non_id,
      travis_geoid = travis_tracts$GEOID_clean[travis_ids],
      travis_eviction_rate = travis_tracts[[evict_col]][travis_ids]
    )
  }
)


# Add assigned hybrid value
touch_table <- touch_table %>%
  left_join(
    non_travis_tracts %>%
      st_drop_geometry() %>%
      select(
        GEOID_clean,
        neighbor_eviction_avg
      ),
    by = c("non_travis_geoid" = "GEOID_clean")
  )


# Calculate neighbor mean (for verification)
touch_table <- touch_table %>%
  group_by(non_travis_geoid) %>%
  mutate(
    neighbor_mean_check = mean(travis_eviction_rate, na.rm = TRUE),
    n_neighbors = n()
  ) %>%
  ungroup()


# View in console (first few rows)
print(head(touch_table, 20))
# A tibble: 18 × 6
   non_travis_geoid travis_geoid travis_eviction_rate neighbor_eviction_avg
   <chr>            <chr>                       <dbl>                 <dbl>
 1 48491020405      48453033800               0.0101                 0.0314
 2 48491020405      48453034300              NA                      0.0314
 3 48491020405      48453033700               0.0215                 0.0314
 4 48491020405      48453032700               0.0626                 0.0314
 5 48491020503      48453043000               0.0657                 0.0501
 6 48491020503      48453034100               0.0344                 0.0501
 7 48209010912      48453033300               0.00837                0.0390
 8 48209010912      48453033200               0.0696                 0.0390
 9 48491020513      48453043000               0.0657                 0.0657
10 48491020410      48453034200               0.0208                 0.0393
11 48491020410      48453032700               0.0626                 0.0393
12 48491020410      48453034100               0.0344                 0.0393
13 48491020404      48453035800               0.0837                 0.0842
14 48491020404      48453034300              NA                      0.0842
15 48491020404      48453034400               0.0848                 0.0842
16 48491020411      48453043000               0.0657                 0.0403
17 48491020411      48453034200               0.0208                 0.0403
18 48491020411      48453034100               0.0344                 0.0403
# ℹ 2 more variables: neighbor_mean_check <dbl>, n_neighbors <int>
# save for documentation

write_csv(
  touch_table,
  "Austin_NonTravis_Touching_Travis_Evictions.csv"
)

################################################################################
summary(tracts_with_evictions$`Eviction filing rate`)
Length  Class   Mode 
     0   NULL   NULL 
sum(is.na(travis_tracts$`Eviction filing rate`))
[1] 0
nrow(travis_tracts)
[1] 233