23

What's the proper R idiom for rbinding SPDFs together when the IDs overlap? Note that here (as is often the case) the IDs are basically meaningless so it's pretty annoying that I can't just make rbind ignore them....

library(sp)
library(UScensus2000)
library(UScensus2000tract)

data(state) # for state names
states <- gsub( " ", "_", tolower(state.name) )
datanames <- paste(states,"tract", sep=".")
data( list=datanames )
lst <- lapply(datanames,get)

nation <- do.call( rbind, lst )
Error in validObject(res) : 
  invalid class “SpatialPolygons” object: non-unique Polygons ID slot values

# This non-exported function designed to solve this doesn't seem to work any more.
d <- sp:::makeUniqueIDs( list(arizona.tract,delaware.tract) )
Error in slot(i, "ID") : 
  no slot of name "ID" for this object of class "SpatialPolygonsDataFrame"

4 Answers 4

23

This is an even simpler approach:

x <- rbind(x1, x2, x3, makeUniqueIDs = TRUE)  
5
  • 1
    I wish this was documented in the rbind help page. I have to look here every time I can't remember the casing rules they used for this argument. Best answer for sure. I don't think it needs more context, and definitely should not be removed!
    – JMT2080AD
    Commented Jul 15, 2016 at 18:26
  • Documentation suggests "make.row.names = TRUE)"...which does not seem to work. Copy-pasting the example did.
    – Mox
    Commented Nov 4, 2016 at 4:25
  • I think the reason this is not documented in the help is because you are making an sp method call when you pass an sp object to rbind. See methods(class = "SpatialLines"). I'm not sure about this but it's my best guess right now. I'm pretty sure Edzer and co. aren't maintaining rbind itself, hence the lack of documentation in rbind.
    – JMT2080AD
    Commented Nov 30, 2016 at 23:00
  • What if there is a long list of objects to merge (x1, x2, x3, ..., xn)? Is there a method to capture the whole list without typing them all out?
    – Phil
    Commented Jan 8, 2018 at 16:10
  • 1
    Only works if the number of columns are equal. Commented Jan 25, 2018 at 15:27
15

ID's, slots, and apply-type functions. My top three least favorite things that are absolutely essential to everything I do. I thought I would respond just to generate more content on this topic.

The code below works, but it retains the "useless" ID values. Better code would take the time to parse things so that every tract had the state FIPS, county FIPS, and tract FIPS as its ID. Just a few more lines to make that happen, but since you don't care about ID's we will leave it out for now.

#Your Original Code
library(sp)
library(UScensus2000)
library(UScensus2000tract)

data(state) # for state names
states <- gsub( " ", "_", tolower(state.name) )
datanames <- paste(states,"tract", sep=".")
data( list=datanames )
lst <- lapply(datanames,get)

#All good up to here, but we need to create unique ID's before rbind

#Modified from Roger Bivand's response at:
# https://stat.ethz.ch/pipermail/r-sig-geo/2007-October/002701.html

#For posterity: We can access the ID in two ways:
class(alaska.tract)
getSlots(class(alaska.tract))
class(slot(alaska.tract, "polygons")[[1]])
getSlots(class(slot(alaska.tract, "polygons")[[1]]))

#So to get all ID's
sapply(slot(alaska.tract, "polygons"), function(x) slot(x, "ID"))
#or
rownames(as(alaska.tract, "data.frame"))
#These should be the same, but they are quite different...sigh. Doesn't matter for
#what follows though

#To make them uniform we can write a function using the spChFIDs function from sp:
makeUniform<-function(SPDF){
  pref<-substitute(SPDF)  #just putting the file name in front.
  newSPDF<-spChFIDs(SPDF,as.character(paste(pref,rownames(as(SPDF,"data.frame")),sep="_")))
  return(newSPDF)
}

#now to do this for all of our state files
newIDs<-lapply(lst,function(x) makeUniform(x))

