Repository navigation
Dataset patching tools #183
Description
Formerly filed in dpylr repo.
I'm doing an analysis that requires a lot of manual corrections to the original dataset. I keep tables of changes that mirror the original (see example in code) and then I use the code below to impute the original values. Probably niche, but who am I to decide. Here's my code:
library(dplyr)
library(lazyeval)
update_with = function(data, update, by) {
# columns to be updated
# (data intersect cols) -- by
orig_order = colnames(data)
update_cols =
setdiff(intersect(colnames(data), colnames(update)), by)
# left join update
data = data %>% left_join(update, by=by)
# transmutate one by one
for(col in update_cols) {
col.x = paste(col, "x", sep=".")
col.y = paste(col, "y", sep=".")
myifelse = function(x, y) {
x[!is.na(y)] <- y[!is.na(y)]
x
}
update = interp( ~myifelse(x, y)
, x=as.name(col.x)
, y=as.name(col.y)
)
data = data %>%
mutate_( .dots = setNames(list(update), col) ) %>%
select( -one_of(c(col.x,col.y)) )
}
# deletes extra columns too
data[,orig_order]
}
a = data.frame(x = 1:100,y = 5, z = 1:50)
b = data.frame(x = 1:10, y = 0)
# sets y to 0 when x is in range 1:10, otherwise a remains the same
a %>% update_with(b, by="x")After using this method for awhile I keep my patches in this format, which assigns value to the cell described by Row and Column.
| Row | Column | Value | Data to help fill in value |
|---|---|---|---|
| 1 | x | 123 | hint: it's as easy as... |
| 2 | y | xyz | link to pathology report |
and I remove the rightmost column use spread on the column Column and feed the subsequent dataframe into update_with. This probably has some unintended side effects when mixing integers and factors together in the value column. This has been surprisingly robust and I've been able to keep a single patch file for each table.
thanks,
Marcus
Activity
hadley commentedon May 16, 2016
I like the idea, but that patch form feels a bit fragile to me - if the row order changes you'll silently patch the wrong values. I think it would be better to supply the value of some keys:
This would require the yet unimplemented key-checking functions from dplyr to make sure that there was a unique match for each key.
@jennybc have you thought about this at all?
jennybc commentedon May 16, 2016
I've only thought about it up to ... agreeing that it would be really useful to have better tools for patching!
I have really awkward workflows for this and it seems nice to think of it as a novelty join, which is how I interpret your comment.
I'd been planning to delve into the new
dplyr::case_when()the next time this came up. But re-skimming that issue (tidyverse/dplyr#631) and reading the help file makes me thinkcase_when()doesn't really solve it? I just recalled that patching did come up explicitly over there.Why would this go in
tidyrvsdplyr?hadley commentedon May 17, 2016
Hmmm, interesting - that
patch()feels more like a "mutate_when", which I'm not currently super big on. Thispatch()feels more like the sort of thing you'd want to create (either by hand or with an add-in) when you discover some bad values in your input.jennybc commentedon May 19, 2016
What I meant by "novelty join:" Like left join, but instead of getting
mpg.x,mpg.y,wt.x, andwt.y, we get justmpgandwtin the result, with the values inpatchestaking precedence. The proposals are pretty different in terms of how to patch more than one variable.BTW, assuming I understand what you mean by "mutate_when", I think one of those just came up on SO.
hadley commentedon May 19, 2016
@jennybc yes, that's what I was picturing too - except I'm not sure if you want to patch individual variables or whole rows. In your scenario I guess you'd use NA in
patchesto indicate that you didn't want to replace the value? I can imagining patching either cells or rows to be useful in different scenarios. It would be easy to convert between the two forms with gather/spread, so it wouldn't matter too much which was primary.jennybc commentedon Jun 1, 2016
Recording a link to a related Twitter discussion. I think it's another example of the type of problem solved by the patching discussed here: "join and update columns, instead of duplicating them". It's an example where it's natural to update multiple variables at once.
MarcusWalz commentedon Jul 20, 2016
Hmm, I'm having to patch a lot in my current project that's a combination of an ETL process and manual data entry for variables my ETL pipeline doesn't capture. It's patches on top of patches on top of patches.
Here's my latest and greatest patch function.
patch_funcontrols the patching behavior. It's defaulted to the functioncoalesce. A custom function can be thrown in for other use cases, life if, for example, you only wish to patchNA's. Columns to patch can be specified in..., otherwise the patch patches all columns common todataandpatch_dataminus the columns listed inby.bycan not be null. The correctness of the patch is checked in a few places. A patch must be in injection onto the data that's being patched (i.e. a one-to-one relationship). I typically have to group and summaries to get my real world patches into a one-to-one relationship.@jennybc example above works as specified.
hadley commentedon Feb 21, 2017
Just a note to say that
upsert()feels like the right name for one of these behaviours.alistaire47 commentedon Apr 20, 2017
I've found myself using this sort of
update_joinwhen aggregating data for a package (coming to GitHub once a few bugs are squashed). It's largely just a left or full join and thencoalesceing the duplicated columns, but a related issue arises:I have redundant key columns, but they're incomplete. Trying to update key columns with
NAs before attempting to join everything requires dropping cases withNAs from one of the tables beforehand to avoid a cross join ofNAs. That's fine, but suggests the possibility of updating keys: if one isNA, use the other to join and update, effectively combining keys by|instead of&.This might fit in with
join_by, maybe as a helper function working likenestingdoes withinexpandorcomplete, e.g. what would currently take something likeor could be written with update joins as something like
could just be
In essence then, it's just a series of crossing update joins on key columns before the final join, and thus shouldn't take much more code.
hadley commentedon Nov 16, 2017
Summarising the discussion, I think there are at least patching cases described here:
Patch individual values described by a location (key variables + name of variable to fix), the new value, and a comment.
Combine two data frames where
ycontains some of the same variables asx. Match on key variables, then either alway takes the values fromy, or only take them ifxisNA.Combine two data frames with identical variables. Replace matching rows in
xwithy, and also add new rows. This feels most likeupsert().I wonder if these could be called
patch_val(),patch_col()andpatch_row().I'm not sure if only replacing missing values is a core feature or an incidental detail.
[-]Dataset Patching tools.[/-][+]Dataset patching tools[/+]billdenney commentedon Dec 30, 2017
I have a use case that is your second patching case ("Combine two data frames where
ycontains some of the same variables asx. Match on key variables, then either always takes the values fromy, or only take them ifxisNA.") and is a variant on that. (From the conversation initiated in the dplyr issue just linked.)What you described as
patch_col()further generalizes as, "If values are missing in column_1, replace them with values in column_2." And more so, allow an arbitrary number of columns and column groups. My use case for this generalization is that I do meta-analyses, and when I do them, I often have various columns representing the potential number of measurements in an observation. usually I work with clinical studies, so the N is number of subjects in a clinical trial, the various N I may have available include from most to least preferred:What do you think of the below as an implementation:
billdenney commentedon Dec 30, 2017
While I was at it, how about this for
patch_val. An extension to this would have the arguments either be a named list (which would look for equality; current version) or a formula (which would be evaluated and coerced to a logical in the environment of the data.frame):ctmann commentedon Jul 28, 2018
I would settle for adding a replacement option to left_join (replace.with.y=TRUE)
lionel- commentedon Oct 29, 2019
There is an implementation in tidyverse/dplyr#4595 (comment)
moodymudskipper commentedon Dec 12, 2019
I had implemented this idea of patching in my package safejoin, which is wrapped around dplyr's join functions.
I have a
conflictargument, which is a 2 argument function will be applies on each names present in pairs of conflicting columns (same name and not included in "by").The patching you describe here can be done by using
conflict = ~dplyr::coalesce(.y, .x). If we want NA values from 2nd data frame to have precedence over non NA values from data frame 1 we use the special valueconflict = "patch"(which seems to be broken in my package but that's another issue).It makes more sense to me to integrate this in join operations than new verbs because we might want to use it with full_join (close equivalent to @hadley's
insertin tidyverse/dplyr#4654) , semi_join (close to @hadley'supdatein tidyverse/dplyr#4654), left_join (patching as described here), or inner_join etc.coalescing is the most common request (see all these SO questions : https://stackoverflow.com/search?q=safejoin ) but having a general
conflictargument allows to add values in some cases, or to create list element, or to pack conflicting columns withconflict = ~tibble(x=.x,y=.y).conflict = ~.xmeans we just keep the first value, so it's safe and we don't have a column name disappearing mysteriously depending on what we have in the second data.frame (which leads to frustrating debugging).With @billdenney 's example above :
Created on 2019-12-12 by the reprex package (v0.3.0)
I'd much rather see these features in dplyr than safejoin.
hadley commentedon Apr 21, 2020
Most likely to be implemented as part of tidyverse/dplyr#4654