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
{{ message }}
Repository navigation
dplyr::arrange interfering with data.table's auto-indexing #5042
This is a follow-up on this StackOverflow question/answer. dplyr::arrange seems to interfere with auto-indexing, leading to unexpected wrong results.
A work-around is to disable auto-indexing with options(datatable.auto.index = FALSE).
As this issue is quite tricky, a warning, a mention to it in FAQ or a protection against this happening would be useful!
library(dplyr);
library(data.table)
DT <-
fread(
"iso3c country income
MOZ Mozambique LIC
ZMB Zambia LMIC
ALB Albania UMIC
MOZ Mozambique LIC
ZMB Zambia LMIC
ALB Albania UMIC
"
)
codes <- c("ALB", "ZMB")
options(datatable.auto.index = TRUE) # Default
DT <- distinct(DT) %>% as.data.table()
# Index creation because %in% is used for the first time
DT[iso3c %in% codes,verbose=T]
# Index mixed up by arrange
DT <- DT %>% arrange(iso3c) %>% as.data.table()
# this is wack because data.table uses the old index where row were rearranged:
DT[iso3c %in% codes,verbose=T]
#> iso3c country income
#> 1: ALB Albania UMIC
# this works because (...) prevents the parser to use auto-index
DT[(iso3c %in% codes)]
#> iso3c country income
#> 1: ALB Albania UMIC
#> 2: ZMB Zambia LMIC
Output of sessionInfo()
R version 4.1.0 (2021-05-18)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 10 x64 (build 18362)
Matrix products: default
locale:
[1] LC_COLLATE=English_United States.1252 LC_CTYPE=English_United States.1252
[3] LC_MONETARY=English_United States.1252 LC_NUMERIC=C
[5] LC_TIME=English_United States.1252
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] data.table_1.14.0 dplyr_1.0.6
I think it might be related somewhat to #4889, and not fixed by #4893. dplyr, AFAIK works with either tibbles or data.frames, so a data.table's index attribute would be ignored during that conversion but retained, leading to an incorrect result. I took a quick look at as.data.table.data.frame, which I believe would be called here, and it doesn't look like it removed the index:
I don't have dplyr on hand to try that out, but as said by @avimallu, as.data.frame should remove index. I pushed that change to #5043 Let me know if it does fix the problem so I will add NEWS entry.
@jangorecki I've tested your patch in the meantime and as I expected it does not solve the problem. One possibility (with probably minimal future maintainence, but that tends to be wishful thinking) would be to clear the attributes in as.data.table and setDT but I'm not really a fan of that.
The following would fix the problem as well as obviating the need to call as.data.table at all here:
but that could start to get hairy as to who has to maintain it and beg the question of how much more should be done to make dplyr play nicely with data.table.
Wouldn't getting as.data.table and setDT to recognize an index attribute and updating it at the time of function call work that doesn't depend on other packages and have much maintenance overhead? I don't know much about the source code for data.table, but I do remember reading discussions about a computationally efficient manner of checking if a vector is already ordered that is in use right now.
A potential problem is if a different package or the user specifically creates an index attribute for a data.frame or derived object, that conflicts with how data.table uses that attribute - a chance that is much less likely.
As @tlapak points out, dplyr defaults to preserving all attributes (since a reasonable number of people store arbitrary metadata in attributes and expect it to be maintained). As mentioned, dplyr_row_slice.data.table method would fix the problem, but it's unclear where it should live — since it affects the combination of dplyr and data.table, it feels odd to put it in either. I think it's slightly more correct for it to live in data.table (since y'all know best how to handle your own attributes), but would that require adding dplyr as a suggested package and a little work to conditionally register the method. Given that, I'd be happy to include the method in dplyr.
@tlapak, you'd be welcome to do a PR if you wanted, and it would be great to get a sense from other data.table developers what attributes you think we should preserve/drop.
Thanks all. #4893 now merged too and I confirmed it doesn't fix this one either, as @avimallu and @tlapak expected.
The root of this issue with dplyr::arrange appears to be vctrs::vec_slice which dplyr:::dplyr_row_slice.data.frame calls.
>DTiso3ccountryincome<char><char><char>1:MOZMozambiqueLIC2:ZMBZambiaLMIC3:ALBAlbaniaUMIC> attr(DT, "index")
integer(0)
attr(,"__iso3c")
[1] 312>>DT2=vctrs::vec_slice(DT, c(3,1,2))
>>DT2iso3ccountryincome<char><char><char>1:ALBAlbaniaUMIC2:MOZMozambiqueLIC3:ZMBZambiaLMIC> attr(DT2, "index")
integer(0)
attr(,"__iso3c")
[1] 312# index left intact and now incorrect> class(DT2)
[1] "data.table""data.frame"# still a data.table
The intention is that [.data.table detects that dplyr and vctrs are not data.table-aware, and any data.frame-code (in this case [ on a data.table from those not-data-table-aware packages) gets redirected through the cedta() catch which drops the key and indices. That happens here :
if (!missing(i) && is.data.table(ans)) setkey(ans, NULL) # See test 304
where that setkey(ans, NULL) removes the index attribute too as well the key. Test 304 was moved to be the plyr::arrange test in other.Rraw which tests that :
Since vctrs::vec_slice is doing its own subsetting at C level, it's bypassing [ method dispatch.
There's been some attention here to as.data.table apparently because there's two calls to as.data.table in the original reproducible example at the top :
DT<- distinct(DT) %>% as.data.table()
....# Index mixed up by arrangeDT<-DT %>% arrange(iso3c) %>% as.data.table()
But neither of those as.data.table() are needed. dplyr::distinct(DT) and dplyr::arrange(DT) already retain the data.table class so those as.data.table() calls just return their input. Hence, even if any potential fix was applied to as.data.table (for example like checking any index was valid and dropping it if not) wouldn't work if as.data.table() isn't used.
Even more minimal example :
require(data.table)
require(dplyr)
DT= data.table(A=c("b","c","a"), B=10:12)
setindex(DT, A)
DT[A=="c"] # correctly returns row 2DT2=dplyr::arrange(DT, A)
class(DT2) # retains data.tabe classDT2[A=="c"] # incorrectly returns no rows
The .internal.selfref is detecting the invalid status as intended :
require(data.table)
require(dplyr)
DT= data.table(A=c("b","c","a"), B=10:12)
setindex(DT, A)
DT[A=="c"] # correctly returns row 2data.table:::selfrefok(DT) # 1 i.e. okDT2=dplyr::arrange(DT, A)
data.table:::selfrefok(DT2) # 0 i.e. not ok
class(DT2) # retains data.tabe classDT2[A=="c", verbose=TRUE] # incorrectly returns no rows# ...# ... using index# ...
We shouldn't be using indexes when the selfref is not ok.
#5084 fixes this and adds a dplyr::arrange test to test.data.table("other.Rraw")
But now we know what we're looking for, I suspect there are other entry points that need catching too. setkey.R::getindex() could be a central place to drop index if selfref isn't ok. Would need to think of a test to trigger that without going through bmerge() which is now caught by this fix to shallow(). [.data.table could be another central place, at the top.
Current thoughts are that it's probably best to merge #5084 so that folk can test dev, and then follow up in a new issue on a more comprehensive review adding tests for dropping indexes when selfref detects the prior copy by another package.
The advantage of this approach over adding dplyr_row_slice.data.table is that it tackles the root more directly and thus would fix other packages that may now or in future do the same thing as vctrs::vec_slice at C level which bypasses [ dispatch. The selfref mechanism was introduced to catch base R copies after all, so it's probably possible to reproduce an invalid index using base R (like the test in #4893) rather than dplyr::arrange (and if so #5084+#5085 would fix that too). We're just using dplyr::arrange as the first known example for the test really. There may be other methods in dplyr that find their way to vctrs::vec_slice, and maybe there are other functions in vctrs that work similarly to vec_slice, or dplyr's method names might change in future. All that should be covered by the selfref approach, so we insulate ourselves from maintenance, and users from specific version incompatibilities.
That's the way I'm thinking currently, anyway. Happy to reconsider.
Activity
MichaelChirico commentedon Jun 12, 2021
could you report this with dplyr as well? at first glance it seems like a bug on that end
avimallu commentedon Jun 12, 2021
I think it might be related somewhat to #4889, and not fixed by #4893.
dplyr, AFAIK works with eithertibbles ordata.frames, so adata.table's index attribute would be ignored during that conversion but retained, leading to an incorrect result. I took a quick look atas.data.table.data.frame, which I believe would be called here, and it doesn't look like it removed the index:The last row returns an output:
which would remain unchanged in an operation such as
dplyr::arrangefollowed by aas.data.table. Source codes for easy reference:as.data.frame.data.table
as.data.table.data.frame
jangorecki commentedon Jun 12, 2021
I don't have dplyr on hand to try that out, but as said by @avimallu, as.data.frame should remove index. I pushed that change to #5043 Let me know if it does fix the problem so I will add NEWS entry.
tlapak commentedon Jun 12, 2021
As far as I can tell, dplyr explicitly preserves all attributes of the input. There needs to be a
dplyr_row_slice.data.tablefunction to accommodate the attributes that depend on row order, seehttps://github.com/tidyverse/dplyr/blob/55917b6194aa8ba5b0ad8b2109ac746193996c96/R/generics.R#L25-L47
Waldi73 commentedon Jun 12, 2021
Thanks all for analyzing this!
@MichaelChirico , reported on dplyr/issues/5917 as well
tlapak commentedon Jun 14, 2021
@jangorecki I've tested your patch in the meantime and as I expected it does not solve the problem. One possibility (with probably minimal future maintainence, but that tends to be wishful thinking) would be to clear the attributes in
as.data.tableandsetDTbut I'm not really a fan of that.The following would fix the problem as well as obviating the need to call
as.data.tableat all here:but that could start to get hairy as to who has to maintain it and beg the question of how much more should be done to make dplyr play nicely with data.table.
avimallu commentedon Jun 14, 2021
Wouldn't getting
as.data.tableandsetDTto recognize anindexattribute and updating it at the time of function call work that doesn't depend on other packages and have much maintenance overhead? I don't know much about the source code fordata.table, but I do remember reading discussions about a computationally efficient manner of checking if a vector is already ordered that is in use right now.A potential problem is if a different package or the user specifically creates an
indexattribute for adata.frameor derived object, that conflicts with howdata.tableuses that attribute - a chance that is much less likely.hadley commentedon Jun 16, 2021
As @tlapak points out, dplyr defaults to preserving all attributes (since a reasonable number of people store arbitrary metadata in attributes and expect it to be maintained). As mentioned,
dplyr_row_slice.data.tablemethod would fix the problem, but it's unclear where it should live — since it affects the combination of dplyr and data.table, it feels odd to put it in either. I think it's slightly more correct for it to live in data.table (since y'all know best how to handle your own attributes), but would that require adding dplyr as a suggested package and a little work to conditionally register the method. Given that, I'd be happy to include the method in dplyr.@tlapak, you'd be welcome to do a PR if you wanted, and it would be great to get a sense from other data.table developers what attributes you think we should preserve/drop.
mattdowle commentedon Jul 27, 2021
Thanks all. #4893 now merged too and I confirmed it doesn't fix this one either, as @avimallu and @tlapak expected.
The root of this issue with
dplyr::arrangeappears to bevctrs::vec_slicewhichdplyr:::dplyr_row_slice.data.framecalls.The intention is that
[.data.tabledetects thatdplyrandvctrsare notdata.table-aware, and anydata.frame-code (in this case[on adata.tablefrom those not-data-table-aware packages) gets redirected through thecedta()catch which drops the key and indices. That happens here :data.table/R/data.table.R
Line 153 in 6fa5cab
where that
setkey(ans, NULL)removes the index attribute too as well the key. Test 304 was moved to be theplyr::arrangetest inother.Rrawwhich tests that :data.table/inst/tests/other.Rraw
Line 59 in 0613157
But for
dplyr::arrange(dplyr not plyr),vctrs::vec_sliceis implemented in C :Since
vctrs::vec_sliceis doing its own subsetting at C level, it's bypassing[method dispatch.There's been some attention here to
as.data.tableapparently because there's two calls toas.data.tablein the original reproducible example at the top :But neither of those
as.data.table()are needed.dplyr::distinct(DT)anddplyr::arrange(DT)already retain thedata.tableclass so thoseas.data.table()calls just return their input. Hence, even if any potential fix was applied toas.data.table(for example like checking any index was valid and dropping it if not) wouldn't work ifas.data.table()isn't used.Even more minimal example :
mattdowle commentedon Jul 27, 2021
The
.internal.selfrefis detecting the invalid status as intended :We shouldn't be using indexes when the selfref is not ok.
mattdowle commentedon Jul 28, 2021
#5084 fixes this and adds a
dplyr::arrangetest totest.data.table("other.Rraw")But now we know what we're looking for, I suspect there are other entry points that need catching too.
setkey.R::getindex()could be a central place to drop index if selfref isn't ok. Would need to think of a test to trigger that without going throughbmerge()which is now caught by this fix toshallow().[.data.tablecould be another central place, at the top.Current thoughts are that it's probably best to merge #5084 so that folk can test dev, and then follow up in a new issue on a more comprehensive review adding tests for dropping indexes when selfref detects the prior copy by another package.
The advantage of this approach over adding
dplyr_row_slice.data.tableis that it tackles the root more directly and thus would fix other packages that may now or in future do the same thing asvctrs::vec_sliceat C level which bypasses[dispatch. The selfref mechanism was introduced to catch base R copies after all, so it's probably possible to reproduce an invalid index using base R (like the test in #4893) rather thandplyr::arrange(and if so #5084+#5085 would fix that too). We're just usingdplyr::arrangeas the first known example for the test really. There may be other methods indplyrthat find their way tovctrs::vec_slice, and maybe there are other functions invctrsthat work similarly tovec_slice, ordplyr's method names might change in future. All that should be covered by the selfref approach, so we insulate ourselves from maintenance, and users from specific version incompatibilities.That's the way I'm thinking currently, anyway. Happy to reconsider.