I want to obtain the latitude and longitude from a shapefile. Until now, I only know how to read the shapefile.
library(rgdal)
centroids.mp <- readOGR(".","35DSE250GC_SIR")
But how I can extract the latitude and longitude from centroids.mp?
There's a few levels to this question.
You ask for longitude and latitude, but that may not be the coordinate system used by this object. You can get the coordinates like this
coordinates(centroids.mp)
Note that the "centroids" will be all of the coordinates if this is a SpatialPointsDataFrame, a list of all the line coordinates if this is a SpatialLinesDataFrame, and just the centroids if this is a SpatialPolygonsDataFrame.
The coordinates may be longitude and latitude, but the object may not know that. Use
proj4string(centroids.mp)
If that is "NA", then the object does not know (A). If it includes "+proj=longlat", the object does know and they are longitude/latitude (B). If it includes "+proj=" and some other name (not "longlat") then the object does know and it's not longitude/latitude (C).
If (A) you'll have to find out, or it might be obvious from the values.
If (B) you are done (though you should check assumptions first, these metadata can be incorrect).
If (C) you can (pretty reliably though you should check assumptions first) transform to longitude latitude (on datum WGS84) like this:
coordinates(spTransform(centroids.mp, CRS("+proj=longlat +datum=WGS84")))
Use coordinates(), like this:
library(maptools)
xx <- readShapePoints(system.file("shapes/baltim.shp", package="maptools")[1])
coordinates(xx)
# coords.x1 coords.x2
# 0 907.0 534.0
# 1 922.0 574.0
# 2 920.0 581.0
# 3 923.0 578.0
# 4 918.0 574.0
# [.......]
st_coordinates solve the problem, however it removes removes covariates linked to the coordinates from the sf object. here I share an alternative in case you need them:
# useful enough
sites_sf %>%
st_coordinates()
#> X Y
#> 1 -80.14401 26.47901
#> 2 -80.10900 26.83000
# alternative to keep covariates within a tibble/sf
sites_sf %>%
st_coordinates_tidy()
#> Joining, by = "rowname"
#> Simple feature collection with 2 features and 3 fields
#> geometry type: POINT
#> dimension: XY
#> bbox: xmin: -80.14401 ymin: 26.479 xmax: -80.109 ymax: 26.83
#> epsg (SRID): 4326
#> proj4string: +proj=longlat +datum=WGS84 +no_defs
#> # A tibble: 2 x 4
#> gpx_point X Y geometry
#> <chr> <dbl> <dbl> <POINT [°]>
#> 1 a -80.1 26.5 (-80.14401 26.47901)
#> 2 b -80.1 26.8 (-80.109 26.83)
complete reprex: https://avallecam.github.io/avallecam/reference/st_coordinates_tidy.html
source code: https://github.com/avallecam/avallecam/blob/master/R/spatially_useful.R
Related
I have a spatVector composed of a single-line geometry that covers the entire road network of my study area.
I would like to create a set of N random points over this geometry. I know how to do it in QGIS but I want to do it in R since I have to iterate this process 1'000 times and I want to create a loop.
Do you know any function to do this?
EDIT
First of all, I read my line shapefile using:
Road_network <- vect("path/to/file.shp)
Then I converted it into an SF object:
Road_network_SF <- st_as_sf(Road_network)
And finally, I use both the st_sample, getting the following results:
Random_points <- st_sample(Road_network_SF, size = 1799)
Random_points
Geometry set for 46350 features (with 44694 geometries empty)
Geometry type: MULTIPOINT
Dimension: XY
Bounding box: xmin: 4503139 ymin: 2504751 xmax: 4622797 ymax: 2613276
Projected CRS: ETRS89-extended / LAEA Europe
First 5 geometries:
MULTIPOINT EMPTY
MULTIPOINT EMPTY
MULTIPOINT EMPTY
MULTIPOINT ((4503139 2574957))
MULTIPOINT EMPTY
and the st_line_sample function, getting the following error:
Random_points <- st_line_sample(Road_network_SF, n = 1799)
Error in st_line_sample(Road_network_SF, n = 1799) :
inherits(x, "sfc_LINESTRING") non è TRUE
When I converted the spatVector to an sf object, this is what I get:
Road_network_SF
Simple feature collection with 1 feature and 2 fields
Geometry type: MULTILINESTRING
Dimension: XY
Bounding box: xmin: 4500176 ymin: 2504157 xmax: 4626207 ymax: 2616041
Projected CRS: ETRS89-extended / LAEA Europe
FURTHER EDIT
The workflow proposed by #Gregory work really good, my error was due to a problem with the road shapefile. I changed it and no further problems occurred, thank you!
Thanks in advance!
You can sample random points along a vector geometry (like roads) with sf::st_sample(), however the results might seem confusing depending on how you look at them. Here's a reproducible example.
library(sf, quietly = TRUE)
#> Linking to GEOS 3.10.2, GDAL 3.4.2, PROJ 8.2.1; sf_use_s2() is TRUE
library(tigris, quietly = TRUE)
#> To enable
#> caching of data, set `options(tigris_use_cache = TRUE)` in your R script or .Rprofile.
library(ggplot2)
suppressMessages(
roads <- roads(state = "NC",
county = "Mecklenburg")
)
set.seed(1)
rpoints <- st_sample(roads, size = 5)
#> although coordinates are longitude/latitude, st_sample assumes that they are
#> planar
ggplot() +
geom_sf(data = roads, color = "grey") +
geom_sf(data = rpoints, color = "black")
We see on the map that we have generated 5 random points, as intended. Surprisingly, if you examine the structure of the rpoints object you'll see that it is a multipoint of length 21672, which you might think is the number of points. However, all but 5 of them have empty geometries. The reason is that there is a geometry (empty for most) for each of the objects that makes up the roads vector.
str(rpoints)
#> sfc_MULTIPOINT of length 21672; first list element: 'XY' num[0 , 1:2] MULTIPOINT EMPTY
head(rpoints)
#> Geometry set for 6 features (with 6 geometries empty)
#> Geometry type: MULTIPOINT
#> Dimension: XY
#> Bounding box: xmin: NA ymin: NA xmax: NA ymax: NA
#> Geodetic CRS: NAD83
#> First 5 geometries:
#> MULTIPOINT EMPTY
#> MULTIPOINT EMPTY
#> MULTIPOINT EMPTY
#> MULTIPOINT EMPTY
#> MULTIPOINT EMPTY
Here's how to get the real points out of there.
rpoints <- rpoints[!st_is_empty(rpoints)]
rpoints
#> Geometry set for 5 features
#> Geometry type: MULTIPOINT
#> Dimension: XY
#> Bounding box: xmin: -81.01691 ymin: 35.07471 xmax: -80.62246 ymax: 35.2948
#> Geodetic CRS: NAD83
#> MULTIPOINT ((-80.88764 35.2948))
#> MULTIPOINT ((-80.62246 35.18395))
#> MULTIPOINT ((-81.01691 35.07471))
#> MULTIPOINT ((-80.78909 35.12663))
#> MULTIPOINT ((-80.83055 35.16959))
Created on 2023-02-01 by the reprex package (v2.0.1)
I am struggling with gCentroid, because it doesn't seem -- to me -- to give the 'right' answer near a pole of the Earth.
For instance:
library(rgeos)
gCentroid(SpatialPoints(coords=data.frame(longitude=c(-135,-45,45,135),latitute=c(80,80,80,80)),proj4string = CRS('EPSG:4326')))
does not give me the North Pole, it gives:
> SpatialPoints:
> x y
> 1 0 80
> Coordinate Reference System (CRS) arguments: +proj=longlat +datum=WGS84 +no_defs
How do I get gCentroid to work on the surface of the Earth?
The GEOS library is limited to planar geometry operations; this can bring issues in edge cases / the poles being a notorious example.
For the centroid via GEOS to work as intended you need to transform your coordinates from WGS84 to a coordinate reference system appropriate to polar regions; for Arctic regions I suggest EPSG:3995.
library(sp)
library(dplyr)
library(rgeos)
points_sp <- SpatialPoints(coords=data.frame(longitude=c(-135,-45,45,135),latitute=c(80,80,80,80)),proj4string = CRS('EPSG:4326'))
points_updated <- points_sp %>%
spTransform(CRS("EPSG:3995")) # a projected CRS apropriate for Arctic regions
centroid <- gCentroid(points_updated) %>%
spTransform(CRS("EPSG:4326")) # back to safety of WGS84!
centroid # looks better now...
# SpatialPoints:
# x y
# 1 0 90
# Coordinate Reference System (CRS) arguments: +proj=longlat +datum=WGS84 +no_defs
Also note that your workflow - while not wrong in principle - is a bit dated, and the {rgeos} package is approaching its end of life.
It may be good time to give a strong consideration to {sf} package, which is newer, actively developed and can, via interface to s2 library from Google, handle spherical geometry operations.
For an example of {sf} based workflow consider this code; the result (centroid = North Pole) is equivalent to the sp / rgeos one.
library(sf)
points_sf <- points_sp %>% # declared earlier
st_as_sf()
centroid_sf <- points_sf %>%
st_union() %>% # unite featrues / from 4 points >> 1 multipoint
st_centroid()
centroid_sf # the North Pole in a slightly different (sf vs sp) format
# Geometry set for 1 feature
# Geometry type: POINT
# Dimension: XY
# Bounding box: xmin: 0 ymin: 90 xmax: 0 ymax: 90
# Geodetic CRS: WGS 84 (with axis order normalized for visualization)
# POINT (0 90)
I have a polygon (zones) and a set of coordinates (points). I'd like to create a spatial kernal density raster for the entire polygon and extract the sum of the density by zone. Points outside of the polygon should be discarded.
library(raster)
library(tidyverse)
library(sf)
library(spatstat)
library(maptools)
load(url("https://www.dropbox.com/s/iv1s5butsx2v01r/example.RData?dl=1"))
# alternatively, links to gists for each object
# https://gist.github.com/ericpgreen/d80665d22dfa1c05607e75b8d2163b84
# https://gist.github.com/ericpgreen/7f4d3cee3eb5efed5486f7f713306e96
ggplot() +
geom_sf(data = zones) +
geom_sf(data = points) +
theme_minimal()
I tried converting to ppp with {spatstat} and then using density(), but I'm confused by the units in the result. I believe the problem is related to the units of the map, but I'm not sure how to proceed.
Update
Here's the code to reproduce the density map I created:
zones_owin <- as.owin(as_Spatial(zones))
pts <- st_coordinates(points)
p <- ppp(pts[,1], pts[,2], window=zones_owin, unitname=c("metre","metres"))
ds <- density(p)
r <- raster(ds)
plot(r)
Units are difficult when you work directly with geographic coordinates (lon, lat). If possible you should convert to planar coordinates (which is a requirement for spatstat) and proceed from there. The planar coordinates would typically be in units of meters, but I guess it depends on the specific projection and underlying ellipsoid etc. You can see this answer for how to project to planar coordinates with sf and export to spatstat format using maptools. Note: You have to manually choose a sensible projection (you can use http://epsg.io to find one) and you have to project both the polygon and the points.
Once everything is in spatstat format you can use density.ppp to do kernel smoothing. The resulting grid values (object of class im) are intensities of points, i.e., number of points per square unit (e.g. square meter). If you want to aggregate over some region you can use integral.im(..., domain = ...) to get the expected number of points in this region for a point process model with the given intensity.
I'm not sure if this answers all of your question, but should be a good start. Clarify in a comment or in your question should you need a different type of output.
It removes all points that are not inside one of the 'zone' polygons, counts them by zone and plots the zones colored by the number of points that fall within.
library(raster)
library(tidyverse)
library(sf)
#> Linking to GEOS 3.6.2, GDAL 2.2.3, PROJ 4.9.3
library(spatstat)
library(maptools)
#> Checking rgeos availability: TRUE
load(url("https://www.dropbox.com/s/iv1s5butsx2v01r/example.RData?dl=1"))
# alternatively, links to gists for each object
# https://gist.github.com/ericpgreen/d80665d22dfa1c05607e75b8d2163b84
# https://gist.github.com/ericpgreen/7f4d3cee3eb5efed5486f7f713306e96
p1 <- ggplot() +
geom_sf(data = zones) +
geom_sf(data = points) +
theme_minimal()
#Remove points outside of zones
points_inside <- st_intersection(points, zones)
#> although coordinates are longitude/latitude, st_intersection assumes that they are planar
#> Warning: attribute variables are assumed to be spatially constant throughout all
#> geometries
nrow(points)
#> [1] 308
nrow(points_inside)
#> [1] 201
p2 <- ggplot() +
geom_sf(data = zones) +
geom_sf(data = points_inside)
points_per_zone <- st_join(zones, points_inside) %>%
count(LocationID.x)
#> although coordinates are longitude/latitude, st_intersects assumes that they are planar
p3 <- ggplot() +
geom_sf(data = points_per_zone,
aes(fill = n)) +
scale_fill_viridis_c(option = 'C')
points_per_zone
#> Simple feature collection with 4 features and 2 fields
#> geometry type: POLYGON
#> dimension: XY
#> bbox: xmin: 34.0401 ymin: -1.076718 xmax: 34.17818 ymax: -0.9755066
#> epsg (SRID): 4326
#> proj4string: +proj=longlat +ellps=WGS84 +no_defs
#> # A tibble: 4 x 3
#> LocationID.x n geometry
#> * <dbl> <int> <POLYGON [°]>
#> 1 10 129 ((34.08018 -0.9755066, 34.0803 -0.9757393, 34.08046 -0.975…
#> 2 20 19 ((34.05622 -0.9959458, 34.05642 -0.9960835, 34.05665 -0.99…
#> 3 30 29 ((34.12994 -1.026372, 34.12994 -1.026512, 34.12988 -1.0266…
#> 4 40 24 ((34.11962 -1.001829, 34.11956 -1.002018, 34.11966 -1.0020…
cowplot::plot_grid(p1, p2, p3, nrow = 2, ncol = 2)
It seems I underestimated the difficulty of your problem. Is something like the plot below (& underlying data) what you're looking for?
It uses raster with ~50x50 grid, raster::focal with a window of 9x9 using the mean to interpolate the data.
At the moment I'm working on a project with point pattern events on a linear network (car crashes) and I'm reading chapter 17 of spatstat book: "Spatial Point Patterns: Methodology and Applications with R".
The authors of the book explain that they defined a new class of objects called lpp for analyzing point patterns on a linear network. The skeleton of each lpp object is a linnet object and there are several functions to create a linnet object. For my application the relevant functions are linnet and as.linnet. The function linnet creates a linear network object from the spatial location of each vertex and information about which vertices are joined by an edge, while the as.linnet function can be applied to a psp object which is transformed into linnet objects inferring the connectivity of the network using a specified distance threshold.
The reason why I'm asking this question is that I don't know how to efficiently create a linnet object starting from a sf object with a LINESTRING geometry. As far as I know, it's possible to transform the sf object into an sp object (i.e. a SpatialLines object), then I can transform the sp object into a psp object (using as.psp function) and then I can transform the psp object into a linnet object using the as.psp.linnet function (which is defined in the maptools package). The main problem with this approach (as the authors of the package said in their book) is that the inferred network is wrong every time an overpass or an underpass occurs in my network data since the corresponding linnet will create artificial intersections in the nework. Moreover, as the authors said in their book, the code gets exponentially slower.
The following code is a simplified version of what I did so far but I think that there must be an easier and better way to create a linnet object from an sf object. I would use the linnet function but the problem is that I don't know how to create a (sparse) adjacency matrix for the corresponding vertices of the network or a matrix of links between the edges of the network.
# packages
library(sf)
#> Linking to GEOS 3.6.1, GDAL 2.2.3, PROJ 4.9.3
library(spatstat)
#> Loading required package: spatstat.data
#> Loading required package: nlme
#> Loading required package: rpart
#>
#> spatstat 1.61-0 (nickname: 'Puppy zoomies')
#> For an introduction to spatstat, type 'beginner'
#>
#> Note: spatstat version 1.61-0 is out of date by more than 11 weeks; a newer version should be available.
library(maptools)
#> Loading required package: sp
#> Checking rgeos availability: TRUE
library(osmdata)
#> Data (c) OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright
# download data
iow_polygon <- getbb("Isle of Wight, South East, England", format_out = "sf_polygon", featuretype = "state") %>%
st_transform(crs = 27700)
iow_highways <- st_read("https://download.geofabrik.de/europe/great-britain/england/isle-of-wight-latest.osm.pbf", layer = "lines") %>%
st_transform(crs = 27700)
#> Reading layer `lines' from data source `https://download.geofabrik.de/europe/great-britain/england/isle-of-wight-latest.osm.pbf' using driver `OSM'
#> Simple feature collection with 44800 features and 9 fields
#> geometry type: LINESTRING
#> dimension: XY
#> bbox: xmin: -5.716262 ymin: 43.35489 xmax: 1.92832 ymax: 51.16517
#> epsg (SRID): 4326
#> proj4string: +proj=longlat +datum=WGS84 +no_defs
# subset the data otherwise the code takes ages
iow_highways <- iow_highways[iow_polygon, ] %>%
subset(grepl(pattern = c("primary|secondary|tertiary"), x = highway))
# transform as sp
iow_highways_sp <- as(iow_highways %>% st_geometry(), "Spatial")
# transform as psp
iow_highways_psp <- as.psp(iow_highways_sp)
# transform as linnet
iow_highways_linnet <- as.linnet.psp(iow_highways_psp, sparse = TRUE)
I can extract the coordinates of each vertex of the network
stplanr::line2points(iow_highways)
#> Simple feature collection with 2814 features and 1 field
#> geometry type: POINT
#> dimension: XY
#> bbox: xmin: 430780.7 ymin: 75702.05 xmax: 464851.7 ymax: 96103.72
#> epsg (SRID): 27700
#> proj4string: +proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs
#> First 10 features:
#> id geometry
#> 1 1 POINT (464851.7 87789.73)
#> 2 1 POINT (464435.4 88250.85)
#> 3 2 POINT (464390.9 87412.27)
#> 4 2 POINT (464851.7 87789.73)
#> 5 3 POINT (462574.6 88987.62)
#> 6 3 POINT (462334.6 88709.92)
#> 7 4 POINT (464066.9 87576.84)
#> 8 4 POINT (464390.9 87412.27)
#> 9 5 POINT (464420 88227.79)
#> 10 5 POINT (464398.7 88225.33)
but then I don't know how to build the adjacency matrix.
Created on 2019-12-02 by the reprex package (v0.3.0)
I’m not sure why you go through the psp format on the way to linnet.
Try to replace the last two lines of your first code chunk by:
iow_highways_linnet <- as.linnet.SpatialLines(iow_highways_sp)
This converts SpatialLines directly to linnet and fuses lines
that share a vertex. I don’t think an underpass will be fused to an
overpass unless both lines have a vertex at the intersection point.
See example below:
l1 <- sf::st_linestring(matrix(c(-1,1,-1,1,1,1,-1,-1), ncol = 2))
l2 <- sf::st_linestring(matrix(c(-1,-1,1,1,2,1,-1,-2), ncol = 2))
l_sf <- sf::st_sf(id = 1:2, geom = sf::st_sfc(l1,l2))
l_sp <- sf::as_Spatial(l_sf)
l <- maptools::as.linnet.SpatialLines(l_sp)
plot(l)
Just confirming that the spatstat package does not provide functions for handling other formats; our expectation is that maptools or other packages will provide format conversion code; this is not yet available for sf object formats, presumably because sf is relatively new.
The key question is whether an sf object with LINESTRING geometry contains enough information to determine connectivity of the network. If so, then I suggest you make a 2-column matrix listing all pairs of vertices which are joined by edges, and invoke spatstat::linnet. If not, then the available data are not sufficient...
Finally please note that the current development version of spatstat(1.61-0.061) available from GitHub is very much faster than the current release (1.61-0) for many operations on linear networks. It will be released publicly soon.
I am struggling with the following issue
I have downloaded the PLUTO NYC Manhattan Shapefile for the NYC tax lots from here https://www1.nyc.gov/site/planning/data-maps/open-data/dwn-pluto-mappluto.page
I am able to read them in sf with a simple st_read
> mydf
Simple feature collection with 42638 features and 90 fields
geometry type: MULTIPOLYGON
dimension: XY
bbox: xmin: 971045.3 ymin: 188447.4 xmax: 1010027 ymax: 259571.5
epsg (SRID): NA
proj4string: +proj=lcc +lat_1=40.66666666666666 +lat_2=41.03333333333333 +lat_0=40.16666666666666 +lon_0=-74 +x_0=300000 +y_0=0 +datum=NAD83 +units=us-ft +no_defs
First 10 features:
Borough Block Lot CD CT2010 CB2010 SchoolDist Council ZipCode FireComp PolicePrct HealthCent HealthArea
1 MN 1545 52 108 138 4000 02 5 10028 E022 19 13 3700
My problem is the following: I have a dataframe as follows
> data_frame('lat' = c(40.785091,40.785091), 'lon' = c(-73.968285, -73.968285))
# A tibble: 2 x 2
lat lon
<dbl> <dbl>
1 40.785091 -73.968285
2 40.785091 -73.968285
I would like to merge this data to the mydf dataframe above, so that I can count how many latitude/longitude observations I have within each tax lot (remember, mydf is at the tax lot granularity), and plot the corresponding map of it. I need to do so using sf.
In essence something similar to
pol <- mydf %>% select(SchoolDist)
plot(pol)
but where the counts for each tax lot come from counting how many points in my latitude/longitude dataframe fall into them.
Of course, in my small example I just have 2 points in the same tax lot, so that would just highlight one single tax lot in the whole area. My real data contains a lot more points.
I think there is an easy way to do it, but I was not able to find it.
Thanks!
This is how I would do it with arbitrary polygon and point data. I wouldn't merge the two and instead just use a geometry predicate to get the counts that you want. Here we:
Use the built in nc dataset and transform to 3857 crs, which is projected rather than lat-long (avoids a warning in st_contains)
Create 1000 random points within the bounding box of nc, using st_bbox and runif. Note that st_as_sf can turn a data.frame with lat long columns into sf points.
Use lengths(st_contains(polygons, points) to get the counts of points per polygon. sgbp objects created by a geometry predicate are basically "for each geometry in sf x, what indices of geometries in sf y satisfy the predicate". So lengths1 effectively gives the number of points that satisfy the predicate for each geometry, in this case number of points contained within each polygon.
Once the counts are in the sf object as a column, we can just select and plot them with the plot.sf method.
For your data, simply replace nc with mydf and leave out the call to tibble, instead use your data.frame with the right lat long pairs.
library(tidyverse)
library(sf)
#> Linking to GEOS 3.6.1, GDAL 2.2.3, proj.4 4.9.3
nc <- system.file("shape/nc.shp", package="sf") %>%
read_sf() %>%
st_transform(3857)
set.seed(1000)
points <- tibble(
x = runif(1000, min = st_bbox(nc)[1], max = st_bbox(nc)[3]),
y = runif(1000, min = st_bbox(nc)[2], max = st_bbox(nc)[4])
) %>%
st_as_sf(coords = c("x", "y"), crs = 3857)
plot(nc$geometry)
plot(points$geometry, add = TRUE)
nc %>%
mutate(pt_count = lengths(st_contains(nc, points))) %>%
select(pt_count) %>%
plot()
Created on 2018-05-02 by the reprex package (v0.2.0).
I tried this on your data, but the intersection is empty for the both sets of points you provided. However, the code should work.
EDIT: Simplified group_by + mutate with add_count:
mydf = st_read("MN_Dcp_Mappinglot.shp")
xydf = data.frame(lat=c(40.758896,40.758896), lon=c(-73.985130, -73.985130))
xysf = st_as_sf(xydf, coords=c('lon', 'lat'), crs=st_crs(mydf))
## NB: make sure to st_transform both to common CRS, as Calum You suggests
xysf %>%
sf::st_intersection(mydf) %>%
dplyr::add_count(LOT)
Reproducible example:
nc = sf::st_read(system.file("shape/nc.shp", package="sf"))
ncxy = sf::st_as_sf(data.frame(lon=c(-80, -80.1, -82), lat=c(35.5, 35.5, 35.5)),
coords=c('lon', 'lat'), crs=st_crs(nc))
ncxy = ncxy %>%
sf::st_intersection(nc) %>%
dplyr::add_count(FIPS)
## a better approach
ncxy = ncxy %>%
sf::st_join(nc, join=st_intersects) %>%
dplyr::add_count(FIPS)
The new column n includes the total number of points per FIPS code.
ncxy %>% dplyr::group_by(FIPS) %>% dplyr::distinct(n)
> although coordinates are longitude/latitude, st_intersects assumes
that they are planar
# A tibble: 2 x 2
# Groups: FIPS [2]
FIPS n
<fctr> <int>
1 37123 2
2 37161 1
I'm not sure why your data results in an empty intersection, but since the code works on the example above there must be a separate issue.
HT: st_join approach from this answer.