You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I wonder if it would be possible to make data.table compatible with the new sf library. The library sf is promising to be a game changer for spatial analysis in R so it sounds like good idea to bring together the power of both libraries.
Currently, the class of an sf object is "sf" "data.frame" and it brings a column named geometry of class "sfc_MULTIPOLYGON" "sfc" .
Right now, I think the main incompatibility between dt and sf is that when we convert an sf data.frame into a data.table using setDT(), the geometry column is ruined and the object is not recognised anymore as an sf class.
I'm sure there are other points to take into account when making these two great libraries together so I just wanted to put the ball rolling if someone has not done this before.
For the record, I've also created an issue in the sf github page #428 because the compatibility between the two libraries might request a bit of collaboration from both sides.
If you lose the sf class you can still rely on the sfc class, but you have to take care of more stuff. Also, returning a geometry in a data.table seems tricky, but my DT skills are rusty, so there might be a better way. There is no reason it shouldn't work in theory, but in practice some things might have to be ironed. Looking at the tidy verbs implementation should give a good idea of what needs to be reconciled.
A quick note that @SymbolixAU has been working on a project creating a spatial.data.table More info HERE. I'm not sure creating a new class is the way forward, but I'm sure there is a good overlap between all these projects.
Reacted by Vijay Lulla, Jaap Walhout, Mel B. and Andrei-WongE
The problem @etiennebr is describing is because aggregation of geometry can lead to two different kinds of geometries (POLYGON and MULTIPOLYGON in this case). Try the following:
nc<- st_read(system.file("shape/nc.shp",package="sf"))
nc_DT<- as.data.table(nc)
nc %>% group_by(SID74) %>% summarise(geom= st_union(geometry))
nc_DT[,st_union(geometry),by=SID74][,table(SID74)] # indices where frequency > 1 are multipolygon geometries!
I still don't understand why data.table shows 83 rows for nc_DT[, st_union(geometry),by=SID74]. Regardless of the geometry aggregation issue shouldn't data.table still return only 23 rows here?
I have realized my error and I know that I have to do nc_DT[, .(st_union(geometry)), by=.(SID74)] but that still doesn't give me a valid object that I can plot!
nc %>% group_by(SID74) %>% summarise(geom=st_union(geometry)) %>% plot# works!
plot(nc_DT[, .(st_union(geometry)), by=.(SID74)]) # does not work...some weird error!
Am I right in thinking it would still be nice if setDT() retained the sf in the class?
Or can this issue be closed now on the data.table side. The solutions above are workarounds?
I reran the reproducible example above with 1.12.0 but it's still the same. Note that geometry column is not really lost though. That's just a printing/display difference. It's still all there :
Instead of putting sf last in the class I tried putting it first : setattr(nc, "class", c("sf", "data.table", "data.frame"))
but this still resulted in problems. Perhaps you could try that and see if those problems can be overcome.
The root issue is perhaps that sf has its own [.sf method which either masks or is masked by [.data.table depending on whether sf is before or after data.table in class().
The class attribute being c("sf", "data.table", "data.frame") makes sense to me on first glance. data.table could change setDT() to retain sf like that. But then sf would likely need to become data.table-aware (import data.table) and call NextMethod() inside its [.sf if inherts(x,"data.table"). Something like that.
Certainly my intention when writing the PR was that downstream packages would define methods for their own classes. sf is in a slightly unique case where it doesn't Depend/Import/Suggest data.table, so it's a bit more of a toss-up... still, I think hosting it in sf would be more appropriate.
If they are unwilling to merge we could do so here, since IINM we don't need to add any dependency, just to create an .sfg method.
I had a look at your PR, it looks quite elaborate for just adding a method! I am assuming because of the invisible-line dependency issue.
Maybe format_list_item.default could check if there's a format method registered for the class of the object, and run that as a default, and only lacking that back up to the current <{class}[dim]> approach.
I'm not great with S3 registration stuff, but if this sounds easy to you, a PR would be great.
Hmmm. Looks I'm in the minority here, thinking that an internal data.table method is the simplest and cleanest thing to do. As Michael (and my earlier post) suggest, it would basically require a single (unexported) line of code.
format_list_item.sfg=function(x, ...) format(x)
Similarly, I don't think this method would change the behaviour for any other data frame(ish) object on the sf side. Both tibbles and vanilla data frames would keep their existing print behaviour. So I'm not sure there's much incentive on their end?
Final argument for keeping this on the data.table side: Converting an sf object to a data.table must not only be done explicitly, but also necessitates a slight change in workflow. (As documented in this thread, we need to refer to the geometry column explicitly when performing spatial operations, e.g. st_union(nc) vs nc_dt[, st_union(geometry)].) That's a perfectly acceptable trade-off from my perspective, for those of us that want to combine the power of the two packages. But the point is we're selecting into this modified workflow. IMHO the print method is just a small (but much appreciated!) convenience layer from the data.table side further enabling this integration.
Now that I've thought about this a bit more, I think I'm with @grantmcdermott on this one.
It'd be really nice to be able to sidestep the elaborate hoops I go through in that sf-side PR just to be able to register the method without adding data.table to sf as an Imports or Depends. As both @grantmcdermott and @MichaelChirico note, adding the method on the data.table side would require just three lines of code and no additional dependencies.
FTR, I looked into @MichaelChirico suggestion that we could possibly modify the code of data.table:::format_list_item.default(). That solution (as implemented by the code below) works just fine for a simple feature geometry. Unfortunately, it's not a great general approach, since it'll cause all sorts of problems when list items have format methods that don't produce such nicely succinct output For example, it fails (throwing an error) when printing a data.table such as this: x <- data.table(list(mtcars, mtcars))
## Alternative implementation, that uses each list element's class to dispatch a format methodformat_list_item.default<-function (x, ...)
{
exists_format_method<-function(x) {
!(is.null(getS3method("format", class=x, optional=TRUE)))
}
if (is.null(x))
""elseif (is.atomic(x) || inherits(x, "formula"))
paste(c(format(head(x, 6L), ...), if (length(x) >6L) "..."),
collapse=",")
elseif (any(sapply(class(x), exists_format_method)))
format(x)
else
paste0("<", class(x)[1L], paste_dims(x), ">")
}
Thanks for investigating Josh. point taken... I am only trying to avoid a maintenance headache if dozens of formatters are eventually requested in data.table.
the most scalable thing seems to me to be keeping a list of classes where the format method plays well.
but I think that can be kicked down the road a bit. I would be fine with a simple PR to data.table as described (with NEWS and mention it in the .Rd)
@MichaelChirico That makes total sense, and I like your idea of, in the longer term, using a list to record classes with format methods that produce reasonable print.data.table-ready output.
In the PR I just pushed, I added this as a new item in NEWS, but could also see folding it into NEWS item 17, which announced the format_list_item() generic.
Also, I didn't add an example to the *.Rd file, only because I didn't want to induce any dependency on sf or trigger any CRAN-check warnings. Let me know if you think an example would be good, though, and I'll see how best to add one that'll satisfy CRAN's requirements.
Activity
MichaelChirico commentedon Jul 17, 2017
mattdowle commentedon Jul 17, 2017
Reproducible example please.
MichaelChirico commentedon Jul 17, 2017
#1310 is related
rafapereirabr commentedon Jul 18, 2017
Reproducible Example
Now when we try setting the
sfobject intodata.table, note that the object looses both its "sf" class and the information in thegeometrycolumnrafapereirabr commentedon Jul 18, 2017
For the record, I've also created an issue in the sf github page #428 because the compatibility between the two libraries might request a bit of collaboration from both sides.
etiennebr commentedon Jul 18, 2017
If you lose the
sfclass you can still rely on thesfcclass, but you have to take care of more stuff. Also, returning a geometry in adata.tableseems tricky, but my DT skills are rusty, so there might be a better way. There is no reason it shouldn't work in theory, but in practice some things might have to be ironed. Looking at the tidy verbs implementation should give a good idea of what needs to be reconciled.But returning geometries is a problem
Another path is to
nc <- st_as_sf(setDT(nc)), but there are still problems.rafapereirabr commentedon Jul 26, 2017
A quick note that @SymbolixAU has been working on a project creating a
spatial.data.tableMore info HERE. I'm not sure creating a new class is the way forward, but I'm sure there is a good overlap between all these projects.vlulla commentedon Sep 7, 2017
The problem @etiennebr is describing is because aggregation of geometry can lead to two different kinds of geometries (POLYGON and MULTIPOLYGON in this case). Try the following:
I still don't understand why data.table shows 83 rows for
nc_DT[, st_union(geometry),by=SID74]. Regardless of the geometry aggregation issue shouldn't data.table still return only 23 rows here?vlulla commentedon Jan 29, 2019
I have realized my error and I know that I have to do
nc_DT[, .(st_union(geometry)), by=.(SID74)]but that still doesn't give me a valid object that I can plot!vlulla commentedon Jan 29, 2019
mattdowle commentedon Jan 29, 2019
Am I right in thinking it would still be nice if
setDT()retained thesfin the class?Or can this issue be closed now on the data.table side. The solutions above are workarounds?
I reran the reproducible example above with 1.12.0 but it's still the same. Note that
geometrycolumn is not really lost though. That's just a printing/display difference. It's still all there :mattdowle commentedon Jan 30, 2019
Instead of putting
sflast in the class I tried putting it first :setattr(nc, "class", c("sf", "data.table", "data.frame"))but this still resulted in problems. Perhaps you could try that and see if those problems can be overcome.
The root issue is perhaps that
sfhas its own[.sfmethod which either masks or is masked by[.data.tabledepending on whethersfis before or afterdata.tableinclass().The class attribute being
c("sf", "data.table", "data.frame")makes sense to me on first glance.data.tablecould changesetDT()to retainsflike that. But thensfwould likely need to become data.table-aware (import data.table) and callNextMethod()inside its[.sfifinherts(x,"data.table"). Something like that.12 remaining items
jangorecki commentedon Oct 14, 2021
I think it may fit better to sf package, then it may be reused in other packages as well.
JoshOBrien commentedon Oct 14, 2021
@jangorecki Sounds good. I'll drop another note here once data.table v1.14.4 is out if my PR then gets pulled into the sf package.
MichaelChirico commentedon Oct 15, 2021
Certainly my intention when writing the PR was that downstream packages would define methods for their own classes.
sfis in a slightly unique case where it doesn't Depend/Import/Suggestdata.table, so it's a bit more of a toss-up... still, I think hosting it insfwould be more appropriate.If they are unwilling to merge we could do so here, since IINM we don't need to add any dependency, just to create an
.sfgmethod.I had a look at your PR, it looks quite elaborate for just adding a method! I am assuming because of the invisible-line dependency issue.
But the meat of your PR is very simple:
Maybe
format_list_item.defaultcould check if there's aformatmethod registered for the class of the object, and run that as a default, and only lacking that back up to the current<{class}[dim]>approach.I'm not great with S3 registration stuff, but if this sounds easy to you, a PR would be great.
grantmcdermott commentedon Oct 15, 2021
Hmmm. Looks I'm in the minority here, thinking that an internal
data.tablemethod is the simplest and cleanest thing to do. As Michael (and my earlier post) suggest, it would basically require a single (unexported) line of code.Similarly, I don't think this method would change the behaviour for any other data frame(ish) object on the
sfside. Both tibbles and vanilla data frames would keep their existing print behaviour. So I'm not sure there's much incentive on their end?Final argument for keeping this on the data.table side: Converting an sf object to a data.table must not only be done explicitly, but also necessitates a slight change in workflow. (As documented in this thread, we need to refer to the
geometrycolumn explicitly when performing spatial operations, e.g.st_union(nc)vsnc_dt[, st_union(geometry)].) That's a perfectly acceptable trade-off from my perspective, for those of us that want to combine the power of the two packages. But the point is we're selecting into this modified workflow. IMHO the print method is just a small (but much appreciated!) convenience layer from the data.table side further enabling this integration.JoshOBrien commentedon Oct 15, 2021
Now that I've thought about this a bit more, I think I'm with @grantmcdermott on this one.
It'd be really nice to be able to sidestep the elaborate hoops I go through in that sf-side PR just to be able to register the method without adding data.table to sf as an Imports or Depends. As both @grantmcdermott and @MichaelChirico note, adding the method on the data.table side would require just three lines of code and no additional dependencies.
FTR, I looked into @MichaelChirico suggestion that we could possibly modify the code of
data.table:::format_list_item.default(). That solution (as implemented by the code below) works just fine for a simple feature geometry. Unfortunately, it's not a great general approach, since it'll cause all sorts of problems when list items have format methods that don't produce such nicely succinct output For example, it fails (throwing an error) when printing a data.table such as this:x <- data.table(list(mtcars, mtcars))MichaelChirico commentedon Oct 15, 2021
Thanks for investigating Josh. point taken... I am only trying to avoid a maintenance headache if dozens of formatters are eventually requested in data.table.
the most scalable thing seems to me to be keeping a list of classes where the format method plays well.
but I think that can be kicked down the road a bit. I would be fine with a simple PR to data.table as described (with NEWS and mention it in the .Rd)
Add format_list_item() method for simple feature geometry columns
JoshOBrien commentedon Oct 16, 2021
@MichaelChirico That makes total sense, and I like your idea of, in the longer term, using a list to record classes with format methods that produce reasonable
print.data.table-ready output.In the PR I just pushed, I added this as a new item in NEWS, but could also see folding it into NEWS item 17, which announced the
format_list_item()generic.Also, I didn't add an example to the
*.Rdfile, only because I didn't want to induce any dependency on sf or trigger any CRAN-check warnings. Let me know if you think an example would be good, though, and I'll see how best to add one that'll satisfy CRAN's requirements.