#back to your code...
nation <- do.call( rbind, newIDs )
3
  • Thanks. I've been meaning to check on this for a few days now but life has intervened. I'm kind of amazed it's this many lines of code. Do you think it would be worth submitting a patch to the SPDF method of rbind in the sp package? I was thinking of turning something like this code into a ,deduplicateIDs=TRUE argument to the method.... Commented Sep 7, 2012 at 12:11
  • Really just three lines of code for the function and one to apply it pre-rbind, but it does take some time to process for your problem. I have always found the handling of the ID in SPDF's to be a problem (any time I load something with rgdal for example), but Roger Bivand always seems to be able to make them behave so I have just assumed it is my own shortcoming. I like the idea of a patch, but wonder if accessing those slots would cause complications for other things in sp.
    – csfowler
    Commented Sep 7, 2012 at 15:36
  • Great answer. Just want to add a word of advice to others that when rbind gets stuck in my code, it usually is because of an earlier mistake (resulting in duplicate IDs). So the error is correct.
    – Chris
    Commented Oct 5, 2012 at 19:47
9

Alright, here's my solution. Suggestions welcome. I'll likely submit this as a patch to sp unless anyone sees any glaring omissions.

#' Get sp feature IDs
#' @aliases IDs IDs.default IDs.SpatialPolygonsDataFrame
#' @param x The object to get the IDs from
#' @param \dots Pass-alongs
#' @rdname IDs
IDs <- function(x,...) {
  UseMethod("IDs",x)
}
#' @method IDs default
#' @S3method IDs default
#' @rdname IDs
IDs.default <- function(x,...) {
  stop("Currently only SpatialPolygonsDataFrames are supported.")
}
#' @method IDs SpatialPolygonsDataFrame
#' @S3method IDs SpatialPolygonsDataFrame
#' @rdname IDs
IDs.SpatialPolygonsDataFrame <- function(x,...) {
  vapply(slot(x, "polygons"), function(x) slot(x, "ID"), "")
}

#' Assign sp feature IDs
#' @aliases IDs<- IDs.default<-
#' @param x The object to assign to
#' @param value The character vector to assign to the IDs
#' @rdname IDs<-
"IDs<-" <- function( x, value ) {
  UseMethod("IDs<-",x)
}
#' @method IDs<- SpatialPolygonsDataFrame
#' @S3method IDs<- SpatialPolygonsDataFrame
#' @rdname IDs<-
"IDs<-.SpatialPolygonsDataFrame" <- function( x, value) {
  spChFIDs(x,value)
}

#' rbind SpatialPolygonsDataFrames together, fixing IDs if duplicated
#' @param \dots SpatialPolygonsDataFrame(s) to rbind together
#' @param fix.duplicated.IDs Whether to de-duplicate polygon IDs or not
#' @return SpatialPolygonsDataFrame
#' @author Ari B. Friedman, with key functionality by csfowler on StackExchange
#' @method rbind.SpatialPolygonsDataFrame
#' @export rbind.SpatialPolygonsDataFrame
rbind.SpatialPolygonsDataFrame <- function(..., fix.duplicated.IDs=TRUE) {
  dots <- as.list(substitute(list(...)))[-1L]
  dots_names <- as.character(dots) # store names of objects passed in to ... so that we can use them to create unique IDs later on
  dots <- lapply(dots,eval)
  names(dots) <- NULL
  # Check IDs for duplicates and fix if indicated
  IDs_list <- lapply(dots,IDs)
  dups.sel <- duplicated(unlist(IDs_list))
  if( any(dups.sel) ) {
    if(fix.duplicated.IDs) {
      dups <- unique(unlist(IDs_list)[dups.sel])
      # Function that takes a SPDF, a string to prepend to the badID, and a character vector of bad IDs
      fixIDs <- function( x, prefix, badIDs ) {
        sel <-  IDs(x) %in% badIDs
        IDs(x)[sel] <- paste( prefix, IDs(x)[sel], sep="." )
        x
      }
      dots <- mapply(FUN=fixIDs , dots, dots_names, MoreArgs=list(badIDs=dups) )
    } else {
      stop("There are duplicated IDs, and fix.duplicated.IDs is not TRUE.")
    }
  }
  # One call to bind them all
  pl = do.call("rbind", lapply(dots, function(x) as(x, "SpatialPolygons")))
  df = do.call("rbind", lapply(dots, function(x) x@data))
  SpatialPolygonsDataFrame(pl, df)
}
1

I've appreciated the detail of other answers here and, building on them, the one-liner I've come to is below. Like OP, I don't care much about the meaning of the ID, but the following could also be adapted to embed a more informative ID too.

lst <- lapply(1:length(lst), function(i) spChFIDs(lst[[i]], paste0(as.character(i), '.', 1:length(lst[[i]]))))

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